Descriptor Notation

A compact human-readable notation for writing Ladder Script spending conditions

What it looks like

Instead of building conditions block by block in JSON, you can write them as a single expression:

ladder(or( and(sig(@hot_key), csv(144)), multisig(2, @key_a, @key_b, @key_c) ))

This describes a vault: spend with a hot key after 144 blocks, or immediately with any 2 of 3 recovery keys. The notation maps directly to the rung/block structure. or() creates multiple rungs. and() puts multiple blocks in one rung. Each function name is a block type.

Grammar
// A ladder wraps one or more outputs in a shared condition tree (PLC model) ladder(output(0, rung)) // single output, single rung ladder(output(0, or(rung, rung, ...))) // single output, multiple rungs (OR logic) ladder(output(0, ...), output(1, ...)) // multiple outputs in one tx // A rung is a single block or multiple blocks combined with AND sig(@alice) // single block = single rung and(sig(@alice), csv(100)) // two blocks in one rung (AND logic) // Inversion: prefix with ! !csv(100) // inverted: SATISFIED when csv FAILS // Keys are referenced by alias with @ @alice // resolved from a key map at parse time
The output() wrapper maps each output index to its conditions within the shared condition tree. One conditions_root per transaction defines the entire tree (TX_MLSC). The 32-byte root is recovered at spend time from a synthetic UTXO entry — see MLSC Specification.
All 65 block types

Every active block type has descriptor notation. Grouped by family:

Signature Family

NotationBlock typeArguments
sig(@alias)SIGKey alias. Optional scheme: sig(@alice, falcon512)
multisig(M, @k1, @k2, ...)MULTISIGThreshold M, then key aliases
adaptor_sig(@signer)ADAPTOR_SIGSingle signing key (v0.7+). Optional scheme: adaptor_sig(@signer, schnorr). Adaptor point T is off-chain only.
musig_threshold(M, @k1, @k2, ...)MUSIG_THRESHOLDMuSig2/FROST aggregate threshold
key_ref_sig(relay, block)KEY_REF_SIGRelay index + block index (cross-rung key sharing)

Timelock Family

NotationBlock typeArguments
csv(N)CSVRelative timelock in blocks
csv_time(N)CSV_TIMERelative timelock in seconds
cltv(N)CLTVAbsolute block height
cltv_time(N)CLTV_TIMEAbsolute time (MTP, ≥ 500000000)

Hash Family

NotationBlock typeArguments
tagged_hash(tag, expected)TAGGED_HASHTwo 32-byte hashes: BIP-340 tag + expected
hash_guarded(hex)HASH_GUARDED32-byte SHA-256 hash commitment

Covenant Family

NotationBlock typeArguments
ctv(hex)CTV32-byte BIP-119 template hash
vault_lock(@recovery, @hot, delay)VAULT_LOCKRecovery key, hot key, CSV delay
amount_lock(min, max)AMOUNT_LOCKOutput value range in satoshis

Recursion Family

NotationBlock typeArguments
recurse_same(depth)RECURSE_SAMEMax recursion depth
recurse_modified(depth, blk, param, delta)RECURSE_MODIFIEDDepth, block index, param index, mutation delta
recurse_until(height)RECURSE_UNTILTerminates at block height
recurse_count(count)RECURSE_COUNTCountdown; terminates at 0
recurse_split(splits, min_sats)RECURSE_SPLITMax splits, minimum sats per output
recurse_decay(depth, blk, param, decay)RECURSE_DECAYDepth, block index, param index, decay per step

Anchor Family

NotationBlock typeArguments
anchor()ANCHORGeneric anchor output
anchor_channel()ANCHOR_CHANNELLightning channel anchor
anchor_pool()ANCHOR_POOLPool anchor
anchor_reserve()ANCHOR_RESERVEReserve anchor (guardian set)
anchor_seal()ANCHOR_SEALSeal anchor
anchor_oracle()ANCHOR_ORACLEOracle anchor
data_return(hex)DATA_RETURN1–40 byte data payload (replaces OP_RETURN)

PLC Family

