Transaction Pipeline

Summary

Transactions pass through 8 stages: receive, sigverify, sanitize, budget/age checks, fee payer validation, account loading, instruction execution, and commit.

Transaction processing pipeline

When a transaction arrives at a validator, it passes through a series of validation and execution stages. The following describes the full pipeline from receipt to commit, with source file references into the agave validator client.

1. Receive and deserialize

The validator receives transaction bytes over UDP/QUIC. The raw bytes must fit within a single packet (PACKET_DATA_SIZE = 1,232 bytes). The bytes are deserialized into a VersionedTransaction, which contains the signatures array and a VersionedMessage (either legacy or v0).

2. Signature verification (sigverify)

Signatures are verified in the sigverify stage before the transaction enters the banking stage. For each signature at index i, the verifier checks Ed25519(signatures[i], account_keys[i], message_bytes). If any signature is invalid, the packet is discarded.

Verification is parallelized: the validator splits packet batches into chunks of VERIFY_PACKET_CHUNK_SIZE (128) and processes them in parallel.

3. Sanitize

The deserialized transaction is sanitized to produce a SanitizedTransaction (or RuntimeTransaction). Sanitization validates structural invariants:

  • Number of signatures matches num_required_signatures in the header
  • All instruction program_id_index and account_indices are within bounds
  • The fee payer (account index 0) is a writable signer

The RuntimeTransaction wrapper caches precomputed metadata from TransactionMeta: the message hash, vote transaction flag, precompile signature counts (Ed25519/secp256k1/secp256r1), compute budget instruction details, and total instruction data length.

4. Check compute budget, age, and status cache

The check_transactions method performs several checks per transaction:

Compute budget: The transaction's compute budget instructions are parsed and validated first. Fee details are calculated from the budget limits and prioritization fee. If the compute budget is invalid or conflicting, the transaction fails with compute-budget parsing errors such as DuplicateInstruction, InstructionError(..., InvalidInstructionData), or InvalidLoadedAccountsDataSizeLimit.

Blockhash age: The transaction's recent_blockhash is looked up in the BlockhashQueue. If the hash is found and its age is within MAX_PROCESSING_AGE (150 slots), the transaction proceeds. If not found, the validator checks for a valid durable nonce.

Status cache: The transaction's message hash is checked against a status cache. If found, the transaction is rejected with AlreadyProcessed.

5. Validate nonce and fee payer

The validate_transaction_nonce_and_fee_payer method in the SVM handles two validations:

Nonce validation (if applicable): For nonce transactions, the validator loads the nonce account and verifies:

  • The account is owned by the System Program
  • It parses as State::Initialized
  • The stored durable nonce matches the transaction's recent_blockhash
  • The nonce can be advanced (its current durable nonce differs from the next durable nonce, i.e., the nonce has not already been used in the current block)
  • The nonce authority has signed the transaction

If valid, the nonce is advanced to the next durable nonce value. See validate_transaction_nonce.

Fee payer validation: The fee payer account (always index 0) is loaded and checked by validate_fee_payer:

  • Account must exist (lamports > 0), otherwise AccountNotFound
  • Account must be a system account or nonce account, otherwise InvalidAccountForFee
  • Lamports must cover min_balance + total_fee, where min_balance is 0 for system accounts or rent.minimum_balance(NonceState::size()) for nonce accounts; otherwise InsufficientFundsForFee
  • After fee deduction, the account must remain rent-exempt (cannot transition from rent-exempt to rent-paying)

The fee is deducted from the fee payer at this stage. A snapshot of the fee-subtracted fee payer (and advanced nonce, if applicable) is saved as RollbackAccounts, which are the accounts that get committed even if execution fails.

6. Load accounts

load_transaction loads all accounts referenced by the transaction. The AccountLoader wraps the external account store and maintains a batch-local cache so that accounts modified by earlier transactions in the same batch are visible to later ones.

For each non-fee-payer account, the loader:

  1. Fetches the account from the cache or accounts-db
  2. Updates rent-exempt status if needed
  3. Accumulates the account's data size toward the loaded_accounts_data_size_limit (default 64 MiB). Each account incurs a base overhead of TRANSACTION_ACCOUNT_BASE_SIZE (64 bytes) plus its data length

For each program invoked by the transaction's instructions, the loader verifies that the program account exists and is owned by a valid loader (NativeLoader or one of the PROGRAM_OWNERS). Invalid programs fail with ProgramAccountNotFound or InvalidProgramForExecution.

LoaderV3 (upgradeable) programs implicitly load their associated programdata account, which also counts toward the loaded data size limit.

If account loading fails but the fee payer was successfully validated, the transaction becomes a FeesOnly result: the fee is still collected but no instructions execute.

7. Execute instructions

execute_loaded_transaction creates a TransactionContext with all loaded accounts and invokes process_message. Instructions execute sequentially in the order they appear in the message. Each instruction invocation creates an InvokeContext and calls the target program.

Instruction processing details

The runtime's process_message function iterates through each instruction and calls the target program:

  1. For each instruction, the runtime calls prepare_next_top_level_instruction, which builds the InstructionContext. This context contains references to the instruction's accounts (resolved from the compiled indices), the instruction data, and the program account index.
  2. The runtime checks whether the program is a precompile (Ed25519, Secp256k1, Secp256r1). Precompiles are verified directly without invoking the BPF VM.
  3. For all other programs, the runtime invokes process_instruction, which loads the program from the cache and executes it in the BPF virtual machine.
  4. After the instruction completes, the runtime verifies that the total lamport balance across all instruction accounts has not changed (UnbalancedInstruction check).
  5. If any instruction fails, the entire transaction is rolled back. No intermediate state changes are committed.

