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 backwards-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:ShardIdent
contains information about the WorkChain and the shard the block belongs to.seq_no
is the sequence number of the current block.root_hash
is the hash of the block data (block header).file_hash
helps 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_hash
using TVM instructions (PREVMCBLOCKS
,PREVMCBLOCKS_100
,PREVKEYBLOCKS
). - 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
MasterChain
block. - Extract the
shard_hashes
field 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_update
field. 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. MatchingreprHashes
ensures 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_UPDATE
cell 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
.
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.