NotationBlock typeArguments
hysteresis_fee(low, high)HYSTERESIS_FEEFee band thresholds
hysteresis_value(low, high)HYSTERESIS_VALUEValue band thresholds
timer_continuous(accumulated, target)TIMER_CONTINUOUSAccumulated + target blocks (2 numerics)
timer_off_delay(remaining)TIMER_OFF_DELAYRemaining hold-off blocks (1 numeric)
latch_set(@pk, state)LATCH_SETSetter key + state value
latch_reset(@pk, state, delay)LATCH_RESETResetter key + state value + delay blocks
counter_down(@pk, count)COUNTER_DOWNEvent signer + counter
counter_preset(@pk, current, preset)COUNTER_PRESETApproval signer + current + preset count
counter_up(@pk, current, target)COUNTER_UPEvent signer + current + target count
compare(op, value_b)COMPAREOperator (1=EQ..7=IN_RANGE) + threshold. Optional value_c for IN_RANGE
sequencer(current_step, total_steps)SEQUENCERCurrent step + total step count (2 numerics)
one_shot(@pk, state, commitment_hex)ONE_SHOTKey + state value + 32-byte commitment hash
rate_limit(max, cap, refill)RATE_LIMITMax per block, accumulation cap, refill blocks
cosign(hex)COSIGN32-byte conditions hash of co-input

Compound Family