Each instruction increments the instruction trace. The trace includes both top-level instructions and any CPIs they invoke. The total trace length (top-level instructions plus all nested CPIs) cannot exceed 64 (MAX_INSTRUCTION_TRACE_LENGTH). Exceeding this limit returns InstructionError::MaxInstructionTraceLengthExceeded.

After execution, the runtime verifies that:

  • The sum of lamports across all accounts has not changed
  • No account transitioned from rent-exempt to rent-paying

8. Commit or rollback

If execution succeeds, the modified account states from the TransactionContext are written back to the AccountLoader's batch-local cache. If execution fails, only the RollbackAccounts (fee payer with fee deducted and advanced nonce) are written back. The fee is still collected, but all other account changes are discarded.

Pipeline summary

Receive packet (UDP/QUIC)
--> Deserialize into VersionedTransaction
--> Sigverify (parallel Ed25519 verification)
--> Sanitize (structural validation, metadata extraction)
--> Parse compute budget, calculate fees
--> Check blockhash age (or verify nonce account)
--> Check status cache (dedup)
--> Validate nonce authority and advanceability (if nonce transaction)
--> Validate fee payer (load, check balance, deduct fee)
--> Load all accounts (with data size limits)
--> Load programs (verify loaders)
--> Execute instructions sequentially
--> Verify post-conditions (lamport balance, rent state)
--> Commit account changes (or rollback on failure)

Transaction error reference

The following table lists all TransactionError variants and at which pipeline stage they occur:

ErrorStageCause
AccountInUseSchedulingAccount is already locked by another transaction in the same batch
AccountLoadedTwiceSchedulingA pubkey appears twice in the transaction's account_keys
AccountNotFoundFee payer validationFee payer account does not exist
ProgramAccountNotFoundAccount loadingAn invoked program does not exist
InsufficientFundsForFeeFee payer validationFee payer cannot cover fee + rent-exempt minimum
InvalidAccountForFeeFee payer validationFee payer is not a system or nonce account
AlreadyProcessedStatus cacheTransaction was already processed
BlockhashNotFoundAge checkBlockhash not in queue and not a valid nonce
InstructionErrorExecutionAn error occurred while processing an instruction (includes instruction index and specific InstructionError)
CallChainTooDeepAccount loadingLoader call chain is too deep
MissingSignatureForFeeSanitizeTransaction requires a fee but has no signature present
InvalidAccountIndexSanitizeTransaction contains an invalid account reference
SignatureFailureSigverifyEd25519 signature does not verify (packet is discarded)
InvalidProgramForExecutionAccount loadingProgram is not owned by a valid loader
SanitizeFailureSanitizeTransaction failed to sanitize accounts offsets correctly
ClusterMaintenanceSchedulingTransactions are currently disabled due to cluster maintenance
AccountBorrowOutstandingExecutionTransaction processing left an account with an outstanding borrowed reference
WouldExceedMaxBlockCostLimitSchedulingTransaction would exceed max block cost limit
UnsupportedVersionSanitizeTransaction version is unsupported
InvalidWritableAccountAccount loadingTransaction loads a writable account that cannot be written
WouldExceedMaxAccountCostLimitSchedulingTransaction would exceed max account cost limit within the block
WouldExceedAccountDataBlockLimitSchedulingTransaction would exceed account data limit within the block
TooManyAccountLocksSchedulingTransaction locked too many accounts
AddressLookupTableNotFoundAccount loadingAddress lookup table account does not exist
InvalidAddressLookupTableOwnerAccount loadingAddress lookup table is owned by the wrong program
InvalidAddressLookupTableDataAccount loadingAddress lookup table contains invalid data
InvalidAddressLookupTableIndexAccount loadingAddress table lookup uses an invalid index
InvalidRentPayingAccountPost-execution checkAccount transitioned from rent-exempt to rent-paying
WouldExceedMaxVoteCostLimitSchedulingTransaction would exceed max vote cost limit
WouldExceedAccountDataTotalLimitSchedulingTransaction would exceed total account data limit
DuplicateInstructionCompute budget parsingDuplicate compute budget instruction variant in the same transaction
InsufficientFundsForRentPost-execution checkAccount does not have enough lamports to cover rent for its data size
MaxLoadedAccountsDataSizeExceededAccount loadingTotal loaded data exceeds 64 MiB limit
InvalidLoadedAccountsDataSizeLimitCompute budget parsingSetLoadedAccountsDataSizeLimit set to 0
ResanitizationNeededSanitizeTransaction differed before/after feature activation and needs resanitization
ProgramExecutionTemporarilyRestrictedAccount loadingProgram execution is temporarily restricted on the referenced account
UnbalancedTransactionPost-execution checkTotal lamport balance before the transaction does not equal the balance after
ProgramCacheHitMaxLimitAccount loadingProgram cache hit max limit
CommitCancelledCommitCommit cancelled internally

Is this page helpful?

द्वारा प्रबंधित

© 2026 सोलाना फाउंडेशन। सर्वाधिकार सुरक्षित।
जुड़े रहें