Skip to main content

Multi-Protocol Commitments

Applies to both RGB lines

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​

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

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.

  1. 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 is w = 2^d.
  2. Place each contract. A contract with id c goes to leaf position
    pos(c) = c mod (w − cofactor)
    where c is read as a 256-bit little-endian integer. The tree builder tries cofactor = 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.
  3. Fill the leaves. Occupied leaves commit to the contract:
    tH_merkle(0x10 || contract_id || message)
    Empty leaves commit to random entropy:
    tH_merkle(0x11 || entropy || pos)
    entropy is a u64 (8 bytes) picked by whoever builds the tree, and pos is a little-endian u32. docs.rgb.info calls entropy "a 64-byte random value". In the code it is 64 bits (Leaf::Entropy { entropy: u64, pos: u32 }).
  4. Hash the nodes. Each branch is
    tH_merkle(branching || depth || width || left || right)
    tH_merkle is a tagged SHA-256 with tag urn:ubideco:merkle:node#2024-01-31. Children are not sorted; their position comes from the leaf index.
  5. Commit. The value that goes into the opret or tapret script is
    mpc::Commitment = tH("urn:ubideco:mpc:commitment#2024-01-31", depth || cofactor || root)
    with depth as one byte and cofactor as a little-endian u16.

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_RETURN output is visible as such, while tapret hides in a taproot key.