Transaction Lifecycle: Complete Transaction Journey

← Back to Rippled II Overview


Introduction

Understanding the complete lifecycle of a transaction—from the moment it's created to its final inclusion in a validated ledger—is crucial for developing applications on the XRP Ledger, debugging transaction issues, and optimizing transaction processing. Every transaction follows a well-defined path through multiple validation stages, consensus rounds, and finalization steps.

This deep dive traces the entire journey of a transaction, explaining each phase, the checks performed, the state transitions, and how to monitor and query transaction status at every step. Whether you're building a wallet, an exchange integration, or contributing to the core protocol, mastering the transaction lifecycle is essential.


Transaction Lifecycle Overview

Complete Journey Diagram

┌─────────────────────────────────────────────────────────────┐
│                    1. Transaction Creation                   │
│              (Client creates and signs transaction)          │
└──────────────────────────┬──────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│                    2. Transaction Submission                 │
│           (Submit via RPC, WebSocket, or peer network)       │
└──────────────────────────┬──────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│                    3. Initial Validation                     │
│        (Signature check, format validation, preflight)       │
└──────────────────────────┬──────────────────────────────────┘

                  ┌────────┴────────┐
                  │                 │
                  ↓                 ↓
              ✓ Valid           ✗ Invalid
                  │                 │
                  │                 └──→ Rejected (temMALFORMED, etc.)

┌─────────────────────────────────────────────────────────────┐
│                  4. Preclaim Validation                      │
│       (Check ledger state, balance, account existence)       │
└──────────────────────────┬──────────────────────────────────┘

                  ┌────────┴────────┐
                  │                 │
                  ↓                 ↓
              ✓ Valid           ✗ Invalid
                  │                 │
                  │                 └──→ Rejected (tecUNFUNDED, etc.)

┌─────────────────────────────────────────────────────────────┐
│               5. Open Ledger Application                     │
│         (Tentatively apply to open ledger for preview)       │
└──────────────────────────┬──────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│                    6. Network Propagation                    │
│              (Broadcast to peers via tmTRANSACTION)          │
└──────────────────────────┬──────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│                    7. Consensus Round                        │
│        (Validators propose and agree on transaction set)     │
└──────────────────────────┬──────────────────────────────────┘

                  ┌────────┴────────┐
                  │                 │
                  ↓                 ↓
           Included in Set    Not Included
                  │                 │
                  │                 └──→ Deferred to next round

┌─────────────────────────────────────────────────────────────┐
│               8. Canonical Application (DoApply)             │
│          (Apply to ledger in canonical order, final)         │
└──────────────────────────┬──────────────────────────────────┘

                  ┌────────┴────────┐
                  │                 │
                  ↓                 ↓
            tesSUCCESS          tecFAILURE
                  │                 │
                  │                 └──→ Failed but fee charged

┌─────────────────────────────────────────────────────────────┐
│                    9. Ledger Closure                         │
│              (Ledger closed with transaction included)       │
└──────────────────────────┬──────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│                    10. Validation Phase                      │
│         (Validators sign and broadcast validations)          │
└──────────────────────────┬──────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│                    11. Fully Validated                       │
│     (Transaction immutable, part of permanent history)       │
└─────────────────────────────────────────────────────────────┘

Typical Timeline

Fast Path (ideal conditions):

T+0s:     Transaction submitted
T+0.1s:   Initial validation complete
T+0.2s:   Open ledger application
T+0.3s:   Network propagation
T+5s:     Consensus round completes
T+5.1s:   Canonical application
T+5.2s:   Ledger closed
T+7s:     Validations collected
T+7s:     Transaction fully validated

Total: ~7 seconds from submission to finality

Slow Path (transaction arrives late in open phase):

T+0s:     Transaction submitted
T+0.1s:   Validation complete
T+25s:    Consensus round starts (waiting for ledger close)
T+30s:    Consensus completes
T+30.1s:  Canonical application
T+30.2s:  Ledger closed
T+32s:    Transaction fully validated

Total: ~32 seconds (depends on when submitted)

Phase 1: Transaction Creation

Transaction Structure

Before submission, a transaction must be properly constructed:

