Compiling Hooks
How to build a Hook, and what a built Hook looks like.
Constraints
All Hooks are compiled to a single webassembly module before they can be set onto an XRPL account.
A Hook always implements and exports exactly one or both of the following functions:
int64_t hook(uint32_t ctx) { ... }
required
- Executed whenever a transaction comes into or leaves from the account the Hook is set on (
ctx = 0
) or - Executed when executed as a Weak Transactional Stakeholder (
ctx > 0
).
int64_t cbak(uint32_t ctx) { ... }
optional
- Executed when an emitted transaction is successfully accepted into a ledger (
ctx = 0
) or - Executed when an emitted transaction cannot be accepted into any ledger (
ctx = 1
).
Hooks are not allowed to specify other functions. Instead they must make clever use of macros to do all their computation within these two functions. This is part of a computational restriction on hooks to keep their runtime predictable.
Additionally Hooks are afforded no heap memory. All required memory must be reserved and used on the stack.
Example
Here is an example Hook written in C. The Hook prints 0...3 to the trace log before accepting the originating transaction.
#include <stdint.h>
#define GUARD(maxiter) _g(__LINE__, (maxiter)+1)
extern int32_t _g (uint32_t id, uint32_t maxiter);
extern int64_t accept (uint32_t read_ptr, uint32_t read_len, int64_t error_code);
extern int64_t trace_num (uint32_t read_ptr, uint32_t read_len, int64_t number);
int64_t hook(uint32_t ctx)
{
for (int i = 0; GUARD(3), i < 3; ++i)
{
trace_num("test", 4, i);
}
accept (0,0,0);
return 0;
}
Hint
For educational purposes the above example deliberately does not include
hookapi.h
(which developers would typically use.)
Compilation
A variety of compilers will generate valid webassembly from a C source file. Once compiled, a Hook exists as a binary .wasm
file. This contains a webassembly module. Using wasmcc
to compile and the wasm2wat
tool to convert to human readable webassembly (both tools provided in the development docker) this binary form can be rendered to the human readable form. Below appears the compilation result of the above example.
(module
(type (;0;) (func (param i32 i32) (result i32)))
(type (;1;) (func (param i32 i32 i64) (result i64)))
(type (;2;) (func))
(type (;3;) (func (param i32) (result i64)))
(import "env" "_g" (func $_g (type 0)))
(import "env" "trace_num" (func $trace_num (type 1)))
(import "env" "accept" (func $accept (type 1)))
(func $__wasm_call_ctors (type 2))
(func $cbak (type 3) (param i32) (result i64)
(local i32 i32 i32 i64)
global.get 0
local.set 1
i32.const 16
local.set 2
local.get 1
local.get 2
i32.sub
local.set 3
i64.const 0
local.set 4
local.get 3
local.get 0
i64.store offset=8
local.get 4
return)
(func $hook (type 3) (param i32) (result i64)
(local i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i64 i32 i32 i32 i64 i32 i32 i32)
global.get 0
local.set 1
i32.const 16
local.set 2
local.get 1
local.get 2
i32.sub
local.set 3
local.get 3
global.set 0
i32.const 0
local.set 4
local.get 3
local.get 0
i64.store offset=8
local.get 3
local.get 4
i32.store offset=4
block ;; label = @1
loop ;; label = @2
i32.const 3
local.set 5
i32.const 14
local.set 6
i32.const 4
local.set 7
local.get 6
local.get 7
call $_g
drop
local.get 3
i32.load offset=4
local.set 8
local.get 8
local.set 9
local.get 5
local.set 10
local.get 9
local.get 10
i32.lt_s
local.set 11
i32.const 1
local.set 12
local.get 11
local.get 12
i32.and
local.set 13
local.get 13
i32.eqz
br_if 1 (;@1;)
i32.const 1024
local.set 14
i32.const 4
local.set 15
local.get 3
i32.load offset=4
local.set 16
local.get 16
local.set 17
local.get 17
i64.extend_i32_s
local.set 18
local.get 14
local.get 15
local.get 18
call $trace_num
drop
local.get 3
i32.load offset=4
local.set 19
i32.const 1
local.set 20
local.get 19
local.get 20
i32.add
local.set 21
local.get 3
local.get 21
i32.store offset=4
br 0 (;@2;)
end
end
i64.const 0
local.set 22
i32.const 0
local.set 23
local.get 23
local.get 23
local.get 22
call $accept
drop
i32.const 16
local.set 24
local.get 3
local.get 24
i32.add
local.set 25
local.get 25
global.set 0
local.get 22
return)
(table (;0;) 1 1 funcref)
(memory (;0;) 2)
(global (;0;) (mut i32) (i32.const 66576))
(global (;1;) i32 (i32.const 1029))
(global (;2;) i32 (i32.const 1024))
(global (;3;) i32 (i32.const 66576))
(global (;4;) i32 (i32.const 1024))
(export "memory" (memory 0))
(export "__wasm_call_ctors" (func $__wasm_call_ctors))
(export "__data_end" (global 1))
(export "__global_base" (global 2))
(export "__heap_base" (global 3))
(export "__dso_handle" (global 4))
(export "cbak" (func $cbak))
(export "hook" (func $hook))
(data (;0;) (i32.const 1024) "test\00"))
The average Hook developer will never need to examine webassembly directly. However it is a useful conceptual exercise to review the contents of the sample Hook.
Above we can see:
- Three functions are imported from the Hooks API (
_g
,accept
,trace_num
) - Two functions are defined by the hook (
cbak
,hook
) - Two functions are exported by the hook (again:
cbak
,hook
) - Some static (constant) data is recorded in the hook (see
data
at the bottom).
It is very important to note that a Hook must only import functions available to it from the Hooks API and must only export the cbak
and hook
functions. In additional all hooks must import _g
from the Hooks API, which is the guard
function.
Hint
Webassembly is a platform-independent general computation
bytecode
language. It has a one-to-one mapping with a human readable equivalent. These are used interchangeably.
Unwanted Exports
Most webassembly compilers (including the one above) produce additional exports for their own linking purposes. In many cases the generation of these is difficult or impossible to disable.
Unwanted exports will lead to an otherwise valid Hook being rejected. Therefore after compilation developers should use the Hook Cleaner Utility to strip out these out. Failure to do so will lead to your Hook being rejected.
Important
Don't forget to use the Hook Cleaner Utility or your Hooks will be rejected.
Updated over 2 years ago