Test coverage and a total order for verificationKeyUpdates - #1
Merged
mellowcroc merged 2 commits intoSep 2, 2026
Merged
Conversation
The base archive_db.sql fixture has 227 blocks_zkapp_commands rows and every one has status 'failed'. It holds one verification-key hash and one account update that sets a verification key. No input can make getVerificationKeyUpdatesQuery return a row against it, so the only assertion available was the empty list. That gap was measured, not assumed: replacing the query body with one that returns nothing at all (AND 1=0) left all 63 integration tests green. verification_key_updates.sql adds blocks 26..32 on top of the base canonical tip, with eleven accounts that each make one distinction observable: alpha sets the target key -> returned beta sets a different key -> filtered by hash gamma sets the target key, on a CUSTOM token -> proves the token join delta sequence_no 0 at height 28 -> command order epsilon sequence_no 1 at height 28 -> command order zeta sets the target key, command FAILED -> excluded eta target hash as a PRECONDITION only -> excluded theta sets the target key in an ORPHANED block -> excluded iota sets the target key in a pending block -> pending only kappa carried by BOTH competing tips at height 32 lambda carried by fork B only eta is the important one. The archive records the key an account update SETS in zkapp_updates.verification_key_id, reached through zkapp_account_update_body.update_id, and the key it merely REQUIRES in zkapp_account_update_body.verification_key_hash_id. A query that reads the second column answers "who called this contract" instead of "who deployed it". The two tips at height 32 carry the same command, which is what a real fork looks like: on the devnet archive one zkApp command was measured in 8 blocks at a single height. Their rows agree on height, sequence_no, account-update position and zkapp_account_update.id alike. The suite now has power. Against the query as it stands: returns nothing (AND 1=0) 10 of 15 tests fail reads the precondition column instead 11 of 15 tests fail failed commands not excluded 14 of 15 tests fail Like action-state-ordering.test.ts, this runs on its own database: the fixture changes the maximum height and the pending chain, which the other integration tests assert on. The placeholder test in integration.test.ts is removed. Its one assertion, that a failed deployment is not discoverable, is now zeta. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmkAwbAi2ogrSg4XU9tYAz
…ates Ordering -------- ORDER BY was height, sequence_no, account-update position, then zkapp_account_update.id. None of those separates two competing tips at the same height, and pending_chain seeds from every block at the maximum pending height, so both tips are in the answer. When both carry the same command — the normal case during a reorg, and one command was measured in 8 blocks at a single height on the devnet archive — all four keys tie and the order was whatever the plan produced. Observed on the new fixture: fork B came back before fork A. state_hash is now the second key, so the order is total and groups by block. Cost ---- The verification-key hash was matched last, after the join tree had already expanded every account update in the range. The planner cannot drive from the hash instead, because zkapp_updates.verification_key_id and zkapp_account_update_body.update_id are both unindexed, so it hashed all of zkapp_updates (173k rows) and zkapp_account_update_body (365k rows) on every call and spilled to disk. The hash is now resolved to its (small) set of zkapp_updates rows in a MATERIALIZED CTE before any block is touched, and applied as a semi-join. Measured on a 681k-block devnet archive, over the 10 000-block maximum a client may request: before 405 ms, sequential scans, hash join spilling to 8 batches after 199 ms That is a shape improvement, not a cure. The remaining cost is inherent while those two columns are unindexed, and the indexes belong to the archive schema in the mina repository, not here. Their effect is still to be measured on a dump we can index, and mainnet is much larger than this one. Behaviour is unchanged: same parameters in the same positions, and the precondition column is still never read. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmkAwbAi2ogrSg4XU9tYAz
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.
For o1-labs#225. Merging this into your branch updates that PR.
This is the part of the review we said was ours rather than yours — it needs a
fixture and a mainnet-scale dump, and you should not have to guess at either.
The query you wrote is correct; I verified the positive path by hand against a
681,000-block devnet archive before writing any of this. Nothing here changes
what the query means.
1. A fixture that can fail
archive_db.sqlhas 227blocks_zkapp_commandsrows and every one has statusfailed. It holds one verification-key hash and one account update that sets averification key. No input can make the query return a row against it, which is
why the only assertion available to you was the empty list.
That is measured, not argued. Replacing the query body with one that returns
nothing at all (
AND 1=0) left all 63 integration tests green.fixtures/verification_key_updates.sqladds blocks 26…32 on top of the basecanonical tip, with eleven accounts that each make one distinction observable:
sequence_no0 and 1 at height 28etais the one worth keeping. It is the wrong-column trap your PR alreadyavoids, now pinned down by a test: the target hash sits in
zkapp_account_update_body.verification_key_hash_id(the key the updaterequires) while its
zkapp_updatesrow hasverification_key_id IS NULL. Aquery that reads that column answers "who called this contract" instead of "who
deployed it".
The suite has power now. Against the query as it stands on your branch:
AND 1=0)It runs on its own database, like
action-state-ordering.test.tsdoes: thefixture changes the maximum height and the pending chain, and the other
integration tests assert on both.
The placeholder test in
integration.test.tsis removed — its one assertion,that a failed deployment is not discoverable, is now
zeta.The fixture is generated, not hand-written:
node tests/integration/fixtures/generate-verification-key-fixture.mjs. Thetests read the expected values from the JSON the same script emits, so they
cannot drift from the SQL.
2. A total order
ORDER BYwas height,sequence_no, account-update position, thenzkapp_account_update.id. None of those separates two competing tips at thesame height, and
pending_chainseeds from every block at the maximum pendingheight, so both tips are in the answer.
When both tips carry the same command all four keys tie. That is the normal case
during a reorg, not a corner: on the devnet archive one zkApp command appears in
8 blocks at a single height. On the new fixture the two rows came back fork B
first, then fork A.
b.state_hashis now the second key. The order is total and groups by block.3. Cost
The hash was matched last, after the join tree had expanded every account update
in the range. The planner cannot drive from the hash instead, because
zkapp_updates.verification_key_idandzkapp_account_update_body.update_idare both unindexed, so it hashed all of
zkapp_updates(173k rows) andzkapp_account_update_body(365k rows) on every call and spilled to disk.The hash is now resolved to its small set of
zkapp_updatesrows in aMATERIALIZEDCTE before any block is touched, then applied as a semi-join.Measured on the 681k-block devnet archive, over the 10,000-block maximum a
client may request:
This is a shape improvement, not a cure. The remaining cost is inherent
while those two columns are unindexed, and those indexes belong to the archive
schema in the mina repository, not to this one. Their effect still has to be
measured on a dump we can index, and mainnet is much larger than this one. We
will follow that up separately; it should not hold your PR.
Still yours, and deliberately left alone
client.unsafeso your unit testkeeps passing unchanged, and left the change to you
public_keysandtokensinUSED_TABLESdocs/getting-started.mdChecks
npm run build,npm run lint,test:unit(all pass, yours unchanged) andtest:integration(77 tests, was 63).🤖 Generated with Claude Code
https://claude.ai/code/session_01MmkAwbAi2ogrSg4XU9tYAz