TransactionInput: make field sequence immutable

Because tweaking is necessary in cases like unit tests, these usages
have been changed to produce new inputs instead and replace them in
transactions as needed.
This commit is contained in:
Andreas Schildbach 2025-02-06 11:10:46 +01:00
parent 1dc99408bc
commit 7fdef658a7
11 changed files with 71 additions and 34 deletions

View file

@ -1013,6 +1013,19 @@ public class Transaction extends BaseMessage {
return addSignedInput(output.getOutPointFor(), output.getScriptPubKey(), output.getValue(), sigKey, sigHash, anyoneCanPay);
}
/**
* Replaces an already added input. This is meant to amend a transaction before it's committed to a wallet.
*
* @param index index of input to replace
* @param input input to replace with
*/
public void replaceInput(int index, TransactionInput input) {
TransactionInput oldInput = inputs.remove(index);
oldInput.setParent(null);
input.setParent(this);
inputs.add(index, input);
}
/**
* Removes all the outputs from this transaction.
* Note that this also invalidates the length attribute
@ -1239,7 +1252,7 @@ public class Transaction extends BaseMessage {
// The signature isn't broken by new versions of the transaction issued by other parties.
for (int i = 0; i < tx.inputs.size(); i++)
if (i != inputIndex)
tx.inputs.get(i).setSequenceNumber(0);
tx.replaceInput(i, tx.getInput(i).withSequence(0));
} else if ((sigHashType & 0x1f) == SigHash.SINGLE.value) {
// SIGHASH_SINGLE means only sign the output at the same index as the input (ie, my output).
if (inputIndex >= tx.outputs.size()) {
@ -1261,7 +1274,7 @@ public class Transaction extends BaseMessage {
// The signature isn't broken by new versions of the transaction issued by other parties.
for (int i = 0; i < tx.inputs.size(); i++)
if (i != inputIndex)
tx.inputs.get(i).setSequenceNumber(0);
tx.replaceInput(i, tx.getInput(i).withSequence(0));
}
if ((sigHashType & SigHash.ANYONECANPAY.value) == SigHash.ANYONECANPAY.value) {

View file

@ -73,7 +73,7 @@ public class TransactionInput {
@Nullable private Transaction parent;
// Allows for altering transactions after they were broadcast. Values below NO_SEQUENCE-1 mean it can be altered.
private long sequence;
private final long sequence;
// Data needed to connect to the output of the transaction we're gathering coins from.
private TransactionOutPoint outpoint;
// The "script bytes" might not actually be a script. In coinbase transactions where new coins are minted there
@ -123,19 +123,33 @@ public class TransactionInput {
this(parentTransaction, scriptBytes, outpoint, NO_SEQUENCE, null);
}
public TransactionInput(@Nullable Transaction parentTransaction, byte[] scriptBytes,
TransactionOutPoint outpoint, long sequence) {
this(parentTransaction, scriptBytes, outpoint, sequence, null);
}
public TransactionInput(@Nullable Transaction parentTransaction, byte[] scriptBytes,
TransactionOutPoint outpoint, @Nullable Coin value) {
this(parentTransaction, scriptBytes, outpoint, NO_SEQUENCE, value);
}
private TransactionInput(@Nullable Transaction parentTransaction, byte[] scriptBytes,
TransactionOutPoint outpoint, long sequence, @Nullable Coin value) {
/** internal use only */
public TransactionInput(Transaction parentTransaction, byte[] scriptBytes, TransactionOutPoint outpoint,
long sequence, @Nullable Coin value) {
this(parentTransaction, null, scriptBytes, outpoint, sequence, value, null);
}
private TransactionInput(@Nullable Transaction parentTransaction, @Nullable Script scriptSig, byte[] scriptBytes,
TransactionOutPoint outpoint, long sequence, @Nullable Coin value,
@Nullable TransactionWitness witness) {
checkArgument(value == null || value.signum() >= 0, () -> "value out of range: " + value);
parent = parentTransaction;
this.scriptBytes = scriptBytes;
this.outpoint = outpoint;
this.scriptSig = scriptSig != null ? new WeakReference<>(scriptSig) : null;
this.scriptBytes = Objects.requireNonNull(scriptBytes);
this.outpoint = Objects.requireNonNull(outpoint);
this.sequence = sequence;
this.value = value;
this.witness = witness;
}
/**
@ -143,12 +157,13 @@ public class TransactionInput {
*/
TransactionInput(Transaction parentTransaction, TransactionOutput output) {
this(parentTransaction,
EMPTY_ARRAY,
null, EMPTY_ARRAY,
output.getParentTransaction() != null ?
new TransactionOutPoint(output.getIndex(), output.getParentTransaction()) :
new TransactionOutPoint(output),
NO_SEQUENCE,
output.getValue());
output.getValue(),
null);
}
/**
@ -253,15 +268,22 @@ public class TransactionInput {
}
/**
* Returns a clone of this input, with a given sequence number.
* <p>
* Sequence numbers allow participants in a multi-party transaction signing protocol to create new versions of the
* transaction independently of each other. Newer versions of a transaction can replace an existing version that's
* in nodes memory pools if the existing version is time locked. See the Contracts page on the Bitcoin wiki for
* examples of how you can use this feature to build contract protocols.
*
* @param sequence sequence number for the clone
* @return clone of input, with given sequence number
*/
public void setSequenceNumber(long sequence) {
public TransactionInput withSequence(long sequence) {
checkArgument(sequence >= 0 && sequence <= ByteUtils.MAX_UNSIGNED_INTEGER, () ->
"sequence out of range: " + sequence);
this.sequence = sequence;
Script scriptSig = this.scriptSig != null ? this.scriptSig.get() : null;
return new TransactionInput(this.parent, scriptSig, this.scriptBytes, this.outpoint, sequence, this.value,
this.witness);
}
/**

View file

@ -670,9 +670,8 @@ public class WalletProtobufSerializer {
byteStringToHash(inputProto.getTransactionOutPointHash())
);
Coin value = inputProto.hasValue() ? Coin.valueOf(inputProto.getValue()) : null;
TransactionInput input = new TransactionInput(tx, scriptBytes, outpoint, value);
if (inputProto.hasSequence())
input.setSequenceNumber(0xffffffffL & inputProto.getSequence());
long sequence = inputProto.hasSequence() ? 0xffffffffL & inputProto.getSequence() : TransactionInput.NO_SEQUENCE;
TransactionInput input = new TransactionInput(tx, scriptBytes, outpoint, sequence, value);
if (inputProto.hasWitness()) {
Protos.ScriptWitness witnessProto = inputProto.getWitness();
if (witnessProto.getDataCount() > 0) {

View file

@ -114,7 +114,7 @@ public class BitcoinSerializerTest {
transaction = (Transaction) serializer.deserialize(ByteBuffer.wrap(TRANSACTION_MESSAGE_BYTES));
assertNotNull(transaction);
transaction.getInput(0).setSequenceNumber(1);
transaction.replaceInput(0, transaction.getInput(0).withSequence(1));
bos = new ByteArrayOutputStream();
serializer.serialize(transaction, bos);
@ -131,7 +131,8 @@ public class BitcoinSerializerTest {
transaction = (Transaction) serializer.deserialize(ByteBuffer.wrap(TRANSACTION_MESSAGE_BYTES));
assertNotNull(transaction);
transaction.getInput(0).setSequenceNumber(transaction.getInputs().get(0).getSequenceNumber());
transaction.replaceInput(0,
transaction.getInput(0).withSequence(transaction.getInput(0).getSequenceNumber())); // no-op?
bos = new ByteArrayOutputStream();
serializer.serialize(transaction, bos);

View file

@ -1193,7 +1193,7 @@ public class FullBlockTestGenerator {
NewBlock b63 = createNextBlock(b60, chainHeadHeight + 19, null, null);
{
b63.block.getTransactions().get(0).setLockTime(0xffffffffL);
b63.block.getTransactions().get(0).getInput(0).setSequenceNumber(0xdeadbeefL);
b63.block.getTransactions().get(0).replaceInput(0, b63.block.getTransactions().get(0).getInput(0).withSequence(0xdeadbeefL));
checkState(!b63.block.getTransactions().get(0).isFinal(chainHeadHeight + 17, b63.block.time()));
}
b63.solve();
@ -1797,7 +1797,7 @@ public class FullBlockTestGenerator {
private void addOnlyInputToTransaction(Transaction t, TransactionOutPointWithValue prevOut, long sequence) throws ScriptException {
TransactionInput input = new TransactionInput(t, new byte[]{}, prevOut.outpoint);
input.setSequenceNumber(sequence);
input = input.withSequence(sequence);
t.addInput(input);
if (prevOut.scriptPubKey.chunks().get(0).equalsOpCode(OP_TRUE)) {

View file

@ -482,8 +482,7 @@ public class TransactionTest {
@Test
public void testToStringWhenLockTimeIsSpecifiedInBlockHeight() {
Transaction tx = FakeTxBuilder.createFakeTx(TESTNET.network());
TransactionInput input = tx.getInput(0);
input.setSequenceNumber(42);
tx.replaceInput(0, tx.getInput(0).withSequence(42));
int TEST_LOCK_TIME = 20;
tx.setLockTime(TEST_LOCK_TIME);
@ -609,7 +608,7 @@ public class TransactionTest {
Transaction tx = FakeTxBuilder.createFakeTx(TESTNET.network());
assertFalse(tx.isOptInFullRBF());
tx.getInput(0).setSequenceNumber(TransactionInput.NO_SEQUENCE - 2);
tx.replaceInput(0, tx.getInput(0).withSequence(TransactionInput.NO_SEQUENCE - 2));
assertTrue(tx.isOptInFullRBF());
}

View file

@ -355,7 +355,7 @@ public class ScriptTest {
TransactionInput txInput = new TransactionInput(null,
new ScriptBuilder().number(0).number(0).build().program(), TransactionOutPoint.UNCONNECTED);
txInput.setSequenceNumber(TransactionInput.NO_SEQUENCE);
txInput = txInput.withSequence(TransactionInput.NO_SEQUENCE);
tx.addInput(txInput);
TransactionOutput txOutput = new TransactionOutput(tx, Coin.ZERO, scriptPubKey.program());
@ -371,7 +371,7 @@ public class ScriptTest {
TransactionInput txInput = new TransactionInput(creditingTransaction, scriptSig.program(),
TransactionOutPoint.UNCONNECTED);
txInput.setSequenceNumber(TransactionInput.NO_SEQUENCE);
txInput = txInput.withSequence(TransactionInput.NO_SEQUENCE);
tx.addInput(txInput);
TransactionOutput txOutput = new TransactionOutput(tx, creditingTransaction.getOutput(0).getValue(),

View file

@ -246,10 +246,10 @@ public class WalletProtobufSerializerTest {
public void testSequenceNumber() throws Exception {
Wallet wallet = Wallet.createDeterministic(BitcoinNetwork.TESTNET, ScriptType.P2PKH);
Transaction tx1 = createFakeTx(TESTNET.network(), Coin.COIN, wallet.currentReceiveAddress());
tx1.getInput(0).setSequenceNumber(TransactionInput.NO_SEQUENCE);
tx1.replaceInput(0, tx1.getInput(0).withSequence(TransactionInput.NO_SEQUENCE));
wallet.receivePending(tx1, null);
Transaction tx2 = createFakeTx(TESTNET.network(), Coin.COIN, wallet.currentReceiveAddress());
tx2.getInput(0).setSequenceNumber(TransactionInput.NO_SEQUENCE - 1);
tx2.replaceInput(0, tx2.getInput(0).withSequence(TransactionInput.NO_SEQUENCE - 1));
wallet.receivePending(tx2, null);
Wallet walletCopy = roundTrip(wallet);
Transaction tx1copy = Objects.requireNonNull(walletCopy.getTransaction(tx1.getTxId()));

View file

@ -90,7 +90,7 @@ public class DefaultRiskAnalysisTest {
assertNull(analysis.getNonFinal());
// Set a sequence number on the input to make it genuinely non-final. Verify it's risky.
input.setSequenceNumber(TransactionInput.NO_SEQUENCE - 1);
tx.replaceInput(0, input.withSequence(TransactionInput.NO_SEQUENCE - 1));
analysis = DefaultRiskAnalysis.FACTORY.create(wallet, tx, NO_DEPS);
assertEquals(RiskAnalysis.Result.NON_FINAL, analysis.analyze());
assertEquals(tx, analysis.getNonFinal());
@ -104,7 +104,9 @@ public class DefaultRiskAnalysisTest {
@Test
public void selfCreatedAreNotRisky() {
Transaction tx = new Transaction();
tx.addInput(MAINNET.getGenesisBlock().getTransactions().get(0).getOutput(0)).setSequenceNumber(1);
TransactionInput input =
tx.addInput(MAINNET.getGenesisBlock().getTransactions().get(0).getOutput(0)).withSequence(1);
tx.replaceInput(0, input);
tx.addOutput(COIN, key1);
tx.setLockTime(TIMESTAMP + 86400);
@ -125,7 +127,9 @@ public class DefaultRiskAnalysisTest {
public void nonFinalDependency() {
// Final tx has a dependency that is non-final.
Transaction tx1 = new Transaction();
tx1.addInput(MAINNET.getGenesisBlock().getTransactions().get(0).getOutput(0)).setSequenceNumber(1);
TransactionInput input1 =
tx1.addInput(MAINNET.getGenesisBlock().getTransactions().get(0).getOutput(0)).withSequence(1);
tx1.replaceInput(0, input1);
TransactionOutput output = tx1.addOutput(COIN, key1);
tx1.setLockTime(TIMESTAMP + 86400);
Transaction tx2 = new Transaction();
@ -239,7 +243,7 @@ public class DefaultRiskAnalysisTest {
@Test
public void optInFullRBF() {
Transaction tx = FakeTxBuilder.createFakeTx(MAINNET.network());
tx.getInput(0).setSequenceNumber(TransactionInput.NO_SEQUENCE - 2);
tx.replaceInput(0, tx.getInput(0).withSequence(TransactionInput.NO_SEQUENCE - 2));
DefaultRiskAnalysis analysis = DefaultRiskAnalysis.FACTORY.create(wallet, tx, NO_DEPS);
assertEquals(RiskAnalysis.Result.NON_FINAL, analysis.analyze());
assertEquals(tx, analysis.getNonFinal());
@ -251,11 +255,11 @@ public class DefaultRiskAnalysisTest {
tx.setVersion(2);
checkState(!tx.hasRelativeLockTime());
tx.getInput(0).setSequenceNumber(TransactionInput.NO_SEQUENCE);
tx.replaceInput(0, tx.getInput(0).withSequence(TransactionInput.NO_SEQUENCE));
DefaultRiskAnalysis analysis = DefaultRiskAnalysis.FACTORY.create(wallet, tx, NO_DEPS);
assertEquals(RiskAnalysis.Result.OK, analysis.analyze());
tx.getInput(0).setSequenceNumber(0);
tx.replaceInput(0, tx.getInput(0).withSequence(0));
analysis = DefaultRiskAnalysis.FACTORY.create(wallet, tx, NO_DEPS);
assertEquals(RiskAnalysis.Result.NON_FINAL, analysis.analyze());
assertEquals(tx, analysis.getNonFinal());

View file

@ -732,8 +732,7 @@ public class PeerTest extends TestWithNetworkConnections {
t2.setLockTime(999999);
// Add a fake input to t3 that goes nowhere.
Sha256Hash t3 = Sha256Hash.of("abc".getBytes(StandardCharsets.UTF_8));
t2.addInput(new TransactionInput(t2, new byte[]{}, new TransactionOutPoint(0, t3)));
t2.getInput(0).setSequenceNumber(0xDEADBEEF);
t2.addInput(new TransactionInput(t2, new byte[] {}, new TransactionOutPoint(0, t3), 0xDEADBEEF));
t2.addOutput(COIN, new ECKey());
Transaction t1 = new Transaction();
t1.addInput(t2.getOutput(0));

View file

@ -664,7 +664,7 @@ public class WalletTool implements Callable<Integer> {
if (lockTimeStr != null) {
tx.setLockTime(parseLockTimeStr(lockTimeStr));
// For lock times to take effect, at least one output must have a non-final sequence number.
tx.getInput(0).setSequenceNumber(0);
tx.replaceInput(0, tx.getInput(0).withSequence(0));
// And because we modified the transaction after it was completed, we must re-sign the inputs.
wallet.signTransaction(req);
}