Skip to main content
Transactions on the Thru blockchain are the core atomic unit of computation that a user can submit for execution. Each transaction represents an indivisible set of operations that either all succeed or all fail together.

Format

A Thru transaction consists of the following components:

Transaction Header (v1)

The transaction header contains critical metadata and parameters:
struct {
  signature_t fee_payer_signature;     // [0,64) bytes: Ed25519 signature
  uint8_t     transaction_version;     // [64,65) bytes: Must be 0x01
  uint8_t     flags;                   // [65,66) bytes: Transaction flags
  uint16_t    readwrite_accounts_cnt;  // [66,68) bytes: Number of writable accounts
  uint16_t    readonly_accounts_cnt;   // [68,70) bytes: Number of readonly accounts
  uint16_t    instr_data_sz;           // [70,72) bytes: Size of instruction data
  uint32_t    req_compute_units;       // [72,76) bytes: Requested compute units
  uint16_t    req_state_units;         // [76,78) bytes: Requested state units
  uint16_t    req_memory_units;        // [78,80) bytes: Requested memory units
  uint64_t    fee;                     // [80,88) bytes: Transaction fee
  uint64_t    nonce;                   // [88,96) bytes: Transaction nonce
  uint64_t    start_slot;              // [96,104) bytes: Earliest valid slot
  uint32_t    expiry_after;            // [104,108) bytes: Slots until expiry
  uint32_t    padding_0;               // [108,112) bytes: Reserved padding
  pubkey_t    fee_payer_pubkey;        // [112,144) bytes: Fee payer's public key
  pubkey_t    program_pubkey;          // [144,176) bytes: Program's public key
}

Account Addresses

At the end of and after the header, the transaction contains arrays of account addresses that will be accessed during execution. The account addresses are organized in a specific order:
  1. The fee payer account is always listed first in the transaction. This is also header field fee_payer_pubkey.
  2. The program account is always listed second in the transaction. This is header field program_pubkey.
  3. The writable accounts array follows, containing all accounts that the program may modify.
  4. The read-only accounts array comes last, containing all accounts that the program may read but not modify.
The account indices in a Thru transaction are always assigned as follows:
  • The fee payer account is at index 0 (corresponding to fee_payer_pubkey in the header).
  • The program account is at index 1 (corresponding to program_pubkey in the header).
  • Writable and read-only accounts follow at subsequent indices.
This means that any time an account index is referenced (for example, in instruction data), index 0 always refers to the fee payer, and index 1 always refers to the program. These indices directly overlap with the order of the account addresses as listed after the header.

Instruction Data

Following the account addresses is the instruction data section. This contains the raw data that will be passed to the program during execution. The size of this data is specified in the header’s instr_data_sz field.

Optional State Proof

For fee payer accounts that are being created or decompressed from a compressed state, an optional state proof may be included at the end of the transaction. This proof validates the account’s existence or non-existence in the state tree.

Complete Transaction Data Layout

The following shows the complete binary layout of a Thru transaction:
struct thru_transaction_layout {
  // Transaction Header (176 bytes)
  struct {
    uint8_t  fee_payer_signature[64];    // [0,64) bytes: Ed25519 signature
    uint8_t  transaction_version;        // [64,65) bytes: Must be 0x01
    uint8_t  flags;                      // [65,66) bytes: Transaction flags
    uint16_t readwrite_accounts_cnt;     // [66,68) bytes: Number of writable accounts
    uint16_t readonly_accounts_cnt;      // [68,70) bytes: Number of readonly accounts
    uint16_t instr_data_sz;              // [70,72) bytes: Size of instruction data
    uint32_t req_compute_units;          // [72,76) bytes: Requested compute units
    uint16_t req_state_units;            // [76,78) bytes: Requested state units
    uint16_t req_memory_units;           // [78,80) bytes: Requested memory units
    uint64_t fee;                        // [80,88) bytes: Transaction fee
    uint64_t nonce;                      // [88,96) bytes: Transaction nonce
    uint64_t start_slot;                 // [96,104) bytes: Earliest valid slot
    uint32_t expiry_after;               // [104,108) bytes: Slots until expiry
    uint32_t padding_0;                  // [108,112) bytes: Reserved padding
    uint8_t  fee_payer_pubkey[32];       // [112,144) bytes: Fee payer's public key
    uint8_t  program_pubkey[32];         // [144,176) bytes: Program's public key
  } header;

  // Account Addresses (variable size)
  uint8_t readwrite_accounts[readwrite_accounts_cnt][32]; // Writable account addresses
  uint8_t readonly_accounts[readonly_accounts_cnt][32];   // Read-only account addresses

  // Instruction Data (variable size)
  uint8_t instruction_data[instr_data_sz];                // Program instruction data

  // Optional State Proof (if TN_TXN_FLAG_HAS_FEE_PAYER_PROOF flag is set)
  struct {
    struct {
      uint64_t type_slot;                // [high 2 bits: proof type, low 62 bits: slot]
      uint8_t  path_bitset[32];          // Bitset indicating proof path indices
    } proof_header;

    // Variable proof body based on type:
    // For CREATION (type=2): existing_leaf_pubkey[32] + existing_leaf_hash[32] + sibling_hashes[]
    // For EXISTING (type=0): sibling_hashes[]
    // For UPDATING (type=1): existing_leaf_hash[32] + sibling_hashes[]
    uint8_t proof_body[popcount(path_bitset)]; // popcount -> count the number of set bits in the provided field
  } fee_payer_state_proof;

  // Optional Account Metadata (only for EXISTING state proof type)
  struct {
    uint16_t magic;                      // [0,2) bytes: Magic number (0xC7A3)
    uint8_t  version;                    // [2,3) bytes: Account version (0x00)
    uint8_t  flags;                      // [3,4) bytes: Account flags
    uint32_t data_sz;                    // [4,8) bytes: Account data size
    uint64_t seq;              // [8,16) bytes: State counter
    uint8_t  owner[32];                  // [16,48) bytes: Account owner pubkey
    uint64_t balance;                    // [48,56) bytes: Account balance
    uint64_t nonce;                      // [56,64) bytes: Account nonce
  } fee_payer_account_meta;
};

Field Descriptions

The following table describes each field in the transaction structure:
FieldOffsetSizeTypeDescription
Transaction Header
fee_payer_signature0-6364 bytesEd25519 signatureCryptographic signature from the fee payer authorizing the transaction
transaction_version641 byteuint8Transaction format version, must be 0x01
flags651 byteuint8Transaction flags (bit 0: has fee payer proof, bits 1-7: reserved)
readwrite_accounts_cnt66-672 bytesuint16Number of accounts that can be modified by the program
readonly_accounts_cnt68-692 bytesuint16Number of accounts that can only be read by the program
instr_data_sz70-712 bytesuint16Size in bytes of the instruction data section
req_compute_units72-754 bytesuint32Maximum compute units the transaction may consume
req_state_units76-772 bytesuint16Maximum state units the transaction may consume
req_memory_units78-792 bytesuint16Maximum memory units the transaction may consume
fee80-878 bytesuint64Transaction fee in native tokens
nonce88-958 bytesuint64Transaction nonce, must match fee payer’s current nonce
start_slot96-1038 bytesuint64Earliest slot when transaction becomes valid
expiry_after104-1074 bytesuint32Number of slots after start_slot when transaction expires
padding_0108-1114 bytesuint32Reserved padding, must be zero
fee_payer_pubkey112-14332 bytesEd25519 pubkeyPublic key of the account paying transaction fees
program_pubkey144-17532 bytesEd25519 pubkeyPublic key of the program to execute
Account Addresses
readwrite_accounts176+32×N bytesEd25519 pubkey[]Array of writable account addresses (sorted ascending)
readonly_accountsVariable32×M bytesEd25519 pubkey[]Array of read-only account addresses (sorted ascending)
Instruction Data
instruction_dataVariableVariableuint8[]Raw data passed to the program during execution
Optional State Proof
type_slotVariable8 bytesuint64Proof type (bits 62-63) and slot number (bits 0-61)
path_bitsetVariable32 bytesuint8[32]Bitset indicating which indices have sibling hashes
proof_bodyVariableVariableuint8[]Variable proof data based on type and path_bitset
Optional Account Metadata
magicVariable2 bytesuint16Magic number identifying account metadata (0xC7A3)
versionVariable1 byteuint8Account metadata version (0x00)
flagsVariable1 byteuint8Account flags (program, privileged, compressed, etc.)
data_szVariable4 bytesuint32Size of account data in bytes
seqVariable8 bytesuint64Account state modification counter
ownerVariable32 bytesEd25519 pubkeyPublic key of the program that owns this account
balanceVariable8 bytesuint64Account balance in native tokens
nonceVariable8 bytesuint64Account nonce for transaction ordering

Transaction Flags

The flags field at byte offset 65 controls optional transaction features. Currently, only one flag is defined:
FlagBit PositionValueDescription
TN_TXN_FLAG_HAS_FEE_PAYER_PROOF00x01Indicates that the transaction includes a state proof for the fee payer account. When this flag is set, a state proof must be included at the end of the transaction after the instruction data.
All other bits in the flags field must be set to 0. The transaction parser will reject any transaction with unknown flags set to ensure forward compatibility.

Limitations

The Thru blockchain enforces several limits on transaction structure and size:
  • The maximum transaction size is 32KiB.
  • Each transaction can reference at most 1024 accounts.
  • All account addresses must be exactly 32 bytes in length.

Validity Criteria

A valid transaction is one which is includable in to the chain. That is to say, a block is considered itself unincludable if it contains an invalid transaction. A transaction must meet the following criteria to be considered valid.

Format Validation

  • The transaction size must not exceed the maximum transmission unit (MTU) of 32KiB.
  • The transaction version must be exactly 0x01.
  • All fields in the transaction header must be properly formatted and aligned.
  • The account arrays must contain the correct number of accounts as specified in the header.
  • The total number of accounts referenced must not exceed 1024.

Signature Verification

  • The fee payer’s Ed25519 signature must be cryptographically valid.
  • The signature must cover the entire transaction body, excluding the signature itself.

Account List Verification

  • No duplicate accounts are allowed anywhere in the transaction.
  • Writable accounts must be sorted in ascending lexicographic order.
  • Read-only accounts must be sorted in ascending lexicographic order.