Lesson 2: Hooks Fundamentals - Architecture Deep Dive | Hooks & Smart Contracts | XRP Academy - XRP Academy
3 free lessons remaining this month

Free preview access resets monthly

Upgrade for Unlimited
Skip to main content
beginner60 min

Lesson 2: Hooks Fundamentals - Architecture Deep Dive

Learning Objectives

Describe the Hook execution model including trigger conditions, execution phases, and transaction flow

Explain the two Hook entry points (hook and cbak) and when each executes

Identify what data Hooks can access including transaction fields, ledger state, and Hook state

Understand Hook chaining and how multiple Hooks interact on a single account

Articulate the technical constraints that make Hooks deterministic and secure

Think of Hooks as automated gatekeepers for your XRPL account. Every transaction that touches your account—incoming payments, outgoing transfers, trust line changes—must pass through your Hook. The Hook can:

  • **Accept** the transaction (let it proceed)
  • **Reject** the transaction (rollback with error)
  • **Emit** new transactions in response
  • **Store data** for future decisions

This isn't arbitrary code execution—it's targeted, bounded logic that operates within strict constraints.

HOOK AS TRANSACTION MIDDLEWARE

Without Hook:
Transaction → Account → Ledger Update

With Hook:
Transaction → Hook Execution → Decision → Ledger Update

Accept/Reject/Emit
```

The key distinction from Ethereum: Hooks don't exist independently. They're attached to accounts and only execute when transactions affect those accounts. You don't "call" a Hook—transactions trigger Hooks automatically.


Every Hook must implement exactly two functions:

// Primary execution - runs on transactions
int64_t hook(uint32_t reserved);

// Callback execution - runs when emitted transactions fail
int64_t cbak(uint32_t reserved);
  • Executes when a transaction affects the Hook's account

  • Receives transaction data for inspection

  • Must return a value indicating success/failure

  • Primary place for your business logic

  • Executes when a previously emitted transaction cannot be included in any ledger

  • Allows cleanup or alternative actions

  • Often minimal or empty

  • Important for robust emission handling

// Minimal valid Hook
#include "hookapi.h"

int64_t hook(uint32_t reserved) {
    accept(SBUF("Accepted"), 0);
    return 0;
}

int64_t cbak(uint32_t reserved) {
    return 0;
}

Hooks can execute in different contexts, indicated by a parameter:

HOOK EXECUTION CONTEXTS

hook(0) - Strong Execution
├── Transaction directly affects Hook's account
├── Hook is the destination or source
├── Full transaction control
└── Can accept or reject

hook(>0) - Weak Execution  
├── Hook account is "weakly" involved
├── Transactional stakeholder
├── Limited control
└── Different behavior possible

cbak(0) - Normal Callback
├── Emitted transaction succeeded then failed
├── Standard cleanup path

cbak(1) - Emission Failure
├── Emitted transaction never succeeded
├── Alternative handling needed

Hook functions communicate outcomes through return values and API calls:

HOOK OUTCOMES

accept(message, code)
├── Transaction proceeds to ledger
├── Message logged for debugging
├── Code available for analysis
└── Execution ends

rollback(message, code)
├── Transaction rejected entirely
├── All changes reverted
├── Error message returned to sender
└── Execution ends

Return value from hook():
├── 0 typically indicates success
├── Non-zero may indicate issues
├── Used with accept/rollback
└── Returned to caller

Critical pattern: Every Hook execution path MUST end with either accept() or rollback(). If your Hook reaches the end without calling either, behavior is undefined.


When a transaction targets an account with a Hook installed, here's the precise execution order:

HOOK EXECUTION PIPELINE
  1. TRANSACTION SUBMITTED
  1. PRELIMINARY VALIDATION
  1. HOOK DETECTION
  1. HOOK EXECUTION
  1. HOOK DECISION
  1. TRANSACTION APPLICATION
  1. EMISSION HANDLING
  1. FINALIZATION

A Hook fires when its account is a "transactional stakeholder"—but there are two types:

TRANSACTIONAL STAKEHOLDER TYPES

STRONG STAKEHOLDER (ctx = 0):
├── Account is transaction source
├── Account is transaction destination
├── Account is directly named in transaction
└── Full control over accept/reject

Examples:
├── Payment TO your account (you're destination)
├── Payment FROM your account (you're source)
├── TrustSet naming your account
└── OfferCreate from your account

WEAK STAKEHOLDER (ctx > 0):
├── Account is indirectly affected
├── Not primary source or destination
├── May be involved through rippling, offers, etc.
└── Limited control

Examples:
├── Payment rippling through your trust line
├── Offer consumed in DEX trade
├── Multi-party transaction involvement
└── Secondary effects
```

