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


Last updated