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.