It is highly recommended to familiarize yourself with Exotic cells first. This article primarily covers situations where you want to verify a proof in a smart contract. However, the same techniques can be used to validate proofs off-chain.
- The only trusted information available in a smart contract is a few recent MasterChain blocks.
- Some data is stored directly within blocks.
- Additional information is maintained within the WorkChain state.
- Blocks serve as diffs that reflect changes to the state over time. Think of blocks as Git commits and the state as your repository.
- Latest TL-B schemas can be found in the TON Monorepo. They may evolve, typically in backward-compatible ways.
More about blocks
We need to examine the block layout to determine what we can prove and how to do it. Each block (ShardChain block, MasterChain block) has a unique block ID:ShardIdentcontains information about the WorkChain and the shard the block belongs to.seq_nois the sequence number of the current block.root_hashis the hash of the block data (block header).file_hashhelps validators optimize processes; typically, you don’t need it.
state_update. This MERKLE_UPDATE cell stores the old and new hashes of the ShardChain state. Note that the MasterChain always consists of a single shard, so inspecting a MasterChain block reveals the MasterChain state hash.
Another relevant field is extra:
McBlockExtra field:
shard_hashes field is essential, as it holds the latest known ShardChain blocks, which are critical for BaseChain proofs.
For detailed inspections, it is convenient to use the official explorer.
High-level overview of proofs
Prove a transaction in MasterChain
To prove a transaction’s existence in the MasterChain:- Obtain a trusted MasterChain block
root_hashusing TVM instructions (PREVMCBLOCKS,PREVMCBLOCKS_100,PREVKEYBLOCKS). - The user provides a complete MasterChain block that should be validated against the trusted hash.
- Parse the block to extract the transaction.
Prove a transaction in BaseChain
For BaseChain transactions:- Follow steps 1-2 above to get a trusted
MasterChainblock. - Extract the
shard_hashesfield from the MasterChain block. - User provides the full ShardChain block that should be validated against the trusted hash.
- Parse the ShardChain block to find the transaction.
Prove account states
Sometimes, data is not in block diffs but within the ShardState itself. To prove an account’s state in the BaseChain:- Parse the ShardChain block’s
state_updatefield. This exotic cell contains two ShardState hashes (before and after the block). - The user provides a ShardState that must be validated against the hash obtained in step 1.
You can only prove the state at block boundaries (not intermediate states).
Understanding pruned branch cells
Familiarize yourself with pruned branch cells and the concept of hash0(cell). v1 is a regular cell tree; in v2, the cell c1 becomes a pruned branch, removing its content and references. However, if you only need c0, there’s no practical difference, as$hash_0(v1) == hash_0(v2)$.
hash0(cell)ignores pruned branches, returning the original tree’s hash.reprHash(cell)accounts for everything. MatchingreprHashesensures cell path equivalency.
Use
HASHCU for representation hash and CHASHI/CHASHIX for different-level hashes.Composing proofs
If you have two cell trees: Approaches:- Parse v1 to get
$hash_0(c1) = x$and verify the provided v2. - Concatenate v2 with v1 to reconstruct the original tree.
- Trusted data hashes may be separated from cells (e.g.,
PREVMCBLOCKS). - Replacing pruned cells with actual cells changes the
MERKLE_UPDATEcell hash. Always manually validate proofs against trusted hashes in these cases.
Real-world example
Let’s consider a scenario where we want to prove that a particular account has a specific state. This is useful because having a state allows you to call a get-method on it or even emulate a transaction. In this particular example, we want to prove the state of a JettonMaster and then call theget_wallet_address method on it. This way, even if a particular JettonMaster does not support TEP-89, it is still possible to obtain the wallet address for a specific account.
The full example is too large for this article, but let’s cover some key points.
This is an example of the proof composition technique described above. It is convenient because for getRawAccountState, the liteserver returns two items:
- the account state itself
- a BoC containing two proofs
AccountState with the ShardState proof, which is a cell tree where all branches are pruned except for the path from the root to the AccountState. The AccountState itself is also pruned so that we will substitute the pruned AccountState with the actual one.
ShardBlock.
Tact
BinTree is a TL-B structure that operates straightforwardly. It stores a single bit to indicate whether the current cell is a leaf. If it is a leaf, it stores the ShardDescr. Otherwise, the cell holds two references: a left child and a right child.
Since a shard identifier is a binary prefix of an address, we can traverse the tree by following the path of bits in the address.