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

Spec

A spec is a chain-tagged decode artifact: an id, a chain tag, and a payload that core never interprets (Spec, crates/blockwatcher-types/src/resource.rs). It is written through PUT /specs/{id} on the HTTP API or as one JSON file under a seed directory’s specs/ subdirectory. The write itself compiles the payload (EvmDecoder::compile_spec, crates/blockwatcher-evm/src/decoder/mod.rs), so every refusal quoted below is raised at that write, as a 422, never later; the same compilation also reruns at boot and on pipeline restart, against payloads the write already proved valid. Writing a spec restarts every network on the spec’s chain, and on a chain reassignment its prior chain too, because every spec sharing a chain compiles together, per Resources § Lifecycle.

A complete spec resource. The payload is the two-declaration ERC-20 ABI the decoder’s own tests compile (erc20_spec_with_function, crates/blockwatcher-evm/src/decoder/mod.rs):

{
  "id": "erc20",
  "chain": "evm",
  "payload": [
    {
      "type": "event",
      "name": "Transfer",
      "anonymous": false,
      "inputs": [
        { "name": "from", "type": "address", "indexed": true },
        { "name": "to", "type": "address", "indexed": true },
        { "name": "value", "type": "uint256", "indexed": false }
      ]
    },
    {
      "type": "function",
      "name": "transfer",
      "stateMutability": "nonpayable",
      "inputs": [
        { "name": "to", "type": "address" },
        { "name": "amount", "type": "uint256" }
      ],
      "outputs": [{ "name": "", "type": "bool" }]
    }
  ]
}

Fields

Spec rejects an unrecognized field (#[serde(deny_unknown_fields)]).

KeyTypeDefaultMeaning
idstringnone (required)The spec’s name, referenced by a monitor selector’s spec field.
chainstringnone (required)The chain family whose decoder compiles the payload. The write refuses a chain with no loaded decoder; shipped builds load evm.
payloadJSONnone (required)The chain’s own decode artifact, opaque to core. For evm: a Solidity JSON ABI array, per the contract below.

The evm payload

The evm decoder parses payload as a Solidity JSON ABI array. A payload that does not parse refuses with spec '<id>': payload is not a JSON ABI array: ..., and a spec whose chain does not match the decoder is refused as an unsupported chain.

What the ABI’s declarations become:

  • Events: every named event overload becomes a schema and a decode plan keyed by the keccak256 hash of its signature (the log’s first topic).
  • Functions: every function overload becomes a schema and a decode plan keyed by the 4-byte selector its calldata leads with.

A monitor’s selector then names these declarations by name; see Monitor for the selector keys and Selectors for what each kind decodes.

What the decoder rejects

All of these refuse the write before a CompiledSpec is ever constructed:

  • An anonymous event: spec '<id>' event '<name>' is anonymous: this decoder identifies events by the keccak256 hash of their signature in the log's first topic, which an anonymous event's log never carries, so it could never be matched at decode time.
  • Two events whose selectors collide: spec '<id>': events '<a>' and '<b>' both hash to the same selector; a decoder cannot tell them apart at decode time, so only one may be declared. Two colliding functions refuse with the same message shape.
  • A payload declaring nothing: spec '<id>' declares zero events and zero functions, so no selector against it could ever match anything.
  • A tuple parameter with no components: tuple field '<name>' has no components, so it would disappear from the schema instead of declaring anything; give it at least one component or remove it from the ABI.
  • Two components of one tuple resolving to the same name: tuple field '<name>': the component at position <n> is named '<x>', which another component of the same tuple already carries; give each component in the tuple a distinct name. When the colliding component is unnamed, the message instead reads ... the unnamed component at position <n> would default to '_<n>', which another component of the same tuple already carries; rename that other field to something other than '_<n>'.

The dotted-name flattening contract

Field names in the compiled schema come from the ABI’s parameters, flattened per flatten_field (crates/blockwatcher-evm/src/decoder/compile.rs):

  • A bare tuple parameter flattens into one field per leaf, dotted by component name (name.component, recursively through nested tuples).
  • An unnamed tuple component is named _<position> (_0, _1, …).
  • Every other shape is one field. An array of tuples (tuple[]) never flattens: it becomes a single array-of-map field, so a predicate can address it as a whole but never a named component inside one element.

Each leaf’s ABI type maps to a canonical value family (address, unsigned or signed integer, bytes, bool, string, array, map); how a predicate types against those families is on Predicates § The type system.

The namespaces every evm spec carries

Alongside its declared events, every compiled evm spec carries the same three predicate namespaces (namespaces, crates/blockwatcher-evm/src/decoder/compile.rs). A field a particular occurrence does not carry (tx.from on a log-decoded event, tx.status on a pending transaction) resolves Unknown at predicate time, never an error:

NamespaceFields
txhash (bytes), index (uint), status (uint), from (address), to (address), value (uint)
blocknumber (uint), hash (bytes), timestamp (uint)
logaddress (address), index (uint)