Guide

Decode a Bitcoin relative lock-time with Python

Read and construct BIP 68 nSequence values for block-based and time-based relative lock-times, with a disposable Python fixture and clear limits.

8 min readTransactions
Decode a Bitcoin relative lock-time with Python

What you will do

A Bitcoin transaction input has a 32-bit nSequence field. For transactions with version 2 or higher, BIP 68 can interpret part of that field as a relative lock-time. The input then becomes valid only after a number of blocks, or after a number of 512-second intervals, relative to the output it spends.

This guide builds a small Python decoder and encoder for those values. You will test block delays, time delays, the disable flag, incompatible delay types, and the maximum supported range. The fixture uses synthetic integers only. It does not create keys, spend coins, connect to a node, or broadcast a transaction.

Relative lock-time is useful in protocols such as payment channels and recovery paths, but a correct integer is only one part of a valid transaction. Script conditions, transaction version, confirmation history, mempool policy, and chain state still matter.

Prerequisites

You need:

  • Python 3.10 or newer
  • a text editor and terminal
  • basic familiarity with hexadecimal integers and bit masks

The fixture was checked with the Python standard library. The consensus references were checked against Bitcoin Core v32.1, released on 28 October 2026. You do not need Bitcoin Core to run the local calculation.

Understand the field before encoding it

BIP 68 assigns several meanings to bits in nSequence:

  • Bit 31 is the disable flag. If it is set, BIP 68 relative lock-time is not enforced for that input.
  • Bit 22 selects the unit. A clear bit means blocks. A set bit means intervals of 512 seconds.
  • The lowest 16 bits carry the relative value.
  • Other bits are not part of the relative value and should not be treated as extra delay.

The 16-bit mask limits a block delay to 65,535 blocks. The largest time value is 65,535 multiplied by 512 seconds, or 33,553,920 seconds. The time form is deliberately coarse. It cannot express an arbitrary number of seconds exactly.

Bitcoin Core applies sequence locks only when the transaction version is at least 2. Its consensus code compares a block-based lock with the height of the output being spent. For a time-based lock, it uses the median time past of the block before the output's confirmation block. That is not the same as the current wall clock.

Create the decoder

Save the following constants and function in bip68_relative_locktime.py:

SEQUENCE_LOCKTIME_DISABLE_FLAG = 1 << 31
SEQUENCE_LOCKTIME_TYPE_FLAG = 1 << 22
SEQUENCE_LOCKTIME_MASK = 0x0000FFFF
SEQUENCE_LOCKTIME_GRANULARITY = 9

def decode_sequence(sequence: int) -> dict:
    if not 0 <= sequence <= 0xFFFFFFFF:
        raise ValueError("sequence must fit in an unsigned 32-bit integer")

    if sequence & SEQUENCE_LOCKTIME_DISABLE_FLAG:
        return {"enabled": False, "kind": "disabled"}

    units = sequence & SEQUENCE_LOCKTIME_MASK
    if sequence & SEQUENCE_LOCKTIME_TYPE_FLAG:
        return {
            "enabled": True,
            "kind": "time",
            "units": units,
            "seconds": units << SEQUENCE_LOCKTIME_GRANULARITY,
        }

    return {
        "enabled": True,
        "kind": "blocks",
        "units": units,
        "blocks": units,
    }

The order matters. Check bit 31 before interpreting the low bits. The common final sequence value 0xffffffff has every low bit set, but it disables relative lock-time. Reading only the low 16 bits would incorrectly report a delay of 65,535 units.

For time-based values, shifting left by 9 is equivalent to multiplying by 512. A stored value of 8 therefore represents 4,096 seconds, not 8 seconds.

Encode block and time delays

A block delay fits directly in the low 16 bits:

def encode_blocks(blocks: int) -> int:
    if not 0 <= blocks <= 0xFFFF:
        raise ValueError("blocks must be between 0 and 65535")
    return blocks

For a requested minimum time, round up to the next 512-second interval:

def encode_seconds(minimum_seconds: int) -> int:
    if minimum_seconds < 0:
        raise ValueError("minimum_seconds cannot be negative")
    quantum = 1 << 9
    units = (minimum_seconds + quantum - 1) // quantum
    if units > 0xFFFF:
        raise ValueError("requested delay exceeds BIP 68's time range")
    return (1 << 22) | units

