Lesson 9: Guards and Loops - Preventing Infinite Execution
Learning Objectives
Explain why guards exist and their role in blockchain consensus
Write correct guard syntax for various loop structures
Calculate guard values for nested loops
Debug guard violations and fix common mistakes
Design bounded algorithms that work within guard constraints
Remember from Lesson 1: it's mathematically proven impossible to determine if arbitrary code will terminate. For Hooks, this creates a problem—validators must agree on execution results, but what if a Hook runs forever?
Ethereum's solution: Gas limits. Pay per instruction; run out of gas = stop.
Hooks' solution: Guards. Declare maximum iterations at compile time; exceed = automatic rollback.
Guards are more restrictive but simpler: you tell the ledger upfront exactly how many times each loop can run.
Guards are compile-time promises about maximum loop iterations:
// Without guard - WILL NOT COMPILE
for (int i = 0; i < 10; ++i) {
// Loop body
}
// With guard - CORRECT
for (int i = 0; GUARD(10), i < 10; ++i) {
// Loop body
}
- "This loop will execute at most 10 times"
- Compiler/ledger trusts this declaration
- If guard is hit more than 10 times → rollback
// GUARD macro definition (simplified)
#define GUARD(maxiter) _g(__LINE__, (maxiter)+1)
// Expands to guard function call with:
// - LINE: unique ID (source line number)
// - maxiter+1: actual maximum (+1 because guard fires before condition check)
```
Why maxiter+1?
The guard is called BEFORE the loop condition is checked. So for a loop that should run 10 times, the guard fires 11 times (10 iterations + 1 final check that exits).
Guards must appear BEFORE the loop condition in the comma expression:
// CORRECT: Guard before condition
for (int i = 0; GUARD(10), i < 10; ++i)
// WRONG: Guard after condition
for (int i = 0; i < 10, GUARD(10); ++i) // TOO LATE!
// CORRECT: While loop with guard
int count = 0;
while (GUARD(100), count < 100 && condition) {
count++;
}
// CORRECT: Do-while (guard in condition)
int x = 0;
do {
x++;
} while (GUARD(50), x < 50);
The most common pattern:
// Standard for loop
for (int i = 0; GUARD(10), i < 10; ++i) {
// Runs exactly 10 times
}
// Variable upper bound (but fixed guard)
int n = get_value(); // Returns 0-10
for (int i = 0; GUARD(10), i < n; ++i) {
// Runs at most 10 times (may be fewer)
}
// Counting down
for (int i = 10; GUARD(11), i > 0; --i) {
// Guard needs to be 11 (10 iterations + final check)
}
// Non-zero start
for (int i = 5; GUARD(6), i < 10; ++i) {
// 5 iterations (5,6,7,8,9)
// Guard = 6 (5 iterations + final check)
}
// Basic while
int x = 0;
while (GUARD(100), x < 100) {
x++;
}
// With external condition
int x = 0;
while (GUARD(50), some_condition() && x < 50) {
x++;
// Guard ensures max 50 iterations even if condition stays true
}
// Infinite loop protection
while (GUARD(1000), 1) {
// This WILL rollback after 1000 iterations
// Used for "run until done" patterns with safety net
if (done) break;
}
```
// Do-while (executes at least once)
int x = 0;
do {
x++;
} while (GUARD(10), x < 10);
// Note: Guard protects the WHILE checkGuards work with break and continue:
for (int i = 0; GUARD(100), i < 100; ++i) {
if (found) {
break; // OK - exits early, guard not exceeded
}
if (skip) {
continue; // OK - continues to next iteration
}
}For nested loops, multiply the guard values:
// Outer loop: 10 iterations
// Inner loop: 10 iterations per outer iteration
// Total inner iterations: 10 × 10 = 100
for (int i = 0; GUARD(10), i < 10; ++i) {
for (int j = 0; GUARD(100), j < 10; ++j) {
// Inner guard is 100 because it could run 100 total times
}
}
Wait, why isn't inner guard 10?
- 10 times when i=0
- 10 times when i=1
- ...
- 10 times when i=9
- Total: 100 times
Formula: Inner guard = Outer iterations × Inner iterations per pass
// Example 1: 5 × 10 = 50 total inner iterations
for (int i = 0; GUARD(5), i < 5; ++i) {
for (int j = 0; GUARD(50), j < 10; ++j) {
// ...
}
}
// Example 2: 3 × 4 × 5 = 60 total innermost iterations
for (int a = 0; GUARD(3), a < 3; ++a) {
for (int b = 0; GUARD(12), b < 4; ++b) { // 3 × 4 = 12
for (int c = 0; GUARD(60), c < 5; ++c) { // 3 × 4 × 5 = 60
// ...
}
}
}
```
Buffer iteration:
// Process 20-byte account ID
uint8_t account[20];
for (int i = 0; GUARD(20), i < 20; ++i) {
account[i] = 0; // Clear buffer
}
Comparing two buffers:
// Compare two 20-byte account IDs
uint8_t a[20], b[20];
int equal = 1;
for (int i = 0; GUARD(20), i < 20; ++i) {
if (a[i] != b[i]) {
equal = 0;
break;
}
}
Searching an array:
// Search list of up to 10 accounts
uint8_t list[10][20]; // 10 accounts, 20 bytes each
uint8_t target[20];
int found = -1;
for (int i = 0; GUARD(10), i < 10; ++i) {
int match = 1;
for (int j = 0; GUARD(200), j < 20; ++j) { // 10 × 20 = 200
if (list[i][j] != target[j]) {
match = 0;
break;
}
}
if (match) {
found = i;
break;
}
}
```
Guards must use literal numbers, not variables:
// WRONG: Variable in guard
int n = 10;
for (int i = 0; GUARD(n), i < n; ++i) // COMPILE ERROR!
// CORRECT: Literal in guard
for (int i = 0; GUARD(10), i < n; ++i) // OK if n <= 10
Why? Guards are evaluated at compile time to calculate worst-case execution. Variables aren't known at compile time.
Guard value must be >= actual iterations:
// If n is always <= 10, this is safe:
for (int i = 0; GUARD(10), i < n; ++i)
// If n could be 11+, this causes rollback when n > 10
// Solution: Either GUARD(higher) or clamp n
int n = get_value();
if (n > 10) n = 10; // Clamp to max
for (int i = 0; GUARD(10), i < n; ++i) {
// Safe now
}
What happens when guard is exceeded:
// This will ALWAYS fail with guard violation
for (int i = 0; GUARD(5), i < 10; ++i) {
// On iteration 6, guard fails
// Hook rollbacks with GUARD_VIOLATION error
}- Check trace output for which guard failed
- Line number identifies the loop
- Either increase guard or fix loop logic
Design with explicit maximum sizes:
// Constants for maximum sizes
#define MAX_ACCOUNTS 10
#define ACCOUNT_SIZE 20
#define MAX_MEMO_SIZE 256
// Use constants in guards
for (int i = 0; GUARD(MAX_ACCOUNTS), i < list_size; ++i) {
// Safe as long as list_size <= MAX_ACCOUNTS
}
Use break to exit early:
// Search with early exit
int found = 0;
for (int i = 0; GUARD(100), i < 100; ++i) {
if (matches(item[i])) {
found = 1;
break; // Exit immediately, don't waste iterations
}
}For operations that might exceed limits, chunk them:
// Instead of processing 1000 items in one Hook:
// Process 100 per transaction, use state to track position
uint8_t pos_buf[8];
int64_t len = state(SBUF(pos_buf), SBUF("position"));
int64_t pos = (len > 0) ? INT64_FROM_BUF(pos_buf) : 0;
// Process next chunk
int64_t end = pos + 100;
if (end > 1000) end = 1000;
for (int64_t i = pos; GUARD(100), i < end; ++i) {
process(i);
}
// Save position for next transaction
INT64_TO_BUF(pos_buf, end);
state_set(SBUF(pos_buf), SBUF("position"));
Some algorithms don't fit guards well—use alternatives:
// Binary search (variable iterations) - tricky with guards
// Linear search (fixed max iterations) - easier with guards
// If you need binary search, guard for log2(max_size):
// For 1024 elements, max 10 iterations
for (int i = 0; GUARD(10), low < high; ++i) {
// Binary search logic
}
// Check if sender is in whitelist of 5 accounts
uint8_t whitelist[5][20] = {
// ... 5 account IDs ...
};
uint8_t sender[20];
otxn_field(SBUF(sender), sfAccount);
int whitelisted = 0;
for (int i = 0; GUARD(5), i < 5; ++i) {
int match = 1;
for (int j = 0; GUARD(100), j < 20; ++j) { // 5 × 20 = 100
if (whitelist[i][j] != sender[j]) {
match = 0;
break;
}
}
if (match) {
whitelisted = 1;
break;
}
}
if (!whitelisted) {
rollback(SBUF("Not whitelisted"), 1);
}
```
// Count non-zero bytes in a buffer
uint8_t data[32];
// ... fill data ...
int count = 0;
for (int i = 0; GUARD(32), i < 32; ++i) {
if (data[i] != 0) {
count++;
}
}
trace(SBUF("Non-zero bytes:"), count);
```
// Sum array of up to 10 int64_t values
int64_t values[10];
int64_t num_values = 7; // Example: 7 values
int64_t sum = 0;
for (int i = 0; GUARD(10), i < num_values; ++i) {
sum += values[i];
// Check for overflow
if (sum < 0 && values[i] > 0) {
rollback(SBUF("Overflow"), 1);
}
}
```
✅ Guards enable deterministic execution bounds. Every validator knows maximum execution time before running the Hook.
✅ The syntax is learnable. Once you understand the pattern, guards become automatic.
✅ Guards prevent infinite loops. A misbehaving Hook cannot hang the network.
⚠️ Optimal guard values for complex algorithms. Some patterns require experimentation.
⚠️ Future relaxation of constraints. Guard requirements might evolve.
🔴 Underestimating guard values. Causes runtime failures that are hard to debug.
🔴 Forgetting nested loop multiplication. Inner guards must account for all outer iterations.
🔴 Putting guard after condition. Guard must fire BEFORE condition check.
Guards are the price of bounded execution. They add friction to development—you must think about maximum iterations for every loop. But they provide critical guarantees: your Hook will terminate, validators can agree, and the network stays healthy. Learn the patterns, use constants, and calculate nested guards carefully.
Assignment: Create a Hook that demonstrates correct guard usage in various scenarios.
Requirements:
Implement a "Transaction Analyzer" Hook that:
Extract sender account ID (20 bytes)
Compare with a hardcoded "admin" account
Use correctly guarded loop for comparison
Store a whitelist of 3 accounts in arrays
Check if sender is in whitelist
Use correctly calculated guards for nested comparison
Parse up to 10 "tags" from transaction
Stop when empty tag found (early exit)
Guard must handle worst case (10 tags)
Implement a "count matching" function
Count how many whitelist accounts have first byte > 128
Show correct guard calculation
Code Structure:
#include "hookapi.h"
#define MAX_WHITELIST 3
#define ACCOUNT_SIZE 20
// Hardcoded whitelist
uint8_t whitelist[MAX_WHITELIST][ACCOUNT_SIZE] = {
// ... account IDs ...
};
int64_t hook(uint32_t reserved) {
_g(1, 1);
// Part 1: Admin check with simple loop
// Part 2: Whitelist check with nested loops
// Part 3: Tag parsing with conditional loop
// Part 4: Counting with guard
accept(SBUF("Analysis complete"), 0);
return 0;
}
int64_t cbak(uint32_t reserved) {
return 0;
}
- Correct guard placement (25%)
- Correct guard value calculation (30%)
- Nested loop guards correct (25%)
- Code compiles and runs (20%)
Time investment: 2-3 hours
Value: Guard mastery is essential for all future Hook development
Knowledge Check
Question 1 of 4Which is the correct guard placement for a for loop?
- xrpl-hooks.readme.io/docs/loops-and-guarding
- Hooks Builder guard checker output
- "Introduction to Algorithms" by Cormen et al. (for bounded algorithm design)
For Next Lesson:
With guards mastered, we'll dive into transaction inspection—reading every detail of the triggering transaction to make intelligent decisions.
End of Lesson 9
Total words: ~4,300
Estimated completion time: 60 minutes reading + 2-3 hours for deliverable
Key Takeaways
Guards declare maximum iterations.
GUARD(n) means "at most n times through this guard."
Guards go BEFORE conditions.
`GUARD(n), condition` not `condition, GUARD(n)`.
Nested loops multiply.
Inner guard = outer iterations × inner iterations per pass.
Literals only.
Guard values must be compile-time constants, not variables.
Design within bounds.
Use constants, early exits, and chunking for larger operations. ---