Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Worked example: a Last Will covenant

Problem. Lock funds so that the owner can move them with a hot key, escape the arrangement with a cold key, and — if the owner goes silent for 180 days — let an heir claim them. All three rules enforced on-chain.

This recipe puts the last few lessons to work on a real, non-trivial contract: the Last Will, adapted from the SimplicityHL examples. It has three spending paths, a relative timelock, and a recursive covenant — and it's a chance to see a multi-path witness selector in a complete file.

The three paths:

PathWhoWhenEffect
Refreshowner's hot keyany timemoves the funds but repeats the covenant
ColdBreakowner's cold keyany timespends out, ending the covenant
Inheritheir's keyafter 180 days of no movementspends out to the heir

The cold key is the escape hatch; the hot key is for everyday moves and is forced to re-lock; the inheritor is the dead-man's switch.

The program

The contract lives in last_will.simf. Two adaptations from the upstream example make it work with tx-manifest-wallet:

  1. The keys and the timelock are compile parameters (param::INHERITOR_PUB_KEY, param::HOT_PUB_KEY, param::COLD_PUB_KEY, and param::INHERIT_BLOCKS) instead of hardcoded constants, so the manifest can wire them — exactly like PUB_KEY in Hello World.
  2. The path is chosen by a dedicated SPEND_PATH witness, and each signature is its own witness. The upstream version nested the signatures inside the selector; tx-manifest-wallet computes signatures as standalone Signature witnesses, so we split them out (the idiom from Witnesses).
fn main() {
    match witness::SPEND_PATH {
        Left(inherit: ()) => inherit_spend(witness::INHERITOR_SIG),
        Right(cold_or_hot: Either<(), ()>) => match cold_or_hot {
            Left(cold: ()) => cold_spend(witness::COLD_SIG),
            Right(hot: ()) => refresh_spend(witness::HOT_SIG),
        },
    }
}

SPEND_PATH has type Either<(), Either<(), ()>>, so the three branches are selected by Left(()), Right(Left(())), and Right(Right(())). Whichever branch you take reads exactly one signature witness — but all four witnesses must still be named on the input, the two you don't read as "unused".

The two interesting helpers:

#![allow(unused)]
fn main() {
fn inherit_spend(inheritor_sig: Signature) {
    let blocks: Distance = param::INHERIT_BLOCKS;   // configurable timelock (a compile param)
    jet::check_lock_distance(blocks);
    checksig(param::INHERITOR_PUB_KEY, inheritor_sig);
}

fn recursive_covenant() {
    assert!(jet::eq_32(jet::num_outputs(), 2));                 // exactly 2 outputs
    let this_script_hash: u256 = jet::current_script_hash();
    let output_script_hash: u256 = unwrap(jet::output_script_hash(0));
    assert!(jet::eq_256(this_script_hash, output_script_hash)); // output 0 = same covenant
    assert!(unwrap(jet::output_is_fee(1)));                     // output 1 = fee
}
}

inherit_spend enforces a relative timelock — the heir's spend is only valid once the UTXO is INHERIT_BLOCKS blocks old (a compile param; ~180 days ≈ 25,920 one-minute Liquid blocks). recursive_covenant (used by the hot-key refresh) forces the spend to recreate the same covenant in output 0 and have the explicit fee in output 1, with nothing else.

The manifest

A will is something you deploy once and then operate — exactly what a contract template models. We define a last_will_contract contract template whose fields are the three keys, and whose actions are the four transactions. A constructor (Fund) records the keys in an instance the first time you set the will up; the spend actions read them back.

"contract_templates": {
  "last_will_contract": {
    "fields": {
      "INHERITOR_PUB_KEY": { "type": "pubkey" },
      "HOT_PUB_KEY": { "type": "pubkey" },
      "COLD_PUB_KEY": { "type": "pubkey" },
      "INHERIT_BLOCKS": { "type": "u16" }
    },
    "actions": { "Fund": { ... }, "ColdBreak": { ... }, "Refresh": { ... }, "Inherit": { ... } }
  }
}

The last_will UTXO type (top-level, as before) wires those three fields into the program — the field names double as the param:: names the script consumes:

"utxo_types": {
  "last_will": {
    "script": {
      "type": "simplicity",
      "source": "./last_will.simf",
      "compile_params": {
        "INHERITOR_PUB_KEY": "INHERITOR_PUB_KEY",
        "HOT_PUB_KEY": "HOT_PUB_KEY",
        "COLD_PUB_KEY": "COLD_PUB_KEY",
        "INHERIT_BLOCKS": "INHERIT_BLOCKS"
      }
    },
    "asset": "lbtc"
  }
}

The constructor: Fund

Fund does double duty — it locks the funds and creates the instance. It takes the three keys as params (two auto-filled from your wallet) and an amount, locks a wallet UTXO into the covenant, then create_instance records the keys. Carrying that block is what makes Fund a constructor — there is no separate flag to set:

