Lesson 16: Performance Optimization - Efficient Hooks | 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
beginner50 min

Lesson 16: Performance Optimization - Efficient Hooks

Learning Objectives

Understand Hook execution costs and fee calculation

Optimize instruction count through efficient coding patterns

Minimize state operations with batching and caching

Design efficient data structures for Hook constraints

Measure and profile Hook performance

Most Hooks don't need aggressive optimization—correctness and security matter more. But optimization becomes important when:

  • Processing high transaction volumes
  • Running complex calculations
  • Managing large state
  • Minimizing fees over many executions
  • Operating on resource-constrained networks
OPTIMIZATION PRIORITIES:

1. CORRECTNESS (Most Important)

1. SECURITY

1. READABILITY

1. PERFORMANCE (When Needed)

---

Hooks are charged based on instructions executed:

// Every operation counts:
int64_t a = 5;        // Assignment: ~1 instruction
int64_t b = a + 3;    // Addition: ~2 instructions
int64_t c = a * b;    // Multiplication: ~3 instructions

// Loops multiply:
for (int i = 0; GUARD(10), i < 10; ++i) {
    sum += values[i];  // ~5 instructions × 10 = ~50
}

// Function calls add overhead:
trace(SBUF("Hello"), 0);  // Many instructions for the call
HOOK FEE FACTORS

Base fee:
├── Minimum transaction fee
└── Network load multiplier

Execution fee:
├── Instruction count
├── State operations
├── Ledger access
└── Emissions

Total Fee = Base + Execution
```

// After Hook execution, metadata includes:
// - HookInstructionCount: Total instructions
// - HookStateChangeCount: State modifications
// - HookEmittedTxnCount: Emissions

// Check explorer or transaction metadata
```


Inefficient:

// Calculate sum of 20-byte account ID
uint8_t account[20];
int64_t sum = 0;
for (int i = 0; GUARD(20), i < 20; ++i) {
    sum = sum + account[i];  // Could be optimized
}

Optimized:

// Unrolled for small fixed sizes
uint8_t account[20];
int64_t sum = account[0] + account[1] + account[2] + account[3] +
              account[4] + account[5] + account[6] + account[7] +
              account[8] + account[9] + account[10] + account[11] +
              account[12] + account[13] + account[14] + account[15] +
              account[16] + account[17] + account[18] + account[19];
// More code, fewer instructions (no loop overhead)

Inefficient:

// Checks all items even after finding match
int found = 0;
for (int i = 0; GUARD(100), i < 100; ++i) {
    if (items[i] == target) {
        found = 1;
        // Still iterates remaining items
    }
}

Optimized:

// Exit as soon as match found
int found = 0;
for (int i = 0; GUARD(100), i < 100; ++i) {
    if (items[i] == target) {
        found = 1;
        break;  // Stop immediately
    }
}

Inefficient:

// Recalculates same value
for (int i = 0; GUARD(10), i < 10; ++i) {
    int64_t threshold = base_amount * multiplier / 100;  // Same every iteration
    if (values[i] > threshold) {
        count++;
    }
}

Optimized:

// Calculate once
int64_t threshold = base_amount * multiplier / 100;
for (int i = 0; GUARD(10), i < 10; ++i) {
    if (values[i] > threshold) {
        count++;
    }
}

Inefficient:

// Expensive check first
if (complex_validation(data) && simple_flag) {
    // complex_validation always runs
}

Optimized:

// Cheap check first (short-circuit evaluation)
if (simple_flag && complex_validation(data)) {
    // complex_validation only runs if simple_flag is true
}

Inefficient:

// Multiple reads of same state
uint8_t buf1[8];
state(SBUF(buf1), SBUF("counter"));
int64_t count1 = INT64_FROM_BUF(buf1);
// ... other code ...
uint8_t buf2[8];
state(SBUF(buf2), SBUF("counter"));  // Reading same key again!
int64_t count2 = INT64_FROM_BUF(buf2);

Optimized:

// Read once, reuse
uint8_t buf[8];
state(SBUF(buf), SBUF("counter"));
int64_t count = INT64_FROM_BUF(buf);
// Use count throughout

Inefficient:

// Multiple writes for related data
uint8_t c1[8], c2[8], c3[8];
INT64_TO_BUF(c1, count1);
state_set(SBUF(c1), SBUF("count1"));
INT64_TO_BUF(c2, count2);
state_set(SBUF(c2), SBUF("count2"));
INT64_TO_BUF(c3, count3);
state_set(SBUF(c3), SBUF("count3"));

Optimized:

// Single write for related data
uint8_t combined[24];  // 3 × 8 bytes
INT64_TO_BUF(combined, count1);
INT64_TO_BUF(combined + 8, count2);
INT64_TO_BUF(combined + 16, count3);
state_set(SBUF(combined), SBUF("counts"));

Inefficient:

// Long verbose keys
state(SBUF(buf), SBUF("user_transaction_counter"));  // 24 bytes
state(SBUF(buf), SBUF("user_total_amount_received"));  // 27 bytes

Optimized:

// Short keys
state(SBUF(buf), SBUF("utc"));  // 3 bytes
state(SBUF(buf), SBUF("uta"));  // 3 bytes

// Or use numeric keys
uint8_t key1[] = {0x01};
uint8_t key2[] = {0x02};
state(SBUF(buf), key1, 1);
state(SBUF(buf), key2, 1);
```


Inefficient:

// Separate state entries for each field
state_set(SBUF(account), SBUF("recipient"));      // 20 bytes
state_set(SBUF(share), SBUF("share"));            // 1 byte
state_set(SBUF(enabled), SBUF("enabled"));        // 1 byte
// 3 state operations

Optimized:

// Packed into single entry
uint8_t packed[22];  // 20 + 1 + 1
for (int i = 0; GUARD(20), i < 20; ++i)
    packed[i] = account[i];
packed[20] = share;
packed[21] = enabled;
state_set(SBUF(packed), SBUF("config"));
// 1 state operation

Inefficient for variable data:

// Fixed array when most are empty
uint8_t all_recipients[10][20];  // 200 bytes, mostly empty
state_set(SBUF(all_recipients), SBUF("recipients"));

Optimized:

// Store count + only active entries
uint8_t active_count = 3;
uint8_t active_data[61];  // 1 (count) + 3*20 (recipients)
active_data[0] = active_count;
// Copy only active recipients
state_set(active_data, 1 + active_count * 20, SBUF("recipients"));

Inefficient:

// Separate booleans
uint8_t is_active;
uint8_t is_verified;
uint8_t is_premium;
uint8_t allows_iou;
// 4 bytes

Optimized:

// Bitmap
uint8_t flags = 0;
#define FLAG_ACTIVE   0x01
#define FLAG_VERIFIED 0x02
#define FLAG_PREMIUM  0x04
#define FLAG_IOU      0x08

// Set flag
flags |= FLAG_ACTIVE;

// Check flag
if (flags & FLAG_PREMIUM) { }

// 1 byte instead of 4
```


Inefficient:

// Check if field exists, then read
int64_t len = otxn_field(0, 0, sfMemos);
if (len > 0) {
    uint8_t memos[256];
    otxn_field(SBUF(memos), sfMemos);  // Second call
}

Optimized:

// Read directly, check result
uint8_t memos[256];
int64_t len = otxn_field(SBUF(memos), sfMemos);
if (len > 0) {
    // Use memos directly
}

Inefficient:

// New slot for each object
slot_set(SBUF(keylet1), 1);
slot_set(SBUF(keylet2), 2);
slot_set(SBUF(keylet3), 3);
// Using 3 slots

Optimized (when sequential access):

// Reuse same slot
slot_set(SBUF(keylet1), 1);
// Process object 1
slot_set(SBUF(keylet2), 1);  // Overwrites slot 1
// Process object 2
slot_set(SBUF(keylet3), 1);  // Overwrites slot 1
// Process object 3
// Using 1 slot

Development:

trace(SBUF("Starting hook"), 0);
trace(SBUF("Amount:"), amount);
trace(SBUF("Sender byte:"), sender[0]);
trace(SBUF("Type:"), type);
trace(SBUF("Processing..."), 0);
trace(SBUF("Complete"), 0);
// Many trace calls - fine for development

Production:

// Remove or minimize trace calls
// Or use conditional compilation
#ifdef DEBUG
trace(SBUF("Amount:"), amount);
#endif

// Method 1: Simple loop
for (int i = 0; GUARD(20), i < 20; ++i) {
    if (a[i] != b[i]) return 0;
}
// ~100+ instructions

