Add support for multiple P2SH funding transactions rather than requiring only one

This commit is contained in:
catbref 2020-05-29 19:10:20 +01:00
parent 0ae232b8ba
commit bef1828404
4 changed files with 40 additions and 36 deletions

View File

@ -543,7 +543,7 @@ public class CrossChainResource {
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString()); List<TransactionOutput> 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.canRedeem = now >= medianBlockTime * 1000L;
p2shStatus.canRefund = now >= crossChainTradeData.lockTime * 1000L; p2shStatus.canRefund = now >= crossChainTradeData.lockTime * 1000L;
} }
@ -642,10 +642,9 @@ public class CrossChainResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString()); List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
if (fundingOutputs.size() != 1) if (fundingOutputs.isEmpty())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
TransactionOutput fundingOutput = fundingOutputs.get(0);
boolean canRefund = now >= crossChainTradeData.lockTime * 1000L; boolean canRefund = now >= crossChainTradeData.lockTime * 1000L;
if (!canRefund) if (!canRefund)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_TOO_SOON); 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())); 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); boolean wasBroadcast = BTC.getInstance().broadcastTransaction(refundTransaction);
if (!wasBroadcast) if (!wasBroadcast)
@ -758,17 +757,16 @@ public class CrossChainResource {
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_BALANCE_ISSUE); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_BALANCE_ISSUE);
List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString()); List<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress.toString());
if (fundingOutputs.size() != 1) if (fundingOutputs.isEmpty())
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
TransactionOutput fundingOutput = fundingOutputs.get(0);
boolean canRedeem = now >= medianBlockTime * 1000L; boolean canRedeem = now >= medianBlockTime * 1000L;
if (!canRedeem) if (!canRedeem)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_TOO_SOON); throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BTC_TOO_SOON);
Coin redeemAmount = p2shBalance.subtract(Coin.valueOf(redeemRequest.bitcoinMinerFee.unscaledValue().longValue())); 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); boolean wasBroadcast = BTC.getInstance().broadcastTransaction(redeemTransaction);
if (!wasBroadcast) if (!wasBroadcast)

View File