"Fund": {
    "params": {
      "INHERITOR_PUB_KEY": {
          "type": "pubkey",
          "description": "The heir's x-only public key. They can claim the funds 180 days after the last move."
      },
      "HOT_PUB_KEY": {
          "type": "pubkey",
          "description": "Owner's hot key. Auto-filled from your wallet signing key."
      },
      "COLD_PUB_KEY": {
          "type": "pubkey",
          "description": "Owner's cold key. Your wallet's oracle key — the covenant escape hatch."
      },
      "INHERIT_BLOCKS": {
          "type": "u16",
          "default": "25920",
          "description": "Blocks of inactivity before the heir may claim. ~180 days ≈ 25920 (1-minute Liquid blocks). Max 65535."
      },
      "amount_sat": {
          "type": "u64",
          "description": "Amount in satoshis to place under the will."
      }
  },
  "inputs": [
        {
            "id": "funding_input",
            "description": "Wallet UTXO providing the funds.",
            "utxo_source": "wallet",
            "asset": "lbtc",
            "amount_sat": {
                "min_amount": "params.amount_sat"
            }
        }
    ],
 "outputs": [
      {
          "id": "will_out",
          "description": "The funded last-will output.",
          "destination": {
              "utxo_type": "last_will"
          },
          "amount_sat": "params.amount_sat",
          "asset": "lbtc"
      },
      {
          "id": "change_out",
          "description": "Change returned to the funding wallet.",
          "destination": "change",
          "asset": "lbtc",
          "optional": true
      }
  ],
  "create_instance": {
    "fields": {
      "INHERITOR_PUB_KEY": "$params.INHERITOR_PUB_KEY",
      "HOT_PUB_KEY": "$params.HOT_PUB_KEY",
      "COLD_PUB_KEY": "$params.COLD_PUB_KEY",
      "INHERIT_BLOCKS": "$params.INHERIT_BLOCKS"
    }
  }
}

HOT_PUB_KEY auto-fills from your wallet signing key; COLD_PUB_KEY is your wallet's oracle key (take it from info — see Creating a wallet); the heir gives you INHERITOR_PUB_KEY. Each param value is written into the compile params, so will_out's covenant address is computed from the keys you just supplied — before the instance exists. After broadcast, create_instance writes those same three keys into txmanifest.instance.json.

One instance per will. Unlike Hello World — which had no contract_templates and so no instance at all — the keys here are a template's fields, persisted at construction. Every later spend reads them from the instance, so you only enter the keys once. This is the full template / instance model from Instance, state & constructors.

Each spend reads the instance

Every spend is an action whose input is the last_will UTXO (found in the state file) with a SPEND_PATH selector and the matching Signature. The signature keys reference instance.* — the fields loaded back from the instance, so you never re-enter them. ColdBreak:

"ColdBreak": {
  "inputs": [
    {
      "id": "will_in",
      "utxo_source": { "utxo_type": "last_will" },
      "witnesses": {
        "SPEND_PATH": { "type": "simplicityhl", "value": "Right(Left(()))" },
        "COLD_SIG": {
          "type": "Signature",
          "sig_type": "sig_hash_all",
          "source": { "type": "wallet", "key": "instance.COLD_PUB_KEY" }
        },
        "HOT_SIG": "unused",
        "INHERITOR_SIG": "unused"
      }
    },
    { "id": "fee_input", "utxo_source": "wallet", "asset": "lbtc", "optional": true }
  ],
  "outputs": [
    { "id": "to_wallet", "destination": "wallet", "asset": "lbtc", "amount_sat": "will_in.amount_sat" },
    { "id": "fee_change", "destination": "change", "asset": "lbtc", "optional": true }
  ]
}

This is exactly the Hello World spend plus a SPEND_PATH selector — and the two signatures this path never reads, named as "unused" because the program declares them. Inherit is the same shape with SPEND_PATH = Left(()), a real INHERITOR_SIG, and COLD_SIG / HOT_SIG unused.

Refresh is the one that's different — the covenant forces it to re-lock:

"Refresh": {
  "inputs": [
    {
      "id": "will_in",
      "utxo_source": { "utxo_type": "last_will" },
      "witnesses": {
        "SPEND_PATH": { "type": "simplicityhl", "value": "Right(Right(()))" },
        "HOT_SIG": { "type": "Signature", "sig_type": "sig_hash_all", "source": { "type": "wallet", "key": "instance.HOT_PUB_KEY" } },
        "COLD_SIG": "unused",
        "INHERITOR_SIG": "unused"
      }
    }
  ],
  "outputs": [
    {
      "id": "will_again",
      "destination": { "utxo_type": "last_will" },
      "asset": "lbtc",
      "amount_sat": "will_in.amount_sat - fee",
      "required_index": 0
    }
  ]
}

