Skip to content

Overview: The Xahau Hook Model ​

What Hooks are ​

A Hook is a WebAssembly program installed on a Xahau account. When a transaction affects that account, the Hook runs on-ledger as part of applying the transaction. During its run a Hook can:

  • read the originating transaction that triggered it (the otxn_* family),
  • read and write persistent state scoped to the account and a namespace (the state* family),
  • read ledger objects (accounts, trustlines, offers, escrows, and more) through slots and keylets,
  • emit brand-new transactions to be applied later (the etxn_* / emit family),
  • do XFL fixed-precision floating-point math (the float_* family),
  • and finally accept the transaction (allowing it to apply) or rollback it (rejecting it).

The Hook API is the set of host functions the WASM module imports from the env module to do all of the above. Every such function is declared in hook/extern.h and registered with the WasmEdge runtime.

Entry points ​

A Hook WASM module exports at most two functions, both verified in the execution engine, which looks up the exports named "hook" and "cbak":

c
// Main entry point. Called when a transaction touches the hook account.
// reserved: 0 = strong execution, 1 = weak (collect-call/TSH) execution,
// 2 = again-as-weak (post-apply re-run requested via hook_again()).
int64_t hook(uint32_t reserved);

// Callback entry point. Called to report the result of a transaction this
// hook previously emitted. Optional — only needed if the hook emits.
// reserved: 0 = the emitted transaction was accepted, 1 = emit failure.
int64_t cbak(uint32_t reserved);

Both must return int64_t. A Hook must export memory and the hook function; cbak is only required if the hook uses callbacks. (The HookSet validator emits diagnostic log codes such as EXPORT_HOOK_FUNC and EXPORT_CBAK_FUNC when the exported signatures are wrong.)

Neither hook nor cbak "returns" a result to the ledger in the usual sense — a Hook terminates by calling accept() or rollback(), which never return.

Execution modes ​

The execution engine distinguishes three modes:

  • Strong — the hook runs before the originating transaction is applied. A strong hook can rollback() to reject the transaction. (Internally tracked via isStrong; the emission flag is hefSTRONG = 0x1.)
  • Weak — a strong pre-apply hook may call hook_again() to request an additional post-apply re-execution: a strong pre-apply hook can nominate additional weak post-apply execution. A weak execution observes the already-applied result and cannot roll it back.
  • Callback — when a transaction the hook emitted resolves, the engine calls cbak (emission flag hefCALLBACK = 0x2). If the emitted transaction failed, the callback is told so.

Which accounts a hook fires for is governed by the TSH (Transactional Stake Holder) mechanism and its flags (tshROLLBACK, tshCOLLECT, tshMIXED). See Glossary for TSH.

Execution environment constraints ​

These are the enforced execution environment limits:

ConstraintValue
Max WASM size65,535 bytes (0xFFFF)
Max hooks per account (chain length)10
Max namespaces per account256
Max entries removed per namespace-delete256
Max parameter key size32 bytes
Max parameter value size256 bytes
Max hook state scale16
Max hook state value size256 × scale (256 min, up to 4096)
Max slots255
Max nonces255
Max emitted transactions255
Max hook parameters16
Max state modifications (per hook)256

There is a single 256-entry limit, enforced at two points: the state-write API returns TOO_MANY_STATE_MODIFICATIONS (-44) once a hook execution's own modified-entry count reaches 256, and the finalize step returns tecHOOK_REJECTED if the accumulated changes across the combined hook chains exceed the same 256.

Loops must be guarded. Every loop in a hook must call _g(guard_id, maxiter) at its top. The HookSet validator, with log codes GUARD_IMPORT and GUARD_MISSING, rejects hooks whose loops are not properly guarded. At runtime, exceeding a guard's iteration count returns GUARD_VIOLATION (-16). The guard rules are versioned by amendments: GuardRuleFix20250131 and GuardRuleDepth32, gated by fix20250131 and fixGuardDepth32. See Compiling Hooks for the full validator behavior.

