Even with careful coding, issues will arise during RPC handler development. This guide provides systematic approaches to diagnosing and fixing problems, from compilation errors to runtime issues to performance bottlenecks.
The key is having the right tools and techniques to isolate the problem quickly.
Common Compilation Errors
Error 1: Undeclared Identifiers
Error Message:
error: 'jss::my_field' was not declared in this scope
Root Causes:
Typo in JSON string constant name
Using string literal instead of jss:: constant
Missing include file
Solution:
Prevention:
Always use jss:: constants from xrpl/protocol/jss.h
Check spelling carefully
Refer to existing handlers for correct constant names
Error 2: Type Mismatch in RPC Functions
Error Message:
Root Causes:
Wrong parameter types
Missing required parameters
Template specialization issues
Solution:
Error 3: Const Correctness Issues
Error Message:
Root Causes:
Discarding const qualifier
Passing const reference where non-const expected
Missing const in function signature
Solution:
Error 4: Missing Include Files
Error Message:
Solution:
Checklist:
RPCHelpers.h for helper functions
Context.h for RPC::JsonContext
ErrorCodes.h for error constants
jss.h for JSON string constants
base_uint.h for uint256 and similar
AccountID.h if using AccountID directly
Runtime Debugging Techniques
Technique 1: Strategic Logging
Adding Debug Output:
Journal Levels:
fatal() - Application stopping
error() - Significant problems
warning() - Unexpected but recoverable
info() - General informational
debug() - Detailed diagnostic (hidden in release builds)
trace() - Very detailed (rarely used)
Viewing Output:
Technique 2: Using the Debugger (GDB)
Building for Debugging:
Launching Debugger:
GDB Commands:
Common GDB Debugging Pattern:
Technique 3: Using LLDB (macOS)
Similar to GDB but with slightly different syntax:
LLDB Commands:
Technique 4: Analyzing Crashes
When handler crashes (segmentation fault):
Common Crash Scenarios:
Log Level Configuration
Setting Log Levels
In Config File (rippled.cfg):
Command Line:
Common Modules to Debug:
rpc - RPC handler execution
ledger - Ledger operations
transaction - Transaction processing
app - Application lifecycle
protocol - Protocol messages
peer - Network peer communication
Interpreting Log Output
Typical Log Format:
Log Levels Mean:
[FATAL] - Rippled about to stop
[ERROR] - Handler failed, returned error
[WARN] - Unusual condition, but handler succeeded
[INFO] - Normal informational messages
[DEBUG] - Detailed execution trace (very verbose)
[TRACE] - Extremely detailed (rarely used)
Testing Strategies
Strategy 1: Unit Testing Handlers
Test File Structure (src/test/rpc/handlers/MyHandler_test.cpp):
// WRONG - String literal
result["my_field"] = value;
// CORRECT - Use jss:: constant
result[jss::my_field] = value;
// If jss::my_field doesn't exist, add to jss.h or define:
namespace ripple { namespace jss {
constexpr auto my_field = "myField";
}} // ripple::jss
error: no matching function for call to 'rpcError'
// WRONG - Wrong return type
result[jss::error] = rpcError(rpcINVALID_PARAMS); // This is Json::Value, not bool
// CORRECT - Return directly
return rpcError(rpcINVALID_PARAMS);
// WRONG - Missing error code
return rpcError("Missing account");
// CORRECT - Include error code
return rpcError(rpcINVALID_PARAMS, "Missing account");
error: invalid conversion from 'const ripple::AccountID*' to 'ripple::AccountID*'
// WRONG - Losing const
auto const account = *accountOpt;
// account is const AccountID, can't be modified
auto nonConst = const_cast<AccountID*>(&account); // DON'T DO THIS
// CORRECT - Keep const where needed
auto const account = *accountOpt;
auto const sle = ledger->read(keylet::account(account));
// WRONG - Function signature drops const
void processAccount(AccountID& account);
// CORRECT - Mark as const
void processAccount(AccountID const& account);
error: 'parseBase58' was not declared in this scope
error: 'RPC::lookupLedger' was not declared in this scope
// Add required includes at top of handler file:
#include <xrpld/app/main/Application.h>
#include <xrpld/rpc/Context.h>
#include <xrpld/rpc/detail/RPCHelpers.h>
#include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/jss.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/basics/Blob.h>
#include <xrpld/core/XRPLedger.h> // For JLOG
Json::Value doMyHandler(RPC::JsonContext& context)
{
JLOG(context.app.journal("RPC").debug())
<< "doMyHandler called with params: "
<< context.params.toStyledString();
if (!context.params.isMember(jss::account)) {
JLOG(context.app.journal("RPC").warning())
<< "Missing account parameter";
return rpcError(rpcINVALID_PARAMS);
}
auto const account = parseBase58<AccountID>(
context.params[jss::account].asString());
if (!account) {
JLOG(context.app.journal("RPC").warning())
<< "Failed to parse account: "
<< context.params[jss::account].asString();
return rpcError(rpcACT_MALFORMED);
}
JLOG(context.app.journal("RPC").debug())
<< "Successfully parsed account: " << to_string(*account);
return buildResponse(*account, ledger);
}
# Build with debug logging enabled
./rippled -a --log-level rpc=debug
# Output typically goes to console or rippled.log
tail -f rippled.log | grep "RPC"
cd rippled
git submodule update --init --recursive
mkdir -p build
cd build
# Build with debug symbols
cmake -DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_CXX_COMPILER=clang++ \
..
make rippled
# Start rippled under GDB with minimal ledger
cd build
gdb --args ./rippled --standalone
# Or attach to running process
gdb attach <process_id>
# Set breakpoint in handler
(gdb) break src/xrpld/rpc/handlers/MyHandler.cpp:45
# Set conditional breakpoint
(gdb) break MyHandler.cpp:45 if account_id == 0x1234
# Run until breakpoint
(gdb) run
# Step into function
(gdb) step
# Step over function
(gdb) next
# Continue execution
(gdb) continue
# Print variable
(gdb) print account
(gdb) print *account
(gdb) print account->field
# Print with pretty formatting
(gdb) set print pretty on
# Examine memory
(gdb) x/32bx account # 32 bytes in hex format
# Get stack trace
(gdb) backtrace
# 1. Set breakpoint at handler entry
(gdb) break doMyHandler
# 2. Run and hit breakpoint
(gdb) run
Breakpoint 1, doMyHandler (context=0x7fff...) at MyHandler.cpp:25
# 3. Print context details
(gdb) print context.params
(gdb) print context.role
# 4. Step through code
(gdb) next
(gdb) print account
# 5. If crash, examine state
(gdb) backtrace
(gdb) frame 0
(gdb) print *ledger
# Build for debug (same as GDB)
cmake -DCMAKE_BUILD_TYPE=Debug ...
# Start LLDB
lldb -- ./rippled --standalone
# Set breakpoint
(lldb) breakpoint set --file MyHandler.cpp --line 45
# More concise
(lldb) br set -f MyHandler.cpp -l 45
# Run
(lldb) run
# Step/next/continue (same as GDB)
(lldb) step
(lldb) next
(lldb) continue
# Print variable
(lldb) frame variable account
(lldb) p context.params
# Backtrace
(lldb) bt
# 1. Get core dump
ulimit -c unlimited
# 2. Run rippled
./rippled --standalone
# 3. When it crashes, analyze core dump
lldb ./rippled -c /cores/core.12345
# 4. In debugger
(lldb) bt # See where crash happened
(lldb) frame variable # Print local variables
(lldb) p pointer_variable # Check for null pointers
// CRASH 1: Null pointer dereference
auto const sle = ledger->read(keylet);
if (!sle) {
auto value = sle->getFieldU32(sfBalance); // CRASH! sle is null
}
// FIX: Check before use
auto const sle = ledger->read(keylet);
if (!sle) {
return rpcError(rpcACT_NOT_FOUND);
}
auto value = sle->getFieldU32(sfBalance); // Safe
// CRASH 2: Dereferencing invalid optional
auto const accountOpt = parseBase58<AccountID>(str);
auto const sle = ledger->read(keylet::account(*accountOpt)); // CRASH if empty
// FIX: Validate before deref
auto const accountOpt = parseBase58<AccountID>(str);
if (!accountOpt) {
return rpcError(rpcACT_MALFORMED);
}
auto const sle = ledger->read(keylet::account(*accountOpt)); // Safe
[rpc]
port = 5005
ip = 127.0.0.1
[logging]
# General log level
log_level = info
# Specific module levels
rpc = debug
ledger = warning
transaction = info
./rippled --log-level rpc=debug --log-level ledger=trace
./rippled -a --log-level rpc=debug # -a for standalone mode
cd build
cmake -DBUILD_TESTS=ON ..
make
# Run all tests
./rippled-tests
# Run specific test suite
./rippled-tests --gtest_filter="MyHandlerTest.*"
# Run with verbose output
./rippled-tests --gtest_filter="MyHandlerTest.*" -v
# From logs, which step takes longest?
tail -f rippled.log | grep MyHandler
// Are you creating large intermediate objects?
std::vector<SLE> allAccounts; // AVOID for large sets
// Better: Stream or use iterators
for (auto const& account : accounts) {
// Process one at a time
}
// Are you keeping references alive?
auto const& ref = expense_object; // Keeps reference
// ... lots of code ...
// Reference still alive, can't be freed