feat(api)!: redesign node log retrieval API around tag-based queries#23625
Merged
Conversation
bc31270 to
b0b1896
Compare
PhilWindle
approved these changes
May 29, 2026
PhilWindle
left a comment
Collaborator
There was a problem hiding this comment.
Node changes look fine
…retrieval interfaces Phase 1 of the log retrieval API redesign. Adds LogResult, LogCursor, and PrivateLogsQuery/PublicLogsQuery (with TagQuery and zod refines) under stdlib/src/logs. Flips L2LogsSource, AztecNode, and Archiver interfaces (+ zod schema maps) to expose only getPrivateLogsByTags / getPublicLogsByTags, dropping getContractClassLogs and getPublicLogs(LogFilter). Deletes the dead Tx.getPublicLogs(logsSource) method. Old types (LogFilter, LogId, TxScopedL2Log, Extended*Log, Get*LogsResponse) are kept for now and deleted in a later cleanup phase once Stages 2/3 stop referencing them. The tree will not compile green until Stages 1+2+3 land together (expected for a coordinated breaking change).
…ased API Phase 2 of the log retrieval API redesign. Migrates every client-side consumer of the log retrieval methods to the new LogResult / LogCursor / PrivateLogsQuery / PublicLogsQuery shape introduced in Phase 1. - pxe/tagging/get_all_logs_by_tags: rewrite getAllPages from a global page counter to per-tag afterLog cursors; tags drop out as soon as they return a short page. - pxe/logs/log_service: push fromBlock/toBlock into the query (deletes the in-memory #extractLogs filter and TODO(F-650)); set includeEffects: true; consume nullifiers[0] with the defensive non-zero check. - pxe sender/recipient sync: consume LogResult; sender sync leaves includeEffects off. - aztec.js getPublicEvents: switch to getPublicLogsByTags, decode from logData.slice(1), source contractAddress from the (now required) filter. - aztec.js wallet: EventFilterBase.afterLog: LogId -> LogCursor; PublicEventFilter contractAddress required; shared refineEventFilter enforces txHash xor range. - aztec.js api/log: drop LogFilter/LogId re-exports, add LogCursor/LogResult. - cli get-logs: require --contract-address and --tag; new --after-log parses LogCursor; paginate per-tag until a short page; print via LogResult.toHumanReadable(). - e2e_ordering: read block.body.txEffects[*].publicLogs directly via getBlock since the new API drops the tag-less, contract-less query path.
… with binary-key map
Phase 3 of the log retrieval API redesign. Lands the archiver storage and node
implementation behind the new API surface introduced in Phase 1 and consumed in Phase 2.
kv-store
- New AztecAsyncBinaryMap interface + LMDB v2 implementation: raw Uint8Array keys and
values that bypass ordered-binary, with a length-prefixed namespace so adjacent map
names cannot alias. Native lmdblib (C++) is untouched - keys are already raw bytes
with default memcmp ordering. Other backends throw on openBinaryMap.
archiver
- log_store.ts: full rewrite. Two binary-key primary maps keyed by
[contract] ++ tag ++ blockNumberBE ++ txIndexInBlockBE ++ logIndexWithinTxBE, plus
two blockNumber -> Buffer[] secondary deletion indices. Composite-key codec and
inc()/endOfTagRange/endOfTxRange helpers isolated as private static methods.
getPrivateLogsByTags / getPublicLogsByTags implement the read pseudocode end to end
inside one db.transactionAsync: txHash xor range validation, referenceBlock reorg
check, cursor exclusive-start via byte-buffer increment, tx-strict end via
inc(key(prefix, txBlk, txIdx, MAX_U32)), and a batched includeEffects fetch deduped
by txHash. Contract-class-log storage is removed entirely.
- block_store.ts: new getNoteHashesAndNullifiers(txHashes) - a batched partial
deserializer that skips the IndexedTxEffect header + revertCode/txHash/transactionFee
and reads only the noteHashes and full nullifiers vectors, stopping before the large
tail.
- data_stores.ts: ARCHIVER_DB_VERSION 6 -> 7 (store self-wipes on schema mismatch).
- data_source_base.ts: 4 forwarders -> 2; getContractClassLogs dropped.
aztec-node
- server.ts: 4 RPC handlers replaced with 2 thin forwarders; referenceBlock resolution
moved into the store (in-transaction).
stdlib
- tests/factories.ts: added randomPrivateLogResult / randomPublicLogResult factories.
- interfaces/{archiver,aztec-node}.test.ts: mocks migrated to the new methods.
end-to-end and pxe test files: migrated the few consumers that still referenced the
old method names. Required contractAddress added to PublicEventFilter in e2e events.
11/20 of the new log_store.test.ts cases pass; 9 fail on test-setup helper ergonomics
(multi-block sequencing) and will be fixed in the final verification step.
Phase 4 of the log retrieval API redesign. - migration_notes.md: document the 4 -> 2 method collapse on AztecNode, removal of getContractClassLogs and getPublicLogs(LogFilter), new PrivateLogsQuery / PublicLogsQuery / LogResult / LogCursor shapes, required contractAddress on PublicEventFilter, afterLog: LogId -> LogCursor, dropped re-exports, CLI get-logs flag changes, and the txHash xor block-range rule. Links LOG_API_REDESIGN.md as the canonical reference. - operator changelog v5: ARCHIVER_DB_VERSION 6 -> 7, archiver self-wipes and re-syncs from L1 on first start; no config change required.
Final-verification cleanups after Stages 1-4: - archiver/log_store, archiver/block_store: drop async on methods that only return a promise from a function call (require-await lint). - aztec.js/wallet: extract EventFilterMutexShape so the refine generic constraint fits on one line (formatter was re-introducing malformed inline JSDoc comments otherwise). - kv-store/binary_map.test: switch from @jest/globals to vitest (the package uses vitest); drop async on the carry-boundary test. - pxe/logs/log_service.test: pass includeEffects: true to randomPrivateLogResult so the mocks match log_service's actual query; rewrite the block-range tests to mock the node returning only in-range logs (range filtering moved into the node) and assert the range was forwarded. - archiver/store/log_store.test: rewrite multi-block test setup to seed a sequential previousArchive chain via blockStore.addCheckpoints (BlockStore enforces a contiguous chain starting from block 1); switch the synchronous-validation tests to expect-throws-sync; tighten the cross-tx no-overlap assertion in the page-boundary test. Final state: yarn build green, yarn lint clean, all modified unit tests pass.
Two real bugs and one perf miss caught by post-implementation review: archiver/log_store - #validateQuery: when txHash is set alongside any tag's afterLog, the cursor's txHash must equal query.txHash. Previously, a mismatched cursor placed `start` before the tx range while `end` stayed tx-strict, causing the scan to leak logs from intervening txs sharing the same tag. Cursors always come from a prior in-tx result, so well-behaved callers are unaffected; this check rejects the edge case loudly. - #resolveCursor: throw when the cursor's tx is unknown instead of silently falling back to (cursor.blockNumber, 0), which would have re-yielded earlier logs in that block. A missing cursor tx means the chain reorged under the client and they should re-sync. archiver/block_store - getTxLocation: read only the IndexedTxEffect header (blockHash + blockNumber + txIndexInBlock = 40 bytes) instead of fully deserializing the entire TxEffect. Matches the plan's O(1) intent and meaningfully shrinks the hot path under txHash and afterLog queries. cli/aztec_node - get-logs action: the command option is --node-url (-> nodeUrl in commander), not aztecNodeRpcUrl. The destructure was receiving undefined and passing it to createAztecNodeClient.
Removes types and tests that became unreferenced after the tag-based log retrieval API landed: LogFilter, LogId, TxScopedL2Log, ExtendedPublicLog, ExtendedContractClassLog, GetPublicLogsResponse, GetContractClassLogsResponse, their barrel re-exports, and the matching TxScopedL2Log random factories. Also drops the now-unused logsMaxPageSize / maxLogs plumbing through the archiver store, factory, config, env var, and snapshot-sync types, and the OutOfOrderLogInsertionError that no code throws. Updates two PXE comments that still referenced TxScopedL2Log to point at LogResult.
…ents - delete the operator v5.x changelog entry and its index link - remove the LOG_API_REDESIGN.md link from migration notes - drop section-separator banner comments in archiver log_store - drop the "removed in v7" notes from archiver-sync.test.ts
…rsor and results LogResult now carries txIndexWithinBlock alongside txHash, matching the archiver's composite-key ordering. LogCursor drops txHash in favor of txIndexWithinBlock so resuming pagination no longer needs a tx-hash lookup; the cursor's (blockNumber, txIndexWithinBlock, logIndexWithinTx) slot directly into the composite-key start computation in LogStore. Other adjustments along the way: - move parseOptionalLogCursor into stdlib as LogCursor.parseOptional; the CLI wraps it to surface InvalidArgumentError - add LogsQueryBase.limitPerTag (capped at MAX_LOGS_PER_TAG) so tests can force pagination without hitting the prod cap - add LogStore.getAllPrivate/PublicLogsForBlock for tests that need a per-block view; data_store_updater.test.ts uses these in place of the removed (contract, tag) sweep helper - introduce queryAllPrivate/PublicLogsByTags in stdlib that drives the per-tag afterLog loop, and refactor PXE's getAllPages* to delegate
… refine - add archiver block_store unit tests for getTxLocation and getNoteHashesAndNullifiers, including a partial-vs-full deserializer agreement check - add log_store tests for the per-block view helpers and the new stdlib queryAll*ByTags drivers (limitPerTag forces multi-round pagination) - export refineTxHashAndRange from stdlib and reuse it in aztec.js wallet event-filter schemas in place of the local copy
… of AztecBinaryMap Replace the AztecAsyncBinaryMap abstraction with hex-string keys over the existing AztecAsyncMap. Composite log keys are now `[contract-]tag-blockHex8-txIdxHex8-logIdxHex8` (separator '-', each numeric segment zero-padded to 8 lowercase hex chars). Fixed-width zero-padded hex preserves lexicographic order under ordered-binary's string encoding, so range scans still answer in canonical (contract, tag, blockNumber, txIndexWithinBlock, logIndexWithinTx) order. Deletes the AztecAsyncBinaryMap interface, the LMDB v2 implementation, `openBinaryMap` on AztecAsyncKVStore (and the stubs on v1/IDB/OPFS), the raw-key helpers in lmdb-v2/utils.ts, and the binary_map test file. Adds a 24th log_store test that writes 1000 logs across 10 sequential blocks and confirms entriesAsync returns them in canonical composite order, exercising the hex-string key encoding directly.
…pts narrowing
Downgrades LogResult from a class to a plain type modeled on BlockResponse:
`LogResultBase + IfFlag<LogIncludeOptions, Opts, 'includeEffects', ...>`.
`toBuffer`/`fromBuffer` are removed (no production callers — the archiver's
log_store has its own StoredLogValue codec). `equals` is removed (tests now
compare via `toEqual`). `schema` becomes the `LogResultSchema` constant,
`toHumanReadable` becomes the `logResultToHumanReadable` free function, and
`random` becomes `randomLogResult`.
The L2LogsSource / AztecNode interfaces stay non-generic (return the widest
LogResult shape) — JSON-RPC schema validation can't preserve a stricter
narrowing across the wire. The pxe `getAllPrivateLogsByTags` /
`getAllPublicLogsByTagsFromContract` wrappers are generic over Opts so
call-site code can pass `{ includeEffects: true }` and receive a narrowed
`LogResult<{ includeEffects: true }>[][]` (no more lying non-null
assertions on `log.nullifiers!`).
Threads `limitPerTag` through both wrappers (`getAllPrivateLogsByTags`, `getAllPublicLogsByTagsFromContract`) into the underlying stdlib query so callers can force pagination at a small page size — primarily useful for tests, but also lets the recipient-sync path scope an upper bound.
…sync Recipient sync now passes `toBlock: anchorBlockNumber + 1` to the node instead of filtering post-anchor logs in memory. The node enforces the upper bound during the range scan, avoiding the extra round-trip cost of fetching logs the client immediately drops. Updates the corresponding test to mock the node honoring `toBlock` and asserts the bound is propagated.
…e anchor The PXE anchors to the genesis block during early sync and passes its hash as the referenceBlock of a tagged-log query. Genesis is synthetic and never indexed in the block store, so the reorg check threw "Reference block not found" and broke e2e sync. LogStore now requires the genesis block hash and resolves a matching referenceBlock to the genesis block number rather than treating it as a reorg; the hash is threaded from the archiver factory through createArchiverStore/createArchiverDataStores. Also includes related log store refactors from the same branch session: extract the key/value codec into log_store_codec.ts free functions with a unit suite, scope logIndexWithinTx independently for private and public logs within a tx, and rename getAllPrivate/PublicLogsForBlock to getPrivate/PublicLogsForBlock.
The log retrieval API redesign removed aztecNode.getPublicLogs and made PublicEventFilter.contractAddress required. Update the aztecjs_advanced sample and how_to_read_data prose accordingly so the docs build type-checks.
Replace the hand-rolled per-tag afterLog pagination loop in the get-logs command with stdlib queryAllPublicLogsByTags, which drains all pages internally. --follow keeps polling indefinitely by draining all currently-available logs each round and carrying the last cursor forward.
PrivateLogsQuerySchema/PublicLogsQuerySchema constrained tags with .min(1) but no upper bound, so the node accepted an unbounded tags array even though the PXE chunks at MAX_RPC_LEN. Add .max(MAX_RPC_LEN) to both schemas so the server enforces the same bound clients assume.
Doubles the per-tag log page size from 10 to 20. Updates the archiver log store pagination tests whose fixtures were sized for the old limit (the multi-page cases now generate enough logs to fill a 20-entry page, with the single-tx case sized off MAX_LOGS_PER_TAG so it survives future bumps).
The events_and_logs guide used the removed node.getPublicLogs(LogFilter); read raw public logs from getBlock(n, { includeTransactions: true }).body.txEffects[*].publicLogs instead, matching the migrated aztec.js examples.
Rebase onto merge-train/spartan merged a base-branch test that still used the old (tags) callback signature; align it with the query-based mock used by the other cases in the file.
55749bb to
fe3fe4c
Compare
This was referenced May 29, 2026
danielntmd
pushed a commit
to danielntmd/aztec-packages
that referenced
this pull request
Jun 4, 2026
BEGIN_COMMIT_OVERRIDE test(e2e): unskip pipelining related e2e tests (AztecProtocol#23642) fix(archiver): prune blocks without proposed checkpoint by end of build slot (AztecProtocol#23606) test: migrate benchmarks to pipelining setup (AztecProtocol#23647) fix(p2p): fall back to archiver in BLOCK_TXS response validation (AztecProtocol#23624) docs(slashing): align operator and slasher docs with AZIP-7 (AztecProtocol#23494) fix(p2p): do not penalize peers that signal a missing block with Fr.ZERO (AztecProtocol#23672) chore: adjust metrics deployment (AztecProtocol#23676) fix(cheat-codes): warpL2TimeAtLeastBy advances relative to leading clock (AztecProtocol#23675) chore: tighten node pool sizes (AztecProtocol#23678) chore: remove archival nodes (AztecProtocol#23630) chore: merge blob sink duties into RPC node (AztecProtocol#23631) fix: sync avm-transpiler Cargo.lock with noir submodule (AztecProtocol#23683) fix(spartan): set validator lag env vars in tps-scenario (AztecProtocol#23684) fix: make world-state hash queries reorg-aware to close getWorldState race (AztecProtocol#23677) fix: pin noir submodule to next's version on merge-train/spartan (AztecProtocol#23690) fix: ensure image ref is used by bench runner (AztecProtocol#23682) fix(ci): retry aztec-nr nargo dependency clone on transient network flake (AztecProtocol#23653) chore: run one-off jobs on network nodes (AztecProtocol#23701) fix: simulate proposals inside target slot (AztecProtocol#23692) chore: smaller eth-devnet (AztecProtocol#23704) chore: enable testnet autoscaling (AztecProtocol#23705) feat(api)!: redesign node log retrieval API around tag-based queries (AztecProtocol#23625) fix(sequencer): set own proposed checkpoint locally instead of via p2p loopback (AztecProtocol#23659) END_COMMIT_OVERRIDE
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
The node exposed four log-retrieval methods with three filter shapes and two return shapes, while the private index was only tag-keyed — so a
(tag, narrow block range)query loaded the entire per-tag history into memory. Pagination was a single globalpagecounter shared across all tags, and the public path was split between aLogFiltermethod and a tag-based method. v5 is already a breaking release, so this collapses everything to a single, fast, tag-based surface with no back-compat.Fixes A-1111
Fixes A-1031
Approach
Two methods,
getPrivateLogsByTags(query)andgetPublicLogsByTags(query), replace the four old ones;getContractClassLogsandgetPublicLogs(LogFilter)are gone. The archiver stores each log under a fixed-width composite hex-string key[contractHex] - tagHex - blockHex8 - txIndexHex8 - logIndexHex8in an LMDB map, so every supported filter (tag, block range, txHash, per-tagafterLogcursor,referenceBlockreorg cap) reduces to a single ordered range scan. Note hashes and nullifiers are never copied into the log index — they're fetched on demand from the block store via a partial deserializer that reads only the relevant prefix of the storedIndexedTxEffect.ARCHIVER_DB_VERSIONbumps 6 → 7, so the archiver self-wipes and re-syncs from L1 on first start.API
Two node methods replace the previous four. Each returns one inner array per element of
query.tags, in input order; an empty inner array means that tag matched nothing.Input
Output
Pagination is per-tag: feed a tag's last
LogResultback as the next query'safterLog({ tag, afterLog: LogCursor.fromLog(last) }). A tag is exhausted once it returns fewer thanlimitPerTagresults. The stdlib helpersqueryAllPrivateLogsByTags/queryAllPublicLogsByTagsdrive this loop and return the fully-drained results.Changes
LogResult,LogCursor,PrivateLogsQuery/PublicLogsQuerytypes with zod schemas;txHash⊕fromBlock/toBlockenforced via.refine(buttxHash+afterLogis allowed, to paginate within a tx's logs).L2LogsSource/AztecNode/Archiverinterfaces and schemas reduced to the two new methods. DeletedLogFilter,LogId,TxScopedL2Log,ExtendedPublicLog,ExtendedContractClassLog,GetPublicLogsResponse,GetContractClassLogsResponse, and the deadTx.getPublicLogs(logsSource).LogStorerewrite — two hex-string-keyedAztecAsyncMapprimary maps (keys are fixed-width zero-padded lowercase hex, soordered-binary's string ordering matches the canonical(contract, tag, block, txIndex, logIndex)tuple and every filter is a single ordered range scan) plus twoblockNumber → string[]secondary indices drivingdeleteLogs(replaces the buggy per-block tag-union list). All reads, including thereferenceBlockexistence check, run inside onedb.transactionAsyncacrossBlockStore+LogStore; areferenceBlockequal to the (synthetic, unindexed) genesis block hash resolves to the genesis block number rather than throwing. NewBlockStore.getNoteHashesAndNullifiers(txHashes)is a batched partial deserializer forincludeEffects, andgetTxLocationreads only the 40-byte header instead of the fullTxEffect. Contract-class-log storage removed entirely.ARCHIVER_DB_VERSION6 → 7.OutOfOrderLogInsertionErrorand theARCHIVER_MAX_LOGSenv var dropped.referenceBlockresolution moved into the store so it shares the transaction.getAllPagesrewritten from a globalpagecounter to per-tagafterLogcursors — each round re-queries only tags that returned a full page, and tags drop out as soon as they return a short page.fromBlock/toBlockare pushed down into the node, eliminating the in-memory#extractLogsrange filter.getPublicEventsmigrated to the new query shape;PublicEventFilter.contractAddressis now required;EventFilterBase.afterLog: LogId → LogCursor.get-logsrequires--contract-addressand--tag;--after-logparses aLogCursorstring<blockNumber>-<txIndexWithinBlock>-<logIndexWithinTx>.e2e_orderingrewritten to readgetBlock().body.txEffects[*].publicLogsdirectly (the new API drops the tag-less, contract-less query shape).