Data available inside a hook ​

  • The originating transaction — read fields with otxn_field, its type with otxn_type, its hash with otxn_id, and any carried HookParameter with otxn_param. See Transaction APIs.
  • Ledger objects — compute a keylet (util_keylet, or one of the KEYLET_* constants) and load the object into a slot with slot_set, then drill into subfields with slot_subfield/slot_subarray. See Slot APIs.
  • Hook state — key/value storage under the hook account and a namespace via state/state_set (and the foreign variants). See State APIs.
  • Parameters — install-time parameters via hook_param; parameters attached to the originating transaction via otxn_param.
  • Ledger info — sequence (ledger_seq), last-close time (ledger_last_time), last hash (ledger_last_hash), base fee (fee_base).

Typical processing flow ​

The common shape of a hook is:

  1. Read fields off the originating transaction and validate them.
  2. Read (and possibly write) hook state.
  3. Optionally reserve and emit transactions.
  4. Terminate with accept() (apply) or rollback() (reject).

A minimal annotated hook ​

This is the smallest meaningful hook: it inspects nothing and simply accepts.

c
#include "hookapi.h"

// Main entry point. `reserved` is 0 here because this hook only runs strong.
int64_t hook(uint32_t reserved)
{
    // Read the account this hook is installed on into a 20-byte buffer.
    uint8_t acc[20];
    hook_account(SBUF(acc));           // SBUF(x) expands to (x, sizeof(x))

    // Emit a debug trace of the account id (as hex) to the node's log.
    TRACEHEX(acc);

    // Terminate: apply the originating transaction. The second argument is a
    // return/exit code recorded in the transaction metadata.
    accept(SBUF("done"), 0);           // never returns

    // Unreachable, but the C compiler wants a return.
    return 0;
}

SBUF, TRACEHEX, and the other helpers are documented in Helper Macros.

Core concepts ​

  • accept() vs rollback() — both terminate the hook immediately and do not return. accept() lets the originating transaction proceed; rollback() rejects it and reverts any state changes the hook made. Internally these map to the ExitType::ACCEPT and ExitType::ROLLBACK values, and to the return sentinels RC_ACCEPT (-20) / RC_ROLLBACK (-19).
  • _g guard — the required loop guard; see the environment constraints above and Control APIs.
  • trace* debugging — trace, trace_num, and trace_float write to the node's debug log (subject to build/log configuration). See Trace APIs.
  • Error convention — API functions return int64_t. A value >= 0 is success (often a length or a computed value); a negative value is an error code from the table below. accept/rollback are the exceptions: they never return.

Error codes ​

The error codes a Hook API function can return are the following named values (hook_api::hook_return_code), also available as #defines for use in C hooks.

