Skip to main content
On the Thru network, data is stored within accounts. The validators of the network maintain the present state of some subset of all of the accounts, and the goal of the blockchain is to ensure that the accounts stored by each validator have the same up-to-date state. However, to process and validate modifications to accounts, the validators each would need to keep many accounts in their storage. Given the large number of accounts and ever growing state, the storage requirement becomes vast. To alleviate this issue, the Thru network supports account compression. Uncompressed accounts are active on the network, and can be modified. On the other hand, compressed accounts are in an archived state, where they cannot be accessed without first being uncompressed. Compressed accounts do not need to be stored by validators and can instead be stored off the chain.

State Proofs

If validators are no longer storing the state of compressed accounts, they no longer have all the information needed to validate every transaction, since the validity may depend on the state of compressed accounts. This is where state proofs come in; they provide cryptographic proof of the state of an account, which a validator can check, without having previously stored the state of that account. Specifically, we need to be able to give proofs for the following situations:
  • TN_STATE_PROOF_TYPE_CREATION: When we are creating a new account, we need to prove that it has never been created before.
  • TN_STATE_PROOF_TYPE_EXISTING: When we compress an account, we can get a proof that the account became compressed in its current state. Later, when we want to use the account again, we need to decompress the account. We can use the proof to show that it was previously compressed in the state that we claim it is.
  • TN_STATE_PROOF_TYPE_UPDATING: When we decompress an account, we can get a proof that the account was previously compressed and in its current state. After modifying an account, we may want to compress the account again. We need to provide the proof to show that the account was previously in a certain compressed state, and should now be updated to a new compressed state.
We can provide cryptographic proofs for these situations by taking advantage of a data structure called a Merkle Tree.

Usage Examples

Here’s a simple smart contract that creates an account using a state proof:

#include <thru-sdk/tn_sdk.h>
#include <thru-sdk/tn_sdk_syscall.h>


/* Error codes */
#define TN_SIMPLE_CREATE_ERR_INVALID_INSTRUCTION_DATA 1

/* Instruction data structure */

struct __attribute__((packed)) simple_create_args {
  ushort account_index;         /* Index of account to create */
  uchar seed[32];              /* Seed for account derivation */
  /* State proof data follows immediately after this struct */
};

typedef struct simple_create_args simple_create_args_t;

TSDK_ENTRYPOINT_FN void
start( uchar const * instruction_data, ulong instruction_data_sz ) {
  /* Check minimum size for instruction data */
  if( instruction_data_sz < sizeof(simple_create_args_t) ) {
    tsdk_revert( TN_SIMPLE_CREATE_ERR_INVALID_INSTRUCTION_DATA );
  }

  simple_create_args_t const * args =
    (simple_create_args_t const *)instruction_data;

  /* Extract proof data that follows the instruction struct */
  uchar const * proof_data = instruction_data + sizeof(simple_create_args_t);
  ulong remaining_bytes = instruction_data_sz - sizeof(simple_create_args_t);

  /* Validate proof header is present */
  if( remaining_bytes < sizeof(tsdk_state_proof_hdr_t) ) {
    tsdk_revert( TN_SIMPLE_CREATE_ERR_INVALID_INSTRUCTION_DATA );
  }

  /* Calculate proof size from header and verify against remaining data */
  tsdk_state_proof_hdr_t const * proof_hdr =
    (tsdk_state_proof_hdr_t const *)proof_data;
  ulong proof_size = tsdk_state_proof_footprint_from_header( proof_hdr );

  if( proof_size != remaining_bytes ) {
    tsdk_revert( TN_SIMPLE_CREATE_ERR_INVALID_INSTRUCTION_DATA );
  }

  /* Create the account using seed and proof */
  ulong result = tsys_account_create( args->account_index,
                                      args->seed,
                                      proof_data,
                                      proof_size );
  if( result != TSDK_SUCCESS ) {
    tsdk_revert( result );
  }

  /* Make account writable and resize to hold some data */
  result = tsys_set_account_data_writable( args->account_index );
  if( result != TSDK_SUCCESS ) {
    tsdk_revert( result );
  }

  result = tsys_account_resize( args->account_index, 64 );
  if( result != TSDK_SUCCESS ) {
    tsdk_revert( result );
  }

  tsdk_return( TSDK_SUCCESS );
}
The state proof data is obtained from RPC endpoints like getStateProof(account_address, "CREATION") and included in the instruction data when calling the smart contract.

Merkle Tree Structure

