Skip to content

Merkleize/mattrs

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

50 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mattrs

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.

Getting started

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 below

Then 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_started

Note 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 the script! macro the MATT opcodes OP_CHECKTEMPLATEVERIFY (0xb3) and OP_CHECKCONTRACTVERIFY (0xbb), which upstream does not know. Do not "upgrade" it back to upstream.

Design: one source of truth

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 → clause lookup,
  • 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.

Defining a contract

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,
}

Spending: a clause is a typed method call

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.

Fraud proofs: mattrs::fraud

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.

Ported examples

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).

Demos

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 --paper

Omit 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.txt

Live 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

Spend-API features

  • CTV templates as clause outputs — a clause's next may return a CtvTemplate, which fixes the transaction outputs and nSequence (see rps).
  • Multi-input batch spendsContractManager::spend_batch(..) merges several instances' outputs by index (pymatt's get_spend_tx semantics).
  • 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, and wait_for_spend(..) (mempool + block scan; or the RPC-free observe_spend(..)) decodes the spending witness back into the clause and its typed arguments and materializes the child instances (see tests/test_observe.rs).

Testing

cargo test                # unit + integration tests (no node required)
cargo test -- --ignored   # also runs the end-to-end tests against a regtest bitcoind

The 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.

About

Framework for sMATT contracts

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages