Skip to content

Time lock vault smart contract

The time lock vault is a non-interactive smart contract that locks coins to an address until a committed unlock value — a block height or, at/above the BIP-65 500,000,000 split, a Unix timestamp — is reached, after which only the holder of a specific private key can spend them.


1. What problem does it solve?

A user wants to lock some coins away from themselves until a future point in time — a self-imposed time vault. The locked value can be native Nexa, group tokens, or authority batons. The constraint must be enforced by the blockchain, not by the wallet (the wallet can be reinstalled, replaced, or compromised; only the chain is authoritative).

The time lock vault produces an on-chain address with two spend conditions:

  1. Time. The spending transaction's nLockTime field must be at or past a committed unlock value — a block height or, at/above the BIP-65 split, a Unix timestamp (§3.4).
  2. Identity. The spending transaction must carry a valid signature for a committed public key.

Both conditions must hold. Coins sent to such an address cannot be moved by anyone — including the original sender — until the unlock value has been reached, and then only by the holder of the matching private key.

The contract is non-interactive: it does not involve a counterparty, has no formation handshake, and cannot be revoked once funded.

A single contract is conceptually a factory of vaults: each successive HD leaf index produces a distinct vault, optionally at a distinct unlock value. Advancing the index and rebuilding the destination at a new unlock value are independent operations; together they let a user open as many independent time-locked vaults as they like from one seed.


2. Prerequisites — concepts the rest of the doc relies on

Term One-line definition
UTXO An unspent transaction output. A wallet's balance is the sum of UTXOs it controls.
Locking script (scriptPubKey) Bytecode attached to a UTXO that specifies the conditions for spending it.
Satisfier / unlocking script Bytecode supplied by the spender that, combined with the locking script, must evaluate to "true".
Hash160 RIPEMD160(SHA256(x)) — the standard 20-byte commitment scheme used in Bitcoin-family chains.
CLTV OP_CHECKLOCKTIMEVERIFY: a script opcode that fails the script unless the spending transaction's nLockTime is ≥ a stack-supplied value.
nLockTime A 32-bit field on every transaction. A value < 500,000,000 is a block height (the tx is rejected from blocks below it); a value at/above that split is a Unix timestamp gated on chain time (§3.4). Required for CLTV to take effect.
nSequence A 32-bit field on every input. Setting it to 0xFFFFFFFF (SEQUENCE_FINAL) tells the consensus engine to ignore nLockTime entirely — which also disables CLTV.
BIP-44 / HD Hierarchical deterministic key derivation: a single seed → a tree of child keys, addressed by integer indices.
Pay-to-Template (P2T) Nexa's address scheme: the address commits to hashes of a template script and a constraint script; the bytecode itself is revealed only at spend time.

3. The on-chain layout — Pay-to-Template, template + constraint + visible args

A time lock UTXO's locking script has the structure shared by every Nexa Pay-to-Template (P2T) output:

locking script = [ tag ] [ template-hash ] [ constraint-hash ] [ visible args... ]
                 │       │                  │                   │
                 │       │                  │                   └── cleartext per-vault
                 │       │                  │                       parameters
                 │       │                  │
                 │       │                  └── hash160 of a constraint script that holds
                 │       │                       per-vault values (here: the owner pubkey)
                 │       │
                 │       └── hash160 of the script that defines spend logic
                 │           (here: CLTV + CHECKSIGVERIFY)
                 │
                 └── version tag (OP_0 + 20-byte length prefix)

The 20-byte hashes commit to bytecode that is not published on-chain at funding time. The spender reveals the full bytecode in the satisfier at spend time, and the network verifies the bytes hash to the committed values before executing them. This keeps funding outputs compact and identical across all vaults; only the constraint hash and visible args change per vault.

Concrete bytes for a time lock output:

Field Source Value
Tag constant 001400 is OP_0 used as a P2T version tag, 14 is the literal 20-byte push opcode (0x14 = 20) that introduces the template hash
Template hash constant 461ad25081cb0119d034385ff154c8d3ad6bdd76 (hash160 of the template script below)
Constraint hash per-owner hash160 of OP_PUSH(pubkey)
Visible args per-vault OP_PUSH(unlockValue), OP_PUSH(hdIndex)

The prefix 0014461ad2…dd76 is fixed across every time lock vault on the network. A wallet uses it during chain scanning to recognise time lock outputs.