Three things the covenant dictates here:

  • The re-locked output is declared first. recursive_covenant checks output_script_hash(0), so will_again has to be output 0 — and it is, because it is the first entry in the outputs array. The required_index: 0 beside it records that dependency for a reader; it is not what enforces it.
  • No change output. The program asserts exactly two outputs (covenant + fee). Because this action declares no "change" output, the builder never adds one — it folds the L-BTC surplus into the fee. There's no separate fee input either: a recursive covenant can't add a wallet change output to return the leftover, so the will pays its own fee and shrinks by it each refresh.
  • amount_sat: "will_in.amount_sat - fee" — the fee keyword. The will is re-locked at its current value minus the network fee. The fee output (output
    1. then ends up being exactly fee.

The fee keyword. fee is a reserved formula word for the estimated network fee. It evaluates to 0 while the outputs are first assembled, then the tool estimates the fee from the transaction's size and re-evaluates any amount that used fee — so will_in.amount_sat - fee lands on the right value before signing. No fee_sat param to guess at.

How the builder decides on change. A change output is added only when an action lists a "destination": "change" output. Methods that omit it — like Refresh — get exactly their declared outputs plus the fee, which is what a recursive covenant needs.

Skip the prompts: a params file

Fund needs three pubkeys. Typing them in is error-prone, so the CLI can read them from a params file: a flat JSON object of param → value that pre-fills (or fully supplies) the prompts.

You don't even pass a flag. The tool auto-discovers a file named <stem>.<network>.json next to the manifest (the <stem> is the manifest's filename stem) — so for testnet it loads txmanifest.testnet.json from examples/last_will/:

{
  "INHERITOR_PUB_KEY": "…heir's wallet pubkey…",
  "HOT_PUB_KEY":       "…owner's wallet pubkey…",
  "COLD_PUB_KEY":      "…owner's oracle pubkey…",
  "INHERIT_BLOCKS":    "25920",
  "amount_sat":        "100000"
}

(An explicit --params <file> works too, and overrides the auto-discovered one.)

In this example the heir is a second wallet, so two of the keys come from one wallet and one from another. Rather than copy three pubkeys out of info by hand, the book ships scripts that do it for you:

.\create_wallet.ps1          # owner wallet -> wallet.json
.\create_inherit_wallet.ps1  # heir wallet  -> wallet-inherit.json
.\make_params.ps1            # reads both, writes examples/last_will/txmanifest.testnet.json

make_params.ps1 runs info on each wallet, pulls out the signing and oracle pubkeys, and writes the params file — INHERITOR_PUB_KEY from the heir wallet, HOT_PUB_KEY / COLD_PUB_KEY from the owner wallet. (HOT_PUB_KEY also auto-fills from the wallet at run time, since it's a wallet compute; the file just makes every value explicit.)

Run it

Run everything from the repository root, where the scripts put the wallets and params file. With the params file in place, construct the will — Fund reads every value from the file, so there's nothing to type. It locks the funds and writes examples/last_will/txmanifest.instance.json:

txw run examples/last_will/txmanifest.json Fund \
  --network testnet --wallet wallet.json

Then the cold-key break-out is the most straightforward spend to run, since your oracle key signs it — and you don't re-enter any keys, because they're read from the instance:

txw run examples/last_will/txmanifest.json ColdBreak \
  --network testnet --wallet wallet.json \
  --instance examples/last_will/txmanifest.instance.1.json \
  --state    examples/last_will/txmanifest.state.1.json

Both flags are needed, and this CLI does not go looking for either: --instance supplies the three keys the covenant address is rebuilt from, and --state supplies the UTXO to spend. Substitute the paths Fund actually wrote.

ColdBreak finds the last_will UTXO in the state file, rebuilds the covenant address from the instance's three fields, signs with the cold (oracle) key, dry-runs the program down the Right(Left(())) branch, and broadcasts.

Inherit and the relative timelock

inherit_spend calls jet::check_lock_distance, which reads the spending input's nSequence. A transaction that doesn't set it fails the check no matter how old the UTXO is, so the input has to ask for the lock explicitly:

{
  "id": "will_in",
  "utxo_source": { "utxo_type": "last_will" },
  "sequence": { "relative_blocks": "instance.INHERIT_BLOCKS" },
  "witnesses": { "…": "…" }
}

sequence takes one of three forms:

FormMeaning
{ "relative_blocks": <expr> }BIP68 block-based lock, at most 65535 blocks
{ "relative_seconds": <expr> }time-based lock, rounded up to 512-second units
a bare integer or expressiona raw nSequence value

Omit it and the input stays at Sequence::MAX, which disables relative locktime — the default, and the reason every other action here leaves it out.

Setting it does not make the UTXO old enough; it declares the age the spend claims, and the network rejects the transaction until the UTXO actually reaches it. Inherit is also signed by the heir's key rather than yours, so running it needs the heir's wallet.

Caveat on Refresh. It relies on the explicit-fee / no-change output layout with the fee keyword. The estimate adds a fixed allowance for the covenant's Simplicity witness (whose exact size is only known after signing), so it errs slightly high to stay above the relay minimum — the extra just goes to the fee. Fund the will with a little headroom so each refresh's fee fits.

Try next

You've now seen multiple spending paths, a timelock, and a recursive covenant in one file. The recipes that go deeper on those building blocks: Covenant UTXO types and Multiple spending paths.