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 is new in TX_MLSC. It maps each output index to its conditions within the shared condition tree. One conditions_root per transaction defines the entire tree. This replaces the old model where each output carried its own 0xC2 + root.
All 61 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, @point)ADAPTOR_SIGSigning key + adaptor point. Optional scheme
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–32 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(blocks)TIMER_CONTINUOUSConsecutive blocks required
timer_off_delay(trigger, hold)TIMER_OFF_DELAYTrigger + hold-off blocks
latch_set(@pk, state)LATCH_SETKey + state value
latch_reset(@pk, state)LATCH_RESETKey + state value
counter_down(@pk, count)COUNTER_DOWNKey + counter target
counter_preset(@pk, count)COUNTER_PRESETKey + preset count
counter_up(@pk, count)COUNTER_UPKey + counter target
compare(op, value_b)COMPAREOperator (1=EQ..7=IN_RANGE) + threshold. Optional value_c for IN_RANGE
sequencer(steps)SEQUENCERNumber of sequence steps
one_shot(@pk, N)ONE_SHOTKey + activation window
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(@sender, @receiver, preimage, csv)HTLCTwo keys, preimage hex, CSV blocks
hash_sig(@pk, preimage)HASH_SIGKey + preimage hex
ptlc(@pk, @point, csv)PTLCKey, adaptor point, CSV blocks
cltv_sig(@pk, height)CLTV_SIGSignature + absolute timelock
timelocked_multisig(M, @k1, ..., csv)TIMELOCKED_MULTISIGThreshold, keys, CSV blocks

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)
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, @adaptor, 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 and MLSC root.
$ ghost-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 }
formatladder
Convert conditions hex back to a descriptor string.
$ ghost-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))

Key aliases are not preserved in the hex (the keys are hashed into the Merkle tree). When formatting back, unknown keys appear as @? or truncated hex.

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 $ ghost-cli -signet getnewaddress tb1q... $ ghost-cli -signet getaddressinfo tb1q... | jq .pubkey "02abc123..." # Create an unsigned tx with a vault condition: # hot key + 144-block delay, OR 2-of-3 multisig recovery $ ghost-cli -signet createtxmlsc \ '[{"txid":"<utxo>","vout":0}]' \ '[{"amount":0.001,"conditions":[{ "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 $ ghost-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.
$ ghost-cli -signet sendrawtransaction "04000000...signed..." → "a1b2c3d4e5f6..." (txid)
That's it. Three commands: createtxmlsc builds the transaction, signladder signs it using a descriptor string, sendrawtransaction broadcasts it. The descriptor handles all serialization, Merkle commitment, creation proof, and witness construction internally. No manual field ordering, no JSON block specs. Works for all 61 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/create \ -H "Content-Type: application/json" \ -d '{"inputs":[...],"outputs":[{"amount":0.001,"conditions":[...]}]}' # Sign (descriptor path) $ curl -X POST https://ladder-script.org/api/ladder/sign-descriptor \ -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..."}'