From c3eb3850665b7ebc2d02ab150e046556fb81e0c3 Mon Sep 17 00:00:00 2001 From: catbref Date: Thu, 18 Jun 2020 18:08:46 +0100 Subject: [PATCH] WIP: cross-chain trading with new lockTimes, requires AT v1.3.5 --- pom.xml | 2 +- .../api/model/CrossChainBuildRequest.java | 2 +- .../api/resource/CrossChainResource.java | 6 +- .../java/org/qortal/controller/TradeBot.java | 35 ++--- .../java/org/qortal/crosschain/BTCACCT.java | 140 +++++++++++------- .../data/crosschain/CrossChainTradeData.java | 3 - .../qortal/data/crosschain/TradeBotData.java | 8 +- .../hsqldb/HSQLDBCrossChainRepository.java | 22 ++- .../java/org/qortal/utils/BitTwiddling.java | 11 ++ .../java/org/qortal/test/btcacct/AtTests.java | 58 ++++++-- .../org/qortal/test/btcacct/DeployAT.java | 14 +- 11 files changed, 172 insertions(+), 129 deletions(-) diff --git a/pom.xml b/pom.xml index a0b05ed6..5c616c83 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ 0.15.5 1.64 ${maven.build.timestamp} - 1.3.4 + 1.3.5 3.6 1.8 1.2.2 diff --git a/src/main/java/org/qortal/api/model/CrossChainBuildRequest.java b/src/main/java/org/qortal/api/model/CrossChainBuildRequest.java index 76fafc9c..e8d38703 100644 --- a/src/main/java/org/qortal/api/model/CrossChainBuildRequest.java +++ b/src/main/java/org/qortal/api/model/CrossChainBuildRequest.java @@ -24,7 +24,7 @@ public class CrossChainBuildRequest { public byte[] bitcoinPublicKeyHash; @Schema(description = "HASH160 of secret", example = "43vnftqkjxrhb5kJdkU1ZFQLEnWV") - public byte[] secretHash; + public byte[] hashOfSecretB; @Schema(description = "Bitcoin P2SH BTC balance for release of secret", example = "0.00864200") @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) diff --git a/src/main/java/org/qortal/api/resource/CrossChainResource.java b/src/main/java/org/qortal/api/resource/CrossChainResource.java index 73f9b100..41089e91 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainResource.java @@ -156,7 +156,7 @@ public class CrossChainResource { if (creatorPublicKey == null || creatorPublicKey.length != Transformer.PUBLIC_KEY_LENGTH) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY); - if (tradeRequest.secretHash == null || tradeRequest.secretHash.length != BTC.HASH160_LENGTH) + if (tradeRequest.hashOfSecretB == null || tradeRequest.hashOfSecretB.length != BTC.HASH160_LENGTH) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); if (tradeRequest.tradeTimeout == null) @@ -181,8 +181,8 @@ public class CrossChainResource { try (final Repository repository = RepositoryManager.getRepository()) { PublicKeyAccount creatorAccount = new PublicKeyAccount(repository, creatorPublicKey); - byte[] creationBytes = BTCACCT.buildQortalAT(creatorAccount.getAddress(), tradeRequest.bitcoinPublicKeyHash, tradeRequest.secretHash, - tradeRequest.tradeTimeout, tradeRequest.qortAmount, tradeRequest.bitcoinAmount); + byte[] creationBytes = BTCACCT.buildQortalAT(creatorAccount.getAddress(), tradeRequest.bitcoinPublicKeyHash, tradeRequest.hashOfSecretB, + tradeRequest.qortAmount, tradeRequest.bitcoinAmount); long txTimestamp = NTP.getTime(); byte[] lastReference = creatorAccount.getLastReference(); diff --git a/src/main/java/org/qortal/controller/TradeBot.java b/src/main/java/org/qortal/controller/TradeBot.java index 373ceb81..806805dc 100644 --- a/src/main/java/org/qortal/controller/TradeBot.java +++ b/src/main/java/org/qortal/controller/TradeBot.java @@ -56,11 +56,12 @@ public class TradeBot { public static byte[] createTrade(Repository repository, TradeBotCreateRequest tradeBotCreateRequest) throws DataException { byte[] tradePrivateKey = generateTradePrivateKey(); - byte[] secret = generateSecret(); - byte[] secretHash = Crypto.digest(secret); + byte[] secretB = generateSecret(); + byte[] hashOfSecretB = Crypto.digest(secretB); byte[] tradeNativePublicKey = deriveTradeNativePublicKey(tradePrivateKey); byte[] tradeNativePublicKeyHash = Crypto.hash160(tradeNativePublicKey); + String tradeAddress = Crypto.toAddress(tradeNativePublicKey); byte[] tradeForeignPublicKey = deriveTradeForeignPublicKey(tradePrivateKey); byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey); @@ -78,7 +79,7 @@ public class TradeBot { String description = "QORT/BTC cross-chain trade"; String aTType = "ACCT"; String tags = "ACCT QORT BTC"; - byte[] creationBytes = BTCACCT.buildQortalAT(creator.getAddress(), tradeNativePublicKeyHash, secretHash, tradeBotCreateRequest.tradeTimeout, tradeBotCreateRequest.qortAmount, tradeBotCreateRequest.bitcoinAmount); + byte[] creationBytes = BTCACCT.buildQortalAT(tradeAddress, tradeForeignPublicKeyHash, hashOfSecretB, tradeBotCreateRequest.qortAmount, tradeBotCreateRequest.bitcoinAmount); long amount = tradeBotCreateRequest.fundingQortAmount; DeployAtTransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, aTType, tags, creationBytes, amount, Asset.QORT); @@ -86,8 +87,8 @@ public class TradeBot { String atAddress = deployAtTransactionData.getAtAddress(); TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, TradeBotData.State.BOB_WAITING_FOR_MESSAGE, - atAddress, tradeBotCreateRequest.tradeTimeout, - tradeNativePublicKey, tradeNativePublicKeyHash, secret, secretHash, + atAddress, + tradeNativePublicKey, tradeNativePublicKeyHash, secretB, hashOfSecretB, tradeForeignPublicKey, tradeForeignPublicKeyHash, tradeBotCreateRequest.bitcoinAmount, null); repository.getCrossChainRepository().save(tradeBotData); @@ -115,7 +116,7 @@ public class TradeBot { byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey); TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, TradeBotData.State.ALICE_WAITING_FOR_P2SH_A, - crossChainTradeData.qortalAtAddress, crossChainTradeData.tradeTimeout, + crossChainTradeData.qortalAtAddress, tradeNativePublicKey, tradeNativePublicKeyHash, secret, secretHash, tradeForeignPublicKey, tradeForeignPublicKeyHash, crossChainTradeData.expectedBitcoin, null); @@ -188,8 +189,6 @@ public class TradeBot { return; } - long atCreationTimestamp = atData.getCreation(); - String address = Crypto.toAddress(tradeBotData.getTradeNativePublicKey()); List messageTransactionsData = repository.getTransactionRepository().getMessagesByRecipient(address, null, null, null); @@ -212,20 +211,15 @@ public class TradeBot { // We're expecting: HASH160(secret) + Alice's Bitcoin pubkeyhash byte[] messageData = messageTransactionData.getData(); - - if (messageData.length != 40) + BTCACCT.OfferMessageData offerMessageData = BTCACCT.extractOfferMessageData(messageData); + if (offerMessageData == null) continue; - byte[] aliceSecretHash = new byte[20]; - System.arraycopy(messageData, 0, aliceSecretHash, 0, 20); - - byte[] aliceForeignPublicKeyHash = new byte[20]; - System.arraycopy(messageData, 20, aliceForeignPublicKeyHash, 0, 20); - + byte[] aliceForeignPublicKeyHash = offerMessageData.recipientBitcoinPKH; + byte[] hashOfSecretA = offerMessageData.hashOfSecretA; + int lockTimeA = (int) offerMessageData.lockTimeA; // Determine P2SH-A address and confirm funded - // First P2SH-A refund timeout is last in chain, so add all of tradeTimeout - int lockTimeA = BTCACCT.calcLockTimeA(atCreationTimestamp, tradeBotData.getTradeTimeout()); - byte[] redeemScript = BTCP2SH.buildScript(aliceForeignPublicKeyHash, lockTimeA, tradeBotData.getTradeForeignPublicKeyHash(), aliceSecretHash); + byte[] redeemScript = BTCP2SH.buildScript(aliceForeignPublicKeyHash, lockTimeA, tradeBotData.getTradeForeignPublicKeyHash(), hashOfSecretA); String p2shAddress = BTC.getInstance().deriveP2shAddress(redeemScript); Long balance = BTC.getInstance().getBalance(p2shAddress); @@ -235,9 +229,10 @@ public class TradeBot { // Good to go - send MESSAGE to AT String aliceNativeAddress = Crypto.toAddress(messageTransactionData.getCreatorPublicKey()); + int lockTimeB = BTCACCT.calcLockTimeB(messageTransactionData.getTimestamp(), lockTimeA); // Build outgoing message, padding each part to 32 bytes to make it easier for AT to consume - byte[] outgoingMessageData = BTCACCT.buildOfferMessage(aliceNativeAddress, aliceForeignPublicKeyHash, aliceSecretHash); + byte[] outgoingMessageData = BTCACCT.buildTradeMessage(aliceNativeAddress, aliceForeignPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey()); MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, tradeBotData.getAtAddress(), outgoingMessageData, false, false); diff --git a/src/main/java/org/qortal/crosschain/BTCACCT.java b/src/main/java/org/qortal/crosschain/BTCACCT.java index e113c1aa..172d24f9 100644 --- a/src/main/java/org/qortal/crosschain/BTCACCT.java +++ b/src/main/java/org/qortal/crosschain/BTCACCT.java @@ -22,6 +22,7 @@ import org.qortal.data.crosschain.CrossChainTradeData; import org.qortal.repository.DataException; import org.qortal.repository.Repository; import org.qortal.utils.Base58; +import org.qortal.utils.BitTwiddling; import com.google.common.hash.HashCode; import com.google.common.primitives.Bytes; @@ -87,7 +88,13 @@ public class BTCACCT { public static final int SECRET_LENGTH = 32; public static final int MIN_LOCKTIME = 1500000000; - public static final byte[] CODE_BYTES_HASH = HashCode.fromString("ae1c6749b08465a5dec0224ab25e7551947f900df404bfed434a02fdad102b03").asBytes(); // SHA256 of AT code bytes + public static final byte[] CODE_BYTES_HASH = HashCode.fromString("ca0dc643fdaba4d12cd5550800a8353746f40a0d9824d8c10d8b4bd0324eac0d").asBytes(); // SHA256 of AT code bytes + + public static class OfferMessageData { + public byte[] recipientBitcoinPKH; + public byte[] hashOfSecretA; + public long lockTimeA; + } private BTCACCT() { } @@ -98,17 +105,14 @@ public class BTCACCT { * tradeTimeout (minutes) is the time window for the recipient to send the * 32-byte secret to the AT, before the AT automatically refunds the AT's creator. * - * @param qortalCreator Qortal address for AT creator, also used for refunds - * @param bitcoinPublicKeyHash 20-byte HASH160 of creator's bitcoin public key - * @param hashOfSecretB 20-byte HASH160 of 32-byte secret - * @param tradeTimeout how many minutes, from AT creation, until AT auto-refunds AT creator - * @param qortAmount how much QORT to pay trade partner if they send correct 32-byte secret to AT + * @param creatorTradeAddress AT creator's trade Qortal address, also used for refunds + * @param bitcoinPublicKeyHash 20-byte HASH160 of creator's trade Bitcoin public key + * @param hashOfSecretB 20-byte HASH160 of 32-byte secret-B + * @param qortAmount how much QORT to pay trade partner if they send correct 32-byte secrets to AT * @param bitcoinAmount how much BTC the AT creator is expecting to trade * @return */ - public static byte[] buildQortalAT(String creatorTradeAddress, byte[] bitcoinPublicKeyHash, byte[] hashOfSecretB, int tradeTimeout, long qortAmount, long bitcoinAmount) { - int refundTimeout = calcRefundTimeout(tradeTimeout); - + public static byte[] buildQortalAT(String creatorTradeAddress, byte[] bitcoinPublicKeyHash, byte[] hashOfSecretB, long qortAmount, long bitcoinAmount) { // Labels for data segment addresses int addrCounter = 0; @@ -125,8 +129,6 @@ public class BTCACCT { final int addrHashOfSecretB = addrCounter; addrCounter += 4; - final int addrTradeTimeout = addrCounter++; - final int addrRefundTimeout = addrCounter++; final int addrQortAmount = addrCounter++; final int addrBitcoinAmount = addrCounter++; @@ -163,6 +165,9 @@ public class BTCACCT { final int addrQortalRecipient3 = addrCounter++; final int addrQortalRecipient4 = addrCounter++; + final int addrLockTimeA = addrCounter++; + final int addrLockTimeB = addrCounter++; + final int addrRefundTimeout = addrCounter++; final int addrRefundTimestamp = addrCounter++; final int addrLastTxTimestamp = addrCounter++; final int addrBlockTimestamp = addrCounter++; @@ -190,7 +195,7 @@ public class BTCACCT { // Data segment ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE); - // AT creator's Qortal address, decoded from Base58 + // AT creator's trade Qortal address, decoded from Base58 assert dataByteBuffer.position() == addrCreatorTradeAddress1 * MachineState.VALUE_SIZE : "addrCreatorTradeAddress1 incorrect"; byte[] creatorTradeAddressBytes = Base58.decode(creatorTradeAddress); dataByteBuffer.put(Bytes.ensureCapacity(creatorTradeAddressBytes, 32, 0)); @@ -199,18 +204,10 @@ public class BTCACCT { assert dataByteBuffer.position() == addrBitcoinPublicKeyHash * MachineState.VALUE_SIZE : "addrBitcoinPublicKeyHash incorrect"; dataByteBuffer.put(Bytes.ensureCapacity(bitcoinPublicKeyHash, 32, 0)); - // Hash of secret + // Hash of secret-B assert dataByteBuffer.position() == addrHashOfSecretB * MachineState.VALUE_SIZE : "addrHashOfSecretB incorrect"; dataByteBuffer.put(Bytes.ensureCapacity(hashOfSecretB, 32, 0)); - // Trade timeout in minutes - assert dataByteBuffer.position() == addrTradeTimeout * MachineState.VALUE_SIZE : "addrTradeTimeout incorrect"; - dataByteBuffer.putLong(tradeTimeout); - - // Refund timeout in minutes (¾ of trade-timeout) - assert dataByteBuffer.position() == addrRefundTimeout * MachineState.VALUE_SIZE : "addrRefundTimeout incorrect"; - dataByteBuffer.putLong(refundTimeout); - // Redeem Qort amount assert dataByteBuffer.position() == addrQortAmount * MachineState.VALUE_SIZE : "addrQortAmount incorrect"; dataByteBuffer.putLong(qortAmount); @@ -235,7 +232,7 @@ public class BTCACCT { assert dataByteBuffer.position() == addrCreatorAddressPointer * MachineState.VALUE_SIZE : "addrCreatorAddressPointer incorrect"; dataByteBuffer.putLong(addrCreatorAddress1); - // Index into data segment of hash, used by GET_B_IND + // Index into data segment of hash of secret B, used by GET_B_IND assert dataByteBuffer.position() == addrHashOfSecretBPointer * MachineState.VALUE_SIZE : "addrHashOfSecretBPointer incorrect"; dataByteBuffer.putLong(addrHashOfSecretB); @@ -251,7 +248,7 @@ public class BTCACCT { assert dataByteBuffer.position() == addrOfferMessageRecipientBitcoinPKHOffset * MachineState.VALUE_SIZE : "addrOfferMessageRecipientBitcoinPKHOffset incorrect"; dataByteBuffer.putLong(32L); - // Index into data segment of hash, used by SET_B_IND + // Index into data segment of recipient's Bitcoin PKH, used by GET_B_IND assert dataByteBuffer.position() == addrRecipientBitcoinPKHPointer * MachineState.VALUE_SIZE : "addrRecipientBitcoinPKHPointer incorrect"; dataByteBuffer.putLong(addrRecipientBitcoinPKH); @@ -259,7 +256,7 @@ public class BTCACCT { assert dataByteBuffer.position() == addrOfferMessageHashOfSecretAOffset * MachineState.VALUE_SIZE : "addrOfferMessageHashOfSecretAOffset incorrect"; dataByteBuffer.putLong(64L); - // Index into data segment of hash, used by GET_B_IND + // Index into data segment of hash of secret A, used by GET_B_IND assert dataByteBuffer.position() == addrHashOfSecretAPointer * MachineState.VALUE_SIZE : "addrHashOfSecretAPointer incorrect"; dataByteBuffer.putLong(addrHashOfSecretA); @@ -289,7 +286,7 @@ public class BTCACCT { Integer labelCheckSecretB = null; Integer labelPayout = null; - ByteBuffer codeByteBuffer = ByteBuffer.allocate(512); + ByteBuffer codeByteBuffer = ByteBuffer.allocate(768); // Two-pass version for (int pass = 0; pass < 2; ++pass) { @@ -301,9 +298,6 @@ public class BTCACCT { // Use AT creation 'timestamp' as starting point for finding transactions sent to AT codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTxTimestamp)); - // Calculate trade timeout refund 'timestamp' by adding addrRefundTimeout minutes to AT creation 'timestamp', then save into addrRefundTimestamp - codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.ADD_MINUTES_TO_TIMESTAMP, addrRefundTimestamp, addrLastTxTimestamp, addrRefundTimeout)); - // Load B register with AT creator's address so we can save it into addrCreatorAddress1-4 codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_CREATOR_INTO_B)); codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrCreatorAddressPointer)); @@ -371,13 +365,28 @@ public class BTCACCT { labelOfferTxExtract = codeByteBuffer.position(); - // Message is expected length, extract recipient's Bitcoin PKH + // Message is expected length, grab next 32 bytes codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrOfferMessageRecipientBitcoinPKHOffset)); - codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrRecipientBitcoinPKHPointer)); - // Extract hash-of-secret-a + // Extract recipient's Bitcoin PKH (we only really use values from B1-B3) + codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrRecipientBitcoinPKHPointer)); + // Also extract lockTimeB + codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, addrLockTimeB)); + + // Grab next 32 bytes codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrOfferMessageHashOfSecretAOffset)); + + // Extract hash-of-secret-a (we only really use values from B1-B3) codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrHashOfSecretAPointer)); + // Extract lockTimeA (from B4) + codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, addrLockTimeA)); + + // Calculate trade refund timeout: (lockTimeA - lockTimeB) / 2 / 60 + codeByteBuffer.put(OpCode.SET_DAT.compile(addrRefundTimeout, addrLockTimeA)); // refundTimeout = lockTimeA + codeByteBuffer.put(OpCode.SUB_DAT.compile(addrRefundTimeout, addrLockTimeB)); // refundTimeout -= lockTimeB + codeByteBuffer.put(OpCode.DIV_VAL.compile(addrRefundTimeout, 2L * 60L)); // refundTimeout /= 2 * 60 + // Calculate trade timeout refund 'timestamp' by adding addrRefundTimeout minutes to this tx 'timestamp', then save into addrRefundTimestamp + codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.ADD_MINUTES_TO_TIMESTAMP, addrRefundTimestamp, addrLastTxTimestamp, addrRefundTimeout)); /* We are in 'trade mode' */ codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, 1)); @@ -533,6 +542,8 @@ public class BTCACCT { ByteBuffer dataByteBuffer = ByteBuffer.wrap(dataBytes); byte[] addressBytes = new byte[25]; + /* Constants */ + // Skip creator's trade address dataByteBuffer.get(addressBytes); tradeData.qortalCreatorTradeAddress = Base58.encode(addressBytes); @@ -548,12 +559,6 @@ public class BTCACCT { dataByteBuffer.get(tradeData.hashOfSecretB); dataByteBuffer.position(dataByteBuffer.position() + 32 - tradeData.hashOfSecretB.length); // skip to 32 bytes - // Trade timeout - tradeData.tradeTimeout = (int) dataByteBuffer.getLong(); - - // AT refund timeout (probably only useful for debugging) - tradeData.refundTimeout = (int) dataByteBuffer.getLong(); - // Redeem payout tradeData.qortAmount = dataByteBuffer.getLong(); @@ -602,7 +607,7 @@ public class BTCACCT { // Skip message data length dataByteBuffer.position(dataByteBuffer.position() + 8); - /* End of constants */ + /* End of constants / begin variables */ // Skip AT creator's address dataByteBuffer.position(dataByteBuffer.position() + 8 * 4); @@ -612,10 +617,19 @@ public class BTCACCT { String qortalRecipient = Base58.encode(addressBytes); dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length); + // Potential lockTimeA (if in trade mode) + int lockTimeA = (int) dataByteBuffer.getLong(); + + // Potential lockTimeB (if in trade mode) + int lockTimeB = (int) dataByteBuffer.getLong(); + + // AT refund timeout (probably only useful for debugging) + tradeData.refundTimeout = (int) dataByteBuffer.getLong(); + // Trade offer timeout (AT 'timestamp' converted to Qortal block height) long tradeRefundTimestamp = dataByteBuffer.getLong(); - // Last transaction timestamp + // Skip last transaction timestamp dataByteBuffer.position(dataByteBuffer.position() + 8); // Skip block timestamp @@ -654,8 +668,8 @@ public class BTCACCT { tradeData.qortalRecipient = qortalRecipient; tradeData.hashOfSecretA = hashOfSecretA; tradeData.recipientBitcoinPKH = recipientBitcoinPKH; - tradeData.lockTimeA = calcLockTimeA(tradeData.creationTimestamp, tradeData.tradeTimeout); - tradeData.lockTimeB = calcLockTimeB(tradeData.creationTimestamp, tradeData.tradeTimeout); + tradeData.lockTimeA = lockTimeA; + tradeData.lockTimeB = lockTimeB; } else { tradeData.mode = CrossChainTradeData.Mode.OFFER; } @@ -663,14 +677,37 @@ public class BTCACCT { return tradeData; } + /** Returns trade-info MESSAGE payload for trade partner/recipient to send to AT creator's trade address. */ + public static byte[] buildOfferMessage(byte[] recipientBitcoinPKH, byte[] hashOfSecretA, int lockTimeA) { + byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA); + return Bytes.concat(recipientBitcoinPKH, hashOfSecretA, lockTimeABytes); + } + + /** Returns trade-info extracted from MESSAGE payload sent by trade partner/recipient, or null if not valid. */ + public static OfferMessageData extractOfferMessageData(byte[] messageData) { + if (messageData == null || messageData.length != 32 + 32 + 8) + return null; + + OfferMessageData offerMessageData = new OfferMessageData(); + offerMessageData.recipientBitcoinPKH = Arrays.copyOfRange(messageData, 0, 20); + offerMessageData.hashOfSecretA = Arrays.copyOfRange(messageData, 20, 40); + offerMessageData.lockTimeA = BitTwiddling.longFromBEBytes(messageData, 40); + + return offerMessageData; + } + /** Returns trade-info MESSAGE payload for AT creator to send to AT. */ - public static byte[] buildOfferMessage(String recipientQortalAddress, byte[] recipientBitcoinPKH, byte[] hashOfSecretA) { + public static byte[] buildTradeMessage(String recipientQortalAddress, byte[] recipientBitcoinPKH, byte[] hashOfSecretA, int lockTimeA, int lockTimeB) { byte[] data = new byte[32 + 32 + 32]; byte[] recipientQortalAddressBytes = Base58.decode(recipientQortalAddress); + byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA); + byte[] lockTimeBBytes = BitTwiddling.toBEByteArray((long) lockTimeB); System.arraycopy(recipientQortalAddressBytes, 0, data, 0, recipientQortalAddressBytes.length); System.arraycopy(recipientBitcoinPKH, 0, data, 32, recipientBitcoinPKH.length); + System.arraycopy(lockTimeBBytes, 0, data, 56, lockTimeBBytes.length); System.arraycopy(hashOfSecretA, 0, data, 64, hashOfSecretA.length); + System.arraycopy(lockTimeABytes, 0, data, 88, lockTimeABytes.length); return data; } @@ -686,7 +723,7 @@ public class BTCACCT { } /** Returns redeem MESSAGE payload for trade partner/recipient to send to AT. */ - public static byte[] buildTradeMessage(byte[] secretA, byte[] secretB) { + public static byte[] buildRedeemMessage(byte[] secretA, byte[] secretB) { byte[] data = new byte[32 + 32]; System.arraycopy(secretA, 0, data, 0, secretA.length); @@ -695,19 +732,10 @@ public class BTCACCT { return data; } - /** Returns AT refundTimeout (minutes) based on tradeTimeout. */ - public static int calcRefundTimeout(int tradeTimeout) { - return tradeTimeout * 3 / 4; - } - - /** Returns P2SH-A lockTime (epoch seconds). */ - public static int calcLockTimeA(long atCreationTimestamp, int tradeTimeout) { - return (int) (atCreationTimestamp / 1000L + tradeTimeout * 60); - } - - /** Returns P2SH-B lockTime (epoch seconds). */ - public static int calcLockTimeB(long atCreationTimestamp, int tradeTimeout) { - return (int) (atCreationTimestamp / 1000L + tradeTimeout / 2 * 60); + /** Returns P2SH-B lockTime (epoch seconds) based on trade partner/recipient's MESSAGE timestamp and P2SH-A locktime. */ + public static int calcLockTimeB(long recipientMessageTimestamp, int lockTimeA) { + // lockTimeB is halfway between recipientMessageTimesamp and lockTimeA + return (int) ((lockTimeA + (recipientMessageTimestamp / 1000L)) / 2L); } } diff --git a/src/main/java/org/qortal/data/crosschain/CrossChainTradeData.java b/src/main/java/org/qortal/data/crosschain/CrossChainTradeData.java index 11207dd5..1c047c13 100644 --- a/src/main/java/org/qortal/data/crosschain/CrossChainTradeData.java +++ b/src/main/java/org/qortal/data/crosschain/CrossChainTradeData.java @@ -49,9 +49,6 @@ public class CrossChainTradeData { @Schema(description = "Timestamp when AT switched to trade mode") public Long tradeModeTimestamp; - @Schema(description = "General trade timeout (minutes) used to derive P2SH locktimes and AT refund timeout") - public int tradeTimeout; - @Schema(description = "How long from AT creation until AT triggers automatic refund to AT creator (minutes)") public int refundTimeout; diff --git a/src/main/java/org/qortal/data/crosschain/TradeBotData.java b/src/main/java/org/qortal/data/crosschain/TradeBotData.java index 6d5c1fb8..c149ead0 100644 --- a/src/main/java/org/qortal/data/crosschain/TradeBotData.java +++ b/src/main/java/org/qortal/data/crosschain/TradeBotData.java @@ -38,7 +38,6 @@ public class TradeBotData { private State tradeState; private String atAddress; - private int tradeTimeout; private byte[] tradeNativePublicKey; private byte[] tradeNativePublicKeyHash; @@ -53,14 +52,13 @@ public class TradeBotData { private byte[] lastTransactionSignature; - public TradeBotData(byte[] tradePrivateKey, State tradeState, String atAddress, int tradeTimeout, + public TradeBotData(byte[] tradePrivateKey, State tradeState, String atAddress, byte[] tradeNativePublicKey, byte[] tradeNativePublicKeyHash, byte[] secret, byte[] secretHash, byte[] tradeForeignPublicKey, byte[] tradeForeignPublicKeyHash, long bitcoinAmount, byte[] lastTransactionSignature) { this.tradePrivateKey = tradePrivateKey; this.tradeState = tradeState; this.atAddress = atAddress; - this.tradeTimeout = tradeTimeout; this.tradeNativePublicKey = tradeNativePublicKey; this.tradeNativePublicKeyHash = tradeNativePublicKeyHash; this.secret = secret; @@ -91,10 +89,6 @@ public class TradeBotData { this.atAddress = atAddress; } - public int getTradeTimeout() { - return this.tradeTimeout; - } - public byte[] getTradeNativePublicKey() { return this.tradeNativePublicKey; } diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBCrossChainRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBCrossChainRepository.java index f6a302e3..2debbc67 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBCrossChainRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBCrossChainRepository.java @@ -19,7 +19,7 @@ public class HSQLDBCrossChainRepository implements CrossChainRepository { @Override public List getAllTradeBotData() throws DataException { - String sql = "SELECT trade_private_key, trade_state, at_address, trade_timeout, " + String sql = "SELECT trade_private_key, trade_state, at_address, " + "trade_native_public_key, trade_native_public_key_hash, " + "secret, secret_hash, " + "trade_foreign_public_key, trade_foreign_public_key_hash, " @@ -40,18 +40,17 @@ public class HSQLDBCrossChainRepository implements CrossChainRepository { throw new DataException("Illegal trade-bot trade-state fetched from repository"); String atAddress = resultSet.getString(3); - int tradeTimeout = resultSet.getInt(4); - byte[] tradeNativePublicKey = resultSet.getBytes(5); - byte[] tradeNativePublicKeyHash = resultSet.getBytes(6); - byte[] secret = resultSet.getBytes(7); - byte[] secretHash = resultSet.getBytes(8); - byte[] tradeForeignPublicKey = resultSet.getBytes(9); - byte[] tradeForeignPublicKeyHash = resultSet.getBytes(10); - long bitcoinAmount = resultSet.getLong(11); - byte[] lastTransactionSignature = resultSet.getBytes(12); + byte[] tradeNativePublicKey = resultSet.getBytes(4); + byte[] tradeNativePublicKeyHash = resultSet.getBytes(5); + byte[] secret = resultSet.getBytes(6); + byte[] secretHash = resultSet.getBytes(7); + byte[] tradeForeignPublicKey = resultSet.getBytes(8); + byte[] tradeForeignPublicKeyHash = resultSet.getBytes(9); + long bitcoinAmount = resultSet.getLong(10); + byte[] lastTransactionSignature = resultSet.getBytes(11); TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, tradeState, - atAddress, tradeTimeout, + atAddress, tradeNativePublicKey, tradeNativePublicKeyHash, secret, secretHash, tradeForeignPublicKey, tradeForeignPublicKeyHash, bitcoinAmount, lastTransactionSignature); @@ -71,7 +70,6 @@ public class HSQLDBCrossChainRepository implements CrossChainRepository { saveHelper.bind("trade_private_key", tradeBotData.getTradePrivateKey()) .bind("trade_state", tradeBotData.getState().value) .bind("at_address", tradeBotData.getAtAddress()) - .bind("trade_timeout", tradeBotData.getTradeTimeout()) .bind("trade_native_public_key", tradeBotData.getTradeNativePublicKey()) .bind("trade_native_public_key_hash", tradeBotData.getTradeNativePublicKeyHash()) .bind("secret", tradeBotData.getSecret()).bind("secret_hash", tradeBotData.getSecretHash()) diff --git a/src/main/java/org/qortal/utils/BitTwiddling.java b/src/main/java/org/qortal/utils/BitTwiddling.java index 4ba48bc8..eda5b4f6 100644 --- a/src/main/java/org/qortal/utils/BitTwiddling.java +++ b/src/main/java/org/qortal/utils/BitTwiddling.java @@ -26,6 +26,17 @@ public class BitTwiddling { return new byte[] { (byte) (value), (byte) (value >> 8), (byte) (value >> 16), (byte) (value >> 24) }; } + /** Convert int to big-endian byte array */ + public static byte[] toBEByteArray(int value) { + return new byte[] { (byte) (value >> 24), (byte) (value >> 16), (byte) (value >> 8), (byte) (value) }; + } + + /** Convert long to big-endian byte array */ + public static byte[] toBEByteArray(long value) { + return new byte[] { (byte) (value >> 56), (byte) (value >> 48), (byte) (value >> 40), (byte) (value >> 32), + (byte) (value >> 24), (byte) (value >> 16), (byte) (value >> 8), (byte) (value) }; + } + /** Convert little-endian bytes to int */ public static int intFromLEBytes(byte[] bytes, int offset) { return (bytes[offset] & 0xff) | (bytes[offset + 1] & 0xff) << 8 | (bytes[offset + 2] & 0xff) << 16 | (bytes[offset + 3] & 0xff) << 24; diff --git a/src/test/java/org/qortal/test/btcacct/AtTests.java b/src/test/java/org/qortal/test/btcacct/AtTests.java index 79a6edd1..5d5b5f2c 100644 --- a/src/test/java/org/qortal/test/btcacct/AtTests.java +++ b/src/test/java/org/qortal/test/btcacct/AtTests.java @@ -49,7 +49,7 @@ public class AtTests extends Common { public static final byte[] secretB = "This string is roughly 32 bytes?".getBytes(); public static final byte[] hashOfSecretB = Crypto.hash160(secretB); // 31f0dd71decf59bbc8ef0661f4030479255cfa58 public static final byte[] bitcoinPublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes(); - public static final int tradeTimeout = 12; // blocks + public static final int tradeTimeout = 20; // blocks public static final long redeemAmount = 80_40200000L; public static final long fundingAmount = 123_45600000L; public static final long bitcoinAmount = 864200L; @@ -63,7 +63,7 @@ public class AtTests extends Common { public void testCompile() { Account deployer = Common.getTestAccount(null, "chloe"); - byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), bitcoinPublicKeyHash, hashOfSecretB, tradeTimeout, redeemAmount, bitcoinAmount); + byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount); System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString()); } @@ -176,8 +176,12 @@ public class AtTests extends Common { Account at = deployAtTransaction.getATAccount(); String atAddress = at.getAddress(); + long recipientMessageTransactionTimestamp = System.currentTimeMillis(); + int lockTimeA = calcTestLockTimeA(recipientMessageTransactionTimestamp); + int lockTimeB = BTCACCT.calcLockTimeB(recipientMessageTransactionTimestamp, lockTimeA); + // Send trade info to AT - byte[] messageData = BTCACCT.buildOfferMessage(recipient.getAddress(), bitcoinPublicKeyHash, hashOfSecretA); + byte[] messageData = BTCACCT.buildTradeMessage(recipient.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress); Block postDeploymentBlock = BlockUtils.mintBlock(repository); @@ -230,8 +234,12 @@ public class AtTests extends Common { Account at = deployAtTransaction.getATAccount(); String atAddress = at.getAddress(); + long recipientMessageTransactionTimestamp = System.currentTimeMillis(); + int lockTimeA = calcTestLockTimeA(recipientMessageTransactionTimestamp); + int lockTimeB = BTCACCT.calcLockTimeB(recipientMessageTransactionTimestamp, lockTimeA); + // Send trade info to AT BUT NOT FROM AT CREATOR - byte[] messageData = BTCACCT.buildOfferMessage(recipient.getAddress(), bitcoinPublicKeyHash, hashOfSecretA); + byte[] messageData = BTCACCT.buildTradeMessage(recipient.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress); BlockUtils.mintBlock(repository); @@ -265,8 +273,12 @@ public class AtTests extends Common { Account at = deployAtTransaction.getATAccount(); String atAddress = at.getAddress(); + long recipientMessageTransactionTimestamp = System.currentTimeMillis(); + int lockTimeA = calcTestLockTimeA(recipientMessageTransactionTimestamp); + int lockTimeB = BTCACCT.calcLockTimeB(recipientMessageTransactionTimestamp, lockTimeA); + // Send trade info to AT - byte[] messageData = BTCACCT.buildOfferMessage(recipient.getAddress(), bitcoinPublicKeyHash, hashOfSecretA); + byte[] messageData = BTCACCT.buildTradeMessage(recipient.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress); Block postDeploymentBlock = BlockUtils.mintBlock(repository); @@ -308,15 +320,19 @@ public class AtTests extends Common { Account at = deployAtTransaction.getATAccount(); String atAddress = at.getAddress(); + long recipientMessageTransactionTimestamp = System.currentTimeMillis(); + int lockTimeA = calcTestLockTimeA(recipientMessageTransactionTimestamp); + int lockTimeB = BTCACCT.calcLockTimeB(recipientMessageTransactionTimestamp, lockTimeA); + // Send trade info to AT - byte[] messageData = BTCACCT.buildOfferMessage(recipient.getAddress(), bitcoinPublicKeyHash, hashOfSecretA); + byte[] messageData = BTCACCT.buildTradeMessage(recipient.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress); // Give AT time to process message BlockUtils.mintBlock(repository); // Send correct secrets to AT, from correct account - messageData = BTCACCT.buildTradeMessage(secretA, secretB); + messageData = BTCACCT.buildRedeemMessage(secretA, secretB); messageTransaction = sendMessage(repository, recipient, messageData, atAddress); // AT should send funds in the next block @@ -366,15 +382,19 @@ public class AtTests extends Common { Account at = deployAtTransaction.getATAccount(); String atAddress = at.getAddress(); + long recipientMessageTransactionTimestamp = System.currentTimeMillis(); + int lockTimeA = calcTestLockTimeA(recipientMessageTransactionTimestamp); + int lockTimeB = BTCACCT.calcLockTimeB(recipientMessageTransactionTimestamp, lockTimeA); + // Send trade info to AT - byte[] messageData = BTCACCT.buildOfferMessage(recipient.getAddress(), bitcoinPublicKeyHash, hashOfSecretA); + byte[] messageData = BTCACCT.buildTradeMessage(recipient.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress); // Give AT time to process message BlockUtils.mintBlock(repository); // Send correct secrets to AT, but from wrong account - messageData = BTCACCT.buildTradeMessage(secretA, secretB); + messageData = BTCACCT.buildRedeemMessage(secretA, secretB); messageTransaction = sendMessage(repository, bystander, messageData, atAddress); // AT should NOT send funds in the next block @@ -412,8 +432,12 @@ public class AtTests extends Common { Account at = deployAtTransaction.getATAccount(); String atAddress = at.getAddress(); + long recipientMessageTransactionTimestamp = System.currentTimeMillis(); + int lockTimeA = calcTestLockTimeA(recipientMessageTransactionTimestamp); + int lockTimeB = BTCACCT.calcLockTimeB(recipientMessageTransactionTimestamp, lockTimeA); + // Send trade info to AT - byte[] messageData = BTCACCT.buildOfferMessage(recipient.getAddress(), bitcoinPublicKeyHash, hashOfSecretA); + byte[] messageData = BTCACCT.buildTradeMessage(recipient.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress); // Give AT time to process message @@ -423,7 +447,7 @@ public class AtTests extends Common { byte[] wrongSecret = new byte[32]; Random random = new Random(); random.nextBytes(wrongSecret); - messageData = BTCACCT.buildTradeMessage(wrongSecret, secretB); + messageData = BTCACCT.buildRedeemMessage(wrongSecret, secretB); messageTransaction = sendMessage(repository, recipient, messageData, atAddress); // AT should NOT send funds in the next block @@ -442,7 +466,7 @@ public class AtTests extends Common { assertEquals("Recipent's balance incorrect", expectedBalance, actualBalance); // Send incorrect secrets to AT, from correct account - messageData = BTCACCT.buildTradeMessage(secretA, wrongSecret); + messageData = BTCACCT.buildRedeemMessage(secretA, wrongSecret); messageTransaction = sendMessage(repository, recipient, messageData, atAddress); // AT should NOT send funds in the next block @@ -497,8 +521,12 @@ public class AtTests extends Common { } } + private int calcTestLockTimeA(long messageTimestamp) { + return (int) (messageTimestamp / 1000L + tradeTimeout * 60); + } + private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer) throws DataException { - byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), bitcoinPublicKeyHash, hashOfSecretB, tradeTimeout, redeemAmount, bitcoinAmount); + byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount); long txTimestamp = System.currentTimeMillis(); byte[] lastReference = deployer.getLastReference(); @@ -557,7 +585,7 @@ public class AtTests extends Common { private void checkTradeRefund(Repository repository, Account deployer, long deployersInitialBalance, long deployAtFee) throws DataException { long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee; - int refundTimeout = BTCACCT.calcRefundTimeout(tradeTimeout); + int refundTimeout = tradeTimeout * 3 / 4 + 1; // close enough // AT should automatically refund deployer after 'refundTimeout' blocks for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount) @@ -588,7 +616,6 @@ public class AtTests extends Common { + "\tHASH160 of secret-B: %s,\n" + "\tredeem payout: %s QORT,\n" + "\texpected bitcoin: %s BTC,\n" - + "\ttrade timeout: %d minutes (from AT creation),\n" + "\tcurrent block height: %d,\n", tradeData.qortalAtAddress, tradeData.qortalCreator, @@ -598,7 +625,6 @@ public class AtTests extends Common { HashCode.fromBytes(tradeData.hashOfSecretB).toString().substring(0, 40), Amounts.prettyAmount(tradeData.qortAmount), Amounts.prettyAmount(tradeData.expectedBitcoin), - tradeData.tradeTimeout, currentBlockHeight)); // Are we in 'offer' or 'trade' stage? diff --git a/src/test/java/org/qortal/test/btcacct/DeployAT.java b/src/test/java/org/qortal/test/btcacct/DeployAT.java index 0aa0b762..74233e25 100644 --- a/src/test/java/org/qortal/test/btcacct/DeployAT.java +++ b/src/test/java/org/qortal/test/btcacct/DeployAT.java @@ -34,20 +34,19 @@ public class DeployAT { if (error != null) System.err.println(error); - System.err.println(String.format("usage: DeployAT ")); + System.err.println(String.format("usage: DeployAT ")); System.err.println(String.format("example: DeployAT " + "AdTd9SUEYSdTW8mgK3Gu72K97bCHGdUwi2VvLNjUohot \\\n" + "\t80.4020 \\\n" + "\t0.00864200 \\\n" + "\t750b06757a2448b8a4abebaa6e4662833fd5ddbb \\\n" + "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n" - + "\t123.456 \\\n" - + "\t10")); + + "\t123.456")); System.exit(1); } public static void main(String[] args) { - if (args.length != 7) + if (args.length != 6) usage(null); Security.insertProviderAt(new BouncyCastleProvider(), 0); @@ -59,7 +58,6 @@ public class DeployAT { byte[] bitcoinPublicKeyHash = null; byte[] secretHash = null; long fundingAmount = 0; - int tradeTimeout = 0; int argIndex = 0; try { @@ -86,10 +84,6 @@ public class DeployAT { fundingAmount = Long.parseLong(args[argIndex++]); if (fundingAmount <= redeemAmount) usage("AT funding amount must be greater than QORT redeem amount"); - - tradeTimeout = Integer.parseInt(args[argIndex++]); - if (tradeTimeout < 10 || tradeTimeout > 50000) - usage("AT trade timeout should be between 10 and 50,000 minutes"); } catch (IllegalArgumentException e) { usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); } @@ -114,7 +108,7 @@ public class DeployAT { System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(secretHash))); // Deploy AT - byte[] creationBytes = BTCACCT.buildQortalAT(refundAccount.getAddress(), bitcoinPublicKeyHash, secretHash, tradeTimeout, redeemAmount, expectedBitcoin); + byte[] creationBytes = BTCACCT.buildQortalAT(refundAccount.getAddress(), bitcoinPublicKeyHash, secretHash, redeemAmount, expectedBitcoin); System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString()); long txTimestamp = System.currentTimeMillis();