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

Glossary

Every term below is defined by how blockwatcher’s code actually behaves, not by a general blockchain-industry sense of the word. Terms are alphabetical; each links to the others it depends on.

At-least-once delivery

blockwatcher’s delivery guarantee for a source whose cursor is a real chain position: a network’s checkpoint advances past an event only once every sink event that event produced has either reached its sink or been recorded as a dead letter after exhausting retries, never while a delivery is still outstanding. Quiet gate hits are not deliveries. An emitted match after a gate is still at-least-once (replay can duplicate). Both Match and Retracted are at-least-once; a consumer must treat a redelivered duplicate as the same occurrence (idempotent on match_id). Because the checkpoint is what a restart resumes from, the only way delivery can go wrong across a crash is repeating work already sent, never dropping work that was never sent. evm-mempool is the one shipped source this does not cover: its cursor is a per-run arrival counter rather than a chain position, so a crash there can lose whatever was in flight, and consumers should deduplicate its matches on the transaction hash rather than the match id.

Backpressure

What happens instead of dropping data when one pipeline stage runs behind another. A pipeline’s stages (source, decode-and-match, per-sink delivery) are connected by fixed-size queues; a stage that produces faster than the next one consumes simply waits for room in that queue before it can hand off more work. A source therefore never sees “buffer full, discard”: it sees its own send call take longer, which is the entire backpressure mechanism.

Canonical value model

The one representation every decoder normalizes chain-native data into, and the only representation a matcher, gate, or sink ever has to understand. It covers null, boolean, arbitrary-precision signed and unsigned integers, byte strings, addresses, strings, arrays, and ordered maps. Arbitrary-precision integers are why a 256-bit token amount compares and serializes exactly rather than losing precision the way a JSON number would; each chain family’s decoder is responsible for encoding its own values into this model without two distinct native values ever colliding on the same canonical one.

Checkpoint

The resume point a network’s pipeline persists: a cursor plus whatever extra state the source that wrote it needs to verify a safe resume (a block hash for reorg linkage, for instance). It advances only past a fully finished prefix of events: one still-outstanding sink event anywhere in that prefix holds the whole checkpoint back, even if later events already finished. A positively detected invalidate is the one case that rewinds it, and only backward to the proven from cursor, only after retracts have finished; a rewind that cannot move the cursor backward (the invalidation’s cursor is not behind the stored checkpoint, no checkpoint is stored, or the post-drain read failed) is refused and counted as blockwatcher_rewinds_refused_total, leaving the stored checkpoint unmoved and an absent one absent rather than fabricated. A gate Retain/Discard does not leave outstanding work; only an emitted match/digest does. Held hits persist in gate_hits without holding the checkpoint. Every checkpoint also records which source module wrote it, and resuming it under a different module is refused outright, because a cursor’s two numbers mean nothing outside the module that produced them.

Contract spec

A resource pairing a chain identifier with that chain’s decode artifact (for the evm chain family, a Solidity ABI) as an opaque payload. Writing a spec triggers its chain’s decoder to compile that artifact once into chain-agnostic event and function schemas; every selector and predicate that references the spec afterward works from those compiled schemas, never from the raw payload again. A spec has no module field of its own: its chain field alone determines which decoder compiles it.

Cursor

A pair of numbers a source uses to mark its own position in its feed, meaningful only inside the module that wrote them. The evm-rpc source uses the first number as a block number and packs the second to order a block’s transactions ahead of its logs; evm-mempool uses the first number as a monotonic arrival count and leaves the second at zero. Core code only ever compares and orders cursors: it never interprets what the numbers mean.

Dead letter

The record blockwatcher keeps of a sink event that could not be delivered after its sink’s retry budget ran out. It carries the match’s id, which monitor and sink produced it, the cursor it traces back to, how many attempts were made, why the last one failed, and, for events produced after this field was added, the original SinkEvent payload, which lets a Match or a Digest be replayed through the API. Legacy rows stored as a bare Match object still load as SinkEvent::Match. A retraction payload cannot be replayed. Recording a dead letter, rather than discarding the event outright, is what lets the checkpoint move past work that genuinely could not be delivered without pretending nothing happened.

Decoder

The port that turns a source’s raw payload into the canonical value model, one implementation per chain family. Nothing about the port boundary limits a deployment to one chain family; evm is simply the only one in the shipped module catalog today. A decoder compiles a contract spec’s payload into reusable schemas once, at write time, and decodes every later occurrence against those schemas rather than re-parsing the original artifact each time.

Delivery journal

The bounded per-network log of Match ids that were delivered or dead-lettered, used to emit Retracted events after an invalidate. Depth is instance config journal_depth (default 1024, in cursor primary units). Rows older than that window are self-pruned on the next write; there is no operator prune API. A successful retract forgets its row; a dead-lettered retract keeps it so a later invalidate can re-offer. See Delivery guarantees § The delivery journal.

Gate

A port that decides, for one monitor, whether a decoded occurrence that already passed selector and predicate becomes a match (or digest), given an engine-owned journal of earlier hits. threshold and max_once key windows on block.timestamp. Omit the monitor’s gate field for passthrough. See Gates. Distinct from sink throttle / aggregate, which run after a match exists.

Invalidate