Thru organizes account data in a compressed binary Merkle tree based on account addresses. The tree is structured as a trie where each bit of an account’s address determines the path:
                Root Hash
               /          \
        Hash(A,B)        Hash(C,D)
        /      \          /      \
   Hash(A)  Hash(B)  Hash(C)  Hash(D)
      |        |        |        |
   Account   Account  Account  Account
      A        B        C        D
  • All accounts are located at leaves at the bottom of the tree, at a maximum depth of 256.
  • To locate an account, follow the sequence of bits in its address. Bit 0 → go left, Bit 1 → go right
  • Leaf nodes: Store hashes of account data (SHA-256(0x00 || pubkey || value_hash)).
  • Internal nodes: Store hashes of their children (SHA-256(0x01 || left_hash || right_hash)). The tree is compressed, meaning that internal nodes only exist when account addresses actually diverge. Nodes with only one child are eliminated, keeping the tree compact.
  • Root node: Single hash representing the entire global account state. This is the only value that validators need to record.

Proof Structure

State proofs contain all information needed to cryptographically verify account operations against historical blockchain state. Variable Size: State proofs have variable size depending on the length of the path the proof takes through the tree. The path_bitset indicates which sibling hashes are included, allowing proofs to be compact for accounts in sparse areas of the tree. In the C SDK, use tn_state_proof_footprint_from_header() to determine the exact size of a proof before allocation. All state proofs share a common header:
struct {
  ulong type_slot;       // High 2 bits: proof type, low 62 bits: slot number
  tn_hash_t path_bitset; // Bitmap indicating which tree levels have sibling hashes
} hdr;
  • type_slot: Encodes both the proof type (0=EXISTING, 1=UPDATING, 2=CREATION) and the historical slot number when the account state existed
  • path_bitset: A 256-bit bitmap where each bit indicates whether a sibling hash is provided for that tree level
The following structure represents the maximum size a state proof can be.
struct tsdk_state_proof {
  struct { /* header as above */ } hdr;
  union {
    // Raw access to all proof data
    tn_hash_t proof_keys[258];

    // CREATION proof (type=2): Proves an account didn't exist
    struct {
      tn_pubkey_t existing_leaf_pubkey; // Existing account that would be
                                        // sibling to new account
      tn_hash_t existing_leaf_hash;     // Hash of that existing account
      tn_hash_t sibling_hashes[256];    // Path from existing account to root
    } creation;

    // EXISTING proof (type=0): Proves an account exists with specific data
    struct {
      tn_hash_t sibling_hashes[256];    // Path from account to root
    } existing;

    // UPDATING proof (type=1): Proves an account's previous state before
    // modification
    struct {
      tn_hash_t existing_leaf_hash;     // Hash of the account's PREVIOUS state
      tn_hash_t sibling_hashes[256];    // Path proving the previous state
                                        // existed
    } updating;
  } proof_body;
};

How State Proofs Work

All state proofs work by reconstructing paths through the Merkle tree and verifying the result matches a known state root. Each proof type targets a different aspect of account state. CREATION proofs demonstrate that a target address is available for a new account. The key insight is that the proof contains an existing account that shares the longest possible address prefix with the target account.
  • Find the divergence point: Follow both the target address and existing account address bit by bit until they diverge - one goes left, the other goes right
  • Verify the existing path: Use the sibling hashes to reconstruct the path from the existing account to the historical state root
  • Confirm availability: Since the existing account’s path is valid and the target address diverges from it, the target address must be unoccupied
The existing account becomes the sibling to the new account when it’s created, which is why the proof must use the account with maximal prefix overlap. EXISTING proofs verify that an account exists with exactly the provided data at a historical point in time.
  • Compute the leaf hash: Hash the provided account metadata and data to get the leaf value
  • Follow the address path: Use each bit of the account address to navigate left or right through the tree
  • Reconstruct to root: At each level, combine the current hash with the appropriate sibling hash to compute the parent hash
  • Verify the result: The final computed hash must match the historical state root stored on-chain
UPDATING proofs prove an account’s previous state before modification, enabling validated state transitions.
  • Start with previous state: Use the provided existing_leaf_hash as the leaf value (the account’s old state)
  • Reconstruct the historical path: Follow the account’s address using the sibling hashes to rebuild the path to root
  • Validate the previous state: The reconstructed path must reach the historical state root, proving the account genuinely had the claimed previous state
This creates an audit trail where every state change can be cryptographically linked to a verified starting point.