Why this matters: Your Hook logic might need different behavior depending on whether you're a strong or weak stakeholder. Check the context parameter:

int64_t hook(uint32_t ctx) {
    if (ctx == 0) {
        // Strong stakeholder - full control
    } else {
        // Weak stakeholder - different logic
    }
}

Hooks fire on both incoming AND outgoing transactions:

HOOK TRIGGERS

INCOMING (to Hook account):
├── Payment received
├── Check cashed to you
├── Escrow finished to you
├── Trust line created to you
└── Your Hook decides: accept or reject?

OUTGOING (from Hook account):
├── Payment sent
├── Offer created
├── Trust line created by you
├── Any transaction you initiate
└── Your Hook decides: allow or block?

BOTH DIRECTIONS:
├── Same Hook fires
├── Check transaction direction
├── Use otxn_field() to inspect
└── Different logic per direction

Detecting direction:

// Get source account
uint8_t source[20];
otxn_field(SBUF(source), sfAccount);

// Get Hook's account
uint8_t self[20];
hook_account(SBUF(self));

// Compare
int is_outgoing = 0;
BUFFER_EQUAL(is_outgoing, source, self, 20);

if (is_outgoing) {
    // Transaction FROM this account
} else {
    // Transaction TO this account
}

Hooks have read access to several data sources:

HOOK DATA ACCESS

1. ORIGINATING TRANSACTION

1. LEDGER STATE (Read-Only)

1. HOOK STATE

1. HOOK PARAMETERS

1. EXECUTION CONTEXT

Equally important—what's off-limits:

HOOK RESTRICTIONS

CANNOT ACCESS:
├── External data (no HTTP, no oracles built-in)
├── Random numbers (must be deterministic)
├── Private keys (security critical)
├── Other accounts' Hook state (without permission)
├── Future ledger state
└── Network information

CANNOT MODIFY:
├── Ledger state directly (only via emit)
├── Other accounts (only send transactions)
├── The originating transaction structure
├── System parameters
└── Other Hooks' state

CANNOT DO:
├── Infinite loops (guards required)
├── Allocate heap memory
├── Make external calls
├── Execute arbitrary code
└── Affect transactions they don't stakeholder in

Hooks access ledger data through "slots"—temporary containers for ledger objects:

SLOT SYSTEM

Concept:
├── Slots are numbered containers (0-255)
├── Load ledger objects into slots
├── Extract fields from slotted objects
├── Slots cleared after execution

  1. Generate keylet (object identifier)
  2. Load object into slot via keylet
  3. Read fields from slotted object
  4. Use data in Hook logic
  1. Create keylet for account root
  2. Load account into slot
  3. Extract Balance field
  4. Use balance in logic
// Example: Read an account's XRP balance
uint8_t keylet[34];
util_keylet(SBUF(keylet), KEYLET_ACCOUNT, 
            account_id, 20, 0, 0, 0, 0);

// Load into slot 1
int64_t slot_no = slot_set(SBUF(keylet), 1);
if (slot_no < 0) {
rollback(SBUF("Account not found"), 1);
}