A typed source outcome, SourceOutcome::Invalidated { from }, meaning everything with cursor > from previously implied by this source is on a dead fork and must not be resumed past from. It is not a SourceError. from is a cursor the source can defend with evidence: a proven fork point, or, when every tracked ancestor is refuted, just below the oldest tracked height (the minimal rewind that evidence permits). A source that holds no such evidence does not return this outcome. Core never names a chain or a reorg; only the source module decides when to return this. The engine drains that network, prunes gate_hits with cursor > from (rows with cursor ≤ from stay), retracts journaled deliveries after from (to that network’s sinks, or after delete to the sinks recorded on its runtime tombstone), rewinds the checkpoint when from is behind it (leaving it unmoved otherwise), then restarts. See Delivery guarantees § Shallow vs deep invalidation.

Match

One occurrence where a monitor’s selectors picked up a decoded event and its predicate, if it has one, accepted it. A match carries a deterministic id derived from the network, the monitor, the decoded event, and an index among the matches that one event produced, so a consumer that receives the same match twice under at-least-once delivery can recognize the duplicate by id, except from evm-mempool, where the same pending transaction can carry two different ids across a restart because its cursor is not a stable position.

Matcher

The port that evaluates a compiled predicate against a decoded event and reports whether it counts as a match. The shipped implementation, expr, is selected once for the whole instance in blockwatcher.toml rather than per monitor, and does not become per-monitor because Gate exists. The port boundary means an alternative predicate engine is a possible module, not a hardcoded choice.

Mempool

The set of transactions a node has received but has not yet included in a mined block. blockwatcher’s evm-mempool source subscribes to one node’s pending-transaction feed and fetches each transaction’s full data as its hash arrives. Because nothing here has settled yet, a pending transaction may never be mined at all: the trade-off this source makes for seeing activity before evm-rpc ever could.

Module

Any of the six swappable behaviors blockwatcher selects by name plus a module-specific config object: source, decoder, matcher, gate, sink, and storage backend. Every module implements exactly one port trait, and a module name that was never registered into the running binary is refused at write or boot time with the list of names that were.

Monitor

A resource that ties everything else together: it names one network to watch, one or more selectors describing what to decode from it, an optional predicate to filter what those selectors decode, an optional gate that decides whether a predicate-true hit becomes a match at all, and the sink ids a resulting match should reach. A monitor is compiled and schema-checked the moment it is written, and edits to it hot-swap into its running pipeline without a process restart.

Network

A resource naming a chain and a source module plus that module’s own config (where blockwatcher looks and how it ingests from there). Exactly one pipeline runs per network, and changing which source module a network uses restarts only that one pipeline, resuming from its persisted checkpoint as long as the checkpoint’s recorded module still matches.

Port

An object-safe Rust trait defining one axis of swappable behavior. blockwatcher defines six: Source, Decoder, Matcher, Gate, Sink, and Storage. Core code depends only on these trait definitions, never on any concrete module’s implementation: the reason a chain family’s code never has to be linked into, or even known by, the engine that drives it.

Predicate

A boolean expression, written in blockwatcher’s own small expression language, evaluated over a decoded event’s fields to decide whether it produces a match. It supports field access by namespace (args.* for decoded event fields; chain-specific namespaces such as tx.* and block.*), comparisons, boolean logic, arithmetic, and chain-native literals like hex addresses and token-decimal numbers. A predicate is type-checked against its monitor’s selector schemas the moment it is written, and at evaluation time a field an occurrence simply doesn’t carry resolves to an explicit “unknown” value rather than raising an error.

Seed

The one-time load of resources (networks, specs, sinks, monitors) from a directory of JSON files into empty storage, run at first boot via a CLI flag. It only ever populates a storage backend that has nothing in it yet; once a backend holds any resource, seeding again is a no-op, and afterward resources are managed exclusively through the REST API.

Selector

Part of a monitor: which addresses, which contract spec, and which of that spec’s events and/or functions to decode. A monitor’s selector entries are OR’d, and each entry is self-contained, so which spec governs which address is never ambiguous. What a given selector can actually produce depends on the network’s source: an events selector has nothing to decode on evm-mempool, which only ever delivers pending transactions, never logs.

Sink

The port responsible for delivering one sink event somewhere: a webhook, an operator-run script, or a log line are the modules in the shipped module catalog. Retry behavior on a failed delivery (how many attempts, backoff timing, when to give up and record a dead letter) is owned entirely by the engine and applied identically regardless of which sink module is delivering; a sink module itself never retries on its own. Every sink event variant shares that path. A sink whose SinkDef sets throttle or aggregate holds matches in a window before delivering, rather than delivering each one as it arrives; see Delivery guarantees § Aggregation. A gate digest is the same Digest wire shape aggregation already defined.

Sink event

What a sink receives: Match (apply this occurrence), Retracted { match_id } (undo the occurrence that id named), or Digest { matches } (apply several matches an aggregate window bundled together; see Sink). A closed set, serialized on the wire as tagged JSON (type: match, type: retracted, or type: digest). All three are at-least-once. See Delivery guarantees § What a sink receives.

Source

The port that pulls raw activity into a pipeline, whether by polling an RPC endpoint for blocks and logs or by subscribing to a node’s pending-transaction feed. A source owns the meaning of its own cursor, must emit events in non-decreasing cursor order, and is the only module family configured on the network resource rather than on a resource of its own. Source::run returns a SourceOutcome (Ended or Invalidated { from }) or a SourceError; a positively detected deep invalidate is the outcome, not the error.

Storage

The port responsible for every resource ever written (versioned, with optimistic concurrency), each network’s checkpoint, its dead-letter queue, its delivery journal, and each monitor’s gate_hits / gate_meta. Unlike the other five ports, a deployment picks exactly one storage backend for the whole process rather than one per resource: memory (no persistence, gone on restart) or sqlite (durable, single-writer) are the two in the shipped module catalog.