Skip to content

Commit

Permalink
Transaction: support segwit in addSignedInput() methods
Browse files Browse the repository at this point in the history
Add addSignedInput() variants to Transaction that provide input value,
add checks and logs.
Add simple transaction building tests to TransactionTest.
This also updates some tests to not use methods that will be deprecated
in the next major release.
  • Loading branch information
msgilligan authored and Andreas Schildbach committed Nov 20, 2022
1 parent cecdb64 commit 4ab757a
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 26 deletions.
80 changes: 67 additions & 13 deletions core/src/main/java/org/bitcoinj/core/Transaction.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
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;

Expand Down Expand Up @@ -990,13 +991,23 @@ public TransactionInput addInput(Sha256Hash spendTxHash, long outputIndex, Scrip
* 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.
*/
public TransactionInput addSignedInput(TransactionOutPoint prevOut, Script scriptPubKey, ECKey sigKey,
* @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, 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)) {
Expand All @@ -1022,27 +1033,70 @@ public TransactionInput addSignedInput(TransactionOutPoint prevOut, Script scrip
}

/**
* 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.
*/
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.
*/
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.
* 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 signingKey) {
return addSignedInput(output.getOutPointFor(), output.getScriptPubKey(), signingKey);
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.
*/
public TransactionInput addSignedInput(TransactionOutput output, ECKey signingKey, SigHash sigHash, boolean anyoneCanPay) {
return addSignedInput(output.getOutPointFor(), output.getScriptPubKey(), signingKey, sigHash, anyoneCanPay);
* @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 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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,21 +183,22 @@ public void testFinalizedBlocks() throws Exception {
// 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();

Expand Down Expand Up @@ -257,8 +258,9 @@ public void testGetOpenTransactionOutputs() throws Exception {
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);
Expand All @@ -273,7 +275,7 @@ public void testGetOpenTransactionOutputs() throws Exception {

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);
Expand Down Expand Up @@ -308,8 +310,9 @@ public void testUTXOProviderWithWallet() throws Exception {
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);
Expand All @@ -327,7 +330,7 @@ public void testUTXOProviderWithWallet() throws Exception {

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);
Expand Down
45 changes: 42 additions & 3 deletions core/src/test/java/org/bitcoinj/core/TransactionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,45 @@ public void testIsMatureReturnsFalseIfTransactionIsCoinbaseAndConfidenceTypeIsNo
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;
Expand Down Expand Up @@ -464,14 +503,14 @@ public void testTheTXByHeightComparator() {
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
Expand Down

0 comments on commit 4ab757a

Please sign in to comment.