@ -122,7 +122,7 @@ public class BTCACCT {
* @param scriptSigBuilder function for building scriptSig using transaction input signature * @param scriptSigBuilder function for building scriptSig using transaction input signature
* @return Signed Bitcoin transaction for spending P2SH * @return Signed Bitcoin transaction for spending P2SH
*/ */
public static Transaction buildP2shTransaction(Coin amount, ECKey spendKey, TransactionOutput fundingOutput, byte[] redeemScriptBytes, Long lockTime, Function<byte[], Script> scriptSigBuilder) { public static Transaction buildP2shTransaction(Coin amount, ECKey spendKey, List<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, Long lockTime, Function<byte[], Script> scriptSigBuilder) {
NetworkParameters params = BTC.getInstance().getNetworkParameters(); NetworkParameters params = BTC.getInstance().getNetworkParameters();
Transaction transaction = new Transaction(params); Transaction transaction = new Transaction(params);
@ -131,30 +131,36 @@ public class BTCACCT {
// Output is back to P2SH funder // Output is back to P2SH funder
transaction.addOutput(amount, ScriptBuilder.createP2PKHOutputScript(spendKey.getPubKeyHash())); transaction.addOutput(amount, ScriptBuilder.createP2PKHOutputScript(spendKey.getPubKeyHash()));
// Input (without scriptSig prior to signing) for (int inputIndex = 0; inputIndex < fundingOutputs.size(); ++inputIndex) {
TransactionInput input = new TransactionInput(params, null, redeemScriptBytes, fundingOutput.getOutPointFor()); TransactionOutput fundingOutput = fundingOutputs.get(inputIndex);
if (lockTime != null)
input.setSequenceNumber(BTC.LOCKTIME_NO_RBF_SEQUENCE); // Use max-value, so no lockTime and no RBF // Input (without scriptSig prior to signing)
else TransactionInput input = new TransactionInput(params, null, redeemScriptBytes, fundingOutput.getOutPointFor());
input.setSequenceNumber(BTC.NO_LOCKTIME_NO_RBF_SEQUENCE); // Use max-value - 1, so lockTime can be used but not RBF if (lockTime != null)
transaction.addInput(input); 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 // Set locktime after inputs added but before input signatures are generated
if (lockTime != null) if (lockTime != null)
transaction.setLockTime(lockTime); transaction.setLockTime(lockTime);
// Generate transaction signature for input for (int inputIndex = 0; inputIndex < fundingOutputs.size(); ++inputIndex) {
final boolean anyoneCanPay = false; // Generate transaction signature for input
TransactionSignature txSig = transaction.calculateSignature(0, spendKey, redeemScriptBytes, SigHash.ALL, anyoneCanPay); final boolean anyoneCanPay = false;
TransactionSignature txSig = transaction.calculateSignature(inputIndex, spendKey, redeemScriptBytes, SigHash.ALL, anyoneCanPay);
// Calculate transaction signature // Calculate transaction signature
byte[] txSigBytes = txSig.encodeToBitcoin(); byte[] txSigBytes = txSig.encodeToBitcoin();
// Build scriptSig using lambda and tx signature // Build scriptSig using lambda and tx signature
Script scriptSig = scriptSigBuilder.apply(txSigBytes); Script scriptSig = scriptSigBuilder.apply(txSigBytes);
// Set input scriptSig // Set input scriptSig
transaction.getInput(0).setScriptSig(scriptSig); transaction.getInput(inputIndex).setScriptSig(scriptSig);
}
return transaction; return transaction;
} }
@ -169,7 +175,7 @@ public class BTCACCT {
* @param lockTime transaction nLockTime - must be at least locktime used in redeemScript * @param lockTime transaction nLockTime - must be at least locktime used in redeemScript
* @return Signed Bitcoin transaction for refunding P2SH * @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<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, long lockTime) {
Function<byte[], Script> refundSigScriptBuilder = (txSigBytes) -> { Function<byte[], Script> refundSigScriptBuilder = (txSigBytes) -> {
// Build scriptSig with... // Build scriptSig with...
ScriptBuilder scriptBuilder = new ScriptBuilder(); ScriptBuilder scriptBuilder = new ScriptBuilder();
@ -187,7 +193,7 @@ public class BTCACCT {
return scriptBuilder.build(); 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 * @param secret actual 32-byte secret used when building redeemScript
* @return Signed Bitcoin transaction for redeeming P2SH * @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<TransactionOutput> fundingOutputs, byte[] redeemScriptBytes, byte[] secret) {
Function<byte[], Script> redeemSigScriptBuilder = (txSigBytes) -> { Function<byte[], Script> redeemSigScriptBuilder = (txSigBytes) -> {
// Build scriptSig with... // Build scriptSig with...
ScriptBuilder scriptBuilder = new ScriptBuilder(); ScriptBuilder scriptBuilder = new ScriptBuilder();
@ -221,7 +227,7 @@ public class BTCACCT {
return scriptBuilder.build(); return scriptBuilder.build();
}; };
return buildP2shTransaction(redeemAmount, redeemKey, fundingOutput, redeemScriptBytes, null, redeemSigScriptBuilder); return buildP2shTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, null, redeemSigScriptBuilder);
} }
/** /**

View File

@ -173,16 +173,16 @@ public class Redeem {
if (fundingOutputs.size() != 1) { if (fundingOutputs.size() != 1) {
System.err.println(String.format("Expecting only one unspent output for P2SH")); System.err.println(String.format("Expecting only one unspent output for P2SH"));
System.exit(2); // No longer fatal
} }
TransactionOutput fundingOutput = fundingOutputs.get(0); for (TransactionOutput fundingOutput : fundingOutputs)
System.out.println(String.format("Using output %s:%d for redeem", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex())); System.out.println(String.format("Using output %s:%d for redeem", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex()));
Coin redeemAmount = p2shBalance.subtract(bitcoinFee); 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))); 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(); byte[] redeemBytes = redeemTransaction.bitcoinSerialize();

View File

@ -177,16 +177,16 @@ public class Refund {
if (fundingOutputs.size() != 1) { if (fundingOutputs.size() != 1) {
System.err.println(String.format("Expecting only one unspent output for P2SH")); System.err.println(String.format("Expecting only one unspent output for P2SH"));
System.exit(2); // No longer fatal
} }
TransactionOutput fundingOutput = fundingOutputs.get(0); for (TransactionOutput fundingOutput : fundingOutputs)
System.out.println(String.format("Using output %s:%d for refund", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex())); System.out.println(String.format("Using output %s:%d for redeem", HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex()));
Coin refundAmount = p2shBalance.subtract(bitcoinFee); 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))); 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(); byte[] redeemBytes = redeemTransaction.bitcoinSerialize();