// Method 2: Unrolled comparison
if (a[0] != b[0]) return 0;
if (a[1] != b[1]) return 0;
// ... all 20
// ~60 instructions (no loop overhead)
```

// After transaction, check metadata:
const meta = tx.result.meta;
const hookExec = meta.HookExecutions[0];

console.log("Instructions:", hookExec.HookInstructionCount);
console.log("State changes:", hookExec.HookStateChangeCount);
console.log("Emissions:", hookExec.HookEmittedTxnCount);
```

// Test different implementations:
// 1. Deploy version A, send test transaction
// 2. Record instruction count
// 3. Deploy version B, send same transaction
// 4. Compare instruction counts

// Note: Small differences don't matter
// Focus on order-of-magnitude improvements
```


// Readable but slower:
int is_whitelisted = check_whitelist(sender);
int is_valid_amount = amount >= MIN_AMOUNT && amount <= MAX_AMOUNT;
int is_correct_type = txn_type == TT_PAYMENT;

if (is_whitelisted && is_valid_amount && is_correct_type) {
accept(SBUF("OK"), 0);
}

// Faster but less readable:
if (check_whitelist(sender) &&
amount >= MIN_AMOUNT && amount <= MAX_AMOUNT &&
txn_type == TT_PAYMENT) {
accept(SBUF("OK"), 0);
}

// Decision: For critical paths, optimize.
// For normal code, prefer readability.
```

// Smaller code (loop):
for (int i = 0; GUARD(20), i < 20; ++i) buf[i] = 0;

// Faster code (unrolled):
buf[0]=0; buf[1]=0; buf[2]=0; buf[3]=0; buf[4]=0;
buf[5]=0; buf[6]=0; buf[7]=0; buf[8]=0; buf[9]=0;
buf[10]=0; buf[11]=0; buf[12]=0; buf[13]=0; buf[14]=0;
buf[15]=0; buf[16]=0; buf[17]=0; buf[18]=0; buf[19]=0;

// Larger binary, but fewer instructions at runtime
```

// Compute every time:
int64_t factorial = 1;
for (int i = 1; GUARD(10), i <= n; ++i) {
    factorial *= i;
}

// Store precomputed:
int64_t factorials[] = {1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880};
int64_t result = (n < 10) ? factorials[n] : 0;

// Trade-off: State costs reserves, computation costs fees
```


Instruction count directly affects fees. Fewer instructions = lower fees.

State operations are expensive. Batching reduces cost.

Early exit optimizations work. Breaking from loops saves instructions.

⚠️ Exact fee calculations. Network conditions affect actual fees.

⚠️ Optimal optimization level. Diminishing returns on micro-optimizations.

🔴 Premature optimization. Optimizing before code works correctly.

🔴 Sacrificing security for speed. Removing validation for performance.

🔴 Unreadable "optimized" code. Future bugs from unclear code.

Most Hooks don't need aggressive optimization. When they do, focus on: minimizing state operations, early exits from loops, caching computed values, and batching related data. Measure before and after to verify improvements. Never sacrifice correctness or security for performance.


Assignment: Optimize a Hook and measure the improvement.

Requirements:

  • Take the Payment Splitter from Lesson 14

  • Deploy and record instruction count

  • Document baseline metrics

  • Batch state operations

  • Optimize loops

  • Cache computed values

  • Improve key design

  • Remove unnecessary operations

  • Deploy optimized version

  • Record new instruction count

  • Calculate percentage improvement

  • List each optimization applied

  • Explain trade-offs considered

  • Show before/after code snippets

  • Recommend which optimizations are worth keeping

  • Valid optimizations applied (40%)

  • Measurable improvement achieved (25%)

  • Documentation quality (25%)

  • Code still correct (10%)

Time investment: 3-4 hours
Value: Practical optimization skills


Knowledge Check

Question 1 of 4

What's the correct optimization priority order?

  • XRPL fee documentation
  • WASM optimization techniques

For Next Lesson:
We'll explore XLS-101 Smart Contracts—the proposed next-generation programmability for XRPL.


End of Lesson 16

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

Key Takeaways

1

Correctness first.

Only optimize working, secure code.

2

Minimize state operations.

Read once, batch writes.

3

Exit early.

Break from loops as soon as possible.

4

Cache computed values.

Don't recalculate in loops.

5

Measure impact.

Check instruction counts to verify improvements. ---