Rounding down could produce a sequence that permits spending earlier than the requested minimum. For example, one hour is 3,600 seconds. Seven intervals represent 3,584 seconds, which is too short. Eight intervals represent 4,096 seconds, so the encoded minimum becomes 68 minutes and 16 seconds.

This rounding rule is appropriate when the input value is a minimum delay. If you are decoding an existing transaction, report the exact encoded interval instead of replacing it with a friendlier approximation.

Run disposable checks

The repository fixture evaluates these cases:

assert decode_sequence(encode_blocks(144))["blocks"] == 144

one_hour = encode_seconds(3600)
assert decode_sequence(one_hour)["seconds"] == 4096

maximum = (1 << 22) | 0xFFFF
assert decode_sequence(maximum)["seconds"] == 33_553_920

assert decode_sequence(0xFFFFFFFF)["enabled"] is False

Run it with:

python3 bip68_relative_locktime.py

Expected results include:

  • 144 blocks encoded as 0x00000090
  • one requested hour encoded as eight time units, producing 4,096 seconds
  • the maximum time value encoded as 0x0040ffff
  • 0xffffffff reported as disabled
  • a block request above 65,535 rejected instead of truncated
  • a block-based requirement rejected as incompatible with a time-based candidate

The type mismatch check is important for OP_CHECKSEQUENCEVERIFY reasoning. A larger raw integer is not necessarily a compatible lock. The block and time flags must describe the same kind before their masked values can be compared.

Connect the value to transaction behavior

Encoding a sequence value does not prove that a future transaction will be accepted. Check all of the following when applying the result:

  1. The spending transaction uses version 2 or higher.
  2. The selected input has bit 31 clear.
  3. The output being spent has the confirmation height or time history required for evaluation.
  4. A script using OP_CHECKSEQUENCEVERIFY requests a compatible block or time type.
  5. Every other input is also mature under its own sequence lock.
  6. The transaction satisfies current consensus rules and node policy.

BIP 112 gives OP_CHECKSEQUENCEVERIFY a script-level role. The opcode checks the operand against the input's nSequence field and fails for incompatible types or insufficient values. It does not wait or count time itself. BIP 68 supplies the transaction-level interpretation used by sequence-lock validation.

For a wallet or protocol implementation, test with a disposable regtest chain before using the logic around real funds. Mine controlled blocks, record the output height, attempt the spend before maturity, and then try again after the required interval. This guide did not perform that node-backed test, so it does not claim mempool or block acceptance.

Troubleshooting

The decoded delay is zero

Zero is a valid masked value. It does not add a relative delay, although other transaction and script rules still apply. Confirm that you did not expect a different input or byte order.

A one-hour request becomes 4,096 seconds

That is expected. BIP 68 time values use 512-second granularity. The encoder rounds up so the represented minimum is not shorter than requested.

A very large value appears to be disabled

Inspect bit 31 first. Values such as 0xffffffff opt out of BIP 68 evaluation even though their low 16 bits are nonzero.

A CSV comparison fails even though the candidate number is larger

Compare the type flag, then compare the masked 16-bit values. A time-based candidate cannot satisfy a block-based script operand, and a block-based candidate cannot satisfy a time-based operand.

The transaction is still non-final

Relative sequence locks are only one source of finality constraints. Check absolute nLockTime, every input, coinbase maturity where relevant, script execution, fee policy, and the node's current chain view.

Limits and next step

The fixture validates bit operations, rounding, ranges, and negative controls against the published BIP definitions and Bitcoin Core v32.1 constants. It does not parse serialized transactions, inspect a live UTXO, execute Script, run Bitcoin Core's functional suite, or submit anything to a mempool. Median time past also means a time-based delay is a consensus-chain measurement, not a promise about a wall-clock deadline.

Use the decoder as a review aid, not as an authorization to spend. For production code, keep the upstream constant names, add transaction-version and UTXO context, and reproduce the behavior on regtest before handling value.

Primary sources

Newsletter

Bitcoin, without the noise

What happened in Bitcoin, what it actually changes, and the sources so you can check us. One issue at a time, straight to your inbox.

  • One email per issue, never a drip campaign
  • No tracking pixels and no shared addresses
  • Unsubscribe from any issue in one click

Get the next issue

One email per issue, no tracking pixels, and unsubscribe from any of them. We do not share your address. Privacy policy