- Commitment schemes in Bitcoin and their use by RGB
- TxO2 diagram example
- Commitment locations in a transaction
- Tapret
- Analysis and practical choices in RGB
- Multi Protocol Commitments - MPC
- Anchors: a global assembly
- Conclusion
In this chapter, we'll look at the implementation of Client-side Validation and Single-use Seals within the Bitcoin blockchain. We'll present the main principles of RGB's commitment layer (layer 1), with a particular focus on the TxO2 scheme, which RGB uses to define and close a seal in a Bitcoin transaction. Next, we'll discuss two important points that haven't yet been covered in detail:
- The deterministic Bitcoin commitments;
- Multi-protocol commitments.
It is the combination of these concepts that enables us to superimpose several systems or contracts on top of a single UTXO and therefore a single blockchain.
It should be remembered that the cryptographic operations described can be applied, in absolute terms, to other blockchains or publishing media, but Bitcoin's characteristics (in terms of decentralization, resistance to censorship and openness to all) make it the ideal foundation for developing advanced programmability such as that required by RGB.
Commitment schemes in Bitcoin and their use by RGB
As we saw in the first chapter of the course, Single-use Seals are a general concept: we make a promise to include a commitment (commitment) in a specific location of a transaction, and this location acts like a seal that we close on a message. However, on the Bitcoin blockchain, there are several options for choosing where to place this commitment.
To understand the logic, let's recall the basic principle: to close a single-use seal, we spend the sealed area by inserting the commitment on a given message. In Bitcoin, this can be done in a number of ways:
- Use a public key or address
We can decide that a specific public key or address is the single-use seal. As soon as this key or address appears on-chain in a transaction, it means that the seal is closed with a certain message.
- Use a Bitcoin transaction output
This means that a single-use seal is defined as a precise outpoint (a TXID + output number pair). As soon as this outpoint is spent, the seal is closed.
While working on RGB, we identified at least 4 different ways to implement these seals on Bitcoin:
- Define the seal via a public key, and close it in an output;
- Define the seal with an outpoint and close it with an output;
- Define the seal via the value of a public key, and close it in a input;
- Define the seal via an outpoint, and close it in an input.
| Schema Name | Seal Definition | Seal Closure | Additional Requirements | Main Application | Possible Commitment Schemes |
| PkO | Public Key Value | Transaction Output | P2(W)PKH | None at the moment | Keytweak, taptweak, opret |
| TxO2 | Transaction Output | Transaction Output | Requires deterministic commitments on Bitcoin | RGBv1 (universal) | Keytweak, tapret, opret |
| PkI | Public Key Value | Transaction Input | Taproot only & not compatible with legacy wallets | Bitcoin-based identities | Sigtweak, witweak |
| TxO1 | Transaction Output | Transaction Input | Taproot only & not compatible with legacy wallets | None at the moment | Sigtweak, witweak |
We won't go into detail about each of these configurations, as in RGB we've chosen to use an outpoint as the definition of the seal, and to place the commitment in the output of the transaction spending this outpoint. We can therefore introduce the following concepts for the sequel:
- "Seal definition ": A given outpoint (identified by TXID + output no.);
- "Seal closing ": The transaction that spends this outpoint, in which a commitment is added to a message.
This scheme has been selected for its compatibility with RGB architecture, but other configurations could be useful for different uses.
The "O2" in "TxO2" reminds us that both definition and closure are based on the expenditure (or creation) of a transaction output.
TxO2 diagram example
As a reminder, defining a single-use seal does not necessarily require publishing an on-chain transaction. It's enough for Alice, for example, to already have an unspent UTXO. She can decide: "This outpoint (already existing) is now my seal". She notes this locally (client-side), and until this UTXO is spent, the seal is considered open.
On the day it wants to close the seal (to signal an event, or to anchor a particular message), it spends this UTXO in a new transaction (this transaction is often called the "witness transaction" (unrelated to segwit, it's just the term we give it). This new transaction will contain the commitment to the message.
Note that in this example:
- No one but Bob (or the people to whom Alice chooses to reveal the full proof) will know that a certain message is hidden in this transaction;
- Everyone can see that the outpoint has been spent, but only Bob holds the proof that the message is actually anchored in the transaction.
To illustrate this TxO2 scheme, we can use a single-use seal as a mechanism for revoking a PGP key. Instead of publishing a revocation certificate on servers, Alice can say: "This Bitcoin output, if spent, means that my PGP key is revoked".
Alice therefore has a specific UTXO, to which a certain state or data (known only to her) is associated locally (on the client side).
Alice informs Bob that if this UTXO is spent, a particular event will be deemed to have occurred. From the outside, all we see is a Bitcoin transaction; but Bob knows that this expenditure has a hidden meaning.
As Alice spends this UTXO, she closes the seal on a message indicating her new key, or simply the revocation of the old one. In this way, anyone monitoring on-chain will see that the UTXO is spent, but only those with the full proof will know that it is precisely the revocation of the PGP key.
In order for Bob or anyone else involved to check the hidden message, Alice must provide him with off-chain information.
Alice must therefore provide Bob with the following:
- The message itself (for example, the new PGP key);
- Cryptographic proof that the message was involved in the transaction (known as extra transaction proof or anchor).
Third parties don't have this information. They only see that a UTXO has been spent. Confidentiality is therefore assured.
To clarify the structure, let's summarize the process in two transactions:
- Transaction 1: This contains the seal definition, i.e. the outpoint that will serve as the seal.
- Transaction 2: Spends this outpoint. This closes the seal and, in the same transaction, inserts the commitment on the message.
We therefore call the second transaction the "witness transaction".
To illustrate this from another angle, we can represent two layers:
- The top layer (blockchain, public): everyone sees the transaction and knows that a outpoint has been spent;
- The lower layer (client-side, private): only Alice (or the person concerned) knows that this expense corresponds to such and such a message, via the cryptographic proof and the message she keeps locally.
But when closing the seal, the question arises as to where the commitment should be inserted.
In the previous section, we briefly mentioned how the Client-side Validation model can be applied to RGB and other systems. Here, we tackle the part about deterministic Bitcoin commitments and how to integrate them into a transaction. The idea is to understand why we are trying to insert a single commitment into the witness transaction, and above all how to ensure that there can be no other undisclosed competing commitments.
Commitment locations in a transaction
When you give someone proof that a certain message is embedded in a transaction, you need to be able to guarantee that there isn't another form of commitment (a second, hidden message) in the same transaction that hasn't been revealed to you. For client-side validation to remain robust, you need a deterministic mechanism for placing a single commitment in the transaction that closes the single-use seal.
The witness transaction spends the famous UTXO (or seal definition) and this expenditure corresponds to the closing of the seal. Technically speaking, we know that each outpoint can only be spent once. This is precisely what underpins Bitcoin's resistance to double spending. But the spending transaction may have several inputs, several outputs, or be composed in a complex way (coinjoins, Lightning channels, etc.). We therefore need to clearly define where to insert the commitment in this structure, unambiguously and uniformly.
Whatever the method (PkO, TxO2, etc.), the commitment can be inserted:
- In an Input via:
- Sigtweak (modifies the
rcomponent of the ECDSA signature, similar to the "Sign-to-contract" principle); - Witweak (the transaction's segregated witness data is modified).
- Sigtweak (modifies the
- In an Output via:
- Keytweak (the recipient's public key is "tweaked" with the message);
- Opret (the message is placed in a non-spendable output
OP_RETURN); - Tapret (or Taptweak), which relies on taproot to insert commitment into the script part of a taproot key, thus modifying the public key deterministically.
Here are the details of each method:
Sig tweak (sign-to-contract):
An earlier scheme involved exploiting the random part of a signature (ECDSA or Schnorr) to embed the commitment: this is the technique known as "Sign-to-contract". You replace the randomly generated nonce with a hash containing the data. In this way, the signature implicitly reveals your commitment, without any additional space in the transaction. This approach has a number of advantages:
- No on-chain overload (you use the same place as the basic nonce);
- In theory, this can be quite discrete, as the nonce is initially a random datum.
However, 2 major drawbacks have emerged:
- Multisig before Taproot: when you have several signatories, you need to decide which signature will carry the commitment. Signatures can be ordered differently, and if a signatory refuses, you lose control over the outcome of the commitment;
- MuSig and the shared nonce: with Schnorr multisig (MuSig), nonce generation is a multiparty algorithm, and it becomes virtually impossible to tweak the nonce individually.
In practice, sig tweak is also not very compatible with existing hardware (hardware wallets) and formats (Lightning, etc.). So this great idea is hard to put into practice.
Key tweak (pay-to-contract):
The key tweak takes up the historical concept of pay-to-contract. We take the public key
X and tweak it by adding the value H(message). Specifically, if X = x * G and h = H(message), then the new key will be X' = X + h * G. This tweaked key hides the commitment to the message. The holder of the original private key can, by adding h to his private key x, prove that he has the key to spend the output. In theory, this is elegant, because:- The commitment is entered without adding any additional fields;
- You don't store any additional on-chain data.
In practice, however, we come up against the following difficulties:
- Wallets no longer recognize the standard public key, since it has been "tweaked", so they can't easily associate UTXO with your usual key;
- Hardware wallets are not designed to sign with a key that is not derived from their standard derivation;
- You need to adapt your scripts, descriptors, etc.
In the context of RGB, this path was envisaged until 2021, but it proved too complicated to make it work with current standards and infrastructure.
Witness tweak:
Another idea, which certain protocols such as inscriptions Ordinals have put into practice, is to place the data directly in the
witness section of the transaction (hence the expression "witness tweak"). However, this method:- Makes engagement immediately visible (you literally paste raw data into the witness);
- May be subject to censorship (miners or nodes may refuse to relay if it is too large or any other arbitrary characteristic);
- Consumes space in the blocks, contrary to RGB's objective of discretion and lightness.
In addition, witness is designed to be prunable in certain contexts, which can make having robust proofs more complicated.
Open-return (opret):
Very simple in its operation, an
OP_RETURN allows you to store a hash or message in a special field of the transaction. But it's immediately detectable: everyone sees that there's a commitment in the transaction, and it can be censored or discarded, as well as adding extra output. Since this increases transparency and size, it's considered less satisfactory from the point of view of a Client-side Validation solution.34-byte_Opret_Commitment = OP_RETURN OP_PUSHBYTE_32 <mpc::Commitment> |_________| |______________| |_________________| 1-byte 1-byte 32 bytes
Tapret
The final option is the use of Taproot (introduced with BIP341) with the Tapret scheme. Tapret is a more complex form of deterministic commitment, which brings improvements in terms of footprint on the blockchain and confidentiality for contract operations. The main idea is to hide the commitment in the
Script Path Spend part of a taproot transaction.Before describing how the commitment is inserted into a taproot transaction, let's look at the exact form of the commitment, which must imperatively correspond to a 64-byte string constructed as follows:
64-byte_Tapret_Commitment = OP_RESERVED ... ... .. OP_RESERVED OP_RETURN OP_PUSHBYTE_33 <mpc::Commitment> <Nonce> |___________________________________| |_________| |______________| |_______________| |______| OP_RESERVED x 29 times = 29 bytes 1 byte 1 byte 32 bytes 1 byte |________________________________________________________________| |_________________________| TAPRET_SCRIPT_COMMITMENT_PREFIX = 31 bytes MPC commitment + NONCE = 33 bytes
- The 29 bytes
OP_RESERVED, followed byOP_RETURN, thenOP_PUSHBYTE_33, form the 31-byte prefix part; - Next comes a 32-byte commitment (usually the Merkle root from MPC), to which we add 1 byte of Nonce (a total of 33 bytes for this second part).
So the 64-byte
Tapret method looks like an Opret to which we've prefixed 29 bytes of OP_RESERVED and added an extra byte as a Nonce.To maintain flexibility in terms of implementation, confidentiality and scaling, the Tapret scheme takes into account various use cases, depending on requirements:
- Unique incorporation of a Tapret commitment into a taproot transaction without a pre-existing Script Path structure;
- Integration of a Tapret commitment into a Taproot transaction already equipped with a Script Path.
Let's take a closer look at each of these two scenarios.
Tapret incorporation without existing Script Path
In this first case, we start from a taproot output key (Taproot Output Key)
Q which contains only the internal public key P (Internal Key), with no associated script path (Script Path):P: the internal public key for the Key Path Spend.G: the generating point of the elliptic curve secp256k1. -t = tH_TWEAK(P)is the tweak factor, calculated via a tagged hash (e.g.SHA-256(SHA-256(TapTweak) || P)), in accordance with BIP86. This proves that there is no hidden script.
To include a Tapret commitment, add a Script Path Spend with a unique script, as follows:
t = tH_TWEAK(P || Script_root)then becomes the new tweak factor, including the Script_root.Script_root = tH_BRANCH(64-byte_Tapret_Commitment)represents the root of this script, which is simply a hash of typeSHA-256(SHA-256(TapBranch) || 64-byte_Tapret_Commitment).
The proof of inclusion and uniqueness in the taproot tree here boils down to the single internal public key
P.Tapret integration into a pre-existing Script Path
The second scenario concerns a more complex
Q taproot output, which already contains several scripts. For example, we have a tree of 3 scripts:tH_LEAF(x)designates the normalized tagged hash function of a leaf script.A, B, Crepresent scripts already included in the taproot structure.
To add the Tapret commitment, we need to insert an unspendable script at the first level of the tree, shifting the existing scripts one level down. Visually, the tree becomes:
tHABCrepresents the tagged hash of the top level groupingA, B, C.tHTrepresents the hash of the script corresponding to the 64-byteTapret.
According to taproot rules, each branch/leaf must be combined according to a lexicographical hash order. There are two possible cases:
tHT>tHABC: the Tapret commitment moves to the right of the tree. The uniqueness proof only needstHABCandP;tHT<tHABC: the Tapret commitment is placed on the left. To prove that there is no other Tapret commitment on the right,tHABandtHCmust be revealed to demonstrate the absence of any other such script.
Visual example for the first case (
tHABC < tHT):Example for the second case (
tHABC > tHT):Optimization with the nonce
To improve confidentiality, we can "mine" (a more accurate term would be "bruteforcing") the value of the
<Nonce> (the last byte of the 64-byte Tapret) in an attempt to obtain a hash tHT such that tHABC < tHT. In this case, the commitment is placed on the right, saving the user from having to divulge the entire contents of existing scripts to prove the Tapret's uniqueness.In summary, the
Tapret offers a discrete and deterministic way of incorporating a commitment into a taproot transaction, while respecting the requirement for uniqueness and unambiguity essential to RGB's Client-side Validation and Single-use Seal logic.Valid exits
For RGB commitment transactions, the main requirement for a valid Bitcoin commitment scheme is as follows: The transaction (witness transaction) must provably contain a single commitment. This requirement makes it impossible to construct an alternative history for client-side validated data within the same transaction. This means that the message around which the single-use seal closes is unique.
To satisfy this principle, and regardless of the number of outputs in a transaction, we require that one and only one output can contain a commitment. For each of the schemes used (Opret or Tapret), the only valid outputs that can contain an RGB commitment are the following:
- The first output
OP_RETURN(if present) for the Opret scheme; - The first taproot output (if present) for the Tapret scheme.
Note that it is quite possible for a transaction to contain a single
Opret commitment and a single Tapret commitment in two separate outputs. Thanks to the deterministic nature of Seal Definition, these two commitments then correspond to two distinct pieces of data validated on the client side.Analysis and practical choices in RGB
When we started RGB, we reviewed all these methods to determine where and how to place a commitment in a transaction in a deterministic way. We defined some criteria:
- Compatibility with different scenarios (e.g. multisig, Lightning, hardware wallets, etc.);
- Impact on on-chain space;
- Difficulty of implementation and maintenance;
- Confidentiality and resistance to censorship.
| Method | On-chain trace & size | Client-side size | Wallet Integration | Hardware Compatibility | Lightning Compatibility | Taproot Compatibility |
| Keytweak (deterministic P2C) | 🟢 | 🟡 | 🔴 | 🟠 | 🔴 BOLT, 🔴 Bifrost | 🟠 Taproot, 🟢 MuSig |
| Sigtweak (deterministic S2C) | 🟢 | 🟢 | 🟠 | 🔴 | 🔴 BOLT, 🔴 Bifrost | 🟠 Taproot, 🔴 MuSig |
| Opret (OP_RETURN) | 🔴 | 🟢 | 🟢 | 🟠 | 🔴 BOLT, 🟠 Bifrost | - |
| Tapret Algorithm: top-left node | 🟠 | 🔴 | 🟠 | 🟢 | 🔴 BOLT, 🟢 Bifrost | 🟢 Taproot, 🟢 MuSig |
| Tapret Algorithm #4: any node + proof | 🟢 | 🟠 | 🟠 | 🟢 | 🔴 BOLT, 🟢 Bifrost | 🟢 Taproot, 🟢 MuSig |
| Deterministic Commitment Scheme | Standard | On-Chain Cost | Proof Size on Client Side |
| Keytweak (Deterministic P2C) | LNPBP-1, 2 | 0 bytes | 33 bytes (non-tweaked key) |
| Sigtweak (Deterministic S2C) | WIP (LNPBP-39) | 0 bytes | 0 bytes |
| Opret (OP_RETURN) | - | 36 (v)bytes (additional TxOut) | 0 bytes |
| Tapret Algorithm: top-left node | LNPBP-6 | 32 bytes in the witness (8 vbytes) for any n-of-m multisig and spending through script path | 0 bytes on scriptless scripts taproot ~270 bytes in a single script case, ~128 bytes if multiple scripts |
| Tapret Algorithm #4: any node + uniqueness proof | LNPBP-6 | 32 bytes in the witness (8 vbytes) for single script cases, 0 bytes in the witness in most other cases | 0 bytes on scriptless scripts taproot, 65 bytes until the Taptree contains a dozen scripts |
| Layer | On-Chain Cost (Bytes/vbytes) | On-Chain Cost (Bytes/vbytes) | On-Chain Cost (Bytes/vbytes) | On-Chain Cost (Bytes/vbytes) | On-Chain Cost (Bytes/vbytes) | Client-Side Cost (Bytes) | Client-Side Cost (Bytes) | Client-Side Cost (Bytes) | Client-Side Cost (Bytes) | Client-Side Cost (Bytes) |
| Type | Tapret | Tapret #4 | Keytweak | Sigtweak | Opret | Tapret | Tapret #4 | Keytweak | Sigtweak | Opret |
| Single-sig | 0 | 0 | 0 | 0 | 32 | 0 | 0 | 32 | 0? | 0 |
| MuSig (n-of-n) | 0 | 0 | 0 | 0 | 32 | 0 | 0 | 32 | ? > 0 | 0 |
| Multi-sig 2-of-3 | 32/8 | 32/8 or 0 | 0 | n/a | 32 | ~270 | 65 | 32 | n/a | 0 |
| Multi-sig 3-of-5 | 32/8 | 32/8 or 0 | 0 | n/a | 32 | ~340 | 65 | 32 | n/a | 0 |
| Multi-sig 2-of-3 with timeouts | 32/8 | 0 | 0 | n/a | 32 | 64 | 65 | 32 | n/a | 0 |
| Layer | On-Chain Cost (vbytes) | On-Chain Cost (vbytes) | On-Chain Cost (vbytes) | Client-Side Cost (bytes) | Client-Side Cost (bytes) |
| Type | Base | Tapret #2 | Tapret #4 | Tapret #2 | Tapret #4 |
| MuSig (n-of-n) | 16.5 | 0 | 0 | 0 | 0 |
| FROST (n-of-m) | ? | 0 | 0 | 0 | 0 |
| Multi_a (n-of-m) | 1+16n+8m | 8 | 8 | 33 * m | 65 |
| Branch MuSig / Multi_a (n-of-m) | 1+16n+8n+8xlog(n) | 8 | 0 | 64 | 65 |
| With timeouts (n-of-m) | 1+16n+8n+8xlog(n) | 8 | 0 | 64 | 65 |
| Method | Privacy & Scalability | Interoperability | Compatibility | Portability | Complexity |
| Keytweak (Deterministic P2C) | 🟢 | 🔴 | 🔴 | 🟡 | 🟡 |
| Sigtweak (Deterministic S2C) | 🟢 | 🔴 | 🔴 | 🟢 | 🔴 |
| Opret (OP_RETURN) | 🔴 | 🟠 | 🔴 | 🟢 | 🟢 |
| Algo Tapret: Top-left node | 🟠 | 🟢 | 🟢 | 🔴 | 🟠 |
| Algo Tapret #4: Any node + proof | 🟢 | 🟢 | 🟢 | 🟠 | 🔴 |
In the course of the study, it became clear that none of the commitment schemes was fully compatible with the current Lightning standard (which does not employ Taproot, muSig2 or additional commitment support). Efforts are underway to modify Lightning's channel construction (BiFrost) to allow the insertion of RGB commitments. This is another area where we need to review the transaction structure, the keys and the way in which channel updates are signed.
The analysis showed that, in fact, other methods (key tweak, sig tweak, witness tweak, etc.) presented other forms of complication:
- Either we have a large on-chain volume;
- Either there is a radical incompatibility with the existing wallet code;
- Either the solution is not viable in non-cooperative multisig.
For RGB, two methods in particular stand out: Opret and Tapret, both classified as "Transaction Output", and compatible with the TxO2 mode used by the protocol.
Multi Protocol Commitments - MPC
In this section, we look at how RGB handles the aggregation of multiple contracts (or, more precisely, their transition bundles) within a single commitment (commitment) recorded in a Bitcoin transaction via a deterministic scheme (according to
Opret or Tapret). To achieve this, the order of Merkelization of the various contracts takes place in a structure called MPC Tree (Multi Protocol Commitment Tree). In this section, we'll look at the construction of this MPC Tree, how to obtain its root, and how multiple contracts can share the same transaction confidentially and unambiguously.Multi Protocol Commitment (MPC) is designed to meet two needs:
- The construction of the
mpc::Commitmenthash: this will be included in the Bitcoin blockchain according to anOpretorTapretscheme, and must reflect all the state changes to be validated; - Simultaneous storage of multiple contracts in a single commitment, enabling separate updates on multiple assets or RGB contracts to be managed in a single Bitcoin transaction.
In concrete terms, each transition bundle belongs to a particular contract. All this information is inserted into a MPC Tree, whose root (
mpc::Root) is then hashed again to give the mpc::Commitment. It is this last hash that is placed in the Bitcoin transaction (witness transaction), according to the deterministic method chosen.MPC Root Hash
The value actually written on-chain (in
Opret or Tapret) is called mpc::Commitment. This is calculated in the form of BIP-341, according to the formula:mpc::Commitment = SHA-256(SHA-256(mpc_tag) || SHA-256(mpc_tag) || depth || cofactor || mpc::Root )
where:
mpc_tagis a tag:urn:ubideco:mpc:commitment#2024-01-31, chosen according to RGB tagging conventions;depth(1 byte) indicates the depth of the MPC Tree;cofactor(16 bits, in Little Endian) is a parameter used to promote the uniqueness of the positions assigned to each contract in the tree;mpc::Rootis the root of MPC Tree, calculated according to the process described in the next section.
MPC Tree construction
To build this MPC Tree, we need to ensure that each contract corresponds to a unique leaf position. Suppose we have:
ccontracts to be included indexed byiini= {0,1,..,C-1};- For each contract
c_i, we have an identifierContractId(i) = c_i.
We then construct a tree of width
w and depth d such that 2^d = w, with w > C, so that each contract can be placed in a separate leaf. The position pos(c_i) of each contract in the tree is determined by:pos(c_i) = c_i mod (w - cofactor)
where
cofactor is an integer that increases the probability of obtaining distinct positions for each contract. In practice, construction follows an iterative process:- We start from a minimum depth (
d=3by convention to hide the exact number of contracts); - We try different
cofactors(up tow/2, or a maximum of 500 for performance reasons); - If we fail to position all contracts without collision, we increment
dand start again.
The aim is to avoid trees that are too tall, while keeping the risk of collision to a minimum. Note that the collision phenomenon follows a random distribution logic, linked to the Anniversary Paradox.
Inhabited leaves
Once
C distinct positions pos(c_i) have been obtained for contracts i = {0,1,..,C-1}, each sheet is filled with a hash function (tagged hash):tH_MPC_LEAF(c_i) = SHA-256(SHA-256(merkle_tag) || SHA-256(merkle_tag) || 0x10 || c_i || BundleId(c_i))
where:
merkle_tag = urn:ubideco:merkle:node#2024-01-31, is always chosen according to the Merkle conventions of RGB;0x10identifies a contract leaf;c_iis the 32-byte contract identifier (derived from the Genesis hash);bundleId(c_i)is a 32-byte hash describing the set ofState Transitionsrelative toc_i(gathered into a Transition Bundle).
Uninhabited leaves
The remaining leaves, not assigned to a contract (i.e.
w - C leaves), are filled with a "dummy" value (entropy leaf):tH_MPC_LEAF(j) = SHA-256(SHA-256(merkle_tag) || SHA-256(merkle_tag) || 0x11 || entropy || j )
where:
merkle_tag = urn:ubideco:merkle:node#2024-01-31, is always chosen according to the Merkle conventions of RGB;0x11denotes an entropy leaf;entropyis a random value of 64 bytes, chosen by the person building the tree;jis the position (in 32 bits Little Endian) of this leaf in the tree.
MPC nodes
After generating the
w leaves (inhabited or not), we proceed to merkelization. Any internal nodes are hashed as follows:tH_MPC_BRANCH(tH1 || tH2) = SHA-256(SHA-256(merkle_tag) || SHA-256(merkle_tag) || b || d || w || tH1 || tH2)
where:
merkle_tag = urn:ubideco:merkle:node#2024-01-31, is always chosen according to the Merkle conventions of RGB;bis the branching factor (8 bits). Most often,b=0x02because the tree is binary and complete;dis the depth of the node in the tree;wis the tree width (in 256-bit Little Endian binary);tH1andtH2are the hashes of the child nodes (or leaves), already calculated as shown above.
Progressing in this way, we obtain the root
mpc::Root. We can then calculate mpc::Commitment (as explained above) and insert it on-chain.To illustrate this, let's imagine an example where
C=3 (three contracts). Their positions are assumed to be pos(c_0)=7, pos(c_1)=4, pos(c_2)=2. The other leaves (positions 0, 1, 3, 5, 6) are entropy leaves. The diagram below shows the sequence of hashes to the root with:BUNDLE_iwhich representsBundleId(c_i);tH_MPC_LEAF(A)and so on, which represent leaves (some for contracts, others for entropy);- Each branch
tH_MPC_BRANCH(...)combines the hashes of its two children.
The final result is the mpc::Root, then the
mpc::Commitment.MPC shaft check
When a verifier wishes to ensure that a
c_i contract (and its BundleId) is included in the final mpc::Commitment, he simply receives a Merkle proof. This proof indicates the nodes needed to trace the leaves (in this case, c_i's contract leaf) back to the root. There's no need to disclose the entire MPC Tree: this protects the confidentiality of other contracts.In the example, a
c_2 verifier only needs an intermediate hash (tH_MPC_LEAF(D)), two tH_MPC_BRANCH(...), the pos(c_2) position proof and the cofactor value. It can then locally reconstruct the root, then recalculate the mpc::Commitment and compare it to the one written in the Bitcoin transaction (within Opret or Tapret).This mechanism ensures that:
- The status relative to
c_2is indeed included in the aggregate information block (client-side); - No one can build an alternative history with the same transaction, because the on-chain commitment points to a single MPC root.
Summary of the MPC structure
Multi Protocol Commitment* (MPC) is the principle that enables RGB to aggregate multiple contracts into a single Bitcoin transaction, while maintaining the uniqueness of commitments and confidentiality vis-à-vis other participants. Thanks to the deterministic construction of the tree, each contract is assigned a unique position, and the presence of "dummy" leaves (Entropy Leaves) partially masks the total number of contracts participating in the transaction.
The entire Merkle tree is never stored on the client. We simply generate a Merkle path for each contract concerned, to be transmitted to the recipient (who can then validate the commitment). In some cases, you may have several assets that have passed through the same UTXO. You can then merge several Merkle paths into a so-called multi-protocol commitment block, to avoid duplicating too much data.
Each Merkle proof is therefore lightweight, especially as the tree depth will not exceed 32 in RGB. There's also a notion of "Merkle block", which retains more information (cross-section, entropy, etc.), useful for combining or separating several branches.
That's why it took so long to finalize RGB. We had the overall vision from 2019: putting everything on client-side, circulating tokens off-chain. But for details like sharding for multiple contracts, the structure of the Merkle tree, how to handle collisions and merge proofs... all this required iterations.
Anchors: a global assembly
Following on from the construction of our commitments (
Opret or Tapret) and our MPC (Multi Protocol Commitment), we need to address the notion of Anchor in the RGB protocol. An Anchor is a client-side validated structure that brings together the elements needed to verify that a Bitcoin commitment actually contains specific contractual information. In other words, an Anchor summarizes all the data needed to validate the commitments described above.An Anchor consists of three ordered fields:
TxidMPC Proof- extra Transaction Proof - ETP
Each of these fields plays a part in the validation process, whether it's a matter of reconstructing the underlying Bitcoin transaction or proving the existence of a hidden commitment (particularly in the case of
Tapret).TxId
The
Txid field corresponds to the 32-byte identifier of the Bitcoin transaction containing the Opret or Tapret commitment.In theory, it would be possible to find this
Txid by tracing the chain of state transitions which themselves point to each witness transaction, following the logic of Single-use Seals. However, to facilitate and accelerate verification, this Txid is simply included in the Anchor, thus saving the validator from having to go back through the entire off-chain history.MPC Proof
The second field,
MPC Proof, refers to the proof that this particular contract (e.g. c_i) is included in the Multi Protocol Commitment. It is a combination of:pos_i, the position of this contract in the MPC tree;- cofactor`, the value defined to resolve position collisions;
- the
Merkle Proof, i.e. the set of nodes and hashes used to reconstruct the MPC root and verify that the contract identifier and itsTransition Bundleare committed to the root.
This mechanism was described in the previous section on building the MPC Tree, where each contract obtains a unique leaf thanks to:
pos(c_i) = c_i mod (w - cofactor)
Then, a deterministic merkelization scheme is used to aggregate all the leaves (contracts + entropy). In the end, the
MPC Proof allows the root to be reconstructed locally and compared with the mpc::Commitment included on-chain.Extra Transaction Proof - ETP
The third field, the ETP, depends on the type of commitment used. If the commitment is of type
Opret, no additional proof is required. The validator inspects the first OP_RETURN output of the transaction and finds the mpc::Commitment directly there.If the commitment is of type
Tapret, an additional proof called Extra Transaction Proof - ETP must be provided. It contains:- The internal public key (
P) of the taproot output in which the commitment is embedded; - The partner nodes of the
Script Path Spend(when the Tapret commitment is inserted in a script), in order to prove the exact location of this script in the taproot tree: - If the
Tapretcommitment is on the right-hand branch, we reveal the left-hand node (e.g.tHABC), - If the
Tapretcommitment is on the left, you need to disclose 2 nodes (e.g.tHABandtHC) to prove that no other commitment is present on the right-hand side. - The
noncemay be used to "mine" the best configuration, allowing the commitment to be placed on the right of the tree (proof optimization).
This additional proof is essential because, unlike
Opret, the Tapret commitment is integrated into the structure of a taproot script, which requires revealing part of the taproot tree in order to correctly validate the location of the commitment.The Anchors therefore encapsulate all the information required to validate a Bitcoin commitment in the context of RGB. They indicate both the relevant transaction (
Txid) and the proof of contract positioning (MPC Proof), while managing the additional proof (ETP) in the case of Tapret. In this way, an Anchor protects the integrity and uniqueness of the off-chain state by ensuring that the same transaction cannot be reinterpreted for other contractual data.Conclusion
In this chapter, we covered:
- How to apply the Single-use Seals concept in Bitcoin (in particular via a outpoint);
- The various methods for deterministically inserting a commitment into a transaction (Sig tweak, Key tweak, witness tweak, op_return, Taproot/Tapret);
- The reasons why RGB focuses on Tapret commitments;
- Multi-contract management via multi-protocol commitments, essential if you don't want to expose an entire state or other contracts when you want to prove a specific point;
- We've also seen the role of Anchors, which bring everything together (transaction TXID, Merkle tree proof and Taproot proof) in a single package.
In practice, the technical implementation is divided between several dedicated Rust crates (in client_side_validation, commit-verify, bp_core, etc.). The fundamental notions are there:
In the next chapter, we'll look at the purely off-chain component of RGB, namely contract logic. We'll see how RGB contracts, organized as partially replicated finite state machines, achieve much higher expressiveness than Bitcoin scripts, while preserving the confidentiality of their data.
Quiz
Quiz1/5
csv4022.2
What is the main purpose of deterministic Bitcoin commitments?