Transaction: support segwit in addSignedInput() methods

Add addSignedInput() variants to Transaction that provide input value,
deprecate non-value methods, add checks and logs.
Add simple transaction building tests to TransactionTest.
This also updates some tests to not use the deprecated methods.
This commit is contained in:
Sean Gilligan 2022-02-09 18:07:03 -08:00 committed by Andreas Schildbach
parent f3a314e2d9
commit 1959dab5e4
3 changed files with 129 additions and 29 deletions

View File

@ -47,6 +47,7 @@ import java.util.*;
import static org.bitcoinj.core.NetworkParameters.ProtocolVersion.WITNESS_VERSION;
import static org.bitcoinj.core.Utils.*;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import java.math.BigInteger;
@ -984,13 +985,23 @@ public class Transaction extends ChildMessage {
* to understand the values of sigHash and anyoneCanPay: otherwise you can use the other form of this method
* that sets them to typical defaults.
*
* @throws ScriptException if the scriptPubKey is not a pay to address or P2PK script.
* @param prevOut A reference to the output being spent
* @param scriptPubKey The scriptPubKey of the output
* @param amount The amount of the output (which is part of the signature hash for segwit)
* @param sigKey The signing key
* @param sigHash enum specifying how the transaction hash is calculated
* @param anyoneCanPay anyone-can-pay hashing
* @return The newly created input
* @throws ScriptException if the scriptPubKey is something we don't know how to sign.
*/
public TransactionInput addSignedInput(TransactionOutPoint prevOut, Script scriptPubKey, ECKey sigKey,
public TransactionInput addSignedInput(TransactionOutPoint prevOut, Script scriptPubKey, Coin amount, ECKey sigKey,
SigHash sigHash, boolean anyoneCanPay) throws ScriptException {
// Verify the API user didn't try to do operations out of order.
checkState(!outputs.isEmpty(), "Attempting to sign tx without outputs.");
TransactionInput input = new TransactionInput(params, this, new byte[] {}, prevOut);
if (amount == null || amount.value <= 0) {
log.warn("Illegal amount value. Amount is required for SegWit transactions.");
}
TransactionInput input = new TransactionInput(params, this, new byte[] {}, prevOut, amount);
addInput(input);
int inputIndex = inputs.size() - 1;
if (ScriptPattern.isP2PK(scriptPubKey)) {
@ -1016,27 +1027,74 @@ public class Transaction extends ChildMessage {
}
/**
* Same as {@link #addSignedInput(TransactionOutPoint, Script, ECKey, Transaction.SigHash, boolean)}
* but defaults to {@link SigHash#ALL} and "false" for the anyoneCanPay flag. This is normally what you want.
* @param prevOut A reference to the output being spent
* @param scriptPubKey The scriptPubKey of the output
* @param sigKey The signing key
* @param sigHash enum specifying how the transaction hash is calculated
* @param anyoneCanPay anyone-can-pay hashing
* @return The newly created input
* @throws ScriptException if the scriptPubKey is something we don't know how to sign.
* @deprecated Use {@link Transaction#addSignedInput(TransactionOutPoint, Script, Coin, ECKey, SigHash, boolean)}
*/
@Deprecated
public TransactionInput addSignedInput(TransactionOutPoint prevOut, Script scriptPubKey, ECKey sigKey,
SigHash sigHash, boolean anyoneCanPay) throws ScriptException {
return addSignedInput(prevOut, scriptPubKey, null, sigKey, sigHash, anyoneCanPay);
}
/**
* Adds a new and fully signed input for the given parameters. Note that this method is <b>not</b> thread safe
* and requires external synchronization.
* Defaults to {@link SigHash#ALL} and "false" for the anyoneCanPay flag. This is normally what you want.
* @param prevOut A reference to the output being spent
* @param scriptPubKey The scriptPubKey of the output
* @param amount The amount of the output (which is part of the signature hash for segwit)
* @param sigKey The signing key
* @return The newly created input
* @throws ScriptException if the scriptPubKey is something we don't know how to sign.
*/
public TransactionInput addSignedInput(TransactionOutPoint prevOut, Script scriptPubKey, Coin amount, ECKey sigKey) throws ScriptException {
return addSignedInput(prevOut, scriptPubKey, amount, sigKey, SigHash.ALL, false);
}
/**
* @param prevOut A reference to the output being spent
* @param scriptPubKey The scriptPubKey of the output
* @param sigKey The signing key
* @return The newly created input
* @throws ScriptException if the scriptPubKey is something we don't know how to sign.
* @deprecated Use {@link Transaction#addSignedInput(TransactionOutPoint, Script, Coin, ECKey)}
*/
@Deprecated
public TransactionInput addSignedInput(TransactionOutPoint prevOut, Script scriptPubKey, ECKey sigKey) throws ScriptException {
return addSignedInput(prevOut, scriptPubKey, sigKey, SigHash.ALL, false);
return addSignedInput(prevOut, scriptPubKey, null, sigKey);
}
/**
* Adds an input that points to the given output and contains a valid signature for it, calculated using the
* signing key. Defaults to {@link SigHash#ALL} and "false" for the anyoneCanPay flag. This is normally what you want.
* @param output output to sign and use as input
* @param sigKey The signing key
* @return The newly created input
*/
public TransactionInput addSignedInput(TransactionOutput output, ECKey sigKey) {
return addSignedInput(output, sigKey, SigHash.ALL, false);
}
/**
* Adds an input that points to the given output and contains a valid signature for it, calculated using the
* signing key.
* @see Transaction#addSignedInput(TransactionOutPoint, Script, Coin, ECKey, SigHash, boolean)
* @param output output to sign and use as input
* @param sigKey The signing key
* @param sigHash enum specifying how the transaction hash is calculated
* @param anyoneCanPay anyone-can-pay hashing
* @return The newly created input
*/
public TransactionInput addSignedInput(TransactionOutput output, ECKey signingKey) {
return addSignedInput(output.getOutPointFor(), output.getScriptPubKey(), signingKey);
}
/**
* Adds an input that points to the given output and contains a valid signature for it, calculated using the
* signing key.
*/
public TransactionInput addSignedInput(TransactionOutput output, ECKey signingKey, SigHash sigHash, boolean anyoneCanPay) {
return addSignedInput(output.getOutPointFor(), output.getScriptPubKey(), signingKey, sigHash, anyoneCanPay);
public TransactionInput addSignedInput(TransactionOutput output, ECKey sigKey, SigHash sigHash, boolean anyoneCanPay) {
checkNotNull(output.getValue(), "TransactionOutput.getValue() must not be null");
checkState(output.getValue().value > 0, "TransactionOutput.getValue() must not be greater than zero");
return addSignedInput(output.getOutPointFor(), output.getScriptPubKey(), output.getValue(), sigKey, sigHash, anyoneCanPay);
}
/**

View File

@ -183,21 +183,22 @@ public abstract class AbstractFullPrunedBlockChainTest {
// Build some blocks on genesis block to create a spendable output
Block rollingBlock = PARAMS.getGenesisBlock().createNextBlockWithCoinbase(Block.BLOCK_VERSION_GENESIS, outKey.getPubKey(), height++);
chain.add(rollingBlock);
TransactionOutPoint spendableOutput = new TransactionOutPoint(PARAMS, 0, rollingBlock.getTransactions().get(0).getTxId());
byte[] spendableOutputScriptPubKey = rollingBlock.getTransactions().get(0).getOutputs().get(0).getScriptBytes();
TransactionOutput spendableOutput = rollingBlock.getTransactions().get(0).getOutput(0);
TransactionOutPoint transactionOutPoint = spendableOutput.getOutPointFor();
Script spendableOutputScriptPubKey = spendableOutput.getScriptPubKey();
for (int i = 1; i < PARAMS.getSpendableCoinbaseDepth(); i++) {
rollingBlock = rollingBlock.createNextBlockWithCoinbase(Block.BLOCK_VERSION_GENESIS, outKey.getPubKey(), height++);
chain.add(rollingBlock);
}
WeakReference<UTXO> out = new WeakReference<>
(store.getTransactionOutput(spendableOutput.getHash(), spendableOutput.getIndex()));
(store.getTransactionOutput(transactionOutPoint.getHash(), transactionOutPoint.getIndex()));
rollingBlock = rollingBlock.createNextBlock(null);
Transaction t = new Transaction(PARAMS);
// Entirely invalid scriptPubKey
t.addOutput(new TransactionOutput(PARAMS, t, FIFTY_COINS, new byte[]{}));
t.addSignedInput(spendableOutput, new Script(spendableOutputScriptPubKey), outKey);
t.addSignedInput(transactionOutPoint, spendableOutputScriptPubKey, spendableOutput.getValue(), outKey);
rollingBlock.addTransaction(t);
rollingBlock.solve();
@ -257,8 +258,9 @@ public abstract class AbstractFullPrunedBlockChainTest {
Block rollingBlock = PARAMS.getGenesisBlock().createNextBlockWithCoinbase(Block.BLOCK_VERSION_GENESIS, outKey.getPubKey(), height++);
chain.add(rollingBlock);
Transaction transaction = rollingBlock.getTransactions().get(0);
TransactionOutPoint spendableOutput = new TransactionOutPoint(PARAMS, 0, transaction.getTxId());
byte[] spendableOutputScriptPubKey = transaction.getOutputs().get(0).getScriptBytes();
TransactionOutput spendableOutput = transaction.getOutput(0);
TransactionOutPoint spendableOutputPoint = spendableOutput.getOutPointFor();
Script spendableOutputScriptPubKey = spendableOutput.getScriptPubKey();
for (int i = 1; i < PARAMS.getSpendableCoinbaseDepth(); i++) {
rollingBlock = rollingBlock.createNextBlockWithCoinbase(Block.BLOCK_VERSION_GENESIS, outKey.getPubKey(), height++);
chain.add(rollingBlock);
@ -273,7 +275,7 @@ public abstract class AbstractFullPrunedBlockChainTest {
Transaction t = new Transaction(PARAMS);
t.addOutput(new TransactionOutput(PARAMS, t, amount, toKey));
t.addSignedInput(spendableOutput, new Script(spendableOutputScriptPubKey), outKey);
t.addSignedInput(spendableOutputPoint, spendableOutputScriptPubKey, spendableOutput.getValue(), outKey);
rollingBlock.addTransaction(t);
rollingBlock.solve();
chain.add(rollingBlock);
@ -308,8 +310,9 @@ public abstract class AbstractFullPrunedBlockChainTest {
Block rollingBlock = PARAMS.getGenesisBlock().createNextBlockWithCoinbase(Block.BLOCK_VERSION_GENESIS, outKey.getPubKey(), height++);
chain.add(rollingBlock);
Transaction transaction = rollingBlock.getTransactions().get(0);
TransactionOutPoint spendableOutput = new TransactionOutPoint(PARAMS, 0, transaction.getTxId());
byte[] spendableOutputScriptPubKey = transaction.getOutputs().get(0).getScriptBytes();
TransactionOutput spendableOutput = transaction.getOutput(0);
TransactionOutPoint spendableOutPoint = new TransactionOutPoint(PARAMS, 0, transaction.getTxId());
Script spendableOutputScriptPubKey = spendableOutput.getScriptPubKey();
for (int i = 1; i < PARAMS.getSpendableCoinbaseDepth(); i++) {
rollingBlock = rollingBlock.createNextBlockWithCoinbase(Block.BLOCK_VERSION_GENESIS, outKey.getPubKey(), height++);
chain.add(rollingBlock);
@ -327,7 +330,7 @@ public abstract class AbstractFullPrunedBlockChainTest {
Transaction t = new Transaction(PARAMS);
t.addOutput(new TransactionOutput(PARAMS, t, amount, toKey));
t.addSignedInput(spendableOutput, new Script(spendableOutputScriptPubKey), outKey);
t.addSignedInput(spendableOutPoint, spendableOutputScriptPubKey, spendableOutput.getValue(), outKey);
rollingBlock.addTransaction(t);
rollingBlock.solve();
chain.add(rollingBlock);

View File

@ -168,6 +168,45 @@ public class TransactionTest {
assertEquals(tx.isMature(), false);
}
@Test
public void testBuildingSimpleP2PKH() {
final Address toAddr = Address.fromKey(TESTNET, new ECKey(), Script.ScriptType.P2PKH);
final Sha256Hash utxo_id = Sha256Hash.wrap("81b4c832d70cb56ff957589752eb4125a4cab78a25a8fc52d6a09e5bd4404d48");
final Coin inAmount = Coin.ofSat(91234);
final Coin outAmount = Coin.ofSat(91234);
ECKey fromKey = new ECKey();
Address fromAddress = Address.fromKey(TESTNET, fromKey, Script.ScriptType.P2PKH);
Transaction tx = new Transaction(TESTNET);
TransactionOutPoint outPoint = new TransactionOutPoint(TESTNET, 0, utxo_id);
TransactionOutput output = new TransactionOutput(TESTNET, null, inAmount, fromAddress);
tx.addOutput(outAmount, toAddr);
tx.addSignedInput(outPoint, ScriptBuilder.createOutputScript(fromAddress), inAmount, fromKey);
byte[] rawTx = tx.bitcoinSerialize();
assertNotNull(rawTx);
}
@Test
public void testBuildingSimpleP2WPKH() {
final Address toAddr = Address.fromKey(TESTNET, new ECKey(), Script.ScriptType.P2WPKH);
final Sha256Hash utxo_id = Sha256Hash.wrap("81b4c832d70cb56ff957589752eb4125a4cab78a25a8fc52d6a09e5bd4404d48");
final Coin inAmount = Coin.ofSat(91234);
final Coin outAmount = Coin.ofSat(91234);
ECKey fromKey = new ECKey();
Address fromAddress = Address.fromKey(TESTNET, fromKey, Script.ScriptType.P2WPKH);
Transaction tx = new Transaction(TESTNET);
TransactionOutPoint outPoint = new TransactionOutPoint(TESTNET, 0, utxo_id);
tx.addOutput(outAmount, toAddr);
tx.addSignedInput(outPoint, ScriptBuilder.createOutputScript(fromAddress), inAmount, fromKey);
byte[] rawTx = tx.bitcoinSerialize();
assertNotNull(rawTx);
}
@Test
public void witnessTransaction() {
String hex;
@ -464,14 +503,14 @@ public class TransactionTest {
public void testAddSignedInputThrowsExceptionWhenScriptIsNotToRawPubKeyAndIsNotToAddress() {
ECKey key = new ECKey();
Address addr = LegacyAddress.fromKey(UNITTEST, key);
Transaction fakeTx = FakeTxBuilder.createFakeTx(UNITTEST, Coin.COIN, addr);
TransactionOutput fakeOutput = FakeTxBuilder.createFakeTx(UNITTEST, Coin.COIN, addr).getOutput(0);
Transaction tx = new Transaction(UNITTEST);
tx.addOutput(fakeTx.getOutput(0));
tx.addOutput(fakeOutput);
Script script = ScriptBuilder.createOpReturnScript(new byte[0]);
tx.addSignedInput(fakeTx.getOutput(0).getOutPointFor(), script, key);
tx.addSignedInput(fakeOutput.getOutPointFor(), script, fakeOutput.getValue(), key);
}
@Test