{
  "TransactionType": "Payment",
  "Account": "rN7n7otQDd6FczFgLdlqtyMVrn3HMtthca",
  "Destination": "rLNaPoKeeBjZe2qs6x52yVPZpZ8td4dc6w",
  "Amount": "1000000",
  "Fee": "12",
  "Sequence": 42,
  "LastLedgerSequence": 75234567,
  "SigningPubKey": "03AB40A0490F9B7ED8DF29D246BF2D6269820A0EE7742ACDD457BEA7C7D0931EDB",
  "TxnSignature": "30450221008..."
}

Required Fields

Universal Fields (all transaction types):

  • TransactionType - Type of transaction (Payment, OfferCreate, etc.)

  • Account - Source account (sender)

  • Fee - Transaction fee in drops (1 XRP = 1,000,000 drops)

  • Sequence - Account sequence number (nonce)

  • SigningPubKey - Public key used for signing

  • TxnSignature - Cryptographic signature (or multi-signatures)

Optional but Recommended:

  • LastLedgerSequence - Expiration ledger (transaction invalid after this)

  • SourceTag / DestinationTag - Integer tags for routing/identification

  • Memos - Arbitrary data attached to transaction

Transaction Signing

Single Signature:

// Using xrpl.js
const xrpl = require('xrpl');

// Create transaction
const tx = {
  TransactionType: 'Payment',
  Account: wallet.address,
  Destination: 'rLNaPoKeeBjZe2qs6x52yVPZpZ8td4dc6w',
  Amount: '1000000',
  Fee: '12'
};

// Auto-fill (adds Sequence, LastLedgerSequence, etc.)
const prepared = await client.autofill(tx);

// Sign transaction
const signed = wallet.sign(prepared);

console.log(signed.tx_blob);  // Signed transaction blob
console.log(signed.hash);     // Transaction hash

Multi-Signature:

// For accounts with SignerList
const tx = {
  TransactionType: 'Payment',
  Account: multiSigAccount,
  Destination: 'rLNaPoKeeBjZe2qs6x52yVPZpZ8td4dc6w',
  Amount: '1000000',
  Fee: '12',
  SigningPubKey: '',  // Empty for multi-sig
  Sequence: 42
};

// Each signer signs independently
const signer1Sig = wallet1.sign(tx, true);
const signer2Sig = wallet2.sign(tx, true);

// Combine signatures
const multisigned = xrpl.multisign([signer1Sig, signer2Sig]);

Transaction Hash

The transaction hash (ID) is calculated from the signed transaction:

// Simplified hash calculation
uint256 calculateTransactionID(STTx const& tx)
{
    // Serialize the entire signed transaction
    Serializer s;
    tx.add(s);
    
    // Hash with SHA-512 and take first 256 bits
    return s.getSHA512Half();
}

Important: The hash is deterministic—the same signed transaction always produces the same hash.


Phase 2: Transaction Submission

Submission Methods

Method 1: RPC Submit

Submit via JSON-RPC:

curl -X POST https://s1.ripple.com:51234/ \
  -H "Content-Type: application/json" \
  -d '{
    "method": "submit",
    "params": [{
      "tx_blob": "120000228000000024..."
    }]
  }'

Response:

{
  "result": {
    "engine_result": "tesSUCCESS",
    "engine_result_code": 0,
    "engine_result_message": "The transaction was applied.",
    "tx_blob": "120000228000000024...",
    "tx_json": {
      "Account": "rN7n7otQDd6FczFgLdlqtyMVrn3HMtthca",
      "Amount": "1000000",
      "Destination": "rLNaPoKeeBjZe2qs6x52yVPZpZ8td4dc6w",
      "Fee": "12",
      "TransactionType": "Payment",
      "hash": "E08D6E9754025BA2534A78707605E0601F03ACE063687A0CA1BDDACFCD1698C7"
    }
  }
}

Method 2: WebSocket Submit

Real-time submission with streaming updates:

const ws = new WebSocket('wss://s1.ripple.com');

ws.on('open', () => {
  ws.send(JSON.stringify({
    command: 'submit',
    tx_blob: '120000228000000024...'
  }));
});

ws.on('message', (data) => {
  const response = JSON.parse(data);
  console.log(response.result.engine_result);
});

Method 3: Peer Network Submission

Transactions submitted to one node propagate to all nodes:

Client → Node A → Overlay Network → All Nodes

Even if submitted to a non-validator, the transaction reaches validators through peer-to-peer propagation.

