diff --git a/src/main/java/org/qortal/api/model/CrossChainBuildRequest.java b/src/main/java/org/qortal/api/model/CrossChainBuildRequest.java index 834eda6f..76fafc9c 100644 --- a/src/main/java/org/qortal/api/model/CrossChainBuildRequest.java +++ b/src/main/java/org/qortal/api/model/CrossChainBuildRequest.java @@ -12,13 +12,9 @@ public class CrossChainBuildRequest { @Schema(description = "AT creator's public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry") public byte[] creatorPublicKey; - @Schema(description = "Initial QORT amount paid when trade agreed", example = "0.00100000") - @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) - public long initialQortAmount; - @Schema(description = "Final QORT amount paid out on successful trade", example = "80.40200000") @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) - public long finalQortAmount; + public long qortAmount; @Schema(description = "QORT amount funding AT, including covering AT execution fees", example = "123.45670000") @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 9ac04308..405d44b8 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainResource.java @@ -48,6 +48,7 @@ import org.qortal.asset.Asset; import org.qortal.controller.TradeBot; import org.qortal.crosschain.BTC; import org.qortal.crosschain.BTCACCT; +import org.qortal.crosschain.BTCP2SH; import org.qortal.crypto.Crypto; import org.qortal.data.at.ATData; import org.qortal.data.crosschain.CrossChainTradeData; @@ -162,17 +163,14 @@ public class CrossChainResource { if (tradeRequest.tradeTimeout < 10 || tradeRequest.tradeTimeout > 50000) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); - if (tradeRequest.initialQortAmount < 0) - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); - - if (tradeRequest.finalQortAmount <= 0) + if (tradeRequest.qortAmount <= 0) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); if (tradeRequest.fundingQortAmount <= 0) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); // funding amount must exceed initial + final - if (tradeRequest.fundingQortAmount <= tradeRequest.initialQortAmount + tradeRequest.finalQortAmount) + if (tradeRequest.fundingQortAmount <= tradeRequest.qortAmount) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA); if (tradeRequest.bitcoinAmount <= 0) @@ -182,7 +180,7 @@ public class CrossChainResource { PublicKeyAccount creatorAccount = new PublicKeyAccount(repository, creatorPublicKey); byte[] creationBytes = BTCACCT.buildQortalAT(creatorAccount.getAddress(), tradeRequest.bitcoinPublicKeyHash, tradeRequest.secretHash, - tradeRequest.tradeTimeout, tradeRequest.initialQortAmount, tradeRequest.finalQortAmount, tradeRequest.bitcoinAmount); + tradeRequest.tradeTimeout, tradeRequest.qortAmount, tradeRequest.bitcoinAmount); long txTimestamp = NTP.getTime(); byte[] lastReference = creatorAccount.getLastReference(); @@ -434,7 +432,7 @@ public class CrossChainResource { if (crossChainTradeData.mode == Mode.OFFER) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); - byte[] redeemScriptBytes = BTCACCT.buildScript(templateRequest.refundPublicKeyHash, crossChainTradeData.lockTime, templateRequest.redeemPublicKeyHash, crossChainTradeData.secretHash); + byte[] redeemScriptBytes = BTCP2SH.buildScript(templateRequest.refundPublicKeyHash, crossChainTradeData.lockTime, templateRequest.redeemPublicKeyHash, crossChainTradeData.secretHash); byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); @@ -485,7 +483,7 @@ public class CrossChainResource { if (crossChainTradeData.mode == Mode.OFFER) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); - byte[] redeemScriptBytes = BTCACCT.buildScript(templateRequest.refundPublicKeyHash, crossChainTradeData.lockTime, templateRequest.redeemPublicKeyHash, crossChainTradeData.secretHash); + byte[] redeemScriptBytes = BTCP2SH.buildScript(templateRequest.refundPublicKeyHash, crossChainTradeData.lockTime, templateRequest.redeemPublicKeyHash, crossChainTradeData.secretHash); byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); @@ -516,7 +514,7 @@ public class CrossChainResource { if (now >= medianBlockTime * 1000L) { // See if we can extract secret List rawTransactions = BTC.getInstance().getAddressTransactions(p2shStatus.bitcoinP2shAddress); - p2shStatus.secret = BTCACCT.findP2shSecret(p2shStatus.bitcoinP2shAddress, rawTransactions); + p2shStatus.secret = BTCP2SH.findP2shSecret(p2shStatus.bitcoinP2shAddress, rawTransactions); } return p2shStatus; @@ -582,7 +580,7 @@ public class CrossChainResource { if (crossChainTradeData.mode == Mode.OFFER) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); - byte[] redeemScriptBytes = BTCACCT.buildScript(refundKey.getPubKeyHash(), crossChainTradeData.lockTime, refundRequest.redeemPublicKeyHash, crossChainTradeData.secretHash); + byte[] redeemScriptBytes = BTCP2SH.buildScript(refundKey.getPubKeyHash(), crossChainTradeData.lockTime, refundRequest.redeemPublicKeyHash, crossChainTradeData.secretHash); byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); @@ -608,7 +606,7 @@ public class CrossChainResource { Coin refundAmount = Coin.valueOf(p2shBalance - refundRequest.bitcoinMinerFee.unscaledValue().longValue()); - org.bitcoinj.core.Transaction refundTransaction = BTCACCT.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, crossChainTradeData.lockTime); + org.bitcoinj.core.Transaction refundTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, crossChainTradeData.lockTime); boolean wasBroadcast = BTC.getInstance().broadcastTransaction(refundTransaction); if (!wasBroadcast) @@ -680,7 +678,7 @@ public class CrossChainResource { if (crossChainTradeData.mode == Mode.OFFER) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); - byte[] redeemScriptBytes = BTCACCT.buildScript(redeemRequest.refundPublicKeyHash, crossChainTradeData.lockTime, redeemKey.getPubKeyHash(), crossChainTradeData.secretHash); + byte[] redeemScriptBytes = BTCP2SH.buildScript(redeemRequest.refundPublicKeyHash, crossChainTradeData.lockTime, redeemKey.getPubKeyHash(), crossChainTradeData.secretHash); byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); @@ -709,7 +707,7 @@ public class CrossChainResource { Coin redeemAmount = Coin.valueOf(p2shBalance - redeemRequest.bitcoinMinerFee.unscaledValue().longValue()); - org.bitcoinj.core.Transaction redeemTransaction = BTCACCT.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, redeemRequest.secret); + org.bitcoinj.core.Transaction redeemTransaction = BTCP2SH.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, redeemRequest.secret); boolean wasBroadcast = BTC.getInstance().broadcastTransaction(redeemTransaction); if (!wasBroadcast) diff --git a/src/main/java/org/qortal/controller/TradeBot.java b/src/main/java/org/qortal/controller/TradeBot.java index 4965ddae..a398559c 100644 --- a/src/main/java/org/qortal/controller/TradeBot.java +++ b/src/main/java/org/qortal/controller/TradeBot.java @@ -8,17 +8,16 @@ import java.util.Random; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.bitcoinj.core.Address; -import org.bitcoinj.core.Coin; import org.bitcoinj.core.ECKey; import org.bitcoinj.core.LegacyAddress; import org.bitcoinj.core.NetworkParameters; import org.qortal.account.PrivateKeyAccount; import org.qortal.account.PublicKeyAccount; import org.qortal.api.model.TradeBotCreateRequest; -import org.qortal.api.resource.TransactionsResource.ConfirmationStatus; import org.qortal.asset.Asset; import org.qortal.crosschain.BTC; import org.qortal.crosschain.BTCACCT; +import org.qortal.crosschain.BTCP2SH; import org.qortal.crypto.Crypto; import org.qortal.data.at.ATData; import org.qortal.data.crosschain.CrossChainTradeData; @@ -32,8 +31,8 @@ import org.qortal.repository.Repository; import org.qortal.repository.RepositoryManager; import org.qortal.transaction.DeployAtTransaction; import org.qortal.transaction.MessageTransaction; -import org.qortal.transaction.Transaction.TransactionType; import org.qortal.transaction.Transaction.ValidationResult; +import org.qortal.transform.TransformationException; import org.qortal.transform.transaction.DeployAtTransactionTransformer; import org.qortal.utils.NTP; @@ -55,7 +54,7 @@ public class TradeBot { return instance; } - public static byte[] createTrade(Repository repository, TradeBotCreateRequest tradeBotCreateRequest) { + public static byte[] createTrade(Repository repository, TradeBotCreateRequest tradeBotCreateRequest) throws DataException { BTC btc = BTC.getInstance(); NetworkParameters params = btc.getNetworkParameters(); @@ -90,12 +89,18 @@ 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, - tradeForeignPublicKey, tradeForeignPublicKeyHash, atAddress, null); + tradeForeignPublicKey, tradeForeignPublicKeyHash, + tradeBotCreateRequest.bitcoinAmount, null); repository.getCrossChainRepository().save(tradeBotData); // Return to user for signing and broadcast as we don't have their Qortal private key - return DeployAtTransactionTransformer.toBytes(deployAtTransactionData); + try { + return DeployAtTransactionTransformer.toBytes(deployAtTransactionData); + } catch (TransformationException e) { + throw new DataException("Failed to transform DEPLOY_AT transaction?", e); + } } public static String startResponse(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException { @@ -113,12 +118,14 @@ public class TradeBot { byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey); TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, TradeBotData.State.ALICE_WAITING_FOR_P2SH_A, + crossChainTradeData.qortalAtAddress, crossChainTradeData.tradeTimeout, tradeNativePublicKey, tradeNativePublicKeyHash, secret, secretHash, - tradeForeignPublicKey, tradeForeignPublicKeyHash, crossChainTradeData.qortalAtAddress, null); + tradeForeignPublicKey, tradeForeignPublicKeyHash, + crossChainTradeData.expectedBitcoin, null); repository.getCrossChainRepository().save(tradeBotData); // P2SH_a to be funded - byte[] redeemScriptBytes = BTCACCT.buildScript(tradeForeignPublicKeyHash, crossChainTradeData.lockTime, crossChainTradeData.foreignPublicKeyHash, secretHash); + byte[] redeemScriptBytes = BTCP2SH.buildScript(tradeForeignPublicKeyHash, crossChainTradeData.lockTime, crossChainTradeData.foreignPublicKeyHash, secretHash); byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); @@ -176,7 +183,7 @@ public class TradeBot { repository.getCrossChainRepository().save(tradeBotData); } - private void handleBobWaitingForMessage(Repository repository, TradeBotData tradeBotData) { + private void handleBobWaitingForMessage(Repository repository, TradeBotData tradeBotData) throws DataException { // Fetch AT so we can determine trade start timestamp ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress()); if (atData == null) { @@ -220,7 +227,7 @@ public class TradeBot { // Determine P2SH address and confirm funded int lockTime = (int) (tradeStartTimestamp / 1000L + tradeBotData.getTradeTimeout() / 4 * 60); // First P2SH locktime is ΒΌ of timeout period - byte[] redeemScript = BTCACCT.buildScript(aliceForeignPublicKeyHash, lockTime, tradeBotData.getTradeForeignPublicKeyHash(), aliceSecretHash); + byte[] redeemScript = BTCP2SH.buildScript(aliceForeignPublicKeyHash, lockTime, tradeBotData.getTradeForeignPublicKeyHash(), aliceSecretHash); String p2shAddress = BTC.getInstance().deriveP2shAddress(redeemScript); Long balance = BTC.getInstance().getBalance(p2shAddress); diff --git a/src/main/java/org/qortal/crosschain/BTCACCT.java b/src/main/java/org/qortal/crosschain/BTCACCT.java index 758c7b91..a11c3ee6 100644 --- a/src/main/java/org/qortal/crosschain/BTCACCT.java +++ b/src/main/java/org/qortal/crosschain/BTCACCT.java @@ -4,23 +4,7 @@ import static org.ciyam.at.OpCode.calcOffset; import java.nio.ByteBuffer; import java.util.Arrays; -import java.util.List; -import java.util.function.Function; -import org.bitcoinj.core.Address; -import org.bitcoinj.core.Coin; -import org.bitcoinj.core.ECKey; -import org.bitcoinj.core.LegacyAddress; -import org.bitcoinj.core.NetworkParameters; -import org.bitcoinj.core.Transaction; -import org.bitcoinj.core.Transaction.SigHash; -import org.bitcoinj.core.TransactionInput; -import org.bitcoinj.core.TransactionOutput; -import org.bitcoinj.crypto.TransactionSignature; -import org.bitcoinj.script.Script; -import org.bitcoinj.script.ScriptBuilder; -import org.bitcoinj.script.ScriptChunk; -import org.bitcoinj.script.ScriptOpCodes; import org.ciyam.at.API; import org.ciyam.at.CompilationException; import org.ciyam.at.FunctionCode; @@ -40,7 +24,6 @@ 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; @@ -72,164 +55,6 @@ public class BTCACCT { public static final int MIN_LOCKTIME = 1500000000; public static final byte[] CODE_BYTES_HASH = HashCode.fromString("edcdb1feb36e079c5f956faff2f24219b12e5fbaaa05654335e615e33218282f").asBytes(); // SHA256 of AT code bytes - /* - * OP_TUCK (to copy public key to before signature) - * OP_CHECKSIGVERIFY (sig & pubkey must verify or script fails) - * OP_HASH160 (convert public key to PKH) - * OP_DUP (duplicate PKH) - * OP_EQUAL (does PKH match refund PKH?) - * OP_IF - * OP_DROP (no need for duplicate PKH) - * - * OP_CHECKLOCKTIMEVERIFY (if this passes, leftover stack is so script passes) - * OP_ELSE - * OP_EQUALVERIFY (duplicate PKH must match redeem PKH or script fails) - * OP_HASH160 (hash secret) - * OP_EQUAL (do hashes of secrets match? if true, script passes else script fails) - * OP_ENDIF - */ - - private static final byte[] redeemScript1 = HashCode.fromString("7dada97614").asBytes(); // OP_TUCK OP_CHECKSIGVERIFY OP_HASH160 OP_DUP push(0x14 bytes) - private static final byte[] redeemScript2 = HashCode.fromString("87637504").asBytes(); // OP_EQUAL OP_IF OP_DROP push(0x4 bytes) - private static final byte[] redeemScript3 = HashCode.fromString("b16714").asBytes(); // OP_CHECKLOCKTIMEVERIFY OP_ELSE push(0x14 bytes) - private static final byte[] redeemScript4 = HashCode.fromString("88a914").asBytes(); // OP_EQUALVERIFY OP_HASH160 push(0x14 bytes) - private static final byte[] redeemScript5 = HashCode.fromString("8768").asBytes(); // OP_EQUAL OP_ENDIF - - /** - * Returns Bitcoin redeemScript used for cross-chain trading. - *

