Multi-Protocol Commitments
The MPC tree and its commitment are byte-identical in both lines. Line A adds one extra layer inside each leaf. See Two RGB lines.
A witness transaction has room for exactly one deterministic commitment. Multi-protocol commitments (MPC) let many RGB contracts share it. Each contract gets one leaf in a Merkle tree, and only the tree's 32-byte commitment goes on chain.
A verifier of one contract sees only that contract's leaf and a Merkle path. It learns nothing about the other contracts in the tree, or even how many there are.
The design comes from LNPBP-4. In RGB, the "protocol" is the contract: the 32-byte ProtocolId of a leaf is the ContractId. There are no protocol tags, versions or per-protocol subtrees.
What a leaf holds​
- v0.11 (rgb-cmd)
- v0.12 (rgb-wallet)
The leaf message is the contract's BundleId: the id of the transition bundle that holds every state transition of that contract closed by this witness transaction (rgb-consensus 0.11.1 src/validation/validator.rs, mpc::Message::from(bundle_id)).
The leaf message is the commitment of a multi-message bundle, mmb::BundleProof. This is a map from witness-transaction input index to message (an operation id), tagged urn:lnp-bp:mmb:bundle#2024-11-18.
Verifying a seal therefore checks two things: that the witness transaction spends the seal's outpoint at some input i, and that the bundle maps i to this operation (bp-core 0.12.0 seals/src/txout.rs). Line A's MerkleProof and MerkleBlock also carry a method field (Sha256t) in their serialized form. The commitment value doesn't change.
Building the tree​
Source: commit_verify 0.12.0 src/mpc/ (Line A) and rgb-consensus 0.11.1 src/commit_verify/mpc/ (Line B). The docs.rgb.info MPC page describes the same construction.
- Pick a size. Start at depth
d = 3, the minimum (MPC_MINIMAL_DEPTH), so that the tree doesn't reveal how few contracts it holds. The width isw = 2^d. - Place each contract. A contract with id
cgoes to leaf positionwherepos(c) = c mod (w − cofactor)cis read as a 256-bit little-endian integer. The tree builder triescofactor = 0, 1, 2, …(at most 500 tries) until every contract lands in a different position. If none works, it adds one level of depth and tries again. - Fill the leaves. Occupied leaves commit to the contract:
Empty leaves commit to random entropy:
tH_merkle(0x10 || contract_id || message)tH_merkle(0x11 || entropy || pos)entropyis au64(8 bytes) picked by whoever builds the tree, andposis a little-endianu32. docs.rgb.info calls entropy "a 64-byte random value". In the code it is 64 bits (Leaf::Entropy { entropy: u64, pos: u32 }). - Hash the nodes. Each branch is
tH_merkle(branching || depth || width || left || right)tH_merkleis a tagged SHA-256 with tagurn:ubideco:merkle:node#2024-01-31. Children are not sorted; their position comes from the leaf index. - Commit. The value that goes into the opret or tapret script is
with
mpc::Commitment = tH("urn:ubideco:mpc:commitment#2024-01-31", depth || cofactor || root)depthas one byte andcofactoras a little-endianu16.
Proofs​
The sender gives each recipient an MPC proof (MerkleProof): the leaf position, the cofactor and the sibling hashes along the path. The recipient recomputes the commitment from their own contract's leaf and checks it against the witness transaction. The tree builder keeps the full MerkleBlock and can reveal or conceal individual leaves.
The Rust types live in commit_verify::mpc (Line A crate commit_verify 0.12.0) and rgbcore::commit_verify::mpc (Line B crate rgb-consensus 0.11.1, library name rgbcore): MultiSource, MerkleTree, MerkleBlock, MerkleProof, ProtocolId, Message and Commitment.
What MPC does and doesn't do​
- Different contracts' state moves can share one Bitcoin transaction, and so one fee.
- Each contract's data stays private from the holders of other contracts in the same tree.
- Within one contract, all transitions closed by the transaction share a single leaf: the bundle (B) or the multi-message bundle (A).
- No Lightning or other non-RGB protocol occupies MPC leaves in either line.
- Opret and tapret reveal the same thing: one 32-byte commitment. The difference is that opret's
OP_RETURNoutput is visible as such, while tapret hides in a taproot key.