// Read balance
int64_t balance = slot_subfield(slot_no, sfBalance, 0);
```


An account can have multiple Hooks installed, which execute in order:

HOOK CHAINING

Account Configuration:
├── Hook 1: Compliance check
├── Hook 2: Amount filtering  
├── Hook 3: Logging
└── Hook 4: Auto-forwarding

Execution Order:
Transaction → Hook 1 → Hook 2 → Hook 3 → Hook 4 → Ledger
               ↓         ↓         ↓         ↓
         accept/     accept/   accept/   accept/
         rollback    rollback  rollback  rollback

CHAINING RULES:
├── Hooks execute in defined order
├── Any Hook can rollback → entire transaction fails
├── All must accept for transaction to proceed
├── State changes from early Hooks visible to later
└── Emissions queued in order

Hooks are installed at specific positions (0-9):

HOOK POSITIONS

Position 0: Primary Hook
Position 1: Secondary Hook
Position 2: Tertiary Hook
...
Position 9: Final Hook

Install at specific position:
SetHook transaction with HookPosition field

Execute order:
Lower positions execute first

Position strategy:
├── 0: Critical security checks
├── 1-3: Business logic
├── 4-6: Automation/emissions
├── 7-9: Logging/monitoring

Each Hook has its own state namespace, preventing conflicts:

HOOK STATE NAMESPACES

Namespace Concept:
├── Each Hook has unique namespace
├── State keys scoped to namespace
├── No accidental collision
├── Explicit cross-Hook access required

Default Namespace:
├── Derived from Hook code hash
├── Same code = same namespace
├── Different code = different namespace

Custom Namespace:
├── Can be specified at install
├── Allows state sharing between Hooks
├── Use carefully (security implications)

Hooks compile to WebAssembly (WASM), providing sandboxed execution:

WEBASSEMBLY SANDBOX

Security Properties:
├── Memory isolation (cannot access host memory)
├── No system calls allowed
├── No file system access
├── No network access
├── Deterministic execution
└── Bounded instruction execution

WASM Advantages:
├── Near-native performance
├── Portable across architectures
├── Well-defined semantics
├── Strong tooling support
├── Proven security model
└── Industry standard

Execution:
├── rippled embeds WASM runtime
├── Hook binary loaded into runtime
├── API functions exposed to Hook
├── Execution monitored for limits
└── Results extracted safely

Hooks use only stack memory—no heap allocation:

HOOK MEMORY MODEL

STACK ONLY:
├── All variables on stack
├── Fixed-size arrays
├── No malloc/free
├── No dynamic allocation
└── Memory automatically cleaned

IMPLICATIONS:
├── Must know sizes at compile time
├── Large data requires careful handling
├── Buffer overflows still possible
├── Stack size is limited
└── Recursion must be bounded

PRACTICAL LIMITS:
├── ~64KB total memory
├── Stack frames for function calls
├── Local variables
├── Temporary buffers
└── Must plan memory usage

EXAMPLE - Fixed buffers:
uint8_t tx_buffer[256];    // ✓ Fixed size, on stack
uint8_t* dynamic;          // ✗ No way to allocate
char message[100];         // ✓ Fixed array

Every Hook has a maximum instruction count:

INSTRUCTION BUDGET

How It Works:
├── Each WASM instruction counted
├── Maximum budget per execution
├── Exceed = automatic rollback
└── No way to buy more

Budget Determination:
├── Calculated at compile time
├── Based on guards and code structure
├── Worst-case execution path
└── Fees based on budget

Practical Impact:
├── Simple Hooks: ~10,000 instructions
├── Complex Hooks: ~100,000 instructions
├── Very complex: May not be possible
└── Optimization matters

Monitoring:
├── Hooks Builder shows estimates
├── Test execution tracks actual
├── Logs show instruction count
└── Optimize hot paths

Every Hook must be fully deterministic:

DETERMINISM RULES

REQUIRED:
├── Same inputs = same outputs
├── No randomness
├── No external data fetching
├── No time-dependent logic*
├── Consistent across all validators
└── Reproducible execution

*Time-aware but not time-dependent:
├── Can read ledger_seq() for "time"
├── Cannot use wall clock time
├── Ledger sequence is consensus-agreed
└── Enables time-based logic safely

WHY DETERMINISM MATTERS:
├── All validators must agree
├── Different results = consensus failure
├── Transactions must be re-playable
├── Network integrity depends on it
└── No "it works on my machine"

VIOLATIONS:
├── Random number generation
├── External API calls
├── Floating point (avoid)
├── Uninitialized variables
└── Race conditions (not possible in Hooks anyway)

Let's trace a complete Hook execution:

EXECUTION TRACE: Payment with Hook

- Alice sends 100 XRP to Bob
- Bob has a Hook: "Reject payments < 50 XRP"

STEP 1: Transaction Submitted
Alice → Network: {
  "TransactionType": "Payment",
  "Account": "rALICE...",
  "Destination": "rBOB...",
  "Amount": "100000000"  // 100 XRP in drops
}

STEP 2: Initial Validation
├── Signature: Valid
├── Fee: Sufficient  
├── Sequence: Correct
└── Basic checks: Pass

STEP 3: Hook Detection
├── Destination rBOB has Hook installed
├── Hook applies to Payment transactions
└── Load Hook WASM module

STEP 4: Hook Execution
┌─────────────────────────────────────┐
│ hook(0) executes:                   │
│                                     │
│ // Get payment amount               │
│ int64_t drops = otxn_amount();      │
│ // drops = 100000000                │
│                                     │
│ // Check threshold (50 XRP)         │
│ if (drops < 50000000) {             │
│     rollback("Too small", 1);       │
│ }                                   │
│                                     │
│ // 100 XRP >= 50 XRP, so accept    │
│ accept("Payment accepted", 0);      │
│                                     │
│ return 0;                           │
└─────────────────────────────────────┘

STEP 5: Decision
├── accept() was called
├── Transaction proceeds
└── No rollback

STEP 6: Ledger Application
├── Alice balance: -100 XRP
├── Bob balance: +100 XRP
├── Fee burned: ~0.00001 XRP
└── Transaction included in ledger

STEP 7: Finalization
├── Transaction hash recorded
├── Hook state (if any) persisted
├── No emissions in this example
└── Complete

Now the same flow with a rejected transaction:

REJECTION TRACE: Payment < 50 XRP

Alice sends 25 XRP to Bob...

STEP 4: Hook Execution
┌─────────────────────────────────────┐
│ hook(0) executes:                   │
│                                     │
│ int64_t drops = otxn_amount();      │
│ // drops = 25000000 (25 XRP)        │
│                                     │
│ if (drops < 50000000) {             │
│     // 25 < 50, condition true      │
│     rollback("Too small", 1);       │
│     // ← EXECUTION ENDS HERE        │
│ }                                   │
│                                     │
│ // Never reached                    │
│ accept("Payment accepted", 0);      │
└─────────────────────────────────────┘

STEP 5: Decision
├── rollback() was called
├── Transaction FAILS
└── Error returned to Alice

STEP 6: NO LEDGER APPLICATION
├── No balance changes
├── Fee still charged to Alice
├── Transaction NOT in ledger
└── Bob never received anything

RESULT:
├── Alice sees: "Transaction failed: Too small"
├── Bob sees: Nothing
├── Ledger shows: Failed transaction
└── Hook protected Bob from small payments

The Hook execution model is deterministic and bounded. Every validator executes the same code with the same inputs and gets the same result—essential for consensus.

WebAssembly provides strong sandboxing. The same WASM security model used by browsers protects the blockchain from malicious Hooks.

Hooks can intercept both incoming and outgoing transactions. This enables comprehensive account protection and automation.

Multiple Hooks can chain with clear execution order. Complex logic can be split into manageable, composable pieces.

⚠️ How complex Hooks can get before hitting practical limits. Instruction budgets and memory constraints limit complexity—exact boundaries vary by use case.

⚠️ Performance impact of widespread Hook adoption. If every account has complex Hooks, network throughput may decrease—extent unknown.

⚠️ Developer ergonomics at scale. The tooling is improving but still less mature than Ethereum's ecosystem.

🔴 Assuming Hooks can do anything smart contracts can. They cannot. The constraints are intentional and fundamental.

🔴 Forgetting that Hooks fire on outgoing transactions too. You might accidentally block your own transfers if your logic isn't careful.

🔴 Not testing all execution paths. A Hook that works for payments might fail for other transaction types hitting your account.

🔴 Ignoring weak stakeholder scenarios. Your Hook might fire unexpectedly when your account is indirectly involved in transactions.

Hooks are elegantly designed transaction middleware—powerful enough for real use cases, constrained enough to be secure. The architecture reflects XRPL's philosophy: predictable, bounded, deterministic execution with no surprises. The trade-off is reduced flexibility compared to EVM smart contracts. You can't build anything you imagine, but what you can build will be more secure by design. Understanding this architecture is essential before writing code—it determines what's possible.


Assignment: Create detailed execution traces for several Hook scenarios to demonstrate your understanding of the architecture.

Requirements:

Part 1: Basic Trace (25%)

Trace execution for a Hook that rejects payments containing memos:

int64_t hook(uint32_t ctx) {
    uint8_t memo[256];
    int64_t memo_len = otxn_field(SBUF(memo), sfMemos);

if (memo_len > 0) {
        rollback(SBUF("No memos allowed"), 1);
    }

accept(SBUF("Clean payment"), 0);
    return 0;
}
  • Transaction that triggers the Hook
  • Each step of execution
  • Decision outcome
  • Ledger state changes (or lack thereof)

Part 2: Emission Trace (25%)

Trace execution for a Hook that emits a "thank you" payment back to sender:

int64_t hook(uint32_t ctx) {
    // Get sender
    uint8_t sender[20];
    otxn_field(SBUF(sender), sfAccount);

// Reserve for emission
    etxn_reserve(1);

// Build thank-you payment (1 XRP)
    uint8_t tx[PREPARE_PAYMENT_SIMPLE_SIZE];
    PREPARE_PAYMENT_SIMPLE(tx, 1000000, sender, 0, 0);

// Emit
    uint8_t hash[32];
    emit(SBUF(hash), SBUF(tx));

accept(SBUF("Payment received, thanks sent"), 0);
    return 0;
}
  • Original transaction
  • Hook execution
  • Emitted transaction creation
  • What happens to emitted transaction
  • Both transactions in ledger

Part 3: Chained Hooks Trace (25%)

Trace execution with two Hooks on the same account:

Hook 1 (Position 0): Reject payments < 10 XRP
Hook 2 (Position 1): Log all accepted payments to state

  • Scenario 1: 5 XRP payment (should fail at Hook 1)
  • Scenario 2: 100 XRP payment (should pass both)
  • Order of execution
  • State changes at each step

Part 4: Edge Cases (25%)

  • Hook runs out of instruction budget mid-execution

  • Hook fails to call accept() or rollback()

  • Emitted transaction fails (cbak scenario)

  • Account is weak stakeholder (ctx > 0)

  • Why it might happen

  • What the outcome is

  • How to prevent/handle it

  • Accuracy of execution traces (30%)

  • Understanding of architecture demonstrated (25%)

  • Edge case handling (20%)

  • Clear documentation (15%)

  • Practical insights (10%)

Time investment: 3-4 hours
Value: Deep understanding of Hook execution prepares you for debugging and optimization


Knowledge Check

Question 1 of 3

When does a Hook's `cbak()` function execute?

  • Hooks Technical Documentation: xrpl-hooks.readme.io
  • "Introduction to Hooks" - XRPL Labs blog
  • WebAssembly specification: webassembly.org
  • XRPL.org: "Transaction Processing"
  • "Consensus and Validation" - XRPL technical docs
  • "WebAssembly Memory Model" - MDN Web Docs
  • Hook execution limits - Xahau documentation

For Next Lesson:
We'll compare Hooks directly to Ethereum smart contracts—not as "better or worse" but as different tools for different jobs. Understanding both helps you choose the right approach for your use case.


End of Lesson 2

Total words: ~4,500
Estimated completion time: 60 minutes reading + 3-4 hours for deliverable

Key Takeaways

1

Hooks are transaction middleware, not independent contracts.

They attach to accounts and fire when transactions affect those accounts—you don't call Hooks, transactions trigger them.

2

Two entry points serve different purposes.

`hook()` handles normal transaction processing; `cbak()` handles emission failures. Both must exist in every Hook.

3

Strong vs. weak stakeholder matters.

Your Hook might fire with full control (strong) or limited involvement (weak). Check the context parameter and handle both cases.

4

Data access is read-only for ledger state.

Hooks can inspect anything on the ledger but can only modify state through emissions or their own Hook state storage.

5

Constraints enable security.

Stack-only memory, instruction limits, determinism requirements, and sandboxed execution aren't limitations—they're the features that prevent exploit categories. ---