Skip to content

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.

// 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.

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());

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.

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.