fix(archiver): key contract instance updates by the block that carried them - #230
Open
spalladino wants to merge 3 commits into
Open
spalladino wants to merge 3 commits into
spalladino wants to merge 3 commits into
Conversation
|
spalladino
force-pushed
the
spl/archiver-contract-update-block-ordering
branch
from
September 17, 2026 18:19
394b962 to
6f72f04
Compare
spalladino
changed the base branch from
spl/a-2041-append-messages-before-txs
to
spl/fast-inbox-network
September 17, 2026 18:20
spalladino
added this pull request to stack #217
September 17, 2026 18:20
spalladino
force-pushed
the
spl/archiver-contract-update-block-ordering
branch
from
September 17, 2026 18:36
6f72f04 to
459530b
Compare
spalladino
force-pushed
the
spl/archiver-contract-update-block-ordering
branch
from
September 17, 2026 21:15
459530b to
9e71ab4
Compare
…d them The archiver indexed each scheduled contract class change under [address, schedulingTimestamp, indexWithinBlock]. Two blocks that share a timestamp therefore collide: the later block's update lands on the same key as the earlier one, so a lookup can resolve the wrong class id, and unwinding the later block deletes the entry the earlier block had written. Key stored updates by [address, schedulingTimestamp, blockNumber, indexWithinBlock] instead, and thread the block number through ingestion and rollback so insertion and deletion agree on the key. Timestamps are encoded as 20-digit decimal strings so that lexicographic key ordering matches numeric ordering; previously an update at a timestamp with fewer digits than the queried one fell outside the lookup range entirely. Range boundaries keep their own two-component type, so a partial key can no longer be built where a stored one is expected. ARCHIVER_DB_VERSION moves to 11: the two layouts cannot coexist in one map and the colliding entries cannot be reconstructed from the surviving index, so nodes resync the archiver on upgrade. Noted in the migration notes.
Supersedes the "Noted in the migration notes" line in the previous commit: the ARCHIVER_DB_VERSION bump carries no migration-notes entry.
Range boundaries were typed as the map's own key type, so a store keyed by a tuple could not express "every key under this address" without inventing values for the components it did not want to constrain. Callers worked around it by widening the map's key type to a union of the stored key and the boundary shape, which also made the key type stop proving that only whole keys are ever stored. Range<K> now takes KeyPrefix<K>: for a tuple key that is the key itself or any of its prefixes, and for every other key type it is the key type unchanged. Array keys already order element-wise in all four backends, so a prefix is a well-defined boundary; this only lets the type say so. Backend helpers that serialize a boundary take Key rather than K, since TypeScript cannot prove K extends KeyPrefix<K> while K is still generic, and they only ever hand the value to the key encoder. mapRange returns CustomRange<K> for the same reason; its result is still assignable at concrete call sites. The archiver's contract instance update map drops its union and is keyed by the stored key alone, and its two key builders become private. Test coverage is added to the shared map suite, so it runs against lmdb, lmdb-v2, indexeddb and sqlite-opfs. It asserts on values rather than keys: lmdb-v2 does not round-trip a tuple key through keysAsync, which is a separate pre-existing issue.
spalladino
force-pushed
the
spl/archiver-contract-update-block-ordering
branch
from
September 18, 2026 01:28
9e71ab4 to
8636218
Compare
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.
Stacked on #216, the tail of the fast inbox stack. Three commits; review the top ones only.
Problem
The archiver indexes each scheduled contract class change under
[address, schedulingTimestamp, indexWithinBlock]. The block that carried the update is not part of the key, so two blocks that share a timestamp collide:getCurrentContractInstanceClassIdcan resolve the stale class id.Separately, timestamps were stored with
toString(). Keys compare lexicographically, so an update scheduled at a timestamp with fewer digits than the queried one sorted outside the lookup range and was missed entirely —99is not found by a query at100.Change
[address, schedulingTimestamp, blockNumber, indexWithinBlock]. The block number is threaded throughupdateContractDataOnDbinto bothaddContractInstanceUpdatesanddeleteContractInstanceUpdates, so insertion and rollback agree on the key.kv-store: prefix range boundaries (second commit)Range<K>typed itsstart/endas the map's own key type, so a store keyed by a tuple could not express "every key under this address" without inventing values for the components it did not want to constrain. The existing workaround — and what this PR first did — was to widen the map's key type to a union of the stored key and the boundary shape, which also makes the key type stop proving that only whole keys are stored.Range<K>now takesKeyPrefix<K>: a tuple key or any of its prefixes, and other key types unchanged. Array keys already order element-wise in all four backends, so a prefix is a well-defined boundary — this only lets the type say so. The archiver map then drops its union and its key builders become private.Backend helpers that serialize a boundary take
Keyrather thanK, because TypeScript cannot proveK extends KeyPrefix<K>whileKis generic; they only hand the value to the key encoder.mapRangereturnsCustomRange<K>for the same reason.Coverage is added to the shared map test suite, so it runs against lmdb, lmdb-v2, indexeddb and sqlite-opfs.
Database version
ARCHIVER_DB_VERSIONgoes from10to11. The two key layouts cannot coexist in one map, and entries lost to a collision cannot be reconstructed from the surviving index, so renaming the map is not enough — it would leave a node marked synchronized with an empty update index. The version manager resets the archiver database and normal synchronization rebuilds it from block logs.Operational consequence: nodes resync their archiver on first start under this version. Not currently written up in the migration notes.
Tests
Red first, then green.
data_store_updater.test.ts, driving realContractInstanceUpdatedpublic logs through the updater:contract_instance_store.test.ts: same timestamp across different blocks, multiple updates within one block, deletion revealing the earlier block, address isolation, a later timestamp in a lower block, the decimal digit boundary, before/at activation, and a query at2^64 - 1.Archiver suite 693 passing, kv-store suite 292 passing. Build, format and lint clean.
Noted while here, not fixed
lmdb-v2 does not round-trip a tuple key through
keysAsync—deserializeKeyreturnsparsed[1], butordered-binaryflattens the nested array, so a['a', 1, 0]key reads back as'a'. Pre-existing and unrelated to ranges; nothing hits it today because tuple-keyed stores read values rather than keys. The new suite test asserts on values to stay clear of it.