Repository navigation
Conversation
mo4islona
marked this pull request as ready for review
October 22, 2026 23:33
mo4islona
force-pushed
the
feat/sets
branch
2 times, most recently
from
October 23, 2026 14:47
3ccda85 to
f3cee89
Compare
A client that filters on hundreds of thousands of addresses cannot send them as a list. It declares a fuse8 structure over them once, under `sets`, and lists a reference to it among a filter's values. The engine only looks values up, by the text the response renders, and refuses a set its canaries do not vouch for: a set built from other bytes matches almost nothing, which reads as a quiet stretch of chain (ADR-17). A set costs a few dozen bytes to declare again under another name, and each reference is a lookup for every row its table scans, so the references a request makes are capped. A catalog that names a table or an alias after a reserved key, `sets` now among them, is refused, since nothing could address it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The 2 MiB came from the reference's HTTP server, not from the engine. The portal's admission and the network's 4 MiB message are the bounds a deployment works under, and a set is bounded by nothing but its bytes, so the engine's bound now sits above them, where neither has to wait for an engine release. Parsing the worst request, short list values, takes up to 452 MiB at 16 MiB. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This was referenced Oct 23, 2026
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.
Implements sets, the filter on value sets too long for a list, as
spec/specifies them (ADR-17, still Proposed), and turns the request bound into a 16 MiB backstop. The research and the measurements behind the design are in #84. Part of #82.The request
{ "type": "evm", "sets": { "pairs": { "fuse8": "<base 64>", "canaries": ["0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc", "0x0d4a11d5…"] } }, "fields": { "log": { "address": true } }, "logs": [{ "address": [{ "set": "pairs" }], "topic0": ["0x1c41…"] }] }A set is declared once under
sets, and filters refer to it by name, as an element of their list. An engine without sets refuses thesetskey withUnknownTable, so the portal can send a request that carries one only to an engine that serves it.Use cases
A set is for a list too long to send. A list of a few thousand values still fits, and it is the better choice: it prunes row groups by their statistics and returns no extra rows. A set prunes none, and about one value in 256 that it does not hold passes, so the client drops the extra rows with its own exact check.
"address": [{ "set": "pairs" }]."address": ["0x<new pool>", "0x<new pool>", { "set": "pairs" }]."address": [{ "set": "uniswap-v2" }, { "set": "sushiswap" }]."transactions": [{ "from": [{ "set": "wallets" }] }, { "to": [{ "set": "wallets" }] }]. The same goes for the two owners of a Solana token balance,preOwnerandpostOwner.A set works only on columns the response renders as text (
hexBytes,base58,utf8): addresses, hashes, Solana accounts. The request must select the filtered field, because the client can drop only the extra rows it sees.What the engine does
UnknownSetfor an undeclared name,InvalidSetfor an unused set or a malformedfuse8,InvalidFilterValuefor a reference on a filter that is not over text,SetFieldNotSelectedwhen the response does not render the column, andRequestTooLargepast 64 references a request: each is a lookup on every scanned row, and a set is cheap to declare again under another name.SetCanaryMismatch). A set built from the wrong text (checksummed, without0x, raw bytes, another seed) is refused instead of matching almost nothing.skipRepeatsmade unconditional. A list with a set prunes no row group.xorfonly builds sets in tests.P-MAX-REQUEST-BYTESto a 16 MiB backstop (second commit). The bounds that matter are the portal's admission and the network's 4 MiB message, and the engine's bound now sits above them. The worst 16 MiB request takes 452 MiB and 0.3 s to parse and compile. ADR-13 gets a revision section.sets(INV-D10).Tests
set_references_are_bounded_in_number: either side of the cap in one list and across items; one set named from more items than the cap is refused, a name repeated in one list counts oncea_set_is_declared_used_and_well_formed; soundness unit tests insrc/sets.rsa_set_stands_only_in_a_list_of_text: a reference on every filter of every bundled catalog, tables and aliases, alone and beside a valuea_sets_canaries_are_values_it_holds: five wrong builds, case folding per column, canary boundsa_set_filtered_column_reaches_the_responsea_set_passes_exactly_what_its_lookup_passes: on every fixture chunk and every filter that takes a set, the answer equals the answer to a list of every valuexorf's own lookup passes, and holds the members' answer; then the same members as a list of a third of them beside two sets of the rest. The sweep asserts that it saw false positives. Unit tests cover dictionaries, byte columns, verdict reuse and a list of values beside two sets.The traceability matrix moves to 70 C, 21 P, 0 U of 91 (0.77).
Benchmarks
One core, on two real chunks as the network serves them now:
evm(ethereum-mainnet-5, 88 blocks, 33 MB) andsolana-hot(solana-mainnet-3, 48 slots). The set holds the addresses that emitSyncin the chunk (on Solana, one token-balance owner in twenty), padded with random values to 400 000. The other structures are #84's prototype, measured in the same session; its numbers are withskipRepeatson, its better setting.logs.address, 400 000 valuesinList, 40 000 valuesxorf)What the numbers say:
xorf's: 7.6 against 8.5 ns per row ontransactions.from, where no value repeats.logs.address, and ontokenBalances.postOwner, which is not in its table's sort key but repeats beside the account that is. It costs 0.55 ns a row ontransactions.from, where values do not repeat. A first version did it only on sort-key columns; this is why it now does it everywhere.All cases
Predicate cost per row in ns, by when a repeated value is looked up once (this PR):
logs.addresstransactions.fromtokenBalances.postOwnerAll structures, 400 000 values. "Request" is the whole request on one page, "ns/row" the predicate alone over the column; the prototype's rows are without and with
skipRepeats.logslogsinList40klogslogslogslogslogslogstransactionstransactionsinList40ktransactionstransactionstransactionstransactionstokenBalancestokenBalancesinList40ktokenBalancestokenBalancestokenBalancestokenBalancesThe set is built from pair addresses, and no pair sends a transaction, so on
transactions.fromevery row it passes is extra: it measures the predicate and the false positives alone.inListcarries the first 40 000 values, the most that fit the old 2 MiB bound. Extra rows are 28 at 400 000 values and 325 at 2.7 million: the share of values that pass by chance does not depend on the set's size, so the difference is which addresses happened to pass and how many rows each has. The "this PR" request times were taken before reuse became unconditional, sotransactionsandtokenBalancesused the sort-key rule there.Gates
Per commit, on the pinned Rust 1.89. The full suite is
cargo test --workspace --features legacy-query -- --include-ignoredwith fixture chunks required, the reference comparisons included.make testA mutant that drops a list's values beside its sets fails the sweep.
Not in this PR
sets, and a query size limit that admits large sets. The portal's limit is set below the engine's today, so it decides how large a set a client can send.🤖 Generated with Claude Code