Journeys October 2026 Live Core Dev Bootcamp in New YorkThe transaction processing pipelineLive now
advanced 30 min

The transaction processing pipeline

The four-phase validation pipeline (preflight → preclaim → doApply → finalization) and what each phase may and may not do.

Prerequisites

Complete these before starting this module:

What you'll learn

  • Walk the four phases and their access levels.
  • Explain why `NotTEC` is returned from preflight.
  • Understand how failures at each stage are handled.
Complete this module by self-assessment and a quiz. Jump to assessment

Introduction

≈30 min · Advanced · builds on Transactor architecture

Every transaction runs a gauntlet before it can touch the ledger. In this module you'll walk the four-phase validation pipeline (preflight, preclaim, doApply and finalization) and learn exactly what each phase may and may not do, why preflight can never claim a fee, and how failures are handled at each stage. It's the beating heart of transaction processing, and the mental model behind every transactor you'll read.


Pipeline Overview

In brief: a transaction passes through four gates in order, and each gate has more access to the ledger, and more responsibility, than the last.

The pipeline has four distinct phases. Read the diagram top to bottom to follow a single transaction from arrival to commit:

The four-phase pipeline: a transaction arrives, preflight does stateless validation and returns NotTEC (failures reject with tem, tef or ter), preclaim reads the ledger and verifies the key's signing authority and can return tec (failures reject or queue), doApply has full read/write access and modifies state (a tec charges the fee and reverts the changes), and finalization commits, records metadata and consumes the sequence

Key idea. Each gate has strictly more power than the one before it: stateless, then read-only, then read/write. That ordering is exactly what lets the node reject bad transactions cheaply, and only charge a fee once it is safe to.


Phase 1: Preflight

In brief: cheap, stateless checks on the transaction itself, run before anything expensive, and before a fee can ever be charged.

Preflight performs stateless validation, checks that depend only on the transaction content itself, not on any ledger state.

Characteristics

  • No ledger access: Only has access to PreflightContext (no view)
  • Deterministic: Same transaction always produces the same result
  • Can run in parallel: No shared state dependencies
  • Returns NotTEC: Cannot return tec codes (those require fee claiming)

What Preflight Checks

  1. Amendment/Feature enablement: Is the transaction type enabled?
  2. Flag validation: Are only valid flags set?
  3. Field presence: Are required fields present?
  4. Field format: Are field values well-formed?
  5. Field ranges: Are numeric values within valid ranges?
  6. Logical consistency: Are field combinations valid?

Implementation Pattern

Transaction-specific preflight is implemented as a static method. Notice that every check below reads only ctx.tx, never the ledger:

Why Preflight Matters

  1. Early rejection: Catches obviously invalid transactions before expensive operations
  2. Security: preflight1 runs before any signature work; preflight2 then does the cryptographic signature check, still stateless, so no ledger resources are touched for garbage input
  3. Cannot claim fees: Returns NotTEC, so no fees can be claimed for malformed transactions

Watch out. Preflight cannot return tec codes because those codes claim the transaction fee, and preflight has no ledger view to charge anything against. It is stateless by design: preflight1 checks the format, preflight2 checks that the signature is cryptographically valid, and neither can touch an account.


Phase 2: Preclaim

In brief: read-only checks against real ledger state. The signature's cryptographic validity was proven in preflight2; preclaim's checkSign now verifies the key's authority for the account (master key, regular key, or signer list). This is the first phase that can charge a fee.

Preclaim performs ledger-state validation with read-only access to the ledger.

Characteristics

  • Read-only ledger access: Has ReadView const& view
  • Signing authority verified: checkSign confirms the (already cryptographically valid) signature comes from a key allowed to sign for this account
  • Can return tec codes: Fee can be claimed
  • Determines if transaction should be queued or applied

What Preclaim Checks

  1. Account existence: Does the sender account exist?
  2. Sequence number: Is the sequence correct?
  3. Fee adequacy: Is the fee sufficient?
  4. Balance sufficiency: Can the account pay the fee?
  5. Destination checks: Does destination exist? Are permissions met?
  6. Trust line state: Are trust lines frozen? Is authorization required?
  7. Expiration: Has the transaction or related object expired?

Implementation Pattern

The same transactor, one phase later. Now it reads ledger state through ctx.view (read-only), and can return tec codes:

Key Utility Functions for Preclaim

These helpers do the read-only ledger lookups preclaim relies on:

Function Purpose
ctx.view.read(keylet) Read a ledger entry
isGlobalFrozen(view, issuer) Check if an issuer has globally frozen
isFrozen(view, account, issue) Check if a specific trust line is frozen
hasExpired(view, expiration) Check if a time has passed
isPseudoAccount(sle) Check if account is a pseudo-account (AMM, etc.)

Phase 3: doApply

In brief: the only phase that actually changes the ledger, and it does so all-or-nothing.

doApply performs the actual ledger modifications. This is where state changes happen.

Characteristics

  • Full read/write access: Has ApplyView& view()
  • Atomic execution: All changes succeed or all are reverted
  • Can create, update, delete ledger entries
  • Must handle reserve requirements

Implementation Pattern