Submission Response

Immediate response indicates initial validation result:

Success Codes:

  • tesSUCCESS - Transaction applied to open ledger

  • terQUEUED - Transaction queued (network busy)

Temporary Failure (can retry):

  • terPRE_SEQ - Sequence too high, earlier tx needed

  • tefPAST_SEQ - Sequence too low (already used)

Permanent Failure (don't retry):

  • temMALFORMED - Malformed transaction

  • temBAD_FEE - Invalid fee

  • temBAD_SIGNATURE - Invalid signature


Phase 3: Initial Validation

Preflight Checks

Before accessing ledger state, static validation occurs:

NotTEC Transactor::preflight(PreflightContext const& ctx)
{
    // 1. Verify signature
    if (!checkSignature(ctx.tx))
        return temBAD_SIGNATURE;
    
    // 2. Check transaction format
    if (!ctx.tx.isFieldPresent(sfAccount))
        return temMALFORMED;
    
    // 3. Validate fee
    auto const fee = ctx.tx.getFieldAmount(sfFee);
    if (fee < ctx.baseFee)
        return telINSUF_FEE_P;
    
    // 4. Check sequence
    if (ctx.tx.getSequence() == 0)
        return temBAD_SEQUENCE;
    
    // 5. Verify amounts are valid
    auto const amount = ctx.tx.getFieldAmount(sfAmount);
    if (amount <= zero)
        return temBAD_AMOUNT;
    
    return tesSUCCESS;
}

Checks Performed:

  • ✓ Cryptographic signature valid

  • ✓ Transaction format correct

  • ✓ Required fields present

  • ✓ Fee sufficient

  • ✓ Amounts positive and properly formatted

  • ✓ No contradictory fields

Why Preflight Matters: Catches obvious errors before expensive ledger state access.


Phase 4: Preclaim Validation

Ledger State Checks

Read-only validation against current ledger state:

TER Transactor::preclaim(PreclaimContext const& ctx)
{
    // 1. Source account must exist
    auto const sleAccount = ctx.view.read(
        keylet::account(ctx.tx[sfAccount]));
    
    if (!sleAccount)
        return terNO_ACCOUNT;
    
    // 2. Check sequence number
    auto const txSeq = ctx.tx.getSequence();
    auto const acctSeq = (*sleAccount)[sfSequence];
    
    if (txSeq != acctSeq)
        return tefPAST_SEQ;  // Already used or too high
    
    // 3. Verify sufficient balance
    auto const balance = (*sleAccount)[sfBalance];
    auto const fee = ctx.tx[sfFee];
    auto const amount = ctx.tx[sfAmount];
    
    if (balance < fee + amount)
        return tecUNFUNDED_PAYMENT;
    
    // 4. Check destination exists (for Payment)
    if (ctx.tx.getTransactionType() == ttPAYMENT)
    {
        auto const dest = ctx.tx[sfDestination];
        auto const sleDest = ctx.view.read(keylet::account(dest));
        
        if (!sleDest)
        {
            // Can create account if amount >= reserve
            if (amount < ctx.view.fees().accountReserve(0))
                return tecNO_DST_INSUF_XRP;
        }
    }
    
    return tesSUCCESS;
}

Checks Performed:

  • ✓ Source account exists

  • ✓ Sequence number correct

  • ✓ Sufficient balance (including fee)

  • ✓ Destination account requirements met

  • ✓ Trust lines exist (for issued currencies)

  • ✓ Account flags permit operation


Phase 5: Open Ledger Application

Tentative Application

Transaction is tentatively applied to provide immediate feedback:

std::pair<TER, bool> 
NetworkOPs::processTransaction(
    std::shared_ptr<Transaction> const& transaction)
{
    // Apply to open ledger
    auto result = app_.openLedger().modify(
        [&](OpenView& view, beast::Journal j)
        {
            return transaction->apply(app_, view, ApplyFlags::tapNONE);
        });
    
    if (result.second)  // Transaction applied successfully
    {
        JLOG(j_.trace()) 
            << "Transaction " << transaction->getID()
            << " applied to open ledger with result: "
            << transToken(result.first);
        
        // Relay to network
        app_.overlay().relay(transaction);
        
        return {result.first, true};
    }
    
    return {result.first, false};
}

Open Ledger Characteristics:

  • Not Final: Open ledger is tentative, changes frequently

  • No Consensus: Local view only, other nodes may differ

  • Immediate Feedback: Clients get instant response

  • Can Change: Transaction may be removed or re-ordered

Why It Matters:

  • Users get immediate confirmation

  • Wallets can show pending transactions

  • Applications can provide real-time updates


Phase 6: Network Propagation

Transaction Broadcasting

Once applied to open ledger, transaction broadcasts to peers:

void Overlay::relay(std::shared_ptr<Transaction> const& tx)
{
    // Create protocol message
    protocol::TMTransaction msg;
    msg.set_rawtransaction(tx->getSerialized());
    msg.set_status(protocol::tsNEW);
    msg.set_receivetimestamp(
        std::chrono::system_clock::now().time_since_epoch().count());
    
    // Wrap in Message object
    auto m = std::make_shared<Message>(msg, protocol::mtTRANSACTION);
    
    // Broadcast to all peers (except source)
    for (auto& peer : getActivePeers())
    {
        if (peer.get() != source)
            peer->send(m);
    }
    
    JLOG(j_.trace()) 
        << "Relayed transaction " << tx->getID() 
        << " to " << getActivePeers().size() << " peers";
}

Propagation Speed:

  • Local network: < 100ms

  • Global network: 200-500ms

  • All nodes receive transaction within 1 second

Deduplication:

  • Nodes track recently seen transactions

  • Duplicate transactions not re-processed

  • Prevents network flooding


Phase 7: Consensus Round

Transaction Set Building

As ledger close approaches, validators build transaction sets:

std::set<TxID> buildInitialPosition(OpenView const& openLedger)
{
    std::set<TxID> position;
    
    // Include transactions from open ledger
    for (auto const& tx : openLedger.transactions())
    {
        // Only include transactions that:
        // 1. Are still valid
        // 2. Have sufficient fee
        // 3. Haven't expired (LastLedgerSequence)
        
        if (isValidForConsensus(tx))
            position.insert(tx.getTransactionID());
    }
    
    return position;
}

Consensus Process

Validators exchange proposals and converge:

Round 1: Initial proposals

Validator A proposes: {TX1, TX2, TX3, TX4}
Validator B proposes: {TX1, TX2, TX3, TX5}
Validator C proposes: {TX1, TX2, TX4, TX5}

Agreement:
TX1: 100% ✓
TX2: 100% ✓
TX3: 67%
TX4: 67%
TX5: 67%

Round 2: Converge on high-agreement transactions

All validators propose: {TX1, TX2}

Agreement:
TX1: 100% ✓ (included)
TX2: 100% ✓ (included)

TX3, TX4, TX5 deferred to next ledger

Transaction Inclusion Criteria:

  • 80% of UNL must agree to include

  • Transaction must still be valid

  • Must not have expired (LastLedgerSequence)


Phase 8: Canonical Application

Deterministic Execution

After consensus, transactions are applied in canonical order:

void applyTransactionsCanonically(
    Ledger& ledger,
    std::set<TxID> const& txSet)
{
    // 1. Retrieve transactions from set
    std::vector<std::shared_ptr<STTx>> transactions;
    for (auto const& id : txSet)
    {
        auto tx = fetchTransaction(id);
        transactions.push_back(tx);
    }
    
    // 2. Sort in canonical order
    std::sort(transactions.begin(), transactions.end(),
        [](auto const& a, auto const& b)
        {
            // Sort by account, then sequence
            if (a->getAccountID(sfAccount) != b->getAccountID(sfAccount))
                return a->getAccountID(sfAccount) < b->getAccountID(sfAccount);
            
            return a->getSequence() < b->getSequence();
        });
    
    // 3. Apply each transaction
    for (auto const& tx : transactions)
    {
        auto const result = applyTransaction(ledger, tx);
        
        // Record in metadata
        ledger.recordTransaction(tx, result);
        
        JLOG(j_.debug()) 
            << "Applied " << tx->getTransactionID()
            << " with result " << transToken(result);
    }
}

DoApply Execution:

TER Payment::doApply()
{
    // 1. Debit source account
    auto const result = accountSend(
        view(),
        account_,
        ctx_.tx[sfDestination],
        ctx_.tx[sfAmount],
        j_);
    
    if (result != tesSUCCESS)
        return result;
    
    // 2. Update sequence number
    auto const sleAccount = view().peek(keylet::account(account_));
    (*sleAccount)[sfSequence] = ctx_.tx.getSequence() + 1;
    view().update(sleAccount);
    
    // 3. Record metadata
    ctx_.deliver(ctx_.tx[sfAmount]);
    
    return tesSUCCESS;
}

Result Codes:

  • tesSUCCESS - Transaction succeeded

  • tecUNFUNDED - Failed but fee charged

  • tecNO_TARGET - Failed but fee charged

Important: Even failed transactions (tec codes) consume the fee and advance the sequence number.


Phase 9: Ledger Closure

Closing the Ledger

After all transactions are applied:

void closeLedger(
    std::shared_ptr<Ledger>& ledger,
    NetClock::time_point closeTime)
{
    // 1. Set close time
    ledger->setCloseTime(closeTime);
    
    // 2. Calculate state hash
    auto const stateHash = ledger->stateMap().getHash();
    ledger->setAccountHash(stateHash);
    
    // 3. Calculate transaction tree hash
    auto const txHash = ledger->txMap().getHash();
    ledger->setTxHash(txHash);
    
    // 4. Calculate ledger hash
    auto const ledgerHash = ledger->getHash();
    
    JLOG(j_.info()) 
        << "Closed ledger " << ledger->seq()
        << " hash: " << ledgerHash
        << " txns: " << ledger->txCount();
    
    // 5. Mark as closed
    ledger->setClosed();
}

Ledger Hash Calculation:

uint256 Ledger::getHash() const
{
    Serializer s;
    s.add32(seq_);                    // Ledger sequence
    s.add64(closeTime_.count());      // Close time
    s.addBitString(parentHash_);      // Parent ledger hash
    s.addBitString(txHash_);          // Transaction tree hash
    s.addBitString(accountHash_);     // Account state hash
    s.add64(totalCoins_);             // Total XRP
    s.add64(closeTimeResolution_);    // Close time resolution
    s.add8(closeFlags_);              // Flags
    
    return s.getSHA512Half();
}

Phase 10: Validation Phase

Creating Validations

Validators sign the closed ledger:

std::shared_ptr<STValidation> createValidation(
    Ledger const& ledger,
    SecretKey const& secretKey)
{
    auto validation = std::make_shared<STValidation>(
        ledger.getHash(),
        ledger.seq(),
        NetClock::now(),
        publicKeyFromSecretKey(secretKey),
        calculateNodeID(publicKeyFromSecretKey(secretKey)),
        [&](STValidation& v)
        {
            v.setFlag(vfFullValidation);
            v.sign(secretKey);
        });
    
    JLOG(j_.info()) 
        << "Created validation for ledger " << ledger.seq()
        << " hash: " << ledger.getHash();
    
    return validation;
}

Broadcasting Validations

void broadcastValidation(std::shared_ptr<STValidation> const& val)
{
    // Create protocol message
    protocol::TMValidation msg;
    Serializer s;
    val->add(s);
    msg.set_validation(s.data(), s.size());
    
    // Broadcast to all peers
    auto m = std::make_shared<Message>(msg, protocol::mtVALIDATION);
    overlay().broadcast(m);
    
    JLOG(j_.trace()) 
        << "Broadcast validation for ledger " << val->getLedgerSeq();
}

Collecting Validations

bool hasValidationQuorum(
    LedgerHash const& hash,
    std::set<NodeID> const& validators)
{
    auto const& unl = getUNL();
    size_t validationCount = 0;
    
    for (auto const& validator : validators)
    {
        if (unl.contains(validator))
            validationCount++;
    }
    
    // Need >80% of UNL
    return validationCount >= (unl.size() * 4 / 5);
}

Phase 11: Fully Validated

Finalization

When quorum is reached, ledger becomes fully validated:

void markLedgerValidated(std::shared_ptr<Ledger> const& ledger)
{
    // 1. Mark as validated
    ledger->setValidated();
    
    // 2. Add to validated chain
    ledgerMaster_.addValidatedLedger(ledger);
    
    // 3. Update current validated ledger
    ledgerMaster_.setValidatedLedger(ledger);
    
    // 4. Publish to subscribers
    publishValidatedLedger(ledger);
    
    // 5. Start next open ledger
    openLedger_.accept(
        ledger,
        orderTx,
        consensusParms,
        {});  // Empty initial transaction set
    
    JLOG(j_.info()) 
        << "Ledger " << ledger->seq() 
        << " validated with " << validationCount(ledger)
        << " validations";
}

Characteristics of Validated Ledger:

  • Immutable: Cannot be changed

  • Permanent: Part of ledger history forever

  • Canonical: All nodes have identical copy

  • Final: Transactions cannot be reversed


Transaction Status Querying

Methods to Check Transaction Status

Method 1: tx RPC

Query by transaction hash:

rippled tx E08D6E9754025BA2534A78707605E0601F03ACE063687A0CA1BDDACFCD1698C7

Response:

{
  "result": {
    "Account": "rN7n7otQDd6FczFgLdlqtyMVrn3HMtthca",
    "Amount": "1000000",
    "Destination": "rLNaPoKeeBjZe2qs6x52yVPZpZ8td4dc6w",
    "Fee": "12",
    "Sequence": 42,
    "TransactionType": "Payment",
    "hash": "E08D6E9754025BA2534A78707605E0601F03ACE063687A0CA1BDDACFCD1698C7",
    "ledger_index": 75234567,
    "validated": true,
    "meta": {
      "TransactionIndex": 5,
      "TransactionResult": "tesSUCCESS",
      "AffectedNodes": [
        // Nodes modified by transaction
      ]
    }
  }
}

Key Fields:

  • validated: true = in validated ledger, false = pending

  • meta.TransactionResult: Final result code

  • ledger_index: Which ledger contains transaction

Method 2: account_tx RPC

Query all transactions for an account:

rippled account_tx rN7n7otQDd6FczFgLdlqtyMVrn3HMtthca

Lists transactions in reverse chronological order.

Method 3: WebSocket Subscriptions

Real-time transaction monitoring:

ws.send(JSON.stringify({
  command: 'subscribe',
  accounts: ['rN7n7otQDd6FczFgLdlqtyMVrn3HMtthca']
}));

ws.on('message', (data) => {
  const msg = JSON.parse(data);
  if (msg.type === 'transaction') {
    console.log('Transaction:', msg.transaction);
    console.log('Status:', msg.validated ? 'Validated' : 'Pending');
  }
});

Subscription Types:

  • accounts - Transactions affecting specific accounts

  • transactions - All transactions network-wide

  • ledger - Ledger close events


Transaction Metadata

Metadata Structure

Metadata records the effects of a transaction:

{
  "meta": {
    "TransactionIndex": 5,
    "TransactionResult": "tesSUCCESS",
    "AffectedNodes": [
      {
        "ModifiedNode": {
          "LedgerEntryType": "AccountRoot",
          "LedgerIndex": "13F1A95D7AAB7108D5CE7EEAF504B2894B8C674E6D68499076441C4837282BF8",
          "PreviousFields": {
            "Balance": "10000000",
            "Sequence": 42
          },
          "FinalFields": {
            "Balance": "8999988",
            "Sequence": 43,
            "Account": "rN7n7otQDd6FczFgLdlqtyMVrn3HMtthca"
          }
        }
      },
      {
        "ModifiedNode": {
          "LedgerEntryType": "AccountRoot",
          "LedgerIndex": "4F83A2CF7E70F77F79A307E6A472BFC2585B806A70833CCD1C26105BAE0D6E05",
          "PreviousFields": {
            "Balance": "5000000"
          },
          "FinalFields": {
            "Balance": "6000000",
            "Account": "rLNaPoKeeBjZe2qs6x52yVPZpZ8td4dc6w"
          }
        }
      }
    ],
    "delivered_amount": "1000000"
  }
}

AffectedNodes Types:

  • CreatedNode - New ledger object created

  • ModifiedNode - Existing object modified

  • DeletedNode - Object deleted

Key Metadata Fields:

  • TransactionIndex - Position in ledger

  • TransactionResult - Final result code

  • delivered_amount - Actual amount delivered (for partial payments)


Transaction Expiration

LastLedgerSequence

Transactions can specify an expiration:

{
  "TransactionType": "Payment",
  "Account": "rN7n7otQDd6FczFgLdlqtyMVrn3HMtthca",
  "Destination": "rLNaPoKeeBjZe2qs6x52yVPZpZ8td4dc6w",
  "Amount": "1000000",
  "LastLedgerSequence": 75234567
}

Behavior:

  • If not included by ledger 75234567, transaction becomes invalid

  • Prevents transactions from being stuck indefinitely

  • Recommended: Set to current ledger + 4

Checking Expiration:

bool isExpired(STTx const& tx, LedgerIndex currentLedger)
{
    if (!tx.isFieldPresent(sfLastLedgerSequence))
        return false;  // No expiration set
    
    return tx[sfLastLedgerSequence] < currentLedger;
}

Hands-On Exercise

Exercise: Track a Transaction Through Its Complete Lifecycle

Objective: Submit a transaction and observe it at each phase of the lifecycle.

Part 1: Prepare and Submit

Step 1: Create and fund test accounts

# Using testnet
rippled account_info <your_address>

Step 2: Prepare transaction with monitoring

const xrpl = require('xrpl');
const client = new xrpl.Client('wss://s.altnet.rippletest.net:51233');

await client.connect();

const wallet = xrpl.Wallet.fromSeed('sXXXXXXXXXXXXXXXX');

// Create transaction
const tx = {
  TransactionType: 'Payment',
  Account: wallet.address,
  Destination: 'rLNaPoKeeBjZe2qs6x52yVPZpZ8td4dc6w',
  Amount: '1000000',
  Fee: '12'
};

// Prepare with LastLedgerSequence
const prepared = await client.autofill(tx);
console.log('Prepared TX:', prepared);
console.log('LastLedgerSequence:', prepared.LastLedgerSequence);

// Sign
const signed = wallet.sign(prepared);
console.log('Transaction Hash:', signed.hash);

Step 3: Submit and record time

const startTime = Date.now();

const result = await client.submit(signed.tx_blob);

console.log('Submission Time:', Date.now() - startTime, 'ms');
console.log('Initial Result:', result.result.engine_result);
console.log('Applied to Open Ledger:', result.result.engine_result === 'tesSUCCESS');

Part 2: Monitor Progress

Step 4: Subscribe to transaction

await client.request({
  command: 'subscribe',
  accounts: [wallet.address]
});

client.on('transaction', (tx) => {
  console.log('Transaction Event:');
  console.log('  Hash:', tx.transaction.hash);
  console.log('  Validated:', tx.validated);
  console.log('  Ledger Index:', tx.ledger_index);
  console.log('  Time:', Date.now() - startTime, 'ms');
  
  if (tx.validated) {
    console.log('✓ Transaction fully validated!');
    console.log('  Result:', tx.meta.TransactionResult);
  }
});

Step 5: Poll for status

async function checkStatus(hash) {
  try {
    const tx = await client.request({
      command: 'tx',
      transaction: hash
    });
    
    console.log('Status Check:');
    console.log('  Found:', true);
    console.log('  Validated:', tx.result.validated);
    console.log('  Ledger:', tx.result.ledger_index);
    console.log('  Result:', tx.result.meta.TransactionResult);
    
    return tx.result.validated;
  } catch (e) {
    console.log('Status Check: Not yet in validated ledger');
    return false;
  }
}

// Poll every second
const pollInterval = setInterval(async () => {
  const validated = await checkStatus(signed.hash);
  if (validated) {
    clearInterval(pollInterval);
    console.log('Total time to validation:', Date.now() - startTime, 'ms');
  }
}, 1000);

Part 3: Analyze Results

Step 6: Examine metadata

const finalTx = await client.request({
  command: 'tx',
  transaction: signed.hash
});

console.log('\n=== Final Transaction Analysis ===');
console.log('Transaction Hash:', finalTx.result.hash);
console.log('Ledger Index:', finalTx.result.ledger_index);
console.log('Result Code:', finalTx.result.meta.TransactionResult);
console.log('Transaction Index:', finalTx.result.meta.TransactionIndex);

// Analyze affected nodes
console.log('\nAffected Nodes:');
for (const node of finalTx.result.meta.AffectedNodes) {
  if (node.ModifiedNode) {
    console.log('  Modified:', node.ModifiedNode.LedgerEntryType);
    console.log('    Account:', node.ModifiedNode.FinalFields.Account);
    if (node.ModifiedNode.PreviousFields.Balance) {
      console.log('    Balance Change:',
        node.ModifiedNode.FinalFields.Balance - 
        node.ModifiedNode.PreviousFields.Balance);
    }
  }
}

// Calculate total time
console.log('\nTiming:');
console.log('  Total time:', Date.now() - startTime, 'ms');

Analysis Questions

Answer these based on your observations:

  1. How long did each phase take?

    • Submission to initial result: ___ ms

    • Initial result to validated: ___ ms

    • Total time: ___ ms

  2. What was the initial result?

    • Did it apply to open ledger?

  3. Which ledger included the transaction?

    • Ledger index?

    • How many ledgers closed between submission and inclusion?

  4. What was the metadata?

    • Which nodes were affected?

    • What balances changed?

  5. Did the transaction expire?

    • Was LastLedgerSequence set?

    • How close to expiration was it?


Key Takeaways

Core Concepts

11-Phase Journey: Transactions go through creation, submission, validation, consensus, application, and finalization

Multiple Validation Stages: Preflight (static), Preclaim (state-based), DoApply (execution)

Open Ledger Preview: Tentative application provides immediate feedback before consensus

Consensus Inclusion: Validators must agree (>80%) to include transaction

Canonical Order: Deterministic ordering ensures all nodes reach identical state

Immutable Finality: Once validated, transactions cannot be reversed

Metadata Records Effects: Complete record of all ledger modifications

Timing Expectations

Fast Path: ~7 seconds submission to validation

Slow Path: ~30 seconds if submitted late in open phase

Network Propagation: <1 second to reach all nodes

Consensus Round: 3-5 seconds

Development Skills

Transaction Construction: Proper signing and field selection

Status Monitoring: Using tx, account_tx, and subscriptions

Error Handling: Understanding result codes (tem/tef/ter/tec/tes)

Expiration Management: Setting LastLedgerSequence appropriately

Metadata Analysis: Understanding transaction effects


Common Issues and Solutions

Issue 1: Transaction Stuck Pending

Symptoms: Transaction not validating after 30+ seconds

Possible Causes:

  • Insufficient fee (transaction queued)

  • Network congestion

  • Sequence gap (earlier transaction missing)

Solutions:

# Check transaction status
rippled tx <hash>

# Check for sequence gaps
rippled account_info <account>

# Increase fee and resubmit if needed

Issue 2: tefPAST_SEQ Error

Symptoms: Sequence number already used

Cause: Sequence out of sync or transaction already processed

Solution:

// Always fetch current sequence
const accountInfo = await client.request({
  command: 'account_info',
  account: wallet.address
});

const currentSeq = accountInfo.result.account_data.Sequence;

Issue 3: Transaction Not Found

Symptoms: tx command returns "txnNotFound"

Possible Causes:

  • Transaction not yet in validated ledger

  • Transaction expired (LastLedgerSequence)

  • Transaction rejected during validation

Solution:

// Wait for validation or check expiration
const ledger = await client.request({command: 'ledger_current'});

if (ledger.result.ledger_current_index > tx.LastLedgerSequence) {
  console.log('Transaction expired');
}

Issue 4: tecUNFUNDED_PAYMENT

Symptoms: Transaction failed with fee charged

Cause: Insufficient balance between submission and execution

Prevention:

// Always check balance including reserves
const reserve = (2 + ownerCount) * baseReserve;
const available = balance - reserve;

if (amount + fee > available) {
  throw new Error('Insufficient funds');
}

Additional Resources

Official Documentation

Codebase References

  • src/ripple/app/tx/impl/Transactor.cpp - Transaction processing

  • src/ripple/app/misc/NetworkOPs.cpp - Network operations and transaction handling

  • src/ripple/app/ledger/OpenLedger.cpp - Open ledger management

  • Transactors - How transactions are validated and executed

  • Consensus Engine - How transactions are included in consensus

  • Protocols - How transactions are propagated across the network


Next Steps

Now that you understand the complete transaction lifecycle, learn how to efficiently navigate the Rippled codebase to find and understand specific functionality.

➡️ Continue to: Codebase Navigation - Efficiently Navigating the Rippled Source

⬅️ Back to: Rippled II Overview


Get Started

Access the course: docs.xrpl-commons.org/core-dev-bootcamp

Got questions? Contact us here: Submit Feedback


© 2025 XRPL Commons - Core Dev Bootcamp

Last updated