Inside the covenant: Simplicity primitives
Before reviewing PR #16, understand how lwk-dart builds Simplicity covenants internally — Taproot trees, witness inputs, and UTXOs the wallet doesn’t own.
Why a primitives layer?
LWK handles wallet concerns (keys, sync, sign). Lending covenants need extra machinery: compile .simf sources, build Taproot addresses with on-chain state, construct PSETs that spend covenant outputs. PR #16 adds this as Rust-only code under rust/src/contracts/ — not exported to Dart yet.
Core building blocks
| Primitive | Purpose |
|---|---|
SimplicityProgram | Compile/run .simf; produces CMR (commitment root) |
SimplicityArguments | Typed compile-time parameters (asset IDs, amounts, hashes) |
SimplicityWitnessValues | Runtime witness for a spend path (Either branches) |
StateTaprootBuilder | Taproot tree: program leaf + optional 32-byte data leaves |
ExternalUtxo | Covenant UTXO not in wallet DB — outpoint + txout + secrets |
Pset / PsetBuilder | Low-level Elements transaction construction |
Stateful Taproot: program + storage
Lending offers store mutable state on-chain in data leaves beside the Simplicity program leaf:
StateTaprootBuilder .addSimplicityLeaf(depth, program CMR) ← covenant logic .addDataLeaf(depth, slot_0: 32 bytes) ← e.g. is_active .addDataLeaf(depth, slot_1: 32 bytes) ← e.g. current_debt .finalize(unspendable internal key) → scriptPubkey for the covenant output
Many protocols use the standard unspendable internal key (BIP-341 NUMS point) so only the script path can spend. The lending CovenantProgram wrapper (PR #13) builds this tree automatically.
ExternalUtxo: not your wallet’s money
When spending a covenant output, that UTXO often isn’t tracked as a normal wallet balance — it’s locked by Simplicity. ExternalUtxo describes it for tx building:
- Outpoint (txid + vout)
- Full
TxOut(script, asset, amount) - Unblinded secrets (needed for confidential assets)
The wallet still signs its own inputs; covenant inputs use Simplicity witness stacks instead of a simple ECDSA signature.
Witness UTXO: what the chain sees
Elements/Bitcoin PSET inputs often need a witness UTXO — the prevout’s explicit txout data attached to the input so validators know what is being spent. In FRB APIs you may see:
witnessUtxoScriptHexwitnessUtxoAssetIdwitnessUtxoAmount
This is not the Simplicity witness (branch values). It is standard tx metadata required when adding covenant or issuance inputs to a PSET. PR #15’s factory methods take these parameters for that reason.
PATH and branch values proving which covenant branch executes. Don’t confuse them in review.
Signing: use the output key
| Taproot style | Key for sighash / finalize |
|---|---|
| Simple P2TR (no state) | Internal x-only key |
Stateful (StateTaprootBuilder) | outputKey() from spend info — not internal key |
Wrong key → invalid signature or node rejection. Finalized Simplicity inputs have a 4-element witness stack: [witness_bytes, program_bytes, cmr_bytes, control_block].
Check your understanding
1. What does a StateTaproot data leaf store?
2. When is ExternalUtxo used in transaction building?
3. For stateful Taproot finalize, which key signs?