emj
shrx

Lesson 4 · ~12 min · Lesson 3

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

PrimitivePurpose
SimplicityProgramCompile/run .simf; produces CMR (commitment root)
SimplicityArgumentsTyped compile-time parameters (asset IDs, amounts, hashes)
SimplicityWitnessValuesRuntime witness for a spend path (Either branches)
StateTaprootBuilderTaproot tree: program leaf + optional 32-byte data leaves
ExternalUtxoCovenant UTXO not in wallet DB — outpoint + txout + secrets
Pset / PsetBuilderLow-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:

  • witnessUtxoScriptHex
  • witnessUtxoAssetId
  • witnessUtxoAmount

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.

Two different “witness” words Witness UTXO = prevout data on a tx input (PSET field). Simplicity witness = PATH and branch values proving which covenant branch executes. Don’t confuse them in review.

Signing: use the output key

Taproot styleKey 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?