Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Hook and Transaction Parameters

Hooks read configuration from two distinct sources, both called “parameters” but attached at very different times: hook parameters, set once when the hook is installed (via SetHook), and originating transaction parameters, attached fresh by whoever submits the triggering transaction. This page covers both — the loose byte-buffer accessors, the hook_parameter!/otxn_parameter! entity macros that pair a name with a value type, and composite (struct-shaped) names via #[derive(ParamName)].

Two sources, one shape

Hook parameterOtxn parameter
set bythe hook’s operator, at SetHook timewhoever submits the triggering transaction
read withhook_param/hook_param_exact/hook_param_typedotxn_param/otxn_param_exact/otxn_param_typed
entity macrohook_parameter!otxn_parameter!
typical useoperator-controlled configuration (a minimum amount, a blocklist entry, a pause switch)per-invocation instructions the caller supplies

Both mechanisms are read-only from the reading hook’s own perspective — a hook parameter is set by whoever installs the hook, not written by the hook itself at runtime (hook_param_set exists, but it writes a different hook’s parameter, taking a raw &[u8], not a typed value — out of scope for this page). Because of that, both sides share the exact same TypedParamName trait and accessor shape; only the underlying host call differs.

The loose accessors

hook_param/otxn_param read a named parameter into a caller-provided buffer, mirroring otxn_field’s shape (see Reading the Originating Transaction):

use rshooks::prelude::*;

let mut buf = [0u8; 32];
let written = hook_param(&mut buf, b"CFG")?;

hook_param_exact/otxn_param_exact require the parameter to be exactly T’s length (any FixedRead type), with T inferred from context, not a turbofish. examples/03_hook-params uses exactly this for a compiled-in default when the operator hasn’t configured a minimum:

const MIN_PARAM: &[u8] = b"MIN";
const DEFAULT_MIN_DROPS: u64 = 1_000_000;

fn min_drops() -> u64 {
    hook_param_exact(MIN_PARAM)
        .map(u64::from_be_bytes)
        .unwrap_or(DEFAULT_MIN_DROPS)
}

hook_param_exact’s return type is inferred as [u8; 8] from the .map(u64::from_be_bytes) call — no turbofish needed. Note the from_be_bytes, not this crate’s FromBytes trait: a raw parameter byte buffer is whatever the caller who set it chose to write, conventionally matching Xahau Binary’s big-endian numeric encoding for a value like this, the same convention Reading the Originating Transaction describes for raw protocol fields. .unwrap_or(DEFAULT_MIN_DROPS) collapses “not configured at all” and “configured with a value of the wrong size” into the same fallback, without treating a malformed parameter as a hard error.

The typed pairing: hook_param_typed/otxn_param_typed

hook_param_exact/otxn_param_exact take the name and the value type T as two independent arguments — nothing stops calling otxn_param_exact::<WrongType>(b"INS") for a name/type combination that was never intended, as long as WrongType: FixedRead (true of nearly every fixed-size type this crate provides, including some other parameter’s value type).

TypedParamName closes that gap: implement it for a name type — directly, or via hook_parameter!/otxn_parameter! — to declare its one paired value type once, then call hook_param_typed/otxn_param_typed with a reference to a name value. The return type is resolved from the name argument itself, never a turbofish and never an independently-chosen type a mismatch could hide behind.

The entity macros: hook_parameter!/otxn_parameter!

These declare a parameter entity — the thing your hook reads — using the identical grammar staircase hook_state! uses for hook state (see Hook State):

formname shapeexample
1fully fixed (a new zero-sized type)hook_parameter!(Cfg, CfgName = b"CFG" => Config);
2struct, with a fixed instancesame shape as hook_state!, applied to a name instead of a key
3struct, constructed per call sitehook_parameter!(SeatVote, SeatParamName {topic: u8, seat: u8} => Vote);
4newtype (tuple struct) around one existing typesame shape as hook_state!
existingname impls on a name type you declaredhook_parameter!(Cfg, existing CfgName = b"CFG" => Config);
pairingwraps a name type you already declared, that already encodeshook_parameter!(SeatVote, SeatParamName => Vote);

otxn_parameter! has the exact same grammar; the only difference is which host call the generated get_value() forwards to (hook_param_typed vs. otxn_param_typed), so the declaration site itself documents which of the two a parameter is meant for.

Form 1, with the compiled-in-default pattern

examples/03_hook-params’s MIN parameter, expressed the typed way:

use rshooks::prelude::*;
use rshooks::{ParamValue, hook_parameter};

hook_parameter!(Cfg, CfgName = b"CFG" => Config {min_amount: u64});

fn min_amount() -> u64 {
    Cfg.get_value().map(|c| c.min_amount).unwrap_or(1_000_000)
}

Form 1 declares both Cfg (the entity) and CfgName (the name component) as new zero-sized types, both encoding the literal b"CFG". Cfg.get_name() -> &'static [u8] is available as a const fn returning that literal directly — no encode step at all — because a plain byte-string name has nothing to compute: its wire encoding is its in-memory representation. Cfg.get_value() is an #[inline(always)] forward to hook_param_typed(&CfgName); the two spellings compile to the same code, so the choice is purely readability.

.unwrap_or(default) is the idiomatic way to give a hook a sensible compiled-in fallback: get_value() returns Err uniformly whether the parameter was never set or was set with the wrong byte length, so one unwrap_or handles “unconfigured” and “malformed” the same way, exactly like the loose hook_param_exact pattern above.

Composite names: #[derive(ParamName)]

A Hook API parameter name isn’t always a plain literal tag — per the Hook API itself it’s a genuine variable-length key of up to 32 bytes, and (like a hook state key) can be a whole composite, struct-shaped value instead of a byte string. #[derive(ParamName)] derives ToBytes (write-only — see below) for a named-field struct used this way. examples/12_typed-data’s typed-data hook uses this for an operator-controlled pause switch, the same idea xahaud’s own genesis governance hook uses for its IS0..IS19 seat parameters:

use rshooks::prelude::*;
use rshooks::{ParamName, ParamValue, hook_parameter};

#[derive(ParamName, Clone, Copy)]
struct AdminName {
    section: u8,
    field: u8,
}

hook_parameter!(AdminPause, AdminName => PauseSwitch {paused: u8});

const ADMIN_PAUSE: AdminPause = AdminPause(AdminName {
    section: 0,
    field: 0,
});

fn deposits_paused() -> bool {
    ADMIN_PAUSE
        .get_value()
        .map(|s| s.paused != 0)
        .unwrap_or(false)
}

This is hook_parameter!’s pairing form — an entity wrapping a name type already declared with #[derive(ParamName)], paired with an inline PauseSwitch value (#[derive(ParamValue)]-equivalent codegen, generated inline here). AdminName encodes to 2 bytes (section then field, no padding), comfortably inside the Hook API’s 1-to-32-byte parameter-name bound. Unlike an oversized HookData state value (no size cap at all), #[derive(ParamName)] checks this bound — both the 1-byte lower bound and the 32-byte upper bound — at the struct’s own definition, so an out-of-range name fails to compile before it’s ever used.

Because AdminName is composite rather than a fixed byte string, its TypedParamName impl has to actually encode at runtime — laying section and field out into a small buffer sized exactly to AdminName::MAX_LEN (not the full 32-byte scratch the trait’s generic default would need, since only a concrete, non-generic impl can size an array by an associated constant). examples/12_typed-data’s README measures this directly: +29 worst-case instructions over the same hook without the composite name, versus the near-zero cost of the plain CFG/INS tags used elsewhere in that same hook.

Why the typed pairing prevents name/value mismatches

The loose hook_param_exact::<T>(name)/otxn_param_exact::<T>(name) calls take name and T as two independent arguments — a typo or a copy-paste error can pair the right name with the wrong type, or the wrong name with the right type, and both compile fine as long as T: FixedRead. Every hook_parameter!/otxn_parameter! declaration removes that degree of freedom: the name type is permanently tied to exactly one value type (TypedParamName::Value), so Cfg.get_value() and Ins.get_value() in examples/12_typed-data can never accidentally decode one parameter’s bytes as the other’s struct shape — the compiler resolves the return type from the entity itself, with no independently-chosen type left for a mismatch to hide in. This is the identical safety property Hook State’s hook_state! gives the key/value side; see Typed Data with Derives for the underlying ParamName/ParamValue derives both macros build on.