- * See comments in {@link BTCACCT} for more details. - * - * @param refunderPubKeyHash 20-byte HASH160 of P2SH funder's public key, for refunding purposes - * @param lockTime seconds-since-epoch threshold, after which P2SH funder can claim refund - * @param redeemerPubKeyHash 20-byte HASH160 of P2SH redeemer's public key - * @param secretHash 20-byte HASH160 of secret, used by P2SH redeemer to claim funds - * @return - */ - public static byte[] buildScript(byte[] refunderPubKeyHash, int lockTime, byte[] redeemerPubKeyHash, byte[] secretHash) { - return Bytes.concat(redeemScript1, refunderPubKeyHash, redeemScript2, BitTwiddling.toLEByteArray((int) (lockTime & 0xffffffffL)), - redeemScript3, redeemerPubKeyHash, redeemScript4, secretHash, redeemScript5); - } - - /** - * Builds a custom transaction to spend P2SH. - * - * @param amount output amount, should be total of input amounts, less miner fees - * @param spendKey key for signing transaction, and also where funds are 'sent' (output) - * @param fundingOutput output from transaction that funded P2SH address - * @param redeemScriptBytes the redeemScript itself, in byte[] form - * @param lockTime (optional) transaction nLockTime, used in refund scenario - * @param scriptSigBuilder function for building scriptSig using transaction input signature - * @return Signed Bitcoin transaction for spending P2SH - */ - public static Transaction buildP2shTransaction(Coin amount, ECKey spendKey, List fundingOutputs, byte[] redeemScriptBytes, Long lockTime, Function scriptSigBuilder) { - NetworkParameters params = BTC.getInstance().getNetworkParameters(); - - Transaction transaction = new Transaction(params); - transaction.setVersion(2); - - // Output is back to P2SH funder - transaction.addOutput(amount, ScriptBuilder.createP2PKHOutputScript(spendKey.getPubKeyHash())); - - for (int inputIndex = 0; inputIndex < fundingOutputs.size(); ++inputIndex) { - TransactionOutput fundingOutput = fundingOutputs.get(inputIndex); - - // Input (without scriptSig prior to signing) - TransactionInput input = new TransactionInput(params, null, redeemScriptBytes, fundingOutput.getOutPointFor()); - if (lockTime != null) - input.setSequenceNumber(BTC.LOCKTIME_NO_RBF_SEQUENCE); // Use max-value, so no lockTime and no RBF - else - input.setSequenceNumber(BTC.NO_LOCKTIME_NO_RBF_SEQUENCE); // Use max-value - 1, so lockTime can be used but not RBF - transaction.addInput(input); - } - - // Set locktime after inputs added but before input signatures are generated - if (lockTime != null) - transaction.setLockTime(lockTime); - - for (int inputIndex = 0; inputIndex < fundingOutputs.size(); ++inputIndex) { - // Generate transaction signature for input - final boolean anyoneCanPay = false; - TransactionSignature txSig = transaction.calculateSignature(inputIndex, spendKey, redeemScriptBytes, SigHash.ALL, anyoneCanPay); - - // Calculate transaction signature - byte[] txSigBytes = txSig.encodeToBitcoin(); - - // Build scriptSig using lambda and tx signature - Script scriptSig = scriptSigBuilder.apply(txSigBytes); - - // Set input scriptSig - transaction.getInput(inputIndex).setScriptSig(scriptSig); - } - - return transaction; - } - - /** - * Returns signed Bitcoin transaction claiming refund from P2SH address. - * - * @param refundAmount refund amount, should be total of input amounts, less miner fees - * @param refundKey key for signing transaction, and also where refund is 'sent' (output) - * @param fundingOutput output from transaction that funded P2SH address - * @param redeemScriptBytes the redeemScript itself, in byte[] form - * @param lockTime transaction nLockTime - must be at least locktime used in redeemScript - * @return Signed Bitcoin transaction for refunding P2SH - */ - public static Transaction buildRefundTransaction(Coin refundAmount, ECKey refundKey, List fundingOutputs, byte[] redeemScriptBytes, long lockTime) { - Function refundSigScriptBuilder = (txSigBytes) -> { - // Build scriptSig with... - ScriptBuilder scriptBuilder = new ScriptBuilder(); - - // transaction signature - scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes)); - - // redeem public key - byte[] refundPubKey = refundKey.getPubKey(); - scriptBuilder.addChunk(new ScriptChunk(refundPubKey.length, refundPubKey)); - - // redeem script - scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes)); - - return scriptBuilder.build(); - }; - - return buildP2shTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime, refundSigScriptBuilder); - } - - /** - * Returns signed Bitcoin transaction redeeming funds from P2SH address. - * - * @param redeemAmount redeem amount, should be total of input amounts, less miner fees - * @param redeemKey key for signing transaction, and also where funds are 'sent' (output) - * @param fundingOutput output from transaction that funded P2SH address - * @param redeemScriptBytes the redeemScript itself, in byte[] form - * @param secret actual 32-byte secret used when building redeemScript - * @return Signed Bitcoin transaction for redeeming P2SH - */ - public static Transaction buildRedeemTransaction(Coin redeemAmount, ECKey redeemKey, List fundingOutputs, byte[] redeemScriptBytes, byte[] secret) { - Function redeemSigScriptBuilder = (txSigBytes) -> { - // Build scriptSig with... - ScriptBuilder scriptBuilder = new ScriptBuilder(); - - // secret - scriptBuilder.addChunk(new ScriptChunk(secret.length, secret)); - - // transaction signature - scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes)); - - // redeem public key - byte[] redeemPubKey = redeemKey.getPubKey(); - scriptBuilder.addChunk(new ScriptChunk(redeemPubKey.length, redeemPubKey)); - - // redeem script - scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes)); - - return scriptBuilder.build(); - }; - - return buildP2shTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, null, redeemSigScriptBuilder); - } - /** * Returns Qortal AT creation bytes for cross-chain trading AT. *

@@ -240,12 +65,11 @@ public class BTCACCT { * @param bitcoinPublicKeyHash 20-byte HASH160 of creator's bitcoin public key * @param secretHash 20-byte HASH160 of 32-byte secret * @param tradeTimeout how many minutes, from start of 'trade mode' until AT auto-refunds AT creator - * @param initialPayout how much QORT to pay trade partner upon switch to 'trade mode' - * @param redeemPayout how much QORT to pay trade partner if they send correct 32-byte secret to AT + * @param qortAmount how much QORT to pay trade partner if they send correct 32-byte secret to AT * @param bitcoinAmount how much BTC the AT creator is expecting to trade * @return */ - public static byte[] buildQortalAT(String qortalCreator, byte[] bitcoinPublicKeyHash, byte[] secretHash, int tradeTimeout, long initialPayout, long redeemPayout, long bitcoinAmount) { + public static byte[] buildQortalAT(String qortalCreator, byte[] bitcoinPublicKeyHash, byte[] secretHash, int tradeTimeout, long qortAmount, long bitcoinAmount) { // Labels for data segment addresses int addrCounter = 0; @@ -263,8 +87,7 @@ public class BTCACCT { addrCounter += 4; final int addrTradeTimeout = addrCounter++; - final int addrInitialPayoutAmount = addrCounter++; - final int addrRedeemPayoutAmount = addrCounter++; + final int addrQortAmount = addrCounter++; final int addrBitcoinAmount = addrCounter++; final int addrMessageTxType = addrCounter++; @@ -319,13 +142,9 @@ public class BTCACCT { assert dataByteBuffer.position() == addrTradeTimeout * MachineState.VALUE_SIZE : "addrTradeTimeout incorrect"; dataByteBuffer.putLong(tradeTimeout); - // Initial payout amount - assert dataByteBuffer.position() == addrInitialPayoutAmount * MachineState.VALUE_SIZE : "addrInitialPayoutAmount incorrect"; - dataByteBuffer.putLong(initialPayout); - - // Redeem payout amount - assert dataByteBuffer.position() == addrRedeemPayoutAmount * MachineState.VALUE_SIZE : "addrRedeemPayoutAmount incorrect"; - dataByteBuffer.putLong(redeemPayout); + // Redeem Qort amount + assert dataByteBuffer.position() == addrQortAmount * MachineState.VALUE_SIZE : "addrQortAmount incorrect"; + dataByteBuffer.putLong(qortAmount); // Expected Bitcoin amount assert dataByteBuffer.position() == addrBitcoinAmount * MachineState.VALUE_SIZE : "addrBitcoinAmount incorrect"; @@ -433,9 +252,6 @@ public class BTCACCT { /* Switch to 'trade mode' */ labelTradeMode = codeByteBuffer.position(); - // Send initial payment to recipient so they have enough funds to message AT if all goes well - codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PAY_TO_ADDRESS_IN_B, addrInitialPayoutAmount)); - // Calculate trade timeout refund 'timestamp' by adding addrTradeTimeout minutes to above message's 'timestamp', then save into addrTradeRefundTimestamp codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.ADD_MINUTES_TO_TIMESTAMP, addrTradeRefundTimestamp, addrLastTxTimestamp, addrTradeTimeout)); @@ -504,7 +320,7 @@ public class BTCACCT { // Load B register with intended recipient address (as pointed to by addrQortalRecipientPointer) codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrQortalRecipientPointer)); // Pay AT's balance to recipient - codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PAY_TO_ADDRESS_IN_B, addrRedeemPayoutAmount)); + codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PAY_TO_ADDRESS_IN_B, addrQortAmount)); // Fall-through to refunding any remaining balance back to AT creator /* Refund balance back to AT creator */ @@ -578,13 +394,10 @@ public class BTCACCT { dataByteBuffer.position(dataByteBuffer.position() + 32 - 20); // skip to 32 bytes // Trade timeout - tradeData.tradeRefundTimeout = dataByteBuffer.getLong(); - - // Initial payout - tradeData.initialPayout = dataByteBuffer.getLong(); + tradeData.tradeTimeout = (int) dataByteBuffer.getLong(); // Redeem payout - tradeData.redeemPayout = dataByteBuffer.getLong(); + tradeData.qortAmount = dataByteBuffer.getLong(); // Expected BTC amount tradeData.expectedBitcoin = dataByteBuffer.getLong(); @@ -623,12 +436,12 @@ public class BTCACCT { // We'll suggest half of trade timeout CiyamAtSettings ciyamAtSettings = BlockChain.getInstance().getCiyamAtSettings(); - int tradeModeSwitchHeight = (int) (tradeData.tradeRefundHeight - tradeData.tradeRefundTimeout / ciyamAtSettings.minutesPerBlock); + int tradeModeSwitchHeight = (int) (tradeData.tradeRefundHeight - tradeData.tradeTimeout / ciyamAtSettings.minutesPerBlock); BlockData blockData = repository.getBlockRepository().fromHeight(tradeModeSwitchHeight); if (blockData != null) { tradeData.tradeModeTimestamp = blockData.getTimestamp(); // NOTE: milliseconds from epoch - tradeData.lockTime = (int) (tradeData.tradeModeTimestamp / 1000L + tradeData.tradeRefundTimeout / 2 * 60); + tradeData.lockTime = (int) (tradeData.tradeModeTimestamp / 1000L + tradeData.tradeTimeout / 2 * 60); } } else { tradeData.mode = CrossChainTradeData.Mode.OFFER; @@ -637,46 +450,4 @@ public class BTCACCT { return tradeData; } - public static byte[] findP2shSecret(String p2shAddress, List rawTransactions) { - NetworkParameters params = BTC.getInstance().getNetworkParameters(); - - for (byte[] rawTransaction : rawTransactions) { - Transaction transaction = new Transaction(params, rawTransaction); - - // Cycle through inputs, looking for one that spends our P2SH - for (TransactionInput input : transaction.getInputs()) { - Script scriptSig = input.getScriptSig(); - List scriptChunks = scriptSig.getChunks(); - - // Expected number of script chunks for redeem. Refund might not have the same number. - int expectedChunkCount = 1 /*secret*/ + 1 /*sig*/ + 1 /*pubkey*/ + 1 /*redeemScript*/; - if (scriptChunks.size() != expectedChunkCount) - continue; - - // We're expecting last chunk to contain the actual redeemScript - ScriptChunk lastChunk = scriptChunks.get(scriptChunks.size() - 1); - byte[] redeemScriptBytes = lastChunk.data; - - // If non-push scripts, redeemScript will be null - if (redeemScriptBytes == null) - continue; - - byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); - Address inputAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); - - if (!inputAddress.toString().equals(p2shAddress)) - // Input isn't spending our P2SH - continue; - - byte[] secret = scriptChunks.get(0).data; - if (secret.length != BTCACCT.SECRET_LENGTH) - continue; - - return secret; - } - } - - return null; - } - } diff --git a/src/main/java/org/qortal/crosschain/BTCP2SH.java b/src/main/java/org/qortal/crosschain/BTCP2SH.java new file mode 100644 index 00000000..90e77710 --- /dev/null +++ b/src/main/java/org/qortal/crosschain/BTCP2SH.java @@ -0,0 +1,232 @@ +package org.qortal.crosschain; + +import java.util.List; +import java.util.function.Function; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.LegacyAddress; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.Transaction.SigHash; +import org.bitcoinj.core.TransactionInput; +import org.bitcoinj.core.TransactionOutput; +import org.bitcoinj.crypto.TransactionSignature; +import org.bitcoinj.script.Script; +import org.bitcoinj.script.ScriptBuilder; +import org.bitcoinj.script.ScriptChunk; +import org.bitcoinj.script.ScriptOpCodes; +import org.qortal.crypto.Crypto; +import org.qortal.utils.BitTwiddling; + +import com.google.common.hash.HashCode; +import com.google.common.primitives.Bytes; + +public class BTCP2SH { + + public static final int SECRET_LENGTH = 32; + public static final int MIN_LOCKTIME = 1500000000; + + /* + * OP_TUCK (to copy public key to before signature) + * OP_CHECKSIGVERIFY (sig & pubkey must verify or script fails) + * OP_HASH160 (convert public key to PKH) + * OP_DUP (duplicate PKH) + * OP_EQUAL (does PKH match refund PKH?) + * OP_IF + * OP_DROP (no need for duplicate PKH) + * + * OP_CHECKLOCKTIMEVERIFY (if this passes, leftover stack is so script passes) + * OP_ELSE + * OP_EQUALVERIFY (duplicate PKH must match redeem PKH or script fails) + * OP_HASH160 (hash secret) + * OP_EQUAL (do hashes of secrets match? if true, script passes else script fails) + * OP_ENDIF + */ + + private static final byte[] redeemScript1 = HashCode.fromString("7dada97614").asBytes(); // OP_TUCK OP_CHECKSIGVERIFY OP_HASH160 OP_DUP push(0x14 bytes) + private static final byte[] redeemScript2 = HashCode.fromString("87637504").asBytes(); // OP_EQUAL OP_IF OP_DROP push(0x4 bytes) + private static final byte[] redeemScript3 = HashCode.fromString("b16714").asBytes(); // OP_CHECKLOCKTIMEVERIFY OP_ELSE push(0x14 bytes) + private static final byte[] redeemScript4 = HashCode.fromString("88a914").asBytes(); // OP_EQUALVERIFY OP_HASH160 push(0x14 bytes) + private static final byte[] redeemScript5 = HashCode.fromString("8768").asBytes(); // OP_EQUAL OP_ENDIF + + /** + * Returns Bitcoin redeemScript used for cross-chain trading. + *

+ * See comments in {@link BTCP2SH} for more details. + * + * @param refunderPubKeyHash 20-byte HASH160 of P2SH funder's public key, for refunding purposes + * @param lockTime seconds-since-epoch threshold, after which P2SH funder can claim refund + * @param redeemerPubKeyHash 20-byte HASH160 of P2SH redeemer's public key + * @param secretHash 20-byte HASH160 of secret, used by P2SH redeemer to claim funds + * @return + */ + public static byte[] buildScript(byte[] refunderPubKeyHash, int lockTime, byte[] redeemerPubKeyHash, byte[] secretHash) { + return Bytes.concat(redeemScript1, refunderPubKeyHash, redeemScript2, BitTwiddling.toLEByteArray((int) (lockTime & 0xffffffffL)), + redeemScript3, redeemerPubKeyHash, redeemScript4, secretHash, redeemScript5); + } + + /** + * Builds a custom transaction to spend P2SH. + * + * @param amount output amount, should be total of input amounts, less miner fees + * @param spendKey key for signing transaction, and also where funds are 'sent' (output) + * @param fundingOutput output from transaction that funded P2SH address + * @param redeemScriptBytes the redeemScript itself, in byte[] form + * @param lockTime (optional) transaction nLockTime, used in refund scenario + * @param scriptSigBuilder function for building scriptSig using transaction input signature + * @return Signed Bitcoin transaction for spending P2SH + */ + public static Transaction buildP2shTransaction(Coin amount, ECKey spendKey, List fundingOutputs, byte[] redeemScriptBytes, Long lockTime, Function scriptSigBuilder) { + NetworkParameters params = BTC.getInstance().getNetworkParameters(); + + Transaction transaction = new Transaction(params); + transaction.setVersion(2); + + // Output is back to P2SH funder + transaction.addOutput(amount, ScriptBuilder.createP2PKHOutputScript(spendKey.getPubKeyHash())); + + for (int inputIndex = 0; inputIndex < fundingOutputs.size(); ++inputIndex) { + TransactionOutput fundingOutput = fundingOutputs.get(inputIndex); + + // Input (without scriptSig prior to signing) + TransactionInput input = new TransactionInput(params, null, redeemScriptBytes, fundingOutput.getOutPointFor()); + if (lockTime != null) + input.setSequenceNumber(BTC.LOCKTIME_NO_RBF_SEQUENCE); // Use max-value, so no lockTime and no RBF + else + input.setSequenceNumber(BTC.NO_LOCKTIME_NO_RBF_SEQUENCE); // Use max-value - 1, so lockTime can be used but not RBF + transaction.addInput(input); + } + + // Set locktime after inputs added but before input signatures are generated + if (lockTime != null) + transaction.setLockTime(lockTime); + + for (int inputIndex = 0; inputIndex < fundingOutputs.size(); ++inputIndex) { + // Generate transaction signature for input + final boolean anyoneCanPay = false; + TransactionSignature txSig = transaction.calculateSignature(inputIndex, spendKey, redeemScriptBytes, SigHash.ALL, anyoneCanPay); + + // Calculate transaction signature + byte[] txSigBytes = txSig.encodeToBitcoin(); + + // Build scriptSig using lambda and tx signature + Script scriptSig = scriptSigBuilder.apply(txSigBytes); + + // Set input scriptSig + transaction.getInput(inputIndex).setScriptSig(scriptSig); + } + + return transaction; + } + + /** + * Returns signed Bitcoin transaction claiming refund from P2SH address. + * + * @param refundAmount refund amount, should be total of input amounts, less miner fees + * @param refundKey key for signing transaction, and also where refund is 'sent' (output) + * @param fundingOutput output from transaction that funded P2SH address + * @param redeemScriptBytes the redeemScript itself, in byte[] form + * @param lockTime transaction nLockTime - must be at least locktime used in redeemScript + * @return Signed Bitcoin transaction for refunding P2SH + */ + public static Transaction buildRefundTransaction(Coin refundAmount, ECKey refundKey, List fundingOutputs, byte[] redeemScriptBytes, long lockTime) { + Function refundSigScriptBuilder = (txSigBytes) -> { + // Build scriptSig with... + ScriptBuilder scriptBuilder = new ScriptBuilder(); + + // transaction signature + scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes)); + + // redeem public key + byte[] refundPubKey = refundKey.getPubKey(); + scriptBuilder.addChunk(new ScriptChunk(refundPubKey.length, refundPubKey)); + + // redeem script + scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes)); + + return scriptBuilder.build(); + }; + + return buildP2shTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime, refundSigScriptBuilder); + } + + /** + * Returns signed Bitcoin transaction redeeming funds from P2SH address. + * + * @param redeemAmount redeem amount, should be total of input amounts, less miner fees + * @param redeemKey key for signing transaction, and also where funds are 'sent' (output) + * @param fundingOutput output from transaction that funded P2SH address + * @param redeemScriptBytes the redeemScript itself, in byte[] form + * @param secret actual 32-byte secret used when building redeemScript + * @return Signed Bitcoin transaction for redeeming P2SH + */ + public static Transaction buildRedeemTransaction(Coin redeemAmount, ECKey redeemKey, List fundingOutputs, byte[] redeemScriptBytes, byte[] secret) { + Function redeemSigScriptBuilder = (txSigBytes) -> { + // Build scriptSig with... + ScriptBuilder scriptBuilder = new ScriptBuilder(); + + // secret + scriptBuilder.addChunk(new ScriptChunk(secret.length, secret)); + + // transaction signature + scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes)); + + // redeem public key + byte[] redeemPubKey = redeemKey.getPubKey(); + scriptBuilder.addChunk(new ScriptChunk(redeemPubKey.length, redeemPubKey)); + + // redeem script + scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes)); + + return scriptBuilder.build(); + }; + + return buildP2shTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, null, redeemSigScriptBuilder); + } + + /** Returns 'secret', if any, given list of raw bitcoin transactions. */ + public static byte[] findP2shSecret(String p2shAddress, List rawTransactions) { + NetworkParameters params = BTC.getInstance().getNetworkParameters(); + + for (byte[] rawTransaction : rawTransactions) { + Transaction transaction = new Transaction(params, rawTransaction); + + // Cycle through inputs, looking for one that spends our P2SH + for (TransactionInput input : transaction.getInputs()) { + Script scriptSig = input.getScriptSig(); + List scriptChunks = scriptSig.getChunks(); + + // Expected number of script chunks for redeem. Refund might not have the same number. + int expectedChunkCount = 1 /*secret*/ + 1 /*sig*/ + 1 /*pubkey*/ + 1 /*redeemScript*/; + if (scriptChunks.size() != expectedChunkCount) + continue; + + // We're expecting last chunk to contain the actual redeemScript + ScriptChunk lastChunk = scriptChunks.get(scriptChunks.size() - 1); + byte[] redeemScriptBytes = lastChunk.data; + + // If non-push scripts, redeemScript will be null + if (redeemScriptBytes == null) + continue; + + byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); + Address inputAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); + + if (!inputAddress.toString().equals(p2shAddress)) + // Input isn't spending our P2SH + continue; + + byte[] secret = scriptChunks.get(0).data; + if (secret.length != BTCP2SH.SECRET_LENGTH) + continue; + + return secret; + } + } + + return null; + } + +} diff --git a/src/main/java/org/qortal/data/crosschain/CrossChainTradeData.java b/src/main/java/org/qortal/data/crosschain/CrossChainTradeData.java index a19ef81d..e994df14 100644 --- a/src/main/java/org/qortal/data/crosschain/CrossChainTradeData.java +++ b/src/main/java/org/qortal/data/crosschain/CrossChainTradeData.java @@ -30,13 +30,9 @@ public class CrossChainTradeData { @Schema(description = "HASH160 of 32-byte secret") public byte[] secretHash; - @Schema(description = "Initial QORT payment that will be sent to Qortal trade partner") - @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) - public long initialPayout; - @Schema(description = "Final QORT payment that will be sent to Qortal trade partner") @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) - public long redeemPayout; + public long qortAmount; @Schema(description = "Trade partner's Qortal address (trade begins when this is set)") public String qortalRecipient; @@ -45,7 +41,7 @@ public class CrossChainTradeData { public Long tradeModeTimestamp; @Schema(description = "How long from beginning trade until AT triggers automatic refund to AT creator (minutes)") - public long tradeRefundTimeout; + public int tradeTimeout; @Schema(description = "Actual Qortal block height when AT will automatically refund to AT creator (after trade begins)") public Integer tradeRefundHeight; diff --git a/src/main/java/org/qortal/data/crosschain/TradeBotData.java b/src/main/java/org/qortal/data/crosschain/TradeBotData.java index 3c8b4f63..6d5c1fb8 100644 --- a/src/main/java/org/qortal/data/crosschain/TradeBotData.java +++ b/src/main/java/org/qortal/data/crosschain/TradeBotData.java @@ -15,6 +15,11 @@ import io.swagger.v3.oas.annotations.media.Schema; @XmlAccessorType(XmlAccessType.FIELD) public class TradeBotData { + // Never expose this + @XmlTransient + @Schema(hidden = true) + private byte[] tradePrivateKey; + public enum State { BOB_WAITING_FOR_AT_CONFIRM(10), BOB_WAITING_FOR_MESSAGE(20), BOB_SENDING_MESSAGE_TO_AT(30), BOB_WAITING_FOR_P2SH_B(40), BOB_WAITING_FOR_AT_REDEEM(50), ALICE_WAITING_FOR_P2SH_A(110), ALICE_WAITING_FOR_AT_LOCK(120), ALICE_WATCH_P2SH_B(130); @@ -30,13 +35,10 @@ public class TradeBotData { return map.get(value); } } - private State tradeState; - // Never expose this - @XmlTransient - @Schema(hidden = true) - private byte[] tradePrivateKey; + private String atAddress; + private int tradeTimeout; private byte[] tradeNativePublicKey; private byte[] tradeNativePublicKeyHash; @@ -47,23 +49,25 @@ public class TradeBotData { private byte[] tradeForeignPublicKey; private byte[] tradeForeignPublicKeyHash; - private String atAddress; + private long bitcoinAmount; private byte[] lastTransactionSignature; - public TradeBotData(byte[] tradePrivateKey, State tradeState, - byte[] tradeNativePublicKey, byte[] tradeNativePublicKeyHash, byte[] secret, byte[] secretHash, - byte[] tradeForeignPublicKey, byte[] tradeForeignPublicKeyHash, String atAddress, - byte[] lastTransactionSignature) { + public TradeBotData(byte[] tradePrivateKey, State tradeState, String atAddress, int tradeTimeout, + 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; this.secretHash = secretHash; this.tradeForeignPublicKey = tradeForeignPublicKey; this.tradeForeignPublicKeyHash = tradeForeignPublicKeyHash; - this.atAddress = atAddress; + this.bitcoinAmount = bitcoinAmount; this.lastTransactionSignature = lastTransactionSignature; } @@ -79,6 +83,18 @@ public class TradeBotData { this.tradeState = state; } + public String getAtAddress() { + return this.atAddress; + } + + public void setAtAddress(String atAddress) { + this.atAddress = atAddress; + } + + public int getTradeTimeout() { + return this.tradeTimeout; + } + public byte[] getTradeNativePublicKey() { return this.tradeNativePublicKey; } @@ -103,12 +119,8 @@ public class TradeBotData { return this.tradeForeignPublicKeyHash; } - public String getAtAddress() { - return this.atAddress; - } - - public void setAtAddress(String atAddress) { - this.atAddress = atAddress; + public long getBitcoinAmount() { + return this.bitcoinAmount; } public byte[] getLastTransactionSignature() { diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBCrossChainRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBCrossChainRepository.java index 4d91dd6e..f6a302e3 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBCrossChainRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBCrossChainRepository.java @@ -19,8 +19,11 @@ public class HSQLDBCrossChainRepository implements CrossChainRepository { @Override public List getAllTradeBotData() throws DataException { - String sql = "SELECT trade_private_key, trade_state, trade_native_public_key, trade_native_public_key_hash, " - + "secret, secret_hash, trade_foreign_public_key, trade_foreign_public_key_hash, at_address, last_transaction_signature " + String sql = "SELECT trade_private_key, trade_state, at_address, trade_timeout, " + + "trade_native_public_key, trade_native_public_key_hash, " + + "secret, secret_hash, " + + "trade_foreign_public_key, trade_foreign_public_key_hash, " + + "bitcoin_amount, last_transaction_signature " + "FROM TradeBotStates"; List allTradeBotData = new ArrayList<>(); @@ -36,18 +39,22 @@ public class HSQLDBCrossChainRepository implements CrossChainRepository { if (tradeState == null) throw new DataException("Illegal trade-bot trade-state fetched from repository"); - byte[] tradeNativePublicKey = resultSet.getBytes(3); - byte[] tradeNativePublicKeyHash = resultSet.getBytes(4); - byte[] secret = resultSet.getBytes(5); - byte[] secretHash = resultSet.getBytes(6); - byte[] tradeForeignPublicKey = resultSet.getBytes(7); - byte[] tradeForeignPublicKeyHash = resultSet.getBytes(8); - String atAddress = resultSet.getString(9); - byte[] lastTransactionSignature = resultSet.getBytes(10); + 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); TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, tradeState, + atAddress, tradeTimeout, tradeNativePublicKey, tradeNativePublicKeyHash, secret, secretHash, - tradeForeignPublicKey, tradeForeignPublicKeyHash, atAddress, lastTransactionSignature); + tradeForeignPublicKey, tradeForeignPublicKeyHash, + bitcoinAmount, lastTransactionSignature); allTradeBotData.add(tradeBotData); } while (resultSet.next()); @@ -63,12 +70,14 @@ 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()) .bind("trade_foreign_public_key", tradeBotData.getTradeForeignPublicKey()) .bind("trade_foreign_public_key_hash", tradeBotData.getTradeForeignPublicKeyHash()) - .bind("at_address", tradeBotData.getAtAddress()) + .bind("bitcoin_amount", tradeBotData.getBitcoinAmount()) .bind("last_transaction_signature", tradeBotData.getLastTransactionSignature()); try { diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java index 54f8f3c3..54a816ab 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -621,11 +621,11 @@ public class HSQLDBDatabaseUpdates { case 20: // Trade bot stmt.execute("CREATE TABLE TradeBotStates (trade_private_key QortalKeySeed NOT NULL, trade_state TINYINT NOT NULL, " + + "at_address QortalAddress, trade_timeout INT NOT NULL, " + "trade_native_public_key QortalPublicKey NOT NULL, trade_native_public_key_hash VARBINARY(32) NOT NULL, " + "secret VARBINARY(32) NOT NULL, secret_hash VARBINARY(32) NOT NULL, " + "trade_foreign_public_key QortalPublicKey NOT NULL, trade_foreign_public_key_hash VARBINARY(32) NOT NULL, " - + "at_address QortalAddress, " - + "last_transaction_signature Signature, PRIMARY KEY (trade_private_key))"); + + "bitcoin_amount BIGINT NOT NULL, last_transaction_signature Signature, PRIMARY KEY (trade_private_key))"); break; default: diff --git a/src/test/java/org/qortal/test/btcacct/AtTests.java b/src/test/java/org/qortal/test/btcacct/AtTests.java index 2026424f..7c6d10ec 100644 --- a/src/test/java/org/qortal/test/btcacct/AtTests.java +++ b/src/test/java/org/qortal/test/btcacct/AtTests.java @@ -47,7 +47,6 @@ public class AtTests extends Common { public static final byte[] bitcoinPublicKeyHash = new byte[20]; // not used in tests public static final byte[] secretHash = Crypto.hash160(secret); // daf59884b4d1aec8c1b17102530909ee43c0151a public static final int refundTimeout = 10; // blocks - public static final long initialPayout = 100000L; public static final long redeemAmount = 80_40200000L; public static final long fundingAmount = 123_45600000L; public static final long bitcoinAmount = 864200L; @@ -61,7 +60,7 @@ public class AtTests extends Common { public void testCompile() { Account deployer = Common.getTestAccount(null, "chloe"); - byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), bitcoinPublicKeyHash, secretHash, refundTimeout, initialPayout, redeemAmount, bitcoinAmount); + byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), bitcoinPublicKeyHash, secretHash, refundTimeout, redeemAmount, bitcoinAmount); System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString()); } @@ -156,44 +155,6 @@ public class AtTests extends Common { } } - @SuppressWarnings("unused") - @Test - public void testInitialPayment() throws DataException { - try (final Repository repository = RepositoryManager.getRepository()) { - PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); - PrivateKeyAccount recipient = Common.getTestAccount(repository, "dilbert"); - - long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); - long recipientsInitialBalance = recipient.getConfirmedBalance(Asset.QORT); - - DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer); - Account at = deployAtTransaction.getATAccount(); - String atAddress = at.getAddress(); - - // Send recipient's address to AT - byte[] recipientAddressBytes = Bytes.ensureCapacity(Base58.decode(recipient.getAddress()), 32, 0); - MessageTransaction messageTransaction = sendMessage(repository, deployer, recipientAddressBytes, atAddress); - - // Initial payment should happen 1st block after receiving recipient address - BlockUtils.mintBlock(repository); - - long expectedBalance = recipientsInitialBalance + initialPayout; - long actualBalance = recipient.getConfirmedBalance(Asset.QORT); - - assertEquals("Recipient's post-initial-payout balance incorrect", expectedBalance, actualBalance); - - describeAt(repository, atAddress); - - // Test orphaning - BlockUtils.orphanLastBlock(repository); - - expectedBalance = recipientsInitialBalance; - actualBalance = recipient.getConfirmedBalance(Asset.QORT); - - assertEquals("Recipient's pre-initial-payout balance incorrect", expectedBalance, actualBalance); - } - } - // TEST SENDING RECIPIENT ADDRESS BUT NOT FROM AT CREATOR (SHOULD BE IGNORED) @SuppressWarnings("unused") @Test @@ -294,7 +255,7 @@ public class AtTests extends Common { ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress); BlockUtils.mintBlock(repository); - long expectedBalance = recipientsInitialBalance + initialPayout - messageTransaction.getTransactionData().getFee() + redeemAmount; + long expectedBalance = recipientsInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount; long actualBalance = recipient.getConfirmedBalance(Asset.QORT); assertEquals("Recipent's post-redeem balance incorrect", expectedBalance, actualBalance); @@ -304,7 +265,7 @@ public class AtTests extends Common { // Orphan redeem BlockUtils.orphanLastBlock(repository); - expectedBalance = recipientsInitialBalance + initialPayout - messageTransaction.getTransactionData().getFee(); + expectedBalance = recipientsInitialBalance - messageTransaction.getTransactionData().getFee(); actualBalance = recipient.getConfirmedBalance(Asset.QORT); assertEquals("Recipent's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance); @@ -347,7 +308,7 @@ public class AtTests extends Common { ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress); BlockUtils.mintBlock(repository); - long expectedBalance = recipientsInitialBalance + initialPayout; + long expectedBalance = recipientsInitialBalance; long actualBalance = recipient.getConfirmedBalance(Asset.QORT); assertEquals("Recipent's balance incorrect", expectedBalance, actualBalance); @@ -389,7 +350,7 @@ public class AtTests extends Common { ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress); BlockUtils.mintBlock(repository); - long expectedBalance = recipientsInitialBalance + initialPayout - messageTransaction.getTransactionData().getFee(); + long expectedBalance = recipientsInitialBalance - messageTransaction.getTransactionData().getFee(); long actualBalance = recipient.getConfirmedBalance(Asset.QORT); assertEquals("Recipent's balance incorrect", expectedBalance, actualBalance); @@ -435,7 +396,7 @@ public class AtTests extends Common { } private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer) throws DataException { - byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), bitcoinPublicKeyHash, secretHash, refundTimeout, initialPayout, redeemAmount, bitcoinAmount); + byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), bitcoinPublicKeyHash, secretHash, refundTimeout, redeemAmount, bitcoinAmount); long txTimestamp = System.currentTimeMillis(); byte[] lastReference = deployer.getLastReference(); @@ -501,7 +462,7 @@ public class AtTests extends Common { // We don't bother to exactly calculate QORT spent running AT for several blocks, but we do know the expected range long expectedMinimumBalance = deployersPostDeploymentBalance; - long expectedMaximumBalance = deployersInitialBalance - deployAtFee - initialPayout; + long expectedMaximumBalance = deployersInitialBalance - deployAtFee; long actualBalance = deployer.getConfirmedBalance(Asset.QORT); @@ -521,7 +482,6 @@ public class AtTests extends Common { + "\tcreation timestamp: %s,\n" + "\tcurrent balance: %s QORT,\n" + "\tHASH160 of secret: %s,\n" - + "\tinitial payout: %s QORT,\n" + "\tredeem payout: %s QORT,\n" + "\texpected bitcoin: %s BTC,\n" + "\ttrade timeout: %d minutes (from trade start),\n" @@ -531,10 +491,9 @@ public class AtTests extends Common { epochMilliFormatter.apply(tradeData.creationTimestamp), Amounts.prettyAmount(tradeData.qortBalance), HashCode.fromBytes(tradeData.secretHash).toString().substring(0, 40), - Amounts.prettyAmount(tradeData.initialPayout), - Amounts.prettyAmount(tradeData.redeemPayout), + Amounts.prettyAmount(tradeData.qortAmount), Amounts.prettyAmount(tradeData.expectedBitcoin), - tradeData.tradeRefundTimeout, + tradeData.tradeTimeout, currentBlockHeight)); // Are we in 'offer' or 'trade' stage? diff --git a/src/test/java/org/qortal/test/btcacct/BtcTests.java b/src/test/java/org/qortal/test/btcacct/BtcTests.java index d0530c47..bd5211fc 100644 --- a/src/test/java/org/qortal/test/btcacct/BtcTests.java +++ b/src/test/java/org/qortal/test/btcacct/BtcTests.java @@ -10,7 +10,7 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.qortal.crosschain.BTC; -import org.qortal.crosschain.BTCACCT; +import org.qortal.crosschain.BTCP2SH; import org.qortal.repository.DataException; import org.qortal.test.common.Common; @@ -56,7 +56,7 @@ public class BtcTests extends Common { List rawTransactions = BTC.getInstance().getAddressTransactions(p2shAddress); byte[] expectedSecret = AtTests.secret; - byte[] secret = BTCACCT.findP2shSecret(p2shAddress, rawTransactions); + byte[] secret = BTCP2SH.findP2shSecret(p2shAddress, rawTransactions); assertNotNull(secret); assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret)); diff --git a/src/test/java/org/qortal/test/btcacct/BuildP2SH.java b/src/test/java/org/qortal/test/btcacct/BuildP2SH.java index 25f0430e..6b6b16e1 100644 --- a/src/test/java/org/qortal/test/btcacct/BuildP2SH.java +++ b/src/test/java/org/qortal/test/btcacct/BuildP2SH.java @@ -13,7 +13,7 @@ import org.bitcoinj.script.Script.ScriptType; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.qortal.controller.Controller; import org.qortal.crosschain.BTC; -import org.qortal.crosschain.BTCACCT; +import org.qortal.crosschain.BTCP2SH; import org.qortal.crypto.Crypto; import org.qortal.repository.DataException; import org.qortal.repository.Repository; @@ -103,7 +103,7 @@ public class BuildP2SH { System.out.println(String.format("Redeem script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime)); System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(secretHash))); - byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash); + byte[] redeemScriptBytes = BTCP2SH.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash); System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes))); byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); diff --git a/src/test/java/org/qortal/test/btcacct/CheckP2SH.java b/src/test/java/org/qortal/test/btcacct/CheckP2SH.java index 25f20c68..935d83eb 100644 --- a/src/test/java/org/qortal/test/btcacct/CheckP2SH.java +++ b/src/test/java/org/qortal/test/btcacct/CheckP2SH.java @@ -15,7 +15,7 @@ import org.bitcoinj.script.Script.ScriptType; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.qortal.controller.Controller; import org.qortal.crosschain.BTC; -import org.qortal.crosschain.BTCACCT; +import org.qortal.crosschain.BTCP2SH; import org.qortal.crypto.Crypto; import org.qortal.repository.DataException; import org.qortal.repository.Repository; @@ -113,7 +113,7 @@ public class CheckP2SH { System.out.println(String.format("P2SH address: %s", p2shAddress)); - byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash); + byte[] redeemScriptBytes = BTCP2SH.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash); System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes))); byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); diff --git a/src/test/java/org/qortal/test/btcacct/DeployAT.java b/src/test/java/org/qortal/test/btcacct/DeployAT.java index dec9f563..0aa0b762 100644 --- a/src/test/java/org/qortal/test/btcacct/DeployAT.java +++ b/src/test/java/org/qortal/test/btcacct/DeployAT.java @@ -34,21 +34,20 @@ 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" - + "\t0.0001 \\\n" + "\t123.456 \\\n" + "\t10")); System.exit(1); } public static void main(String[] args) { - if (args.length != 8) + if (args.length != 7) usage(null); Security.insertProviderAt(new BouncyCastleProvider(), 0); @@ -59,7 +58,6 @@ public class DeployAT { long expectedBitcoin = 0; byte[] bitcoinPublicKeyHash = null; byte[] secretHash = null; - long initialPayout = 0; long fundingAmount = 0; int tradeTimeout = 0; @@ -85,10 +83,6 @@ public class DeployAT { if (secretHash.length != 20) usage("Hash of secret must be 20 bytes"); - initialPayout = Long.parseLong(args[argIndex++]); - if (initialPayout < 0) - usage("Initial QORT payout must be positive"); - fundingAmount = Long.parseLong(args[argIndex++]); if (fundingAmount <= redeemAmount) usage("AT funding amount must be greater than QORT redeem amount"); @@ -120,7 +114,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, initialPayout, redeemAmount, expectedBitcoin); + byte[] creationBytes = BTCACCT.buildQortalAT(refundAccount.getAddress(), bitcoinPublicKeyHash, secretHash, tradeTimeout, redeemAmount, expectedBitcoin); System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString()); long txTimestamp = System.currentTimeMillis(); diff --git a/src/test/java/org/qortal/test/btcacct/Redeem.java b/src/test/java/org/qortal/test/btcacct/Redeem.java index 5a14906b..40968450 100644 --- a/src/test/java/org/qortal/test/btcacct/Redeem.java +++ b/src/test/java/org/qortal/test/btcacct/Redeem.java @@ -18,7 +18,7 @@ import org.bitcoinj.script.Script.ScriptType; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.qortal.controller.Controller; import org.qortal.crosschain.BTC; -import org.qortal.crosschain.BTCACCT; +import org.qortal.crosschain.BTCP2SH; import org.qortal.crypto.Crypto; import org.qortal.repository.DataException; import org.qortal.repository.Repository; @@ -121,7 +121,7 @@ public class Redeem { System.out.println(String.format("P2SH address: %s", p2shAddress)); - byte[] redeemScriptBytes = BTCACCT.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash); + byte[] redeemScriptBytes = BTCP2SH.buildScript(refundBitcoinAddress.getHash(), lockTime, redeemAddress.getHash(), secretHash); System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes))); byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); @@ -182,7 +182,7 @@ public class Redeem { Coin redeemAmount = Coin.valueOf(p2shBalance).subtract(bitcoinFee); System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.format(redeemAmount), BTC.format(bitcoinFee))); - Transaction redeemTransaction = BTCACCT.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, secret); + Transaction redeemTransaction = BTCP2SH.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, secret); byte[] redeemBytes = redeemTransaction.bitcoinSerialize(); diff --git a/src/test/java/org/qortal/test/btcacct/Refund.java b/src/test/java/org/qortal/test/btcacct/Refund.java index b4ab94b5..c6fd88ed 100644 --- a/src/test/java/org/qortal/test/btcacct/Refund.java +++ b/src/test/java/org/qortal/test/btcacct/Refund.java @@ -18,7 +18,7 @@ import org.bitcoinj.script.Script.ScriptType; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.qortal.controller.Controller; import org.qortal.crosschain.BTC; -import org.qortal.crosschain.BTCACCT; +import org.qortal.crosschain.BTCP2SH; import org.qortal.crypto.Crypto; import org.qortal.repository.DataException; import org.qortal.repository.Repository; @@ -120,7 +120,7 @@ public class Refund { Address refundAddress = Address.fromKey(params, refundKey, ScriptType.P2PKH); System.out.println(String.format("Refund recipient (PKH): %s (%s)", refundAddress, HashCode.fromBytes(refundAddress.getHash()))); - byte[] redeemScriptBytes = BTCACCT.buildScript(refundAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash); + byte[] redeemScriptBytes = BTCP2SH.buildScript(refundAddress.getHash(), lockTime, redeemBitcoinAddress.getHash(), secretHash); System.out.println(String.format("Redeem script: %s", HashCode.fromBytes(redeemScriptBytes))); byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); @@ -186,7 +186,7 @@ public class Refund { Coin refundAmount = Coin.valueOf(p2shBalance).subtract(bitcoinFee); System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.format(refundAmount), BTC.format(bitcoinFee))); - Transaction redeemTransaction = BTCACCT.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime); + Transaction redeemTransaction = BTCP2SH.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime); byte[] redeemBytes = redeemTransaction.bitcoinSerialize();