From bef1828404c3e9ee518c37b303f93097ee3e364e Mon Sep 17 00:00:00 2001 From: catbref Date: Fri, 29 May 2020 19:10:20 +0100 Subject: [PATCH] Add support for multiple P2SH funding transactions rather than requiring only one --- .../api/resource/CrossChainResource.java | 12 ++--- .../java/org/qortal/crosschain/BTCACCT.java | 48 +++++++++++-------- .../java/org/qortal/test/btcacct/Redeem.java | 8 ++-- .../java/org/qortal/test/btcacct/Refund.java | 8 ++-- 4 files changed, 40 insertions(+), 36 deletions(-) diff --git a/src/main/java/org/qortal/api/resource/CrossChainResource.java b/src/main/java/org/qortal/api/resource/CrossChainResource.java index 3de1b56d..3235a4a7 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainResource.java @@ -543,7 +543,7 @@ public class CrossChainResource { List fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString()); - if (p2shBalance.value >= crossChainTradeData.expectedBitcoin && fundingOutputs.size() == 1) { + if (p2shBalance.value >= crossChainTradeData.expectedBitcoin && !fundingOutputs.isEmpty()) { p2shStatus.canRedeem = now >= medianBlockTime * 1000L; p2shStatus.canRefund = now >= crossChainTradeData.lockTime * 1000L; } @@ -642,10 +642,9 @@ public class CrossChainResource { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN); List fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString()); - if (fundingOutputs.size() != 1) + if (fundingOutputs.isEmpty()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); - TransactionOutput fundingOutput = fundingOutputs.get(0); boolean canRefund = now >= crossChainTradeData.lockTime * 1000L; if (!canRefund) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_TOO_SOON); @@ -655,7 +654,7 @@ public class CrossChainResource { Coin refundAmount = p2shBalance.subtract(Coin.valueOf(refundRequest.bitcoinMinerFee.unscaledValue().longValue())); - org.bitcoinj.core.Transaction refundTransaction = BTCACCT.buildRefundTransaction(refundAmount, refundKey, fundingOutput, redeemScriptBytes, crossChainTradeData.lockTime); + org.bitcoinj.core.Transaction refundTransaction = BTCACCT.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, crossChainTradeData.lockTime); boolean wasBroadcast = BTC.getInstance().broadcastTransaction(refundTransaction); if (!wasBroadcast) @@ -758,17 +757,16 @@ public class CrossChainResource { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_BALANCE_ISSUE); List fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString()); - if (fundingOutputs.size() != 1) + if (fundingOutputs.isEmpty()) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); - TransactionOutput fundingOutput = fundingOutputs.get(0); boolean canRedeem = now >= medianBlockTime * 1000L; if (!canRedeem) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_TOO_SOON); Coin redeemAmount = p2shBalance.subtract(Coin.valueOf(redeemRequest.bitcoinMinerFee.unscaledValue().longValue())); - org.bitcoinj.core.Transaction redeemTransaction = BTCACCT.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutput, redeemScriptBytes, redeemRequest.secret); + org.bitcoinj.core.Transaction redeemTransaction = BTCACCT.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, redeemRequest.secret); boolean wasBroadcast = BTC.getInstance().broadcastTransaction(redeemTransaction); if (!wasBroadcast) diff --git a/src/main/java/org/qortal/crosschain/BTCACCT.java b/src/main/java/org/qortal/crosschain/BTCACCT.java index 1ee6baf5..a0246d04 100644 --- a/src/main/java/org/qortal/crosschain/BTCACCT.java +++ b/src/main/java/org/qortal/crosschain/BTCACCT.java @@ -122,7 +122,7 @@ public class BTCACCT { * @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, TransactionOutput fundingOutput, byte[] redeemScriptBytes, Long lockTime, Function scriptSigBuilder) { + 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); @@ -131,30 +131,36 @@ public class BTCACCT { // Output is back to P2SH funder transaction.addOutput(amount, ScriptBuilder.createP2PKHOutputScript(spendKey.getPubKeyHash())); - // 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); + 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); - // Generate transaction signature for input - final boolean anyoneCanPay = false; - TransactionSignature txSig = transaction.calculateSignature(0, spendKey, redeemScriptBytes, SigHash.ALL, anyoneCanPay); + 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(); + // Calculate transaction signature + byte[] txSigBytes = txSig.encodeToBitcoin(); - // Build scriptSig using lambda and tx signature - Script scriptSig = scriptSigBuilder.apply(txSigBytes); + // Build scriptSig using lambda and tx signature + Script scriptSig = scriptSigBuilder.apply(txSigBytes); - // Set input scriptSig - transaction.getInput(0).setScriptSig(scriptSig); + // Set input scriptSig + transaction.getInput(inputIndex).setScriptSig(scriptSig); + } return transaction; } @@ -169,7 +175,7 @@ public class BTCACCT { * @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, TransactionOutput fundingOutput, byte[] redeemScriptBytes, long lockTime) { + public static Transaction buildRefundTransaction(Coin refundAmount, ECKey refundKey, List fundingOutputs, byte[] redeemScriptBytes, long lockTime) { Function refundSigScriptBuilder = (txSigBytes) -> { // Build scriptSig with... ScriptBuilder scriptBuilder = new ScriptBuilder(); @@ -187,7 +193,7 @@ public class BTCACCT { return scriptBuilder.build(); }; - return buildP2shTransaction(refundAmount, refundKey, fundingOutput, redeemScriptBytes, lockTime, refundSigScriptBuilder); + return buildP2shTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime, refundSigScriptBuilder); } /** @@ -200,7 +206,7 @@ public class BTCACCT { * @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, TransactionOutput fundingOutput, byte[] redeemScriptBytes, byte[] secret) { + public static Transaction buildRedeemTransaction(Coin redeemAmount, ECKey redeemKey, List fundingOutputs, byte[] redeemScriptBytes, byte[] secret) { Function redeemSigScriptBuilder = (txSigBytes) -> { // Build scriptSig with... ScriptBuilder scriptBuilder = new ScriptBuilder(); @@ -221,7 +227,7 @@ public class BTCACCT { return scriptBuilder.build(); }; - return buildP2shTransaction(redeemAmount, redeemKey, fundingOutput, redeemScriptBytes, null, redeemSigScriptBuilder); + return buildP2shTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, null, redeemSigScriptBuilder); } /** diff --git a/src/test/java/org/qortal/test/btcacct/Redeem.java b/src/test/java/org/qortal/test/btcacct/Redeem.java index 85670d68..091f2234 100644 --- a/src/test/java/org/qortal/test/btcacct/Redeem.java +++ b/src/test/java/org/qortal/test/btcacct/Redeem.java @@ -173,16 +173,16 @@ public class Redeem { if (fundingOutputs.size() != 1) { System.err.println(String.format("Expecting only one unspent output for P2SH")); - System.exit(2); + // No longer fatal } - TransactionOutput fundingOutput = fundingOutputs.get(0); - System.out.println(String.format("Using output %s:%d for redeem", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex())); + for (TransactionOutput fundingOutput : fundingOutputs) + System.out.println(String.format("Using output %s:%d for redeem", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex())); Coin redeemAmount = p2shBalance.subtract(bitcoinFee); System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.FORMAT.format(redeemAmount), BTC.FORMAT.format(bitcoinFee))); - Transaction redeemTransaction = BTCACCT.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutput, redeemScriptBytes, secret); + Transaction redeemTransaction = BTCACCT.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 57835041..a694ee14 100644 --- a/src/test/java/org/qortal/test/btcacct/Refund.java +++ b/src/test/java/org/qortal/test/btcacct/Refund.java @@ -177,16 +177,16 @@ public class Refund { if (fundingOutputs.size() != 1) { System.err.println(String.format("Expecting only one unspent output for P2SH")); - System.exit(2); + // No longer fatal } - TransactionOutput fundingOutput = fundingOutputs.get(0); - System.out.println(String.format("Using output %s:%d for refund", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex())); + for (TransactionOutput fundingOutput : fundingOutputs) + System.out.println(String.format("Using output %s:%d for redeem", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex())); Coin refundAmount = p2shBalance.subtract(bitcoinFee); System.out.println(String.format("Spending %s of output, with %s as mining fee", BTC.FORMAT.format(refundAmount), BTC.FORMAT.format(bitcoinFee))); - Transaction redeemTransaction = BTCACCT.buildRefundTransaction(refundAmount, refundKey, fundingOutput, redeemScriptBytes, lockTime); + Transaction redeemTransaction = BTCACCT.buildRefundTransaction(refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime); byte[] redeemBytes = redeemTransaction.bitcoinSerialize();