Network
A network names one feed to watch: an id, a chain tag, and a source
module selection (Network, crates/blockwatcher-types/src/resource.rs). It
is written through PUT /networks/{id} on the HTTP API or
as one JSON file under a seed directory’s networks/ subdirectory. Writing a
network always restarts that network’s pipeline; the restart is cheap because
the checkpoint survives it, per
Resources § Lifecycle.
A complete network resource. The source.config object is
registry_examples/evm_rpc.json from crates/blockwatcher-evm/src/,
copied verbatim from the same file the crate’s family-completeness test
constructs (the resource envelope around it precludes a literal include);
any change to that file updates this example in the same change, per the
wiki-parity rule:
{
"id": "eth-mainnet",
"chain": "evm",
"source": {
"module": "evm-rpc",
"config": {
"start_block": 18000000,
"endpoints": [
{
"name": "alchemy",
"url_secret": "env:BLOCKWATCHER_EXAMPLE_EVM_RPC_URL",
"priority": "high",
"rate_limit": { "rps": 25 }
},
{
"name": "public",
"url_secret": "env:BLOCKWATCHER_EXAMPLE_EVM_RPC_URL_FALLBACK",
"priority": "low"
}
],
"confirmations": 12
}
}
}
Fields
Network rejects an unrecognized field (#[serde(deny_unknown_fields)]), so
a typo’d key fails the write rather than being silently ignored.
| Key | Type | Default | Meaning |
|---|---|---|---|
id | string | none (required) | The network’s name, referenced by every monitor’s network field. |
chain | string | none (required) | The chain family tag. The write refuses a chain with no loaded decoder; shipped builds load evm. |
source | object | none (required) | A ModuleSel: which source module feeds this network, plus that module’s own config. |
source is the universal module envelope (ModuleSel, resource.rs):
| Key | Type | Default | Meaning |
|---|---|---|---|
module | string | none (required) | The source module’s name: evm-rpc or evm-mempool in shipped builds. |
config | object | none (required) | The named module’s own config; its shape depends on module and is documented per module below. |
Write-time validation constructs the named module with the given config, so
every refusal quoted below is raised at the write (as a 422) or at seed
validation, never discovered later at runtime. Updating an existing network
additionally recompiles every monitor already stored for it against the
incoming chain, refusing a chain reassignment that would strand them, per
put_network (crates/blockwatcher-core/src/control/writes.rs).
Endpoints: the shared pool vocabulary
Both source modules name a pool of HTTP JSON-RPC endpoints with the same
shape (EndpointDef, crates/blockwatcher-evm/src/source/endpoint.rs). Each
endpoint object:
| Key | Type | Default | Meaning |
|---|---|---|---|
name | string | none (required) | Labels every metric and log line for this endpoint. A repeat refuses: endpoint name 'primary' is used by more than one endpoint; endpoint names must be unique. |
url_secret | string | none (required) | An env:NAME reference to where the URL lives, never the URL itself. A value that is not a reference refuses with endpoint '<name>' url_secret is not a secret reference: ..., deliberately never echoing what was written: a provider URL routinely carries an API key. |
priority | string: high or low | "high" | The selection tier; high endpoints are always tried before low. |
rate_limit | object { "rps": u32 } | none | A preemptive request rate enforced before a call leaves the pool. A zero refuses: endpoint '<name>' rate_limit.rps is 0; it must be at least 1. |
weight | u32 | 1 | Ring slots in the tier’s rotation; meaningful only under round_robin selection, inert under ordered (and therefore always inert on evm-mempool, whose pool is fixed to ordered and exposes no selection knob). A zero refuses: endpoint '<name>' weight is 0; it must be at least 1. Above the pool’s cap of 100 (blockwatcher_rpc::MAX_WEIGHT) refuses: endpoint '<name>' weight (101) exceeds the maximum (100). |
evm-rpc
Polls confirmed blocks over HTTP JSON-RPC: events via eth_getLogs, and,
only while some monitor watches functions, full transaction bodies too. Its
config (EvmRpcConfig, crates/blockwatcher-evm/src/source/rpc/config.rs)
rejects unrecognized fields, including a network key: which network a
source’s events belong to is the engine’s to supply, never the config’s.
The annotated example above is the module’s complete example config. The tunables:
| Key | Type | Default | Meaning |
|---|---|---|---|
endpoints | array | none (required) | The pool, per the table above. An empty array refuses: evm-rpc requires at least one endpoint. |
selection | string: ordered or round_robin | "ordered" | How the pool orders a tier’s endpoints when no window pin dictates the choice: ordered concentrates calls on the first admissible endpoint in configuration order; round_robin spreads windows across the tier, weighted by each endpoint’s weight. A misspelling is refused by serde naming both accepted spellings. |
start_block | u64 | none (required) | Where a run with no persisted checkpoint begins scanning. Deliberately absolute and without a default: a head-relative start would re-derive a different block on every restart and silently skip the gap. |
confirmations | u64 | 12 | How deep a block must age before it is emitted; trades delivery latency for reorg safety. |
max_lag_blocks | u64 | 3 | How far behind the pool’s most current head an endpoint may report before it is excluded from serving. |
poll_interval_ms | u64 | 3000 | How often, while caught up, one eth_blockNumber head check advances the emission barrier. |
logs_window | object | { "initial": 512, "max": 2048 } | How wide an eth_getLogs scan window starts and how far it may grow, per the table below. |
full_block_window | u64 | 8 | Ceiling on a range fetched with full transaction bodies, applied only while some monitor watches functions. A zero refuses: full_block_window (0) must be at least 1. |
probe_interval_ms | u64 | 10000 | How often every non-open endpoint is probed for health changes, bypassing rate limits. |
retry_backoff_max_ms | u64 | 30000 | Cap on the doubling backoff for a window the run loop cannot fetch; the schedule starts at one poll interval. A cap below poll_interval_ms refuses: retry_backoff_max_ms (2999) is below poll_interval_ms (3000); the retry schedule starts at one poll interval, so a smaller cap silently disables the backoff. |
receipts | string: always or when_read | "always" | When a matching transaction spends an eth_getTransactionReceipt, which buys exactly tx.status. always keeps the derived match id a function of chain content alone; when_read saves the round trip when no predicate reads tx.status, at the cost of a payload (and match id) that changes with the monitor set. A misspelling is refused by serde naming both accepted values. |
receipt_concurrency | u64 | 4 | How many eth_getTransactionReceipt calls may be in flight at once for one leaf’s matching transactions. Receipts are unpinned consensus reads, so concurrency changes pacing alone and never consistency; the per-endpoint rate limiter and breaker still gate every call. Raising it shortens function-heavy leaves at the cost of burstier provider load. A zero refuses: receipt_concurrency (0) must be at least 1. |
header_batch | u64 | 20 | How many eth_getBlockByNumber requests ride one JSON-RPC batch, so a window of w blocks costs ceil(w / header_batch) header round trips. A batch is one HTTP request against one endpoint, so it inherits the window pin exactly as single calls do. 1 sends classic single calls, which is the setting for a provider that rejects batch arrays: such a provider fails its first window visibly and the run loop retries it under Degraded. A zero refuses: header_batch (0) must be at least 1. |
bloom_screen | bool | true | Whether a window may skip its eth_getLogs call when every fetched header’s logsBloom proves no monitored address or topic0 can be present. Against a protocol-conforming node the skip is lossless, since a bloom is a superset of its own block’s logs. Enabling it nonetheless adds a dependency the unscreened path does not carry: correctness rests on the bloom as well as on the logs response, so an endpoint or caching proxy that serves correct logs behind a zeroed or otherwise-inaccurate bloom loses those logs silently. Disable it for endpoints whose blooms are not trusted. A broad filter, carrying neither addresses nor topic0s, is never screened, and a header without a readable bloom is never screened regardless of this setting. |
logs_window (LogsWindow, same file):
| Key | Type | Default | Meaning |
|---|---|---|---|
initial | u64 | 512 | Starting scan width. A zero refuses: logs_window.initial (0) must be at least 1. A value above max refuses: logs_window.initial (2048) exceeds logs_window.max (512). |
max | u64 | 2048 | How far the width may grow back after a provider forces it narrower. At runtime the first width a provider refuses drops the run’s effective ceiling pool-wide, so one narrow-limited endpoint caps every endpoint until the source next starts. |
evm-mempool
Subscribes to pending transaction hashes over WebSocket and hydrates each
against an HTTP pool: a notification carries a hash and nothing else, so
every candidate costs one eth_getTransactionByHash. Its config
(EvmMempoolConfig, crates/blockwatcher-evm/src/source/mempool/config.rs)
has no start_block and no confirmations: a pending stream has no history
to begin from and no reorg barrier to honor, and a start_block copied over
from an evm-rpc network is refused by name as an unknown field rather than
silently ignored. What that difference means for delivery guarantees is on
Selectors § The position problem.
The module’s complete example config, registry_examples/evm_mempool.json
from crates/blockwatcher-evm/src/, verbatim, the same file the crate’s
family-completeness test constructs:
{
"ws_url_secret": "env:BLOCKWATCHER_EXAMPLE_EVM_MEMPOOL_WS_URL",
"endpoints": [
{
"name": "primary",
"url_secret": "env:BLOCKWATCHER_EXAMPLE_EVM_RPC_URL"
}
]
}
| Key | Type | Default | Meaning |
|---|---|---|---|
ws_url_secret | string | none (required) | An env:NAME reference naming where the ws:// or wss:// subscription endpoint lives. A value that is not a reference refuses with ws_url_secret is not a secret reference: ..., never echoing what was written. |
endpoints | array | none (required) | The HTTP hydration pool, per the shared endpoint table above. An empty array refuses: evm-mempool requires at least one endpoint. |
reconnect_ms | u64 | 1000 | How long to wait after a dropped subscription before dialing again; a fixed interval with no jitter and no backoff. A zero refuses: reconnect_ms is 0; it must be at least 1. |
idle_policy | object | { "ping_after_ms": 30000, "pong_deadline_ms": 10000 } | When to suspect the subscription half-open, and how long to wait for proof before redialing, per the table below. |
idle_policy (IdlePolicyDef, same file, the wire form of ws::IdlePolicy):
| Key | Type | Default | Meaning |
|---|---|---|---|
ping_after_ms | u64 | 30000 | How long a subscription may sit silent before an idle WebSocket ping goes out. A zero refuses: idle_policy.ping_after_ms is 0; it must be at least 1. |
pong_deadline_ms | u64 | 10000 | How long to wait for any frame after an idle ping before presuming the connection half-open and redialing. A zero refuses: idle_policy.pong_deadline_ms is 0; it must be at least 1. |