Witnesses
Problem. Provide the values a Simplicity covenant needs to authorise a spend — signatures and branch selectors — and understand what the tool does with them.
You met your first witness in
Hello World, Part 2: the SIGNATURE that
satisfied p2pk.simf. This recipe steps back and covers the witnesses map in
full — what it is, the forms a witness takes, and the rule that every witness the
program declares must be named.
A witness only matters when you spend a covenant. Creating a covenant output
(Pay) commits to a program; nothing is checked. Spending it (Receive) runs the
program, and the program reads its witnesses to decide whether to allow the spend.
Where witnesses live
Witnesses sit on an input — specifically a covenant input (utxo_source is a
utxo_type, not "wallet"). Plain wallet inputs sign themselves the ordinary
way and have no witnesses map.
{
"id": "p2pk_in",
"utxo_source": { "utxo_type": "p2pk_output", "compile_params": { "PUB_KEY": "params.pubkey" } },
"witnesses": {
"SIGNATURE": { "type": "Signature", "sig_type": "sig_hash_all", "source": { "type": "wallet", "key": "params.pubkey" } }
}
}
Each key is a SimplicityHL witness name — it must match a witness::NAME in
the .simf source. Our program reads witness::SIGNATURE, so the map has a
SIGNATURE entry.
The map and the program must agree exactly, in both directions. Every witness the program declares has to appear in the map, and every entry in the map has to name a witness the program declares. A name the program doesn't have is an error ("'X' is declared here but the program has no such witness"); so is a witness you left out. Nothing is inferred from an omission — see Witnesses on the path you didn't take.
taproot_leafis the single exception, because it is not a program witness at all. More on that below.
The forms a witness takes
Signature — a computed BIP340 signature
This is the one from Part 2. You don't write a signature by hand; the tool computes it while signing.
"SIGNATURE": {
"type": "Signature",
"sig_type": "sig_hash_all",
"source": { "type": "wallet", "key": "params.pubkey" }
}
sig_type: "sig_hash_all"selects the message to sign: Simplicity'ssig_all_hash, a commitment over the whole transaction. This is not the classic Bitcoin/ElementsSIGHASH_ALL— it's Simplicity's own hash, computed via the transaction environment (CTxEnv::sighash_all()). It's currently the onlysig_typedefined.source: { "type": "wallet", "key": ... }identifies the signing key. Thekeyresolves to an x-only pubkey — from an actionparam(params.pubkey, as here), a contract-template field (instance.BORROWER_PUB_KEY, the form the lending example uses), or a literal hex value. The tool searches your wallet's BIP86 derivation paths for the private key matching that pubkey and signs with it.
Under the hood the tool computes the hash, signs it, and rewrites the entry as
a simplicityhl witness holding the 64-byte signature as 0x… hex — so a
Signature is really sugar over the next kind.
simplicityhl — a literal typed value
A fixed value, parsed against the witness's type. Use it for branch selectors, indices, and raw byte values.
"PATH": {
"type": "simplicityhl",
"value": "Left(())",
"simplicity_type": "Either<(), ()>",
"description": "Take the first spending path."
}
valueis a SimplicityHL value expression:Left(())/Right(())to choose a branch of anEither,0x<hex>for a byte array,42for an integer.simplicity_typeis optional and documentary. The tool takes the real type from the compiled program's ABI, not from this field — it's there to help a human reader. Provide it for clarity; leave it off and nothing breaks.
Branch selectors are the most common use. A covenant with two spending paths
typically reads a witness::PATH of type Either<(), ()>; supplying Left(())
or Right(()) picks which path runs. That's the subject of
Multiple spending paths.
taproot_leaf — which leaf to spend
Not a program witness at all. A covenant whose tap tree has more than one leaf needs to say which one this spend uses, and that selection is made outside the program:
"SPEND_PATH": {
"type": "taproot_leaf",
"source": { "type": "formula", "expr": "pre_lock_leaf" }
}
Because the program never reads it, it is exempt from the agreement rule above:
it does not have to correspond to a witness:: name, and it is skipped when the
map is matched against the program's witness list. Do not confuse it with a
PATH selector — PATH chooses a branch inside one program; SPEND_PATH
chooses which program runs.
"unused" — a witness this path doesn't read
The bare string, in place of an object:
"HOT_SIG": "unused"
Covered in full in the next section.
Witnesses on the path you didn't take
A program declares every witness it could read, but a single spend travels one path. The other branches still need values — before Simplicity prunes them, every witness node needs some concrete bit-vector — so those slots have to be filled with zeros.
You have to ask for that zero explicitly, by name. Write the string
"unused" where the object would go:
"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"
}
That is the last_will cold-key spend: four witnesses declared, one selector, one
real signature, two zeros — and all four named.
Why name them at all, rather than let the tool fill in the blanks? Because then a misspelled
HOT_SIGand a deliberate omission would be the same thing. Both would leave the slot at zero, the transaction would build, and the mistake would surface much later as a covenant that simply does not satisfy — with nothing pointing at the cause. Naming every witness costs one line each and lets the tool check your map against the.simfbefore it builds anything.
The clearest illustration is one covenant spent two ways. The lending pre_lock
program declares PATH and SIGNATURE. SetupLending takes the left path,
which reads no signature:
"PATH": { "type": "simplicityhl", "value": "Left(())" },
"SIGNATURE": "unused"
CancelOffer takes the right path, which does:
"PATH": { "type": "simplicityhl", "value": "Right(())" },
"SIGNATURE": { "type": "Signature", "sig_type": "sig_hash_all",
"source": { "type": "wallet", "key": "instance.BORROWER_PUB_KEY" } }
Same two names in both. Only the values differ.
Our single-path p2pk.simf declares just SIGNATURE and always reads it, so
nothing is ever "unused" there.
What the tool builds
Once witnesses are resolved, the tool satisfies the program against the spending transaction and writes the final Simplicity tapscript witness stack — exactly four items, in this order:
[ witness_bits, pruned_program, cmr_script, control_block ]
You never assemble this yourself; it's the output of finalisation. The Simplicity dry-run executes the program against this stack before broadcast, so a missing or wrong witness is caught locally rather than rejected by the network.
When it goes wrong
The four legal forms are "type": "simplicityhl", "type": "Signature",
"type": "taproot_leaf", and the bare string "unused". Anything else is
rejected by name, so the common mistakes report themselves:
| Message | Cause |
|---|---|
'X' is declared here but the program has no such witness | A name in the map the .simf doesn't declare — usually a typo, or a witness renamed in the program |
| missing / nothing supplied for a declared witness | A witness left out entirely. Add it, with "unused" if this path doesn't read it |
'X' has unrecognized type 'Y' | A type outside the four above |
'X' is a Signature witness that was never signed | No signer was available — the source key isn't one your wallet can derive |
That last one is worth recognising: it means the entry was still a Signature
when the tool went to build the witness stack, i.e. the key lookup failed. It is
not a problem with the program.
See also
- Multiple spending paths — using a
PATHselector witness in anger. - Covenant UTXO types — how the covenant address (and its tapleaf) is derived in the first place.