A Rust toolkit for building MATT (Merkleize-All-The-Things) Bitcoin covenant
contracts using CHECKCONTRACTVERIFY (CCV) and CHECKTEMPLATEVERIFY (CTV).
It provides witness (de)serialization, typed clauses with type-erased runtime
dispatch, P2TR / augmented-P2TR contract templates, a taproot tree, derive macros
(mattrs-derive), a contract! DSL that generates a typed handle with one spend
method per clause, a generic bisection fraud-proof module (mattrs::fraud), and
an RPC-driven ContractManager for funding and spending instances on regtest.
For the conceptual model behind all of this — programs, clauses, merkleized
state, and the Contract{params}[vars] notation used to specify protocols as
finite state machines — see Designing contracts.
The crate is unpublished; use it as a path dependency:
[dependencies]
mattrs = { path = "../mattrs2" }
bitcoin = "0.32"
bitcoin-script = { path = "../mattrs2/bitcoin-script" } # patched vendor, see belowThen run the offline example — it defines a two-clause contract with the
contract! DSL, derives its address, and builds a signed spend without a node:
cargo run --example getting_startedNote on
bitcoin-script/: this repository vendors a patched copy of BitVM/rust-bitcoin-script v0.2.0. The patch (bitcoin-script/src/parse.rs) teaches thescript!macro the MATT opcodesOP_CHECKTEMPLATEVERIFY(0xb3) andOP_CHECKCONTRACTVERIFY(0xbb), which upstream does not know. Do not "upgrade" it back to upstream.
A contract is a taproot output whose tapscript leaves are clauses (name +
script + typed args + a function computing the next outputs). A
ClauseTree is built once, and everything else is derived from it:
- the address-bearing script taptree,
- the spend-time
name → clauselookup, - the witness layout (argument order).
Because they are all derived from the same tree, a contract's address can never drift from the witness that spends it. Contracts are also self-describing: each carries its encoded params, so child instances recover their params from the contract rather than from a parallel field.
A single contract! { .. } block generates the per-clause *Args structs, the
clause objects and taptree, a contract struct (new / fund / address /
taptree_root / as_erased), and a typed handle with one spend method per
clause. Params/state stay ordinary derived structs; the tapscripts stay as
reviewable functions the DSL references. The derives are self-contained — a
contract file imports only the traits it actually names.
#[derive(Debug, Clone, ContractParams)]
struct VaultParams { /* ... */ }
contract! {
contract Vault {
params VaultParams;
// optional; defaults to the NUMS key (no key-spend path)
internal_key |p| internal_key_or_nums(p.alternate_pk);
clause trigger {
args {
#[signer(p.unvault_pk)] sig: Signature, // auto-filled at spend time
ctv_hash: [u8; 32],
out_i: i64,
}
script Vault::trigger_script; // fn(&VaultParams) -> ScriptBuf
// The body yields Result<T, ClauseError> where T is anything
// Into<NextOutputs>: Vec<ClauseOutput>, CtvTemplate, or NextOutputs.
next(p, a) { /* ... */ }
}
// ... more clauses ...
tree [trigger, [trigger_and_revault, recover]];
}
}Multi-field contract state commits with a derive instead of a manual impl:
#[derive(Debug, Clone, ContractState)]
#[commit(merkle)] // encode() = Merkle root of the fields' leaves
pub struct G256S2State {
pub t_a: [u8; 32], // a raw 32-byte leaf
#[leaf(sha256)] pub y: i64, // leaf = sha256 of the field's witness encoding
#[leaf(sha256)] pub x: i64,
}let vault = Vault::new(params).fund(&mut manager, amount)?; // VaultHandle
// (augmented contracts take the initial state too: `.fund(&mut manager, amount, state)`)
// trigger: signed, exactly one child, returned typed
let unvaulting: UnvaultingHandle = vault
.trigger(ctv_hash, 0)
.sign(HotSigner::new(unvault_key))
.exec_one(&mut manager)?
.try_into()?;
// withdraw: terminal CTV spend with explicit outputs
unvaulting
.withdraw(ctv_hash)
.outputs(withdraw_outputs)
.sequence(10)
.exec_none(&mut manager)?;Signatures are never hand-built: a #[signer] field stays in the *Args struct
(so the struct alone is the witness layout), the generated new() omits it, and
.sign(..) fills it by matching the clause's required pubkey — or the spend fails
with MissingSigner.
A complete worked example (a two-stage vault) lives in examples/vault/contracts.rs.
The generic bisection fraud-proof protocol (pymatt's hub/fraud.py) is a library
module: Alice claims an n-step computation's result, Bob disputes it, and the
Bisect_1/Bisect_2 contracts bisect the execution trace down to one step, which
the Leaf contract re-runs on-chain. The whole machinery is generic over a
Computer — the step function and value-commitment as script fragments plus the
witness specs of one value:
let compute2x = Computer {
encoder: script! { OP_SHA256 }, // value commitment
func: script! { OP_DUP OP_ADD }, // one step: y = 2x
specs: vec![ArgSpec { name: "x".into(), arg_type: Arc::new(IntType) }],
};
let leaf_factory: LeafFactory =
Arc::new(move |_i| Leaf::new(LeafParams { alice_pk, bob_pk }, &compute2x));
let challenge = Bisect1::new(BisectParams { alice_pk, bob_pk, i: 0, j: 7 },
&leaf_factory, /*forfait_timeout=*/10);tests/support/game256.rs instantiates it for the game256 example in ~30 lines.
The examples from the Python reference framework (pymatt) are ported under
tests/support/, each with tests asserting byte-for-byte taproot compatibility
(the taptree root / address matches pymatt's) and, where applicable, the spend:
| Example | Demonstrates | Status |
|---|---|---|
vault (examples/vault/contracts.rs) |
two-stage vault; CCV + CTV; augmented state; multi-input trigger-with-revault | address matches; regtest e2e; interactive REPL |
rps (examples/rps/contracts.rs) |
hashed state; clause-owned CTV templates for payouts; check_in/out_contract |
roots match; regtest e2e; two-player demo |
ram (ram.rs) |
a Merkle-committed cell vector; the WitProof<N> witness arg; expanded state |
root matches; write spends |
game256 (game256.rs) |
the bisection fraud proof (mattrs::fraud) driven by the G256S0/1/2 game stages |
all taptrees match; full challenge regtest e2e |
minivault (minivault.rs) |
CCV-only vault with feature-toggled clauses: a runtime-shaped taptree via the StandardClause/ClauseTree escape hatch |
all 4 feature combos regtest e2e |
Supporting MATT infrastructure lives in the library for downstream reuse: the
generic fraud-proof contracts (mattrs::fraud), a data MerkleTree /
MerkleProof / WitProof / MerkleProofType (mattrs::merkle), the
merkle_root(n) / dup(n) / drop(n) / check_input_contract /
check_output_contract / older / timeout_sig_script / opaque_p2tr script
fragments (mattrs::script_helpers), commit_int (mattrs::script_utils), and
offline_client() / fund_fake(..) for building spends without a node
(mattrs::testutil, used by the examples and fixtures).
Both demos run against a regtest node with a funded testwallet (cookie auth
or BITCOIN_RPC_* env vars).
Two-player RPS (examples/rps/) plays a Rock-Paper-Scissors game between
two separate processes, negotiated over a TCP socket and played entirely
on-chain: Alice funds the game behind a hiding move commitment, Bob reveals his
move with a typed spend, and each side follows the other's turn with chain
observation. In two terminals:
cargo run --example rps -- --alice --rock
cargo run --example rps -- --bob --paperOmit the move flag to be prompted for one; --addr host:port and
--wallet name override the defaults.
Vault REPL (examples/vault/) drives the two-stage vault interactively:
fund vaults, trigger several at once towards a CTV withdrawal template (with
the leftover revaulted in the same transaction), then withdraw after the
timelock — or recover to sweep to the recovery key at any time. Sample
scripts live in examples/vault/scripts/:
cargo run --example vault # interactive
cargo run --example vault -- --script examples/vault/scripts/revault.txtLive inspector — both demos take --inspector (or --inspector-port <P>)
when built with the inspector feature: the manager then serves a JSON snapshot
of every tracked instance over TCP on each state change, and the
mattrs-inspector workspace crate renders it as a live TUI (instance table +
detail pane). In two terminals:
cargo run --example vault --features inspector -- --inspector
cargo run -p mattrs-inspector # or: nc localhost 34443- CTV templates as clause outputs — a clause's
nextmay return aCtvTemplate, which fixes the transaction outputs andnSequence(see rps). - Multi-input batch spends —
ContractManager::spend_batch(..)merges several instances' outputs by index (pymatt'sget_spend_txsemantics). - Expanded state — an instance can carry logical state richer than its on-chain
commitment (e.g. RAM's cells vs their Merkle root), recovered by
next_outputs. - Chain observation — follow a covenant driven by someone else:
track_instance(..)registers an externally funded instance, andwait_for_spend(..)(mempool + block scan; or the RPC-freeobserve_spend(..)) decodes the spending witness back into the clause and its typed arguments and materializes the child instances (seetests/test_observe.rs).
cargo test # unit + integration tests (no node required)
cargo test -- --ignored # also runs the end-to-end tests against a regtest bitcoindThe end-to-end tests are #[ignore]d by default because they need a configured
regtest bitcoind with a funded testwallet (cookie or env-var RPC auth; see
tests/support/testkit.rs). They also write markdown reports of every
transaction they broadcast (inputs, outputs, per-input witness breakdown with
sizes) to reports/ — see mattrs::report.