The final phase. Watch it check the reserve first, then create the Check entry and wire it into the owner directories:

Key Operations in doApply

The read/write operations a transactor uses to change ledger state:

Operation Method Description
Read entry view().peek(keylet) Get modifiable reference to entry
Create entry view().insert(sle) Add new entry to ledger
Update entry view().update(sle) Mark entry as modified
Delete entry view().erase(sle) Remove entry from ledger
Add to directory view().dirInsert(...) Add entry to owner directory
Update owner count adjustOwnerCount(...) Increment/decrement owner count

Phase 4: Finalization

In brief: the engine, not your transactor, charges the fee, bumps the sequence, and commits (or reverts) the result.

After doApply succeeds (or fails with a tec code), the transaction is finalized:

  1. Fee consumption: Transaction fee is deducted from the sender
  2. Sequence advancement: Account sequence number is incremented
  3. Metadata recording: Changes are recorded in transaction metadata
  4. State commitment: Changes are committed to the ledger (or reverted for tec)

This phase is handled by the engine, not by individual transactors.


Error Propagation

In brief: which result codes each phase may return, and exactly when a fee sticks.

Different phases can return different categories of result codes:

Phase Can Return Fee Charged? Notes
Preflight tem*, tef*, ter*, tes No No tec codes allowed
Preclaim All codes For tec* only May queue for ter*
doApply All codes For tec*, tes Changes reverted for tec*

Practical Example: Tracing a CheckCreate

In brief: the same CheckCreate followed gate by gate, so you can watch the theory run.

Take this transaction and follow it through all four phases:

Transaction: CheckCreate
  Account: rAlice
  Destination: rBob
  SendMax: 100 XRP
  Expiration: 750000000

Preflight:

  1. Check rAlice != rBob → Pass
  2. Validate 100 XRP is positive and legal → Pass
  3. Validate expiration 750000000 != 0 → Pass
  4. Result: tesSUCCESS

Preclaim:

  1. Read Bob's account → Exists
  2. Check lsfDisallowIncomingCheck → Not set
  3. Check isPseudoAccount(Bob) → False
  4. Check lsfRequireDestTag → Not set (or tag provided)
  5. Check isGlobalFrozen(XRP) → XRP can't be frozen, skip
  6. Check hasExpired(750000000) → Not expired
  7. Result: tesSUCCESS

doApply:

  1. Read Alice's account (peek)
  2. Calculate reserve for +1 owner count → e.g., 12 XRP
  3. Check preFeeBalance_ >= reserve → Pass
  4. Create Check SLE with all fields
  5. Insert Check into ledger
  6. Add to Bob's owner directory → Get page number
  7. Add to Alice's owner directory → Get page number
  8. Increment Alice's owner count
  9. Result: tesSUCCESS

Finalization:

  1. Deduct fee from Alice
  2. Increment Alice's sequence
  3. Record metadata
  4. Commit changes

Best Practices for Implementing Transactors

In brief: the habits that keep a transactor correct, cheap, and safe.

  1. Preflight should be stateless: Never access ledger state in preflight
  2. Fail early: Check the cheapest conditions first
  3. Use appropriate error codes: tem* for format errors, tec* for state-dependent failures
  4. Check reserves before creating objects: Use preFeeBalance_ for reserve checks
  5. Always update owner count: When creating or deleting owned objects
  6. Always update directories: Add/remove from owner directories
  7. Log warnings: Use JLOG to help with debugging

Codebase References

Where this pipeline lives in the source:

File Description
src/libxrpl/tx/applySteps.cpp Transaction dispatch and phase orchestration
src/libxrpl/tx/apply.cpp Core apply logic
src/libxrpl/tx/Transactor.cpp Base class phase implementations

Summary

This module walked the four-phase validation pipeline: preflight (stateless), preclaim (read-only state), doApply (read/write state), and finalization. You learned exactly what each phase may and may not do, why preflight can never claim a fee (it is stateless, with no ledger to charge against), how the signature is checked in two layers (cryptographic validity in preflight2, signing authority in preclaim's checkSign), and how failures are handled at each stage, with the fee sticking only from preclaim onward. Each gate has strictly more access than the one before, which is what makes early rejection cheap and safe.

To remember:

  • Order: preflight, then preclaim, then doApply, then finalization; each gate has strictly more access
  • preflight: stateless; preflight1 checks format, preflight2 checks the signature cryptographically; returns NotTEC (no tec possible)
  • preclaim: read-only ledger view; checkSign verifies signing authority (master/regular/signer list); can return tec
  • doApply: the only writer, all-or-nothing through the ApplyView
  • Finalization is engine work, not the transactor's: fee, sequence, metadata, commit
  • A fee is charged only for tes and tec outcomes
  • Dispatch: src/libxrpl/tx/applySteps.cpp; shared phase logic: src/libxrpl/tx/Transactor.cpp
  • Watch out: tec from preflight would let attackers drain fees with unsigned junk; that is exactly why NotTEC exists

Next up. The gates are clear; now look at what every passage costs. Next: state modification, fees and sequences, the bookkeeping that no transaction escapes.

Assignments

0 of 2 complete

Unlocks

Finishing this module opens up:

XRPL Academy © 2026