Lock time, sequence numbers and versions
Three fields control when, and under which rules, a Transaction is valid. These are nLockTime on the
Transaction, a sequence number on each Input, and the Transaction’s version number. The Developer Guide’s
transaction format page explains what lock time and
sequence numbers mean. The examples use the setup from
the Transaction builder.
All three are covered by the Signatures. The TransactionBuilder sets all three before it signs, so in most
cases we simply tell the builder what we want. When we change a field ourselves, the pattern is always the
same: we build without signers, change the fields, and then sign.
Setting the lock time
Section titled “Setting the lock time”// Below 500,000,000 the lock time is a block height. For a Unix timestamp, we use lockUntilUnixTime().Transaction tx = new TransactionBuilder() .spendFromTransaction(new TransactionSigner(SIGHASH, alice), fundingTx, 0, TransactionInput.MAX_SEQ_NUMBER, new P2PKHUnlockBuilder(alice.getPublicKey())) .spendTo(new P2PKHLockBuilder(bobAddress), BigInteger.valueOf(50_000)) .sendChangeTo(aliceAddress) .lockUntilBlockHeight(900_000) .build(true);
// The builder took our Input out of its final state, so that the lock time counts.assertEquals(900_000, tx.getLockTime());assertEquals(TransactionInput.MAX_SEQ_NUMBER - 1, tx.getInputs().get(0).getSequenceNumber());It should be noted that nLockTime only has an effect if at least one Input has a sequence number below
0xFFFFFFFF (TransactionInput.MAX_SEQ_NUMBER). With every Input at the maximum, the Transaction is final,
whatever the lock time says. For that reason, when we set a lock time, the builder lowers any Input at the
maximum to 0xFFFFFFFE, and leaves the other sequence numbers as we gave them.
Transaction.LOCKTIME_THRESHOLD(500,000,000) is the boundary between block heights and timestamps.
Changing sequence numbers
Section titled “Changing sequence numbers”The builder takes each Input’s sequence number as an argument of spendFrom...(). To change one afterwards,
we set it before signing.
Transaction tx = new TransactionBuilder() .spendFromTransaction(fundingTx, 0, TransactionInput.MAX_SEQ_NUMBER, new P2PKHUnlockBuilder(alice.getPublicKey())) .spendTo(new P2PKHLockBuilder(bobAddress), BigInteger.valueOf(50_000)) .sendChangeTo(aliceAddress) .build(true);
// We change the sequence number before signing.tx.getInputs().get(0).setSequenceNumber(0);new TransactionSigner(SIGHASH, alice).sign(tx, utxo, 0);TransactionInput.isFinal() tells us whether an Input’s sequence number is at the maximum.
TransactionInput finalInput = new TransactionInput(new byte[32], 0, TransactionInput.MAX_SEQ_NUMBER, new DefaultUnlockBuilder());TransactionInput lockedInput = new TransactionInput(new byte[32], 0, TransactionInput.MAX_SEQ_NUMBER - 1, new DefaultUnlockBuilder());
assertTrue(finalInput.isFinal());assertFalse(lockedInput.isFinal());Setting the version number
Section titled “Setting the version number”New Transactions are version 2, which is the version BSV uses today. Under the Chronicle rules the version
matters to Script. Version 2 and up relaxes some malleability checks, and the OP_VER family of opcodes reads
it. We choose the version with withTxVersion().
// New Transactions are version 2. withTxVersion() gives us another version, before the builder signs.Transaction tx = new TransactionBuilder() .spendFromTransaction(new TransactionSigner(SIGHASH, alice), fundingTx, 0, TransactionInput.MAX_SEQ_NUMBER, new P2PKHUnlockBuilder(alice.getPublicKey())) .spendTo(new P2PKHLockBuilder(bobAddress), BigInteger.valueOf(50_000)) .sendChangeTo(aliceAddress) .withTxVersion(1) .build(true);Please note that a version 1 Transaction spending the Outputs of version 2 Transactions can break those rules, so change the version only when you have a reason to.
Change first, sign last
Section titled “Change first, sign last”A Signature commits to the version, the lock time and the sequence numbers. If we change any of them after signing, the Signature no longer verifies.
// Changing the Transaction after signing invalidates the Signature.Transaction tx = new TransactionBuilder() .spendFromTransaction(new TransactionSigner(SIGHASH, alice), fundingTx, 0, TransactionInput.MAX_SEQ_NUMBER, new P2PKHUnlockBuilder(alice.getPublicKey())) .spendTo(new P2PKHLockBuilder(bobAddress), BigInteger.valueOf(50_000)) .sendChangeTo(aliceAddress) .build(true); // signed here
tx.setLockTime(900_000);assertThrows(ScriptException.class, () -> Fixtures.verifySpend(tx, 0, utxo));Signing again does not fix it. The unlocker keeps every Signature it is given, and uses the first one.
// Signing an Input again adds a second Signature to its unlocker, and P2PKHUnlockBuilder// keeps using the first one. We make all our changes first, and sign each Input once.P2PKHUnlockBuilder unlocker = new P2PKHUnlockBuilder(alice.getPublicKey());Transaction tx = new TransactionBuilder() .spendFromTransaction(fundingTx, 0, TransactionInput.MAX_SEQ_NUMBER - 1, unlocker) .spendTo(new P2PKHLockBuilder(bobAddress), BigInteger.valueOf(50_000)) .sendChangeTo(aliceAddress) .build(true);TransactionSigner signer = new TransactionSigner(SIGHASH, alice);signer.sign(tx, utxo, 0);
tx.setLockTime(900_000);signer.sign(tx, utxo, 0);
assertEquals(2, unlocker.getSignatures().size());assertThrows(ScriptException.class, () -> Fixtures.verifySpend(tx, 0, utxo));If we must re-sign, we replace the Input with one that has a fresh unlocker
(tx.replaceInput(index, new TransactionInput(...))), and then sign that.