NameValueMeaning
SUCCESS0Success (values > 0 may carry a payload such as a length).
OUT_OF_BOUNDS-1Could not read/write a pointer the hook supplied.
INTERNAL_ERROR-2Internal error (e.g. a corrupt directory).
TOO_BIG-3Value you tried to store was too big.
TOO_SMALL-4Value you tried to store/provide was too small.
DOESNT_EXIST-5Requested thing was not found.
NO_FREE_SLOTS-6No free slot (max 255).
INVALID_ARGUMENT-7Invalid argument.
ALREADY_SET-8A one-time parameter was already set.
PREREQUISITE_NOT_MET-9A required parameter was not set first.
FEE_TOO_LARGE-10Operation would produce an absurd fee.
EMISSION_FAILURE-11Emitted transaction was not accepted.
TOO_MANY_NONCES-12Exceeded the nonce limit.
TOO_MANY_EMITTED_TXN-13Emitted more than reserved.
NOT_IMPLEMENTED-14API reserved for a future version.
INVALID_ACCOUNT-15Expected an account id, got something else.
GUARD_VIOLATION-16A guarded loop/function exceeded its max iterations.
INVALID_FIELD-17Requested field is sfInvalid.
PARSE_ERROR-18Asked to parse invalid content.
RC_ROLLBACK-19Terminate due to a rollback() call.
RC_ACCEPT-20Terminate due to an accept() call.
NO_SUCH_KEYLET-21Invalid keylet or keylet type.
NOT_AN_ARRAY-22Count requested on a non-array.
NOT_AN_OBJECT-23Subfield requested from a non-object.
INVALID_FLOAT-10024Sentinel that can never be a valid exponent (note: not -24).
DIVISION_BY_ZERO-25Division by zero.
MANTISSA_OVERSIZED-26Mantissa too large.
MANTISSA_UNDERSIZED-27Mantissa too small.
EXPONENT_OVERSIZED-28Exponent too large.
EXPONENT_UNDERSIZED-29Exponent too small.
XFL_OVERFLOW-30A float operation overflowed.
NOT_IOU_AMOUNT-31Amount was not an IOU amount.
NOT_AN_AMOUNT-32Value was not an amount.
CANT_RETURN_NEGATIVE-33Cannot return a negative value.
NOT_AUTHORIZED-34Not authorized (e.g. foreign state without a grant).
PREVIOUS_FAILURE_PREVENTS_RETRY-35A previous failure prevents retry.
TOO_MANY_PARAMS-36Too many parameters.
INVALID_TXN-37Invalid transaction.
RESERVE_INSUFFICIENT-38A new state object would exceed the account reserve.
COMPLEX_NOT_SUPPORTED-39Complex operation not supported.
DOES_NOT_MATCH-40Two keylets required to match by type did not.
INVALID_KEY-41User-supplied key was not valid.
NOT_A_STRING-42Missing NUL terminator on a string argument.
MEM_OVERLAP-43Two specified buffers overlap in memory.
TOO_MANY_STATE_MODIFICATIONS-44Exceeded the 256-entry state-modification limit (see Execution environment constraints above).
TOO_MANY_NAMESPACES-45Exceeded the namespace limit.

Note the deliberate gap: the sequence goes -23, then INVALID_FLOAT = -10024, then -25; the value -24 is unused.

Execution results in transaction metadata ​

Each hook execution is recorded in the transaction's metadata. The engine builds an sfHookExecution object and collects them into an sfHookExecutions array. The fields set on each sfHookExecution object are:

  • sfHookResult — the exit type (ExitType: ACCEPT, ROLLBACK, WASM_ERROR).
  • sfHookAccount — the account the hook that produced this entry is installed on.
  • sfHookReturnCode — the exit/return code the hook passed to accept/rollback.
  • sfHookReturnString — the message buffer passed to accept/rollback.
  • sfHookInstructionCount — instructions executed.
  • sfHookEmitCount — number of transactions this execution emitted.
  • sfHookExecutionIndex — the execution's index in the chain.
  • sfHookStateChangeCount — number of state entries this execution changed.
  • sfHookHash — the hash of the HookDefinition (bytecode) that ran.

SetHook lifecycle summary ​

Hooks are installed, updated, and removed with the SetHook transaction. The per-hook operation is one of the HookSetOperation values:

OperationValueMeaning
hsoINVALID-1Not a valid operation.
hsoNOOP0No operation.
hsoCREATE1Create a new hook (upload WASM).
hsoINSTALL2Install an existing hook definition by hash.
hsoDELETE3Delete the hook.
hsoNSDELETE4Delete a namespace.
hsoUPDATE5Update hook parameters/fields.

For the fields that control when an installed hook fires and what it may emit() — HookOn, HookOnIncoming, HookOnOutgoing, HookCanEmit, and the hook-targeting HookName field — see SetHook Fields.

The associated HookSetFlags:

FlagValueMeaning
hsfOVERRIDE0x01Permit overriding/deleting an existing hook.
hsfNSDELETE0x02Permit deleting a namespace.
hsfCOLLECT0x04Allow collect calls on this hook.

Compilation pipeline ​

Hooks are compiled with hooks-compiler, a single command that runs clang, wasm-opt, and hook-cleaner and emits an install-ready module. Under the hood these are the classic wasmcc, hook-cleaner, and (for WAT-text hooks) wat2wasm tools from wasienv, hook-cleaner-c, and wabt, which can still be run by hand. See Compiling Hooks for a full walkthrough of the toolchain and why a stock build needs cleaning before it validates, and Validating Hooks for checking the result with the hook_validate RPC before you install it.