3.1 The template script — spend logic

The template script is the same for every time lock vault on the network. It runs only at spend time and decides whether the spend is allowed.

FROMALTSTACK DROP            ; pop derivation index, ignore it
FROMALTSTACK CLTV DROP       ; pop unlock value, require nLockTime ≥ it
FROMALTSTACK CHECKSIGVERIFY  ; pop pubkey, verify the satisfier's signature

The altstack at the start of execution contains, top to bottom: derivation index, unlock value, pubkey. The visible args (index, unlock value) are pushed onto the altstack by P2T framing; the pubkey comes from the constraint script.

CHECKLOCKTIMEVERIFY is the time gate; CHECKSIGVERIFY is the identity gate. The derivation index is committed in the locking script (and therefore in the address) but plays no role in script evaluation — it exists purely so a wallet can re-derive the owning private key during vault rediscovery. See section 5.2.

Stack evolution during execution

Nexa script has two stacks: the main stack (usually just called "the stack") where most opcodes operate, and the alt stack for parking values, moved between the two with TOALTSTACK / FROMALTSTACK. The satisfier (scriptSig) places the signature on the main stack; P2T framing places [derivation_index, unlock_value, pubkey] on the alt stack before the template runs.

Stacks are drawn vertically with the top of the stack at the top:

Initial state (just before template runs)
─────────────────────────────────────────
  main                  alt
  ┌──────────────────┐  ┌──────────────────┐  ← top
  │       sig        │  │ derivation_index │
  └──────────────────┘  ├──────────────────┤
                        │   unlock_value   │
                        ├──────────────────┤
                        │      pubkey      │
                        └──────────────────┘


① FROMALTSTACK   — pop alt, push to main
─────────────────────────────────────────
  main                  alt
  ┌──────────────────┐  ┌──────────────────┐
  │ derivation_index │  │   unlock_value   │
  ├──────────────────┤  ├──────────────────┤
  │       sig        │  │      pubkey      │
  └──────────────────┘  └──────────────────┘