NotationBlock typeArguments
timelocked_sig(@pk, csv)TIMELOCKED_SIGSignature + relative timelock
htlc(@receiver, @sender, preimage | h:HASH256, csv)HTLCReceiver key first (path=0 hashlock), sender key second (path=1 refund), preimage hex (auto-SHA256'd) or the h: form supplying the 32-byte hash directly, CSV blocks. The path indicator is added by the witness builder.
hash_sig(@pk, preimage | h:HASH256)HASH_SIGKey + preimage hex (auto-SHA256'd) or the h: form supplying the 32-byte hash directly.
ptlc(@pk, csv)PTLCSingle signing key (v0.7+) + CSV blocks. Adaptor point T is off-chain only.
cltv_sig(@pk, height)CLTV_SIGSignature + absolute timelock
timelocked_multisig(M, @k1, ..., csv)TIMELOCKED_MULTISIGThreshold, keys, CSV blocks
anchor_fee(@local, @remote, min_fee, max_fee, max_weight, commitment)ANCHOR_FEE2-of-2 sigs + fee rate band + weight limit + spender commitment numeric (anti-pinning L2 anchor)

Governance Family

NotationBlock typeArguments
epoch_gate(epoch_size, window)EPOCH_GATEBlocks per epoch, window size
weight_limit(max)WEIGHT_LIMITMaximum transaction weight
input_count(min, max)INPUT_COUNTInput count bounds
output_count(min, max)OUTPUT_COUNTOutput count bounds
relative_value(num, den)RELATIVE_VALUENumerator/denominator ratio
accumulator(root)ACCUMULATOR32-byte Merkle root
output_check(idx, min, max, hex)OUTPUT_CHECKOutput index, value range, script hash

Legacy Family

NotationBlock typeArguments
p2pk(@pk)P2PK_LEGACYPublic key
p2pkh(@pk)P2PKH_LEGACYPublic key (hashed to HASH160)
p2sh(hex)P2SH_LEGACYInner conditions hex
p2wpkh(@pk)P2WPKH_LEGACYPublic key (SegWit)
p2wsh(hex)P2WSH_LEGACYInner conditions hex
p2tr(@pk)P2TR_LEGACYInternal key (Taproot key-path)
p2tr_script(hex)P2TR_SCRIPT_LEGACYInner script hex (Taproot script-path)

QABI / PQ Family

NotationBlock typeArguments
qabi_prime()QABI_PRIMEPriming marker — no committed fields
qabi_spend(auth_tip, root, depth, expiry, owner_id)QABI_SPENDBatch-spend gate: auth chain tip, committed root, depth, expiry, owner id (all hex)
pq_batch(pubkey_hash)PQ_BATCH32-byte SHA256(falcon_pubkey) — anchor input reveals pubkey + sig once per tx
Signature schemes

The optional second argument to sig() selects the signature scheme:

NameSchemeExample
schnorrBIP-340 Schnorr (default)sig(@alice)
ecdsaECDSAsig(@alice, ecdsa)
falcon512FALCON-512 (PQ)sig(@alice, falcon512)
falcon1024FALCON-1024 (PQ)sig(@alice, falcon1024)
dilithium3Dilithium3 (PQ)sig(@alice, dilithium3)
sphincs_shaSPHINCS+-SHA2 (PQ)sig(@alice, sphincs_sha)
Examples
Simple spend
One key, one signature.
ladder(sig(@alice))
Vault with recovery
Hot key + 1 day delay, or 2-of-3 multisig immediately.
ladder(or( and(sig(@hot), csv(144)), multisig(2, @cold_a, @cold_b, @cold_c) ))
Dead man's switch
Owner spends any time. If inactive for 1 year, heir can claim.
ladder(or( sig(@owner), and(csv(52560), sig(@heir)) ))
Inverted timelock
Spendable only BEFORE block 100 passes (emergency window).
ladder(and(sig(@admin), !csv(100)))
Post-quantum vault
Classical Schnorr for daily use, FALCON-512 fallback.
ladder(or( sig(@daily), sig(@pq_backup, falcon512) ))
Governance with output check
2-of-3 treasury, output 0 must have at least 1M sats.
ladder(and( multisig(2, @dir_a, @dir_b, @dir_c), output_check(0, 1000000, 4294967295, 0000...0000) ))
Atomic swap (HTLC)
Claim with preimage + sender sig, or refund after 144 blocks.
ladder(or( htlc(@alice, @bob, a1b2...preimage...c3d4, 144), and(sig(@alice), csv(144)) ))
Recursive covenant (DCA)
Dollar-cost averaging: spend N times, decrementing counter each hop.
ladder(and( sig(@owner), amount_lock(50000, 100000), recurse_count(12) ))
Rate-limited wallet
Sign to spend, but capped at 200k sats per block with 10-block refill.
ladder(and( sig(@owner), rate_limit(200000, 1000000, 10) ))
PTLC payment channel
Scriptless payment: adaptor signature + 1 block CSV.
ladder(or( ptlc(@alice, 1), and(sig(@bob), cltv(900000)) ))
Vault with clawback
Recovery key sweeps immediately. Hot key needs 144 blocks.
ladder(vault_lock(@recovery, @hot, 144))
Legacy P2PKH with PQ fallback
Standard P2PKH spend, or FALCON-512 cold key after 1 year.
ladder(or( p2pkh(@legacy_key), and(csv(52560), sig(@pq_cold, falcon512)) ))
Using the RPC

Two RPC commands work with descriptor notation:

parseladder
Parse a descriptor into conditions hex, MLSC root, and the per-rung pubkey side-channel arrays needed for a key-aware round-trip.
$ bitcoin-cli -signet parseladder \ "ladder(or(sig(@alice), and(csv(52560), sig(@bob))))" \ '{"alice":"02abc...","bob":"03def..."}' { "conditions_hex": "020100010203fd50cd0001010101000000", "mlsc_root": "adbd9257578360bc6884ef46b18f30e1...", "n_rungs": 2, "pubkeys_hex": [["02abc..."], ["03def..."]], "merkle_pubkeys_hex": [[[]], [[], []]] }
formatladder
Convert conditions hex back to a descriptor string. Pass the pubkeys_hex + key map + merkle_pubkeys_hex from parseladder to render @aliases faithfully (see the Round-trip section below).
$ bitcoin-cli -signet formatladder "01010364010101000000" { "descriptor": "ladder(csv(100))" }
Key aliases

Aliases like @alice are resolved from a JSON key map passed as the second argument to parseladder. The map contains alias names and their compressed public keys:

{ "alice": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", "bob": "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5" }

Different aliases with different keys produce different MLSC roots. The keys are folded into the Merkle leaf hash via merkle_pub_key, so they never appear in the on-chain output.

Round-trip

Descriptors round-trip through parseladder and formatladder:

ladder(csv(100)) → 01010364010101000000ladder(csv(100))

Public keys are folded into the Merkle leaf hash and not present in the conditions blob. To round-trip a descriptor that names @aliases, pass parseladder's pubkeys_hex and merkle_pubkeys_hex arrays plus the original key map back to formatladder:

$ R=$(bitcoin-cli -signet parseladder \ "ladder(multisig(2, @alice, @bob, @carol))" \ '{"alice":"02ab...","bob":"03cd...","carol":"02ef..."}') $ CH=$(echo "$R" | jq -r .conditions_hex) $ PKS=$(echo "$R" | jq -c .pubkeys_hex) $ MPKS=$(echo "$R" | jq -c .merkle_pubkeys_hex) $ bitcoin-cli -signet formatladder "$CH" "$PKS" \ '{"alice":"02ab...","bob":"03cd...","carol":"02ef..."}' "$MPKS" { "descriptor": "ladder(multisig(2, @alice, @bob, @carol))" }

If you call formatladder with only the conditions hex (no pubkey side-data), pubkey-bearing blocks render with truncated-hex placeholders that won't reparse. The pubkeys_hex + merkle_pubkeys_hex + key-map combination is required for descriptors that name keys (sig, multisig, htlc, vault_lock, anchor_fee, timelocked_sig, ptlc, etc.).

Build your first transaction

Three commands to create, sign, and broadcast a Ladder Script transaction on the live signet. This is the wallet integration path.

Step 1: Create
Get a keypair from the wallet, then build an unsigned transaction with a ladder output.
# Get a keypair $ bitcoin-cli -signet getnewaddress tb1q... $ bitcoin-cli -signet getaddressinfo tb1q... | jq .pubkey "02abc123..." # Create an unsigned tx with a vault condition: # hot key + 144-block delay, in a single rung governing output 0. # createrungtx takes inputs, output amounts, and a rungs array # (shared-tree shape — one conditions_root for the whole tx). $ bitcoin-cli -signet createrungtx \ '[{"txid":"<utxo>","vout":0}]' \ '[0.001]' \ '[{"output_index":0,"blocks":[ {"type":"SIG","fields":[{"type":"PUBKEY","hex":"02abc..."},{"type":"SCHEME","hex":"01"}]}, {"type":"CSV","fields":[{"type":"NUMERIC","hex":"90000000"}]} ]}]' 0 → {"hex":"04000000..."}
Step 2: Sign
Sign the spending transaction using descriptor notation. One string, one key map.
# Sign with signladder — the descriptor defines the conditions, # the key map provides WIF private keys $ bitcoin-cli -signet signladder \ "04000000..." \ "ladder(and(sig(@hot), csv(144)))" \ '{"hot":"cVt4o7B..."}' \ '[{"amount":0.001,"scriptPubKey":"df..."}]' → {"hex":"04000000...signed...","complete":true}
Step 3: Broadcast
Send the signed transaction to the signet network.
$ bitcoin-cli -signet sendrawtransaction "04000000...signed..." → "a1b2c3d4e5f6..." (txid)
That's it. Three commands: createrungtx builds the transaction, signladder signs it using a descriptor string, sendrawtransaction broadcasts it. The descriptor handles all serialisation, Merkle commitment, and witness construction internally. No manual field ordering, no JSON block specs. Works for all 65 block types.
HTTP API (no node required)

If you don't want to run a node, the same flow works via the public proxy API at ladder-script.org:

# Create $ curl -X POST https://ladder-script.org/api/ladder/createrungtx \ -H "Content-Type: application/json" \ -d '{"inputs":[...],"outputs":[0.001],"rungs":[{"output_index":0,"blocks":[...]}]}' # Sign (descriptor path) $ curl -X POST https://ladder-script.org/api/ladder/signladder \ -H "Content-Type: application/json" \ -d '{"hex":"...","descriptor":"ladder(sig(@alice))","keys":{"alice":"cVt..."},"spent_outputs":[...]}' # Broadcast $ curl -X POST https://ladder-script.org/api/ladder/broadcast \ -H "Content-Type: application/json" \ -d '{"hex":"...signed..."}'