uniswap-event-audit
Per-block completeness digest of every event the pools package decodes, for a
backend check that pulls it per chain and compares it with what landed in the
warehouse. Stateless, no RPC, no sink and no dataset: the consumer streams the
modules directly.
Modules
counts_out, digest_out and keys_out read the deployed uniswap-database-changes-pools-<chain> package's
uniswap_v{2,3,4}:map_events (imported by URL, so the parent hashes are the
live ones and the provider serves them from cache) and output flat
DatabaseChanges, one emission per block.
| Module | Table | Rows | When |
|---|
counts_out | block_digest_counts | an all row every block (empty blocks included) and one per event type present: event_type, event_count | every tick; 3.7 MiB per 1,000 Base blocks |
digest_out | block_digest_event_manifest | the same rows plus ordinals and block_indexes, roaring portable bitmaps (hex), empty on all | over a range whose counts disagree |
keys_out | event_key | one row per event: event_type, block_index, log_index, ordinal, transaction_index, tx_hash, pool_address (pool id for v4) | over a flagged bucket only |
final_head | (a Clock) | each block's clock, nothing else: no stores, no parents | once per tick, to learn the provider's final block (below) |
- Every list in the three
map_events outputs is an event type, named
<version>_<list> (44 today). A test reads the Events messages from the
protos, so a new list fails the build until it's covered here.
- The three
associated_evm_transaction types are transactions, not logs:
their ordinals hold the tx begin ordinal and block_indexes the tx index.
- Ordinals must fit 32 bits for the bitmaps; a larger one is an error.
Row envelope on every table: block_num, block_hash, timestamp,
detail_level (BASE or EXTENDED), index, global_sequence
((block << 64) | row index, 39 digits) and ordering_key (the event type on
counts and manifest rows, the pool on key rows). chain_id isn't emitted: the
consumer tags it from the chain it streams.
These tables come from the pool-state package's digest_out / keys_out and
match them field for field apart from ordering_key, without the pool-state
sink's pool_changeset row (tests/digest.rs).
Build and publish
./scripts/build.sh <chain> <version>
make counts NETWORK=base START_BLOCK=51990000 ENDPOINT=base-mainnet.streamingfast.io:443
The build resolves the pools version the prod sink streams for the chain
(infrastructure/config/sinks/prod, override with POOLS_VERSION or
POOLS_SPKG), records it in each module's doc (the parents' spkg URL), and
fails unless the
parent map_events and store_pools hash exactly like the deployed pools
package's. Publish with the "Build and Optionally Upload Database Changes
Substreams Package" workflow, event-audit. The package is
uniswap-event-audit-<chain>.
Cost, measured on Base (2026-10-02)
| Request | Processed blocks | Egress |
|---|
counts_out, 20 blocks, first request | 1,000 (one whole segment) | 62 KiB |
counts_out, 1 block, segment already cached | 0 | 2.8 KiB |
counts_out, 1,000 blocks, segment already cached | 0 | 3.7 MiB |
digest_out, 20 blocks | 1,000 | 99 KiB |
keys_out, 20 blocks | 1,000 | 1.5 MiB |
A request is billed at least a whole 1,000-block segment the first time, and
cached segments meter egress only, so the consumer should ask for
1,000-block-aligned ranges. Counts are about 3.8 KiB per Base block, more than
the 1 KB estimate, since each of a block's ~8 rows carries the full envelope.
Finding the final block
There's no Substreams call that returns the final block, but every block
message carries final_block_height. Request final_head from -1 (relative
to the chain head) without final-blocks-only, read final_block_height from the
first block and close the stream: 1 processed block and about half a second on
Base (2026-10-02), with the provider's own idea of final (head − 200 on Base,
against head − 604 for the RPC's finalized). Don't add final-blocks-only:
-1 is relative to the chain head, so that waits for the head block to become
final (~400 s on Base). And don't use counts_out for it: from -1 it pays the
store catch-up since the segment started (1,400 processed blocks in the same
test).