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
- TRANSACTION SUBMITTED
- PRELIMINARY VALIDATION
- HOOK DETECTION
- HOOK EXECUTION
- HOOK DECISION
- TRANSACTION APPLICATION
- EMISSION HANDLING
- 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
- Generate keylet (object identifier)
- Load object into slot via keylet
- Read fields from slotted object
- Use data in Hook logic
- Create keylet for account root
- Load account into slot
- Extract Balance field
- 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 3When 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
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.
Two entry points serve different purposes.
`hook()` handles normal transaction processing; `cbak()` handles emission failures. Both must exist in every Hook.
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.
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.
Constraints enable security.
Stack-only memory, instruction limits, determinism requirements, and sandboxed execution aren't limitations—they're the features that prevent exploit categories. ---