② DROP           — discard top of main (we don't need the index here)
─────────────────────────────────────────
  main                  alt
  ┌──────────────────┐  ┌──────────────────┐
  │       sig        │  │   unlock_value   │
  └──────────────────┘  ├──────────────────┤
                        │      pubkey      │
                        └──────────────────┘


③ FROMALTSTACK   — pop alt, push to main
─────────────────────────────────────────
  main                  alt
  ┌──────────────────┐  ┌──────────────────┐
  │   unlock_value   │  │      pubkey      │
  ├──────────────────┤  └──────────────────┘
  │       sig        │
  └──────────────────┘


④ CLTV           — assert tx.nLockTime ≥ top-of-main; stacks unchanged
─────────────────────────────────────────                ← time gate passed
  main                  alt
  ┌──────────────────┐  ┌──────────────────┐
  │   unlock_value   │  │      pubkey      │
  ├──────────────────┤  └──────────────────┘
  │       sig        │
  └──────────────────┘


⑤ DROP           — discard unlock_value now that CLTV is satisfied
─────────────────────────────────────────
  main                  alt
  ┌──────────────────┐  ┌──────────────────┐
  │       sig        │  │      pubkey      │
  └──────────────────┘  └──────────────────┘


⑥ FROMALTSTACK   — pop alt, push to main
─────────────────────────────────────────
  main                  alt
  ┌──────────────────┐
  │      pubkey      │  (empty)
  ├──────────────────┤
  │       sig        │
  └──────────────────┘


⑦ CHECKSIGVERIFY — pops pubkey, then sig; verifies; aborts on failure
─────────────────────────────────────────                ← identity gate passed
  main: (empty)         alt: (empty)

A few things worth noticing from the trace:

  • The alt stack drains top-down in the order it was loaded: index → unlock value → pubkey. That order is why the template can be written as three nearly-identical FROMALTSTACK … lines.
  • CLTV is non-consuming — it inspects the top of the main stack but leaves it there, which is why each FROMALTSTACK is followed by an explicit DROP (except the last, where CHECKSIGVERIFY consumes both items itself).
  • The derivation index is loaded onto the alt stack and immediately dropped — it does nothing at spend time. It exists purely so wallet rediscovery can read it from the locking script (see section 5.2).
  • The script ends with both stacks empty. CHECKSIGVERIFY (vs plain CHECKSIG) means a bad signature aborts the script outright rather than leaving a false on top, so reaching the end implies both gates passed.

3.2 The constraint script — per-vault data

For time lock, the constraint script is one instruction: push the owner's compressed (33-byte) public key. Its hash160 is what gets committed in the locking script (and therefore in the address), so two vaults with different owners have different addresses even if their unlock value and derivation index match.

3.3 Visible args — why they are cleartext

The visible args (unlockValue, hdIndex) sit in the locking script as ordinary script pushes, not behind a hash. They have to be visible because the template script reads them at spend time (via FROMALTSTACK). Their cleartext presence is also load-bearing for wallet discovery: when scanning the chain after a wallet restore, the wallet reads these values directly from the UTXO to figure out which HD child key controls it, instead of having to brute-force every possible index.

Encoding uses the smallest valid push form: a single-byte OP_C0OP_C16 opcode for values 0..16, and a length-prefixed script-number push for anything larger. Timestamp unlock values (≥ 500,000,000) always take the script-number path, and in practice height values do too — but the creation range (§3.4) admits heights 1..16, which encode as the single-byte compact form. Implementations must apply the smallest-form rule uniformly to both visible args rather than hard-coding the script-number form for the unlock value: the hdIndex routinely uses both forms (indices 0..16 compact, 17 and above script-number), and a height vault at 1..16 must reproduce the compact form to derive the correct address. This encoding is part of the on-chain address — any change to it would break compatibility with existing vaults.

3.4 The unlock value — block height vs. Unix timestamp

CHECKLOCKTIMEVERIFY (and nLockTime) reads the committed unlock value two ways, split at 500,000,000 — the standard LOCKTIME_THRESHOLD from BIP-65:

  • a value below 500,000,000 is a block height — the spend becomes final once the chain reaches that height;
  • a value at or above 500,000,000 is a Unix timestamp (epoch seconds) — the spend becomes final once the chain's median time passes it (§6).

The split works because 500,000,000 seconds is 1985-11-05 (always in the past for a timestamp) while block 500,000,000 is many millennia away (out of reach for a height), so every value has exactly one meaning. The template script is identical on both sides — a timestamp vault is simply a vault whose committed value sits on the other side of the split, with no script-level flag or variant.

A vault-creating wallet must keep new commitments inside the satisfiable range of the intended side:

  • Heights: 1 … 499,999,999. A "height" at or above the split would actually lock to a timestamp.
  • Timestamps: 500,000,000 … 0xFFFFFFFD. Below the split the value would be read as a height. The ceiling sits two below the uint32 cap because of two stacked walls: the node accepts a timestamp nLockTime only when it is strictly below its median-time cutoff (§6), and the block that would confirm the claim must itself carry a uint32 timestamp strictly above the previous block's median-time-past (the time-too-old consensus rule) — so the highest cutoff any minable block can ever be checked against is 0xFFFFFFFE, and the highest commitment that can ever confirm is 0xFFFFFFFD. Coins committed above that could never be claimed and would be stuck forever.

Recovery is deliberately more liberal than creation: a wallet re-deriving an existing on-chain vault must reproduce any positive committed value, on either side of the split and with no upper bound — including values above the creation ceilings, such as the observed 2147483646 (a claimable 2038-era timestamp) and even values above 0xFFFFFFFD (unclaimable, but still recognisable as owned) — so the recomputed locking script can be matched byte-for-byte against the on-chain output (§5.2). Non-positive committed values are outside the recoverable domain: no conforming creator can produce them, and such outputs are degenerate on-chain (a zero lock is trivially satisfied, while CLTV unconditionally fails a negative argument), so wallets are not required to recover them — the reference implementation refuses to construct a vault for them on every path: creation, recovery, and reload alike.

Interoperability note. Vaults committing 2147483646 (2³¹ − 2) have been observed on mainnet. Under the split that value is a timestamp — roughly 2038-01-19, the 32-bit time_t boundary — not a far-future block height; such a vault becomes spendable once the chain's median time passes that point. Height-only wallets handle timestamp vaults poorly: the Otoplo wallet (as of this writing) creates vaults exclusively by block height — its "Future Date" option converts the date to an estimated height — displays any committed value as "Locked until block N", keeps the claim action disabled while the block-height tip is below the committed value (for a timestamp, effectively forever), and builds claims with nLockTime set to the current tip height, which for a timestamp vault sits on the wrong side of the split and cannot satisfy CLTV. Wallets implementing this specification should handle both sides of the split.


4. The satisfier — what the spender publishes

The satisfier (scriptSig) for a time lock UTXO is three pushes:

[ push(templateBytes) ][ push(constraintBytes) ][ push(schnorrSig) ]
  1. Template bytes. The locking script only commits to hash160(template), so the spender must reveal the full template here for the network to verify the hash matches. (Contrast with the pay-to-public-key template, whose template is a well-known script the network already knows; that form uses two pushes instead of three.)
  2. Constraint bytes. Same story — the locking script commits to hash160(constraint). Revealing the constraint also puts the owner pubkey on the altstack where the template expects it.
  3. Schnorr signature over the canonical sighash. The scriptCode used in the sighash is the template bytes (not the locking script, not the constraint) — that's the Nexa template-script convention.

Maximum signed size per input is 66 (sig push) + 8 (template push) + 35 (constraint push) = 109 bytes, which lets a wallet reserve fee for a vault input before signing it.


5. Address derivation

A vault address is a pure function of four inputs:

address ← f(chainSelector, ownerPubkey, unlockValue, hdIndex)

Concretely:

  1. Build the constraint script OP_PUSH(ownerPubkey) and take its hash160.
  2. Build the visible-args opcodes OP_PUSH(unlockValue), OP_PUSH(hdIndex).
  3. Assemble the locking script with the constant template hash, the constraint hash, and the visible args.
  4. Encode the locking script as a nexa: address.

Three direct consequences of "address is a pure function":

  • The same inputs always produce the same address.
  • Different owner pubkeys → different addresses (constraint hash differs).
  • Different unlockValue or different hdIndex for the same owner pubkey → different addresses (visible args differ, even though the constraint hash doesn't).

A contract instance does not need to store its address: any party with the four inputs can recompute it. A wallet only needs to persist the inputs.

5.1 Owner key derivation — the BIP-44 path

The owner pubkey is derived at:

m / 44' / coinType' / accountIndex' / 0 / index
  • purpose is fixed at 44 (BIP-44).
  • coinType is 29223 for Nexa.
  • account is accountIndex, default 1.
  • change is always 0 (external chain).
  • address index is index — the contract's current leaf.

Each index produces a distinct child key and therefore a distinct vault address. Advancing index is how one contract issues a fresh, independent vault.

accountIndex is the only derivation input that distinguishes one contract instance (factory) from another under the same seed — no contract name or label enters the path or the on-chain bytes. A wallet hosting more than one such contract under one seed must therefore assign each a distinct accountIndex: two contracts sharing one generate a byte-identical vault address space (the same address for equal (unlockValue, hdIndex)) and are indistinguishable on-chain; the reference implementation refuses to create a second contract at an already-used accountIndex in the same account. Note also that accountIndex never appears on-chain — it only shapes the derived owner pubkey whose hash160 becomes the constraint hash — so rediscovery (§5.2) must repeat its step 3 for every accountIndex the wallet may have used.

Leaf indices are constrained to non-hardened (0 ≤ hdIndex < 2^31), matching the standard BIP-32/BIP-44 convention — hardened derivation is reserved for upper levels of the path, where it stops a compromised child key from reverse-engineering its parent.

5.2 Rediscovery from chain scan

Because the visible args are cleartext and the template hash is fixed, a wallet restoring from seed can find its vaults without an indexing service or brute-force search:

  1. Scan UTXOs for ones whose locking script starts with the time-lock prefix 0014461ad2…dd76.
  2. Read unlockValue and hdIndex directly from the visible-args region.
  3. Derive the child pubkey at hdIndex from the BIP-44 path (§5.1).
  4. Recompute the constraint hash from that pubkey and confirm it matches the one in the UTXO. If it does, this UTXO belongs to a vault under this seed.

6. Spending a vault

A transaction that spends a time lock UTXO must satisfy three on-chain requirements:

  1. nLockTime ≥ unlockValue for every vault input being claimed, with all values on the same side of the BIP-65 split (§3.4). This is what CLTV reads at spend time. CLTV alone is not enough, though — the transaction must also be final (see below).
  2. nSequence != 0xFFFFFFFF on every input. CLTV's failure conditions include the input being marked final, so a final sequence disables the time gate. The canonical non-final sentinel is 0xFFFFFFFE, which disables BIP-68 relative-locktime semantics (which use the low bits of sequence) while still letting nLockTime take effect.
  3. A valid Schnorr signature over the canonical sighash, where the scriptCode is the template bytes (§4). The signature is what CHECKSIGVERIFY consumes against the owner pubkey from the constraint.

Finality — when the network actually accepts the claim. Beyond CLTV, the node refuses any transaction whose nLockTime has not yet been reached (IsFinalTx), and the two sides of the split use different cutoffs:

  • Heights, inclusive. The mempool cutoff is tip height + 1 and finality requires nLockTime < cutoff, so a claim becomes acceptable in the very block where the tip reaches the unlock height (tip ≥ unlockValue).
  • Timestamps, strict, against median-time-past. The cutoff is the median of the last 11 block header times (BIP-113), not the tip header's own time, and finality requires nLockTime to be strictly below it. Median-time-past trails the tip header's time by roughly half the 11-block window (~10 minutes at Nexa's 2-minute block target), so a claim broadcast the moment a wall-clock countdown reaches zero is rejected as non-final for several more blocks. Wallets must gate timestamp claims on median-time-past, not on the newest header's timestamp (§7.2).

A spend is not limited to a single vault. nLockTime is a single per-transaction field, but CLTV imposes a floor, not an equality: a transaction whose nLockTime is at or past the largest committed unlock value among its inputs satisfies every input's CLTV check at once, provided each committed value lies on the same side of the BIP-65 split as the nLockTime (CLTV fails on a height/time type mismatch). A claim may therefore combine mature UTXOs from any number of a contract's vaults — different (unlockValue, hdIndex) addresses — in one transaction, each input carrying its own signature for its own vault's child key. What one transaction can never do is combine a height vault with a timestamp vault: its single nLockTime is read as either a height or a time, never both, so a contract whose vaults straddle the split needs at least two claim transactions. Finality (above) is then governed by the largest committed unlock value in the set — the claim is not acceptable to the network until every vault it draws from has matured. The reference implementation sweeps every mature vault of a contract on one side of the split per claim, choosing the side that holds the greater spendable value and setting nLockTime to the maximum swept unlock value. (Consensus imposes no same-owner requirement — restricting a claim to one contract's vaults is wallet policy.)

BIP-65 defines CHECKLOCKTIMEVERIFY's failure conditions. The Nexa consensus engine fails the script iff the input's sequence equals SEQUENCE_FINAL (0xFFFFFFFF). Without this rule, a spender could set nSequence = 0xFFFFFFFF and bypass the time gate entirely — making CLTV toothless.

A wallet typically routes change to an ordinary wallet address, not back to the vault, so the change does not become subject to the same time lock. How the claim's fee is funded has implications at funding time — see §7.3.


7. Wallet behaviour

Wallet integration is implementation-defined, but the on-chain design implies a few behaviours that any conforming wallet needs to handle.

7.1 UTXO discovery

When a wallet ingests a UTXO, it can recognise time-lock outputs by the fixed script prefix 0014461ad2…dd76. From a matching UTXO, the wallet reads unlockValue and hdIndex from the visible-args region and derives the child pubkey at the BIP-44 path. If the constraint hash matches, the UTXO belongs to one of this wallet's vaults.

A pre-unlock UTXO is still owned by the wallet — it must be surfaced in the balance and protected from being selected by ordinary spend logic — but it cannot be moved until the unlock value is reached (§3.4).

7.2 Balance vs. spendability

Two related but distinct questions:

  • "How much is in this contract's vaults?" is answered by summing every UTXO the wallet has identified as belonging to any vault under this contract, across all (index, unlockValue) slots the contract has ever issued.
  • "How much can I spend right now?" is answered by selecting UTXOs that satisfy three additional constraints:
    1. Maturity, judged per UTXO against that UTXO's own committed unlockValue (every vault a contract has issued matures independently), mirroring the node's finality rule for each side of the split (§6): for a height, chain tip ≥ unlockValue (inclusive); for a timestamp, the chain's median-time-past strictly greater than unlockValue. Judging a timestamp vault by the newest header's time — or with an inclusive compare — makes the wallet build and sign claims the network rejects as non-final. Pre-unlock UTXOs cannot contribute to a current spend.
    2. Same side of the split — a single transaction commits to one nLockTime, so mature UTXOs are grouped by which side of the BIP-65 split their committed unlock value lies on, and a claim draws from exactly one group — spanning all (index, unlockValue) vaults the contract has ever issued, past and current alike (§6). The reference implementation selects whichever side holds the greater spendable value and sets the claim's nLockTime to that group's maximum unlock value.
    3. Filtering — caller-supplied asset filter (native Nexa, group tokens, group authorities, etc.).

A consequence worth being explicit about: UTXOs in past vaults of the same contract (after the contract advanced its index) still count toward balance and remain directly spendable — once mature, they are selected alongside the current vault's UTXOs in any claim whose inputs all sit on their side of the split, with no re-pointing of the contract required. The only vault UTXOs a given claim cannot reach are immature ones and those on the other side of the split; the latter need a separate claim transaction whose nLockTime sits on their side.

7.3 Fee funding

A claim transaction must pay a relay fee, and the fee's source matters at funding time. The reference implementation pays a claim's fee exclusively from native value held at the contract's own vault addresses — the vaults' ungrouped native UTXOs plus whatever native dust rides on their token UTXOs — and refuses to sign a claim that consumes any input outside those addresses, so the fee is never drawn from the rest of the wallet. A vault holding only group tokens, whose accompanying native value cannot cover the claim's outputs plus the fee, is therefore unclaimable by such wallets. Funding wallets should always include some native coin when funding a vault.

At pure consensus level such coins are not permanently stuck: a claimant may add external native fee-paying inputs, since CLTV inspects only the vault input's own sequence and a single non-final input keeps nLockTime finality in force for the whole transaction — but wallets that restrict claim inputs to vault addresses do not support this, so recovery may require a purpose-built transaction.


8. Lifecycle

The lifecycle below traces a single vault from construction to spend. Throughout, index refers to the BIP-44 derivation index — the HD leaf that owns this vault's key (§5.1). It is fixed for the life of the vault.

   ┌──────────────────────────┐
   │ 1. Build a vault         │
   │    at (index, unlock)    │
   └────────────┬─────────────┘
                │ derives pubkey at index from BIP-44 path,
                │ computes destination + address
                ▼
   ┌──────────────────────────┐
   │ 2. Persist               │
   │    (accountIndex,        │
   │    unlock, index)        │
   └────────────┬─────────────┘
                │
                ▼
   ┌──────────────────────────┐
   │ 3. Fund                  │
   │    send coins to         │
   │    vault address         │
   └────────────┬─────────────┘
                │ UTXOs surface in wallet,
                │ tagged as belonging to this vault
                ▼
   ┌──────────────────────────┐
   │ 4. Wait                  │
   │    balance shows funds,  │
   │    spending refused      │
   │    until chain ≥ unlock  │
   └────────────┬─────────────┘
                │
                ▼
   ┌──────────────────────────┐
   │ 5. Spend                 │
   │    • locktime ≥ max      │
   │      unlock of swept     │
   │      vault(s) (§6)       │
   │    • sequence = FFFFFFFE │
   │    • Schnorr sign        │
   └──────────────────────────┘

Across restarts, a wallet reconstructs any vault destination from a persisted (accountIndex, unlockValue, index) record plus the seed; the record's byte-level encoding is implementation-defined. The accountIndex must be persisted (or otherwise known) alongside unlockValue and index: it is a derivation input (§5.1) that appears nowhere on-chain — the visible args carry only unlockValue and hdIndex, and the pubkey it selects is hidden behind the constraint hash — so it cannot be read back from the UTXO. A wallet that issues multiple vaults must retain (or rediscover via the chain scan of §5.2) every vault's (unlockValue, index) — not only the current one's — to keep recognising deposits to earlier vaults.


9. Compatibility invariants — what must never change

Anything that flows into the locking script bytes of a vault is part of the on-chain address. Changing it means: existing vaults still exist on chain, but no wallet can compute the same address from the same inputs, and therefore none can recognise or spend them.

The protected surface:

  1. The exact template script bytecode. Its hash160 (461ad25081cb0119d034385ff154c8d3ad6bdd76) is committed in every vault address ever created.
  2. The constraint script shape (one push of the 33-byte compressed pubkey).
  3. The visible-args encoding (push of unlockValue then push of hdIndex, in that order, smallest-form encoded).
  4. The BIP-44 path (§5.1). Changing the accountIndex convention would silently re-derive to a different child key for every vault.

10. Open source implementations