diff --git a/src/db/sql/verification-key-updates/queries.ts b/src/db/sql/verification-key-updates/queries.ts index 8fe110c..761816d 100644 --- a/src/db/sql/verification-key-updates/queries.ts +++ b/src/db/sql/verification-key-updates/queries.ts @@ -57,6 +57,20 @@ export function getVerificationKeyUpdatesQuery( WHERE parent.chain_status <> 'canonical' AND child.id <> child.parent_id ), + -- Resolve the requested hash to the set of zkapp_updates rows that SET + -- that verification key, before touching any block. The set is small — one + -- row per distinct update that ever wrote this key — and MATERIALIZED + -- keeps the planner from inlining it back into the join tree, where it + -- would hash the whole of zkapp_updates and zkapp_account_update_body once + -- per query. Measured on a 681k-block devnet archive over the maximum + -- 10 000-block range: 405 ms inlined, 199 ms materialised. + target_updates AS MATERIALIZED ( + SELECT zu.id + FROM zkapp_verification_key_hashes vkh + INNER JOIN zkapp_verification_keys vk ON vk.hash_id = vkh.id + INNER JOIN zkapp_updates zu ON zu.verification_key_id = vk.id + WHERE vkh.value = $1 + ), full_chain AS ( SELECT b.* FROM blocks b @@ -71,7 +85,7 @@ export function getVerificationKeyUpdatesQuery( zau.id AS account_update_id, pk.value AS address, t.value AS token_id, - vkh.value AS verification_key_hash, + $1::text AS verification_key_hash, b.state_hash, b.parent_hash, b.height, @@ -95,21 +109,29 @@ export function getVerificationKeyUpdatesQuery( INNER JOIN LATERAL UNNEST(zc.zkapp_account_updates_ids) WITH ORDINALITY AS update_ref(id, position) ON TRUE INNER JOIN zkapp_account_update zau ON zau.id = update_ref.id + -- An account update SETS a verification key through update_id. The other + -- column that names a verification key on this table, + -- verification_key_hash_id, is the PRECONDITION: the key the update + -- requires the account to already have. Filtering on that one would answer + -- "who called this contract" instead of "who deployed it". INNER JOIN zkapp_account_update_body zaub ON zaub.id = zau.body_id - INNER JOIN zkapp_updates zu ON zu.id = zaub.update_id - INNER JOIN zkapp_verification_keys vk ON vk.id = zu.verification_key_id - INNER JOIN zkapp_verification_key_hashes vkh ON vkh.id = vk.hash_id + AND zaub.update_id IN (SELECT id FROM target_updates) INNER JOIN account_identifiers ai ON ai.id = zaub.account_identifier_id INNER JOIN public_keys pk ON pk.id = ai.public_key_id INNER JOIN tokens t ON t.id = ai.token_id - WHERE vkh.value = $1 - AND bzc.status = 'applied' + WHERE bzc.status = 'applied' ${statusClause} + -- state_hash is what makes this order total. Two competing tips sit at the + -- same height, and pending_chain seeds from every block at the maximum + -- pending height, so both are in the answer. A command carried by both — + -- the normal case during a reorg; one command was measured in 8 blocks at + -- a single height on devnet — then produces rows that agree on height, + -- sequence_no, account-update position and zkapp_account_update.id alike. ORDER BY b.height ASC, + b.state_hash ASC, bzc.sequence_no ASC, - update_ref.position ASC, - zau.id ASC + update_ref.position ASC `, params ); diff --git a/tests/integration/fixtures/generate-verification-key-fixture.mjs b/tests/integration/fixtures/generate-verification-key-fixture.mjs new file mode 100644 index 0000000..1720d47 --- /dev/null +++ b/tests/integration/fixtures/generate-verification-key-fixture.mjs @@ -0,0 +1,420 @@ +/** + * Generates `verification_key_updates.sql`. + * + * WHY THIS FIXTURE EXISTS + * ----------------------- + * The base `archive_db.sql` fixture has 227 `blocks_zkapp_commands` rows and + * every one of them has status `failed`. It also holds exactly one + * verification-key hash and one account update that sets a verification key. + * No input can therefore make `getVerificationKeyUpdatesQuery` return a row + * against the base fixture, and a test written on it can only ever assert the + * empty list. + * + * That is not a theoretical gap. Replacing the query body with one that returns + * nothing at all (`AND 1=0`) leaves the whole integration suite green. The base + * fixture cannot tell a working query from a broken one. + * + * WHAT THE FIXTURE CONTAINS + * ------------------------- + * Blocks 26…32 on top of the base fixture's canonical tip at height 25, and + * eleven zkApp accounts. Each account exists to make exactly one distinction + * observable: + * + * height 27 canonical one applied command, three account updates + * pos 1 ALPHA sets TARGET -> returned + * pos 2 BETA sets OTHER (a different hash) -> filtered by hash + * pos 3 GAMMA sets TARGET, on a CUSTOM token -> returned, proves the + * token join is real + * + * height 28 canonical two applied commands, to fix the sequence_no order + * seq 0 DELTA sets TARGET -> returned + * seq 1 EPSILON sets TARGET -> returned + * + * height 29 canonical + * seq 0 ZETA sets TARGET, command FAILED -> excluded + * seq 1 ETA TARGET as a verification-key + * PRECONDITION only -> excluded + * + * height 30 canonical spacer, no commands + * height 30 ORPHANED THETA sets TARGET -> excluded + * height 31 pending IOTA sets TARGET -> pending only + * height 32 pending fork A KAPPA sets TARGET -> pending only + * height 32 pending fork B LAMBDA sets TARGET -> pending only + * + * ETA IS THE IMPORTANT ONE. The archive records a verification key that an + * account update SETS in `zkapp_updates.verification_key_id`, reached through + * `zkapp_account_update_body.update_id`. It records a verification key that an + * account update 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" — for a widely used zkApp that is every caller, + * not every deployment. ETA has the target hash in the precondition column and + * a `zkapp_updates` row with `verification_key_id IS NULL`, so it is returned + * only by the wrong query. + * + * THE TWO FORKS AT HEIGHT 32 are not decoration either. `pending_chain` seeds + * from every block at the maximum pending height, so two competing tips are both + * in the answer. Their rows share height, sequence_no and account-update + * position, so they tie on every key the query originally ordered by, and the + * order of the two rows was whatever the plan happened to produce. + * + * Run: node tests/integration/fixtures/generate-verification-key-fixture.mjs + */ +import { writeFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +const here = dirname(fileURLToPath(import.meta.url)); + +// ── Identifiers ────────────────────────────────────────────────────────── +// Everything this fixture inserts uses ids at or above 910000, so it can never +// collide with the base `archive_db.sql` fixture (max id 243) nor with +// `action_state_order_inversion.sql` (900000…900999). +const BASE_CANONICAL_TIP_HEIGHT = 25; + +const DEFAULT_TOKEN_ID = 1; // wSHV2S4… , from the base fixture. +const CUSTOM_TOKEN_ID = 910001; +const CUSTOM_TOKEN_VALUE = 'wZZZVkFixtureCustomToken00000000000000000000000000'; + +const ZERO_FIELD_ID = 1; // zkapp_field '0', from the base fixture. +const EMPTY_EVENTS_ID = 1; // zkapp_events with element_ids '{}', from the base fixture. +const NO_UPDATE_ID = 1; // zkapp_updates row with verification_key_id IS NULL. +const FEE_PAYER_BODY_ID = 1; // zkapp_fee_payer_body, from the base fixture. + +// The two verification keys this fixture can set. +const TARGET = { + hashId: 910001, + keyId: 910001, + hash: '910000000000000000000000000000000000000000000000000000000000000001', + key: 'AAVkFixtureTargetVerificationKeyBlob', +}; +const OTHER = { + hashId: 910002, + keyId: 910002, + hash: '910000000000000000000000000000000000000000000000000000000000000002', + key: 'AAVkFixtureOtherVerificationKeyBlob', +}; + +// `zkapp_updates` rows: one per verification key this fixture sets. +const SET_TARGET_UPDATE_ID = 910001; +const SET_OTHER_UPDATE_ID = 910002; + +const BLOCK = { + anchor26: { id: 910026, height: 26, status: 'canonical', hash: '3NVkFixtureBlock26' }, + h27: { id: 910027, height: 27, status: 'canonical', hash: '3NVkFixtureBlock27' }, + h28: { id: 910028, height: 28, status: 'canonical', hash: '3NVkFixtureBlock28' }, + h29: { id: 910029, height: 29, status: 'canonical', hash: '3NVkFixtureBlock29' }, + h30: { id: 910030, height: 30, status: 'canonical', hash: '3NVkFixtureBlock30' }, + h30orphan: { id: 910130, height: 30, status: 'orphaned', hash: '3NVkFixtureBlock30Orphaned' }, + h31pending: { id: 910031, height: 31, status: 'pending', hash: '3NVkFixtureBlock31Pending' }, + h32forkA: { id: 910032, height: 32, status: 'pending', hash: '3NVkFixtureBlock32ForkA' }, + h32forkB: { id: 910132, height: 32, status: 'pending', hash: '3NVkFixtureBlock32ForkB' }, +}; + +/** + * Every account this fixture creates. `sets` names the verification key the + * account update writes, or `null` when the update writes none — the case that + * separates a real deployment from a verification-key precondition. + */ +const ACCOUNTS = [ + { key: 'alpha', id: 910001, address: 'B62qVkFixtureAlphaSetsTargetCanonical00000000000000000', token: DEFAULT_TOKEN_ID }, + { key: 'beta', id: 910002, address: 'B62qVkFixtureBetaSetsOtherHash000000000000000000000000', token: DEFAULT_TOKEN_ID }, + { key: 'gamma', id: 910003, address: 'B62qVkFixtureGammaSetsTargetCustomToken0000000000000', token: CUSTOM_TOKEN_ID }, + { key: 'delta', id: 910004, address: 'B62qVkFixtureDeltaSetsTargetSequenceZero0000000000000', token: DEFAULT_TOKEN_ID }, + { key: 'epsilon', id: 910005, address: 'B62qVkFixtureEpsilonSetsTargetSequenceOne000000000000', token: DEFAULT_TOKEN_ID }, + { key: 'zeta', id: 910006, address: 'B62qVkFixtureZetaSetsTargetInFailedCommand00000000000', token: DEFAULT_TOKEN_ID }, + { key: 'eta', id: 910007, address: 'B62qVkFixtureEtaTargetAsPreconditionOnly00000000000000', token: DEFAULT_TOKEN_ID }, + { key: 'theta', id: 910008, address: 'B62qVkFixtureThetaSetsTargetInOrphanedBlock0000000000', token: DEFAULT_TOKEN_ID }, + { key: 'iota', id: 910009, address: 'B62qVkFixtureIotaSetsTargetInPendingBlock000000000000', token: DEFAULT_TOKEN_ID }, + { key: 'kappa', id: 910010, address: 'B62qVkFixtureKappaSetsTargetPendingForkA000000000000', token: DEFAULT_TOKEN_ID }, + { key: 'lambda', id: 910011, address: 'B62qVkFixtureLambdaSetsTargetPendingForkB00000000000', token: DEFAULT_TOKEN_ID }, +]; +const account = (key) => ACCOUNTS.find((a) => a.key === key); + +/** + * One account update per entry. `updateId` decides what the update SETS; + * `preconditionHashId` decides what it merely REQUIRES. + */ +const ACCOUNT_UPDATES = [ + { key: 'alpha', id: 910001, updateId: SET_TARGET_UPDATE_ID, preconditionHashId: null, authorizationKind: 'Proof' }, + { key: 'beta', id: 910002, updateId: SET_OTHER_UPDATE_ID, preconditionHashId: null, authorizationKind: 'Proof' }, + { key: 'gamma', id: 910003, updateId: SET_TARGET_UPDATE_ID, preconditionHashId: null, authorizationKind: 'Proof' }, + { key: 'delta', id: 910004, updateId: SET_TARGET_UPDATE_ID, preconditionHashId: null, authorizationKind: 'Proof' }, + { key: 'epsilon', id: 910005, updateId: SET_TARGET_UPDATE_ID, preconditionHashId: null, authorizationKind: 'Signature' }, + { key: 'zeta', id: 910006, updateId: SET_TARGET_UPDATE_ID, preconditionHashId: null, authorizationKind: 'Proof' }, + // Sets nothing; only requires the target key. Must never be returned. + { key: 'eta', id: 910007, updateId: NO_UPDATE_ID, preconditionHashId: TARGET.hashId, authorizationKind: 'Proof' }, + { key: 'theta', id: 910008, updateId: SET_TARGET_UPDATE_ID, preconditionHashId: null, authorizationKind: 'Proof' }, + { key: 'iota', id: 910009, updateId: SET_TARGET_UPDATE_ID, preconditionHashId: null, authorizationKind: 'Proof' }, + { key: 'kappa', id: 910010, updateId: SET_TARGET_UPDATE_ID, preconditionHashId: null, authorizationKind: 'Proof' }, + { key: 'lambda', id: 910011, updateId: SET_TARGET_UPDATE_ID, preconditionHashId: null, authorizationKind: 'Proof' }, +]; +const accountUpdate = (key) => ACCOUNT_UPDATES.find((u) => u.key === key); + +/** A command names one block, or several when competing tips carry it. */ +const blocksOf = (cmd) => (Array.isArray(cmd.block) ? cmd.block : [cmd.block]); + +/** + * The commands, in the blocks that hold them. `updates` is the account-update + * array in its on-chain order — `zkapp_account_updates_ids` — which is what + * `UNNEST … WITH ORDINALITY` turns into the account-update position. + */ +const COMMANDS = [ + { + id: 910001, block: BLOCK.h27, sequenceNo: 0, status: 'applied', + memo: 'vk-fixture-three-updates', hash: 'CkpZVkFixtureTx910001', + updates: ['alpha', 'beta', 'gamma'], + }, + { + id: 910002, block: BLOCK.h28, sequenceNo: 0, status: 'applied', + memo: 'vk-fixture-seq-zero', hash: 'CkpZVkFixtureTx910002', + updates: ['delta'], + }, + { + id: 910003, block: BLOCK.h28, sequenceNo: 1, status: 'applied', + memo: 'vk-fixture-seq-one', hash: 'CkpZVkFixtureTx910003', + updates: ['epsilon'], + }, + { + id: 910004, block: BLOCK.h29, sequenceNo: 0, status: 'failed', + memo: 'vk-fixture-failed', hash: 'CkpZVkFixtureTx910004', + updates: ['zeta'], + }, + { + id: 910005, block: BLOCK.h29, sequenceNo: 1, status: 'applied', + memo: 'vk-fixture-precondition', hash: 'CkpZVkFixtureTx910005', + updates: ['eta'], + }, + { + id: 910006, block: BLOCK.h30orphan, sequenceNo: 0, status: 'applied', + memo: 'vk-fixture-orphaned', hash: 'CkpZVkFixtureTx910006', + updates: ['theta'], + }, + { + id: 910007, block: BLOCK.h31pending, sequenceNo: 0, status: 'applied', + memo: 'vk-fixture-pending', hash: 'CkpZVkFixtureTx910007', + updates: ['iota'], + }, + // ONE command, carried by BOTH competing tips at height 32. This is what a + // real fork looks like: on the devnet archive a single zkApp command was + // measured in as many as 8 blocks at the same height. The two rows it + // produces agree on height, sequence_no, account-update position AND + // zkapp_account_update.id, so nothing in the original ORDER BY separated them. + { + id: 910008, block: [BLOCK.h32forkA, BLOCK.h32forkB], sequenceNo: 0, status: 'applied', + memo: 'vk-fixture-both-forks', hash: 'CkpZVkFixtureTx910008', + updates: ['kappa'], + }, + // Carried by fork B only, so the two tips are not identical. + { + id: 910009, block: BLOCK.h32forkB, sequenceNo: 1, status: 'applied', + memo: 'vk-fixture-fork-b-only', hash: 'CkpZVkFixtureTx910009', + updates: ['lambda'], + }, +]; + +// The chain shape: each block and the parent it is built on. +const CHAIN = [ + { block: BLOCK.anchor26, parent: null }, + { block: BLOCK.h27, parent: BLOCK.anchor26 }, + { block: BLOCK.h28, parent: BLOCK.h27 }, + { block: BLOCK.h29, parent: BLOCK.h28 }, + { block: BLOCK.h30, parent: BLOCK.h29 }, + { block: BLOCK.h30orphan, parent: BLOCK.h29 }, + { block: BLOCK.h31pending, parent: BLOCK.h30 }, + { block: BLOCK.h32forkA, parent: BLOCK.h31pending }, + { block: BLOCK.h32forkB, parent: BLOCK.h31pending }, +]; + +// ── Emit ───────────────────────────────────────────────────────────────── +const out = []; +const say = (...lines) => out.push(...lines); + +say( + '-- GENERATED FILE — DO NOT EDIT BY HAND.', + '-- Regenerate with: node tests/integration/fixtures/generate-verification-key-fixture.mjs', + '--', + '-- Applied on top of archive_db.sql. See the generator for what each account', + '-- in here is meant to prove.', + '' +); + +say('-- Tokens ------------------------------------------------------------'); +say( + `INSERT INTO tokens (id, value) VALUES (${CUSTOM_TOKEN_ID}, '${CUSTOM_TOKEN_VALUE}');`, + '' +); + +say('-- Verification keys -------------------------------------------------'); +for (const vk of [TARGET, OTHER]) { + say( + `INSERT INTO zkapp_verification_key_hashes (id, value) VALUES (${vk.hashId}, '${vk.hash}');`, + `INSERT INTO zkapp_verification_keys (id, verification_key, hash_id)`, + ` VALUES (${vk.keyId}, '${vk.key}', ${vk.hashId});` + ); +} +say(''); + +say('-- Update rows: what an account update SETS --------------------------'); +say('-- zkapp_updates row 1 of the base fixture has verification_key_id IS NULL'); +say('-- and is reused for the account update that only has a precondition.'); +say( + `INSERT INTO zkapp_updates (id, app_state_id, verification_key_id)`, + ` VALUES (${SET_TARGET_UPDATE_ID}, 1, ${TARGET.keyId});`, + `INSERT INTO zkapp_updates (id, app_state_id, verification_key_id)`, + ` VALUES (${SET_OTHER_UPDATE_ID}, 1, ${OTHER.keyId});`, + '' +); + +say('-- Accounts ----------------------------------------------------------'); +for (const acct of ACCOUNTS) { + say( + `INSERT INTO public_keys (id, value) VALUES (${acct.id}, '${acct.address}');`, + `INSERT INTO account_identifiers (id, public_key_id, token_id)`, + ` VALUES (${acct.id}, ${acct.id}, ${acct.token});` + ); +} +say(''); + +say('-- Blocks 26…32 on top of the base fixture canonical tip -------------'); +for (const { block, parent } of CHAIN) { + const parentRef = + parent === null + ? `(SELECT id FROM blocks WHERE height = ${BASE_CANONICAL_TIP_HEIGHT}` + + ` AND chain_status = 'canonical' ORDER BY id LIMIT 1)` + : `${parent.id}`; + say( + `INSERT INTO blocks (id, state_hash, parent_id, parent_hash, creator_id, block_winner_id, last_vrf_output,`, + ` snarked_ledger_hash_id, staking_epoch_data_id, next_epoch_data_id, min_window_density,`, + ` sub_window_densities, total_currency, ledger_hash, height, global_slot_since_hard_fork,`, + ` global_slot_since_genesis, protocol_version_id, proposed_protocol_version_id, timestamp, chain_status)`, + ` SELECT ${block.id}, '${block.hash}', p.id, p.state_hash, p.creator_id,`, + ` p.block_winner_id, p.last_vrf_output, p.snarked_ledger_hash_id, p.staking_epoch_data_id,`, + ` p.next_epoch_data_id, p.min_window_density, p.sub_window_densities, p.total_currency, p.ledger_hash,`, + ` ${block.height}, p.global_slot_since_hard_fork + 1, p.global_slot_since_genesis + 1, p.protocol_version_id,`, + ` p.proposed_protocol_version_id, (p.timestamp::bigint + 180000)::text, '${block.status}'`, + ` FROM blocks p WHERE p.id = ${parentRef};` + ); +} +say(''); + +say('-- Account updates ---------------------------------------------------'); +for (const upd of ACCOUNT_UPDATES) { + const acct = account(upd.key); + const precondition = upd.preconditionHashId === null ? 'NULL' : `${upd.preconditionHashId}`; + const note = + upd.updateId === NO_UPDATE_ID + ? ' -- sets NOTHING; target hash sits in the precondition column only' + : ''; + say( + `-- ${upd.key}${note}`, + `INSERT INTO zkapp_account_update_body (id, account_identifier_id, update_id, balance_change,`, + ` increment_nonce, events_id, actions_id, call_data_id, call_depth, zkapp_network_precondition_id,`, + ` zkapp_account_precondition_id, use_full_commitment, implicit_account_creation_fee, may_use_token,`, + ` authorization_kind, verification_key_hash_id)`, + ` VALUES (${upd.id}, ${acct.id}, ${upd.updateId}, '0', false, ${EMPTY_EVENTS_ID}, ${EMPTY_EVENTS_ID},`, + ` ${ZERO_FIELD_ID}, 0, 1, 1, false, false, 'No', '${upd.authorizationKind}', ${precondition});`, + `INSERT INTO zkapp_account_update (id, body_id) VALUES (${upd.id}, ${upd.id});` + ); +} +say(''); + +say('-- Commands ----------------------------------------------------------'); +for (const cmd of COMMANDS) { + const ids = cmd.updates.map((k) => accountUpdate(k).id).join(', '); + const blocks = blocksOf(cmd); + say( + `-- ${blocks.map((b) => `block ${b.height} (${b.status})`).join(' and ')},` + + ` sequence_no ${cmd.sequenceNo}, ${cmd.status}: ${cmd.updates.join(' -> ')}`, + `INSERT INTO zkapp_commands (id, zkapp_fee_payer_body_id, zkapp_account_updates_ids, memo, hash)`, + ` VALUES (${cmd.id}, ${FEE_PAYER_BODY_ID}, '{${ids}}', '${cmd.memo}', '${cmd.hash}');` + ); + for (const block of blocks) { + say( + `INSERT INTO blocks_zkapp_commands (block_id, zkapp_command_id, sequence_no, status)`, + ` VALUES (${block.id}, ${cmd.id}, ${cmd.sequenceNo}, '${cmd.status}');` + ); + } + say(''); +} + +const target = join(here, 'verification_key_updates.sql'); +writeFileSync(target, out.join('\n') + '\n'); +console.log(`wrote ${target} (${out.length} lines)`); + +// ── Machine-readable description ───────────────────────────────────────── +// The tests import this so the expected values can never drift from the SQL: +// both come from the constants above. +const DEFAULT_TOKEN_VALUE = 'wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf'; +const positionOf = (cmd, key) => cmd.updates.indexOf(key) + 1; +const commandOf = (key) => COMMANDS.find((c) => c.updates.includes(key)); + +/** + * Every occurrence the query should return for one account. A command carried by + * two competing tips is TWO occurrences, one per block — the query answers with + * account-update occurrences, not with distinct accounts. + */ +const occurrences = (key) => { + const cmd = commandOf(key); + const acct = account(key); + const upd = accountUpdate(key); + return blocksOf(cmd).map((block) => ({ + account: key, + accountUpdateId: String(upd.id), + address: acct.address, + tokenId: acct.token === CUSTOM_TOKEN_ID ? CUSTOM_TOKEN_VALUE : DEFAULT_TOKEN_VALUE, + height: block.height, + stateHash: block.hash, + chainStatus: block.status, + sequenceNumber: cmd.sequenceNo, + position: positionOf(cmd, key), + transactionHash: cmd.hash, + memo: cmd.memo, + authorizationKind: upd.authorizationKind, + })); +}; + +/** + * The order the query must produce: by block height, then by state hash so two + * competing tips at one height never tie, then by the order of the command in + * the block and of the account update in the command. + */ +const inQueryOrder = (rows) => + [...rows].sort( + (a, b) => + a.height - b.height || + a.stateHash.localeCompare(b.stateHash) || + a.sequenceNumber - b.sequenceNumber || + a.position - b.position + ); + +const meta = { + targetVerificationKeyHash: TARGET.hash, + otherVerificationKeyHash: OTHER.hash, + defaultTokenId: DEFAULT_TOKEN_VALUE, + customTokenId: CUSTOM_TOKEN_VALUE, + // The canonical blocks the fixture adds, and the pending tip heights. + heights: { + anchor: BLOCK.anchor26.height, + firstCommand: BLOCK.h27.height, + lastPending: BLOCK.h32forkA.height, + afterAll: BLOCK.h32forkA.height + 1, + }, + // In the order the query must return them. + expected: { + canonical: inQueryOrder(['alpha', 'gamma', 'delta', 'epsilon'].flatMap(occurrences)), + pending: inQueryOrder(['iota', 'kappa', 'lambda'].flatMap(occurrences)), + }, + // Accounts that must never appear, and the reason each one is excluded. + excluded: { + beta: 'sets a different verification key', + zeta: 'the command failed', + eta: 'the target hash is a precondition, not a key this update sets', + theta: 'the block is orphaned', + }, + excludedAddresses: Object.fromEntries( + ['beta', 'zeta', 'eta', 'theta'].map((k) => [k, account(k).address]) + ), +}; +const metaTarget = join(here, 'verification_key_updates.json'); +writeFileSync(metaTarget, JSON.stringify(meta, null, 2) + '\n'); +console.log(`wrote ${metaTarget}`); diff --git a/tests/integration/fixtures/verification_key_updates.json b/tests/integration/fixtures/verification_key_updates.json new file mode 100644 index 0000000..9fe35af --- /dev/null +++ b/tests/integration/fixtures/verification_key_updates.json @@ -0,0 +1,142 @@ +{ + "targetVerificationKeyHash": "910000000000000000000000000000000000000000000000000000000000000001", + "otherVerificationKeyHash": "910000000000000000000000000000000000000000000000000000000000000002", + "defaultTokenId": "wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf", + "customTokenId": "wZZZVkFixtureCustomToken00000000000000000000000000", + "heights": { + "anchor": 26, + "firstCommand": 27, + "lastPending": 32, + "afterAll": 33 + }, + "expected": { + "canonical": [ + { + "account": "alpha", + "accountUpdateId": "910001", + "address": "B62qVkFixtureAlphaSetsTargetCanonical00000000000000000", + "tokenId": "wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf", + "height": 27, + "stateHash": "3NVkFixtureBlock27", + "chainStatus": "canonical", + "sequenceNumber": 0, + "position": 1, + "transactionHash": "CkpZVkFixtureTx910001", + "memo": "vk-fixture-three-updates", + "authorizationKind": "Proof" + }, + { + "account": "gamma", + "accountUpdateId": "910003", + "address": "B62qVkFixtureGammaSetsTargetCustomToken0000000000000", + "tokenId": "wZZZVkFixtureCustomToken00000000000000000000000000", + "height": 27, + "stateHash": "3NVkFixtureBlock27", + "chainStatus": "canonical", + "sequenceNumber": 0, + "position": 3, + "transactionHash": "CkpZVkFixtureTx910001", + "memo": "vk-fixture-three-updates", + "authorizationKind": "Proof" + }, + { + "account": "delta", + "accountUpdateId": "910004", + "address": "B62qVkFixtureDeltaSetsTargetSequenceZero0000000000000", + "tokenId": "wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf", + "height": 28, + "stateHash": "3NVkFixtureBlock28", + "chainStatus": "canonical", + "sequenceNumber": 0, + "position": 1, + "transactionHash": "CkpZVkFixtureTx910002", + "memo": "vk-fixture-seq-zero", + "authorizationKind": "Proof" + }, + { + "account": "epsilon", + "accountUpdateId": "910005", + "address": "B62qVkFixtureEpsilonSetsTargetSequenceOne000000000000", + "tokenId": "wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf", + "height": 28, + "stateHash": "3NVkFixtureBlock28", + "chainStatus": "canonical", + "sequenceNumber": 1, + "position": 1, + "transactionHash": "CkpZVkFixtureTx910003", + "memo": "vk-fixture-seq-one", + "authorizationKind": "Signature" + } + ], + "pending": [ + { + "account": "iota", + "accountUpdateId": "910009", + "address": "B62qVkFixtureIotaSetsTargetInPendingBlock000000000000", + "tokenId": "wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf", + "height": 31, + "stateHash": "3NVkFixtureBlock31Pending", + "chainStatus": "pending", + "sequenceNumber": 0, + "position": 1, + "transactionHash": "CkpZVkFixtureTx910007", + "memo": "vk-fixture-pending", + "authorizationKind": "Proof" + }, + { + "account": "kappa", + "accountUpdateId": "910010", + "address": "B62qVkFixtureKappaSetsTargetPendingForkA000000000000", + "tokenId": "wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf", + "height": 32, + "stateHash": "3NVkFixtureBlock32ForkA", + "chainStatus": "pending", + "sequenceNumber": 0, + "position": 1, + "transactionHash": "CkpZVkFixtureTx910008", + "memo": "vk-fixture-both-forks", + "authorizationKind": "Proof" + }, + { + "account": "kappa", + "accountUpdateId": "910010", + "address": "B62qVkFixtureKappaSetsTargetPendingForkA000000000000", + "tokenId": "wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf", + "height": 32, + "stateHash": "3NVkFixtureBlock32ForkB", + "chainStatus": "pending", + "sequenceNumber": 0, + "position": 1, + "transactionHash": "CkpZVkFixtureTx910008", + "memo": "vk-fixture-both-forks", + "authorizationKind": "Proof" + }, + { + "account": "lambda", + "accountUpdateId": "910011", + "address": "B62qVkFixtureLambdaSetsTargetPendingForkB00000000000", + "tokenId": "wSHV2S4qX9jFsLjQo8r1BsMLH2ZRKsZx6EJd1sbozGPieEC4Jf", + "height": 32, + "stateHash": "3NVkFixtureBlock32ForkB", + "chainStatus": "pending", + "sequenceNumber": 1, + "position": 1, + "transactionHash": "CkpZVkFixtureTx910009", + "memo": "vk-fixture-fork-b-only", + "authorizationKind": "Proof" + } + ] + }, + "excluded": { + "beta": "sets a different verification key", + "zeta": "the command failed", + "eta": "the target hash is a precondition, not a key this update sets", + "theta": "the block is orphaned" + }, + "excludedAddresses": { + "beta": "B62qVkFixtureBetaSetsOtherHash000000000000000000000000", + "zeta": "B62qVkFixtureZetaSetsTargetInFailedCommand00000000000", + "eta": "B62qVkFixtureEtaTargetAsPreconditionOnly00000000000000", + "theta": "B62qVkFixtureThetaSetsTargetInOrphanedBlock0000000000" + } +} diff --git a/tests/integration/fixtures/verification_key_updates.sql b/tests/integration/fixtures/verification_key_updates.sql new file mode 100644 index 0000000..6cf7911 --- /dev/null +++ b/tests/integration/fixtures/verification_key_updates.sql @@ -0,0 +1,299 @@ +-- GENERATED FILE — DO NOT EDIT BY HAND. +-- Regenerate with: node tests/integration/fixtures/generate-verification-key-fixture.mjs +-- +-- Applied on top of archive_db.sql. See the generator for what each account +-- in here is meant to prove. + +-- Tokens ------------------------------------------------------------ +INSERT INTO tokens (id, value) VALUES (910001, 'wZZZVkFixtureCustomToken00000000000000000000000000'); + +-- Verification keys ------------------------------------------------- +INSERT INTO zkapp_verification_key_hashes (id, value) VALUES (910001, '910000000000000000000000000000000000000000000000000000000000000001'); +INSERT INTO zkapp_verification_keys (id, verification_key, hash_id) + VALUES (910001, 'AAVkFixtureTargetVerificationKeyBlob', 910001); +INSERT INTO zkapp_verification_key_hashes (id, value) VALUES (910002, '910000000000000000000000000000000000000000000000000000000000000002'); +INSERT INTO zkapp_verification_keys (id, verification_key, hash_id) + VALUES (910002, 'AAVkFixtureOtherVerificationKeyBlob', 910002); + +-- Update rows: what an account update SETS -------------------------- +-- zkapp_updates row 1 of the base fixture has verification_key_id IS NULL +-- and is reused for the account update that only has a precondition. +INSERT INTO zkapp_updates (id, app_state_id, verification_key_id) + VALUES (910001, 1, 910001); +INSERT INTO zkapp_updates (id, app_state_id, verification_key_id) + VALUES (910002, 1, 910002); + +-- Accounts ---------------------------------------------------------- +INSERT INTO public_keys (id, value) VALUES (910001, 'B62qVkFixtureAlphaSetsTargetCanonical00000000000000000'); +INSERT INTO account_identifiers (id, public_key_id, token_id) + VALUES (910001, 910001, 1); +INSERT INTO public_keys (id, value) VALUES (910002, 'B62qVkFixtureBetaSetsOtherHash000000000000000000000000'); +INSERT INTO account_identifiers (id, public_key_id, token_id) + VALUES (910002, 910002, 1); +INSERT INTO public_keys (id, value) VALUES (910003, 'B62qVkFixtureGammaSetsTargetCustomToken0000000000000'); +INSERT INTO account_identifiers (id, public_key_id, token_id) + VALUES (910003, 910003, 910001); +INSERT INTO public_keys (id, value) VALUES (910004, 'B62qVkFixtureDeltaSetsTargetSequenceZero0000000000000'); +INSERT INTO account_identifiers (id, public_key_id, token_id) + VALUES (910004, 910004, 1); +INSERT INTO public_keys (id, value) VALUES (910005, 'B62qVkFixtureEpsilonSetsTargetSequenceOne000000000000'); +INSERT INTO account_identifiers (id, public_key_id, token_id) + VALUES (910005, 910005, 1); +INSERT INTO public_keys (id, value) VALUES (910006, 'B62qVkFixtureZetaSetsTargetInFailedCommand00000000000'); +INSERT INTO account_identifiers (id, public_key_id, token_id) + VALUES (910006, 910006, 1); +INSERT INTO public_keys (id, value) VALUES (910007, 'B62qVkFixtureEtaTargetAsPreconditionOnly00000000000000'); +INSERT INTO account_identifiers (id, public_key_id, token_id) + VALUES (910007, 910007, 1); +INSERT INTO public_keys (id, value) VALUES (910008, 'B62qVkFixtureThetaSetsTargetInOrphanedBlock0000000000'); +INSERT INTO account_identifiers (id, public_key_id, token_id) + VALUES (910008, 910008, 1); +INSERT INTO public_keys (id, value) VALUES (910009, 'B62qVkFixtureIotaSetsTargetInPendingBlock000000000000'); +INSERT INTO account_identifiers (id, public_key_id, token_id) + VALUES (910009, 910009, 1); +INSERT INTO public_keys (id, value) VALUES (910010, 'B62qVkFixtureKappaSetsTargetPendingForkA000000000000'); +INSERT INTO account_identifiers (id, public_key_id, token_id) + VALUES (910010, 910010, 1); +INSERT INTO public_keys (id, value) VALUES (910011, 'B62qVkFixtureLambdaSetsTargetPendingForkB00000000000'); +INSERT INTO account_identifiers (id, public_key_id, token_id) + VALUES (910011, 910011, 1); + +-- Blocks 26…32 on top of the base fixture canonical tip ------------- +INSERT INTO blocks (id, state_hash, parent_id, parent_hash, creator_id, block_winner_id, last_vrf_output, + snarked_ledger_hash_id, staking_epoch_data_id, next_epoch_data_id, min_window_density, + sub_window_densities, total_currency, ledger_hash, height, global_slot_since_hard_fork, + global_slot_since_genesis, protocol_version_id, proposed_protocol_version_id, timestamp, chain_status) + SELECT 910026, '3NVkFixtureBlock26', p.id, p.state_hash, p.creator_id, + p.block_winner_id, p.last_vrf_output, p.snarked_ledger_hash_id, p.staking_epoch_data_id, + p.next_epoch_data_id, p.min_window_density, p.sub_window_densities, p.total_currency, p.ledger_hash, + 26, p.global_slot_since_hard_fork + 1, p.global_slot_since_genesis + 1, p.protocol_version_id, + p.proposed_protocol_version_id, (p.timestamp::bigint + 180000)::text, 'canonical' + FROM blocks p WHERE p.id = (SELECT id FROM blocks WHERE height = 25 AND chain_status = 'canonical' ORDER BY id LIMIT 1); +INSERT INTO blocks (id, state_hash, parent_id, parent_hash, creator_id, block_winner_id, last_vrf_output, + snarked_ledger_hash_id, staking_epoch_data_id, next_epoch_data_id, min_window_density, + sub_window_densities, total_currency, ledger_hash, height, global_slot_since_hard_fork, + global_slot_since_genesis, protocol_version_id, proposed_protocol_version_id, timestamp, chain_status) + SELECT 910027, '3NVkFixtureBlock27', p.id, p.state_hash, p.creator_id, + p.block_winner_id, p.last_vrf_output, p.snarked_ledger_hash_id, p.staking_epoch_data_id, + p.next_epoch_data_id, p.min_window_density, p.sub_window_densities, p.total_currency, p.ledger_hash, + 27, p.global_slot_since_hard_fork + 1, p.global_slot_since_genesis + 1, p.protocol_version_id, + p.proposed_protocol_version_id, (p.timestamp::bigint + 180000)::text, 'canonical' + FROM blocks p WHERE p.id = 910026; +INSERT INTO blocks (id, state_hash, parent_id, parent_hash, creator_id, block_winner_id, last_vrf_output, + snarked_ledger_hash_id, staking_epoch_data_id, next_epoch_data_id, min_window_density, + sub_window_densities, total_currency, ledger_hash, height, global_slot_since_hard_fork, + global_slot_since_genesis, protocol_version_id, proposed_protocol_version_id, timestamp, chain_status) + SELECT 910028, '3NVkFixtureBlock28', p.id, p.state_hash, p.creator_id, + p.block_winner_id, p.last_vrf_output, p.snarked_ledger_hash_id, p.staking_epoch_data_id, + p.next_epoch_data_id, p.min_window_density, p.sub_window_densities, p.total_currency, p.ledger_hash, + 28, p.global_slot_since_hard_fork + 1, p.global_slot_since_genesis + 1, p.protocol_version_id, + p.proposed_protocol_version_id, (p.timestamp::bigint + 180000)::text, 'canonical' + FROM blocks p WHERE p.id = 910027; +INSERT INTO blocks (id, state_hash, parent_id, parent_hash, creator_id, block_winner_id, last_vrf_output, + snarked_ledger_hash_id, staking_epoch_data_id, next_epoch_data_id, min_window_density, + sub_window_densities, total_currency, ledger_hash, height, global_slot_since_hard_fork, + global_slot_since_genesis, protocol_version_id, proposed_protocol_version_id, timestamp, chain_status) + SELECT 910029, '3NVkFixtureBlock29', p.id, p.state_hash, p.creator_id, + p.block_winner_id, p.last_vrf_output, p.snarked_ledger_hash_id, p.staking_epoch_data_id, + p.next_epoch_data_id, p.min_window_density, p.sub_window_densities, p.total_currency, p.ledger_hash, + 29, p.global_slot_since_hard_fork + 1, p.global_slot_since_genesis + 1, p.protocol_version_id, + p.proposed_protocol_version_id, (p.timestamp::bigint + 180000)::text, 'canonical' + FROM blocks p WHERE p.id = 910028; +INSERT INTO blocks (id, state_hash, parent_id, parent_hash, creator_id, block_winner_id, last_vrf_output, + snarked_ledger_hash_id, staking_epoch_data_id, next_epoch_data_id, min_window_density, + sub_window_densities, total_currency, ledger_hash, height, global_slot_since_hard_fork, + global_slot_since_genesis, protocol_version_id, proposed_protocol_version_id, timestamp, chain_status) + SELECT 910030, '3NVkFixtureBlock30', p.id, p.state_hash, p.creator_id, + p.block_winner_id, p.last_vrf_output, p.snarked_ledger_hash_id, p.staking_epoch_data_id, + p.next_epoch_data_id, p.min_window_density, p.sub_window_densities, p.total_currency, p.ledger_hash, + 30, p.global_slot_since_hard_fork + 1, p.global_slot_since_genesis + 1, p.protocol_version_id, + p.proposed_protocol_version_id, (p.timestamp::bigint + 180000)::text, 'canonical' + FROM blocks p WHERE p.id = 910029; +INSERT INTO blocks (id, state_hash, parent_id, parent_hash, creator_id, block_winner_id, last_vrf_output, + snarked_ledger_hash_id, staking_epoch_data_id, next_epoch_data_id, min_window_density, + sub_window_densities, total_currency, ledger_hash, height, global_slot_since_hard_fork, + global_slot_since_genesis, protocol_version_id, proposed_protocol_version_id, timestamp, chain_status) + SELECT 910130, '3NVkFixtureBlock30Orphaned', p.id, p.state_hash, p.creator_id, + p.block_winner_id, p.last_vrf_output, p.snarked_ledger_hash_id, p.staking_epoch_data_id, + p.next_epoch_data_id, p.min_window_density, p.sub_window_densities, p.total_currency, p.ledger_hash, + 30, p.global_slot_since_hard_fork + 1, p.global_slot_since_genesis + 1, p.protocol_version_id, + p.proposed_protocol_version_id, (p.timestamp::bigint + 180000)::text, 'orphaned' + FROM blocks p WHERE p.id = 910029; +INSERT INTO blocks (id, state_hash, parent_id, parent_hash, creator_id, block_winner_id, last_vrf_output, + snarked_ledger_hash_id, staking_epoch_data_id, next_epoch_data_id, min_window_density, + sub_window_densities, total_currency, ledger_hash, height, global_slot_since_hard_fork, + global_slot_since_genesis, protocol_version_id, proposed_protocol_version_id, timestamp, chain_status) + SELECT 910031, '3NVkFixtureBlock31Pending', p.id, p.state_hash, p.creator_id, + p.block_winner_id, p.last_vrf_output, p.snarked_ledger_hash_id, p.staking_epoch_data_id, + p.next_epoch_data_id, p.min_window_density, p.sub_window_densities, p.total_currency, p.ledger_hash, + 31, p.global_slot_since_hard_fork + 1, p.global_slot_since_genesis + 1, p.protocol_version_id, + p.proposed_protocol_version_id, (p.timestamp::bigint + 180000)::text, 'pending' + FROM blocks p WHERE p.id = 910030; +INSERT INTO blocks (id, state_hash, parent_id, parent_hash, creator_id, block_winner_id, last_vrf_output, + snarked_ledger_hash_id, staking_epoch_data_id, next_epoch_data_id, min_window_density, + sub_window_densities, total_currency, ledger_hash, height, global_slot_since_hard_fork, + global_slot_since_genesis, protocol_version_id, proposed_protocol_version_id, timestamp, chain_status) + SELECT 910032, '3NVkFixtureBlock32ForkA', p.id, p.state_hash, p.creator_id, + p.block_winner_id, p.last_vrf_output, p.snarked_ledger_hash_id, p.staking_epoch_data_id, + p.next_epoch_data_id, p.min_window_density, p.sub_window_densities, p.total_currency, p.ledger_hash, + 32, p.global_slot_since_hard_fork + 1, p.global_slot_since_genesis + 1, p.protocol_version_id, + p.proposed_protocol_version_id, (p.timestamp::bigint + 180000)::text, 'pending' + FROM blocks p WHERE p.id = 910031; +INSERT INTO blocks (id, state_hash, parent_id, parent_hash, creator_id, block_winner_id, last_vrf_output, + snarked_ledger_hash_id, staking_epoch_data_id, next_epoch_data_id, min_window_density, + sub_window_densities, total_currency, ledger_hash, height, global_slot_since_hard_fork, + global_slot_since_genesis, protocol_version_id, proposed_protocol_version_id, timestamp, chain_status) + SELECT 910132, '3NVkFixtureBlock32ForkB', p.id, p.state_hash, p.creator_id, + p.block_winner_id, p.last_vrf_output, p.snarked_ledger_hash_id, p.staking_epoch_data_id, + p.next_epoch_data_id, p.min_window_density, p.sub_window_densities, p.total_currency, p.ledger_hash, + 32, p.global_slot_since_hard_fork + 1, p.global_slot_since_genesis + 1, p.protocol_version_id, + p.proposed_protocol_version_id, (p.timestamp::bigint + 180000)::text, 'pending' + FROM blocks p WHERE p.id = 910031; + +-- Account updates --------------------------------------------------- +-- alpha +INSERT INTO zkapp_account_update_body (id, account_identifier_id, update_id, balance_change, + increment_nonce, events_id, actions_id, call_data_id, call_depth, zkapp_network_precondition_id, + zkapp_account_precondition_id, use_full_commitment, implicit_account_creation_fee, may_use_token, + authorization_kind, verification_key_hash_id) + VALUES (910001, 910001, 910001, '0', false, 1, 1, + 1, 0, 1, 1, false, false, 'No', 'Proof', NULL); +INSERT INTO zkapp_account_update (id, body_id) VALUES (910001, 910001); +-- beta +INSERT INTO zkapp_account_update_body (id, account_identifier_id, update_id, balance_change, + increment_nonce, events_id, actions_id, call_data_id, call_depth, zkapp_network_precondition_id, + zkapp_account_precondition_id, use_full_commitment, implicit_account_creation_fee, may_use_token, + authorization_kind, verification_key_hash_id) + VALUES (910002, 910002, 910002, '0', false, 1, 1, + 1, 0, 1, 1, false, false, 'No', 'Proof', NULL); +INSERT INTO zkapp_account_update (id, body_id) VALUES (910002, 910002); +-- gamma +INSERT INTO zkapp_account_update_body (id, account_identifier_id, update_id, balance_change, + increment_nonce, events_id, actions_id, call_data_id, call_depth, zkapp_network_precondition_id, + zkapp_account_precondition_id, use_full_commitment, implicit_account_creation_fee, may_use_token, + authorization_kind, verification_key_hash_id) + VALUES (910003, 910003, 910001, '0', false, 1, 1, + 1, 0, 1, 1, false, false, 'No', 'Proof', NULL); +INSERT INTO zkapp_account_update (id, body_id) VALUES (910003, 910003); +-- delta +INSERT INTO zkapp_account_update_body (id, account_identifier_id, update_id, balance_change, + increment_nonce, events_id, actions_id, call_data_id, call_depth, zkapp_network_precondition_id, + zkapp_account_precondition_id, use_full_commitment, implicit_account_creation_fee, may_use_token, + authorization_kind, verification_key_hash_id) + VALUES (910004, 910004, 910001, '0', false, 1, 1, + 1, 0, 1, 1, false, false, 'No', 'Proof', NULL); +INSERT INTO zkapp_account_update (id, body_id) VALUES (910004, 910004); +-- epsilon +INSERT INTO zkapp_account_update_body (id, account_identifier_id, update_id, balance_change, + increment_nonce, events_id, actions_id, call_data_id, call_depth, zkapp_network_precondition_id, + zkapp_account_precondition_id, use_full_commitment, implicit_account_creation_fee, may_use_token, + authorization_kind, verification_key_hash_id) + VALUES (910005, 910005, 910001, '0', false, 1, 1, + 1, 0, 1, 1, false, false, 'No', 'Signature', NULL); +INSERT INTO zkapp_account_update (id, body_id) VALUES (910005, 910005); +-- zeta +INSERT INTO zkapp_account_update_body (id, account_identifier_id, update_id, balance_change, + increment_nonce, events_id, actions_id, call_data_id, call_depth, zkapp_network_precondition_id, + zkapp_account_precondition_id, use_full_commitment, implicit_account_creation_fee, may_use_token, + authorization_kind, verification_key_hash_id) + VALUES (910006, 910006, 910001, '0', false, 1, 1, + 1, 0, 1, 1, false, false, 'No', 'Proof', NULL); +INSERT INTO zkapp_account_update (id, body_id) VALUES (910006, 910006); +-- eta -- sets NOTHING; target hash sits in the precondition column only +INSERT INTO zkapp_account_update_body (id, account_identifier_id, update_id, balance_change, + increment_nonce, events_id, actions_id, call_data_id, call_depth, zkapp_network_precondition_id, + zkapp_account_precondition_id, use_full_commitment, implicit_account_creation_fee, may_use_token, + authorization_kind, verification_key_hash_id) + VALUES (910007, 910007, 1, '0', false, 1, 1, + 1, 0, 1, 1, false, false, 'No', 'Proof', 910001); +INSERT INTO zkapp_account_update (id, body_id) VALUES (910007, 910007); +-- theta +INSERT INTO zkapp_account_update_body (id, account_identifier_id, update_id, balance_change, + increment_nonce, events_id, actions_id, call_data_id, call_depth, zkapp_network_precondition_id, + zkapp_account_precondition_id, use_full_commitment, implicit_account_creation_fee, may_use_token, + authorization_kind, verification_key_hash_id) + VALUES (910008, 910008, 910001, '0', false, 1, 1, + 1, 0, 1, 1, false, false, 'No', 'Proof', NULL); +INSERT INTO zkapp_account_update (id, body_id) VALUES (910008, 910008); +-- iota +INSERT INTO zkapp_account_update_body (id, account_identifier_id, update_id, balance_change, + increment_nonce, events_id, actions_id, call_data_id, call_depth, zkapp_network_precondition_id, + zkapp_account_precondition_id, use_full_commitment, implicit_account_creation_fee, may_use_token, + authorization_kind, verification_key_hash_id) + VALUES (910009, 910009, 910001, '0', false, 1, 1, + 1, 0, 1, 1, false, false, 'No', 'Proof', NULL); +INSERT INTO zkapp_account_update (id, body_id) VALUES (910009, 910009); +-- kappa +INSERT INTO zkapp_account_update_body (id, account_identifier_id, update_id, balance_change, + increment_nonce, events_id, actions_id, call_data_id, call_depth, zkapp_network_precondition_id, + zkapp_account_precondition_id, use_full_commitment, implicit_account_creation_fee, may_use_token, + authorization_kind, verification_key_hash_id) + VALUES (910010, 910010, 910001, '0', false, 1, 1, + 1, 0, 1, 1, false, false, 'No', 'Proof', NULL); +INSERT INTO zkapp_account_update (id, body_id) VALUES (910010, 910010); +-- lambda +INSERT INTO zkapp_account_update_body (id, account_identifier_id, update_id, balance_change, + increment_nonce, events_id, actions_id, call_data_id, call_depth, zkapp_network_precondition_id, + zkapp_account_precondition_id, use_full_commitment, implicit_account_creation_fee, may_use_token, + authorization_kind, verification_key_hash_id) + VALUES (910011, 910011, 910001, '0', false, 1, 1, + 1, 0, 1, 1, false, false, 'No', 'Proof', NULL); +INSERT INTO zkapp_account_update (id, body_id) VALUES (910011, 910011); + +-- Commands ---------------------------------------------------------- +-- block 27 (canonical), sequence_no 0, applied: alpha -> beta -> gamma +INSERT INTO zkapp_commands (id, zkapp_fee_payer_body_id, zkapp_account_updates_ids, memo, hash) + VALUES (910001, 1, '{910001, 910002, 910003}', 'vk-fixture-three-updates', 'CkpZVkFixtureTx910001'); +INSERT INTO blocks_zkapp_commands (block_id, zkapp_command_id, sequence_no, status) + VALUES (910027, 910001, 0, 'applied'); + +-- block 28 (canonical), sequence_no 0, applied: delta +INSERT INTO zkapp_commands (id, zkapp_fee_payer_body_id, zkapp_account_updates_ids, memo, hash) + VALUES (910002, 1, '{910004}', 'vk-fixture-seq-zero', 'CkpZVkFixtureTx910002'); +INSERT INTO blocks_zkapp_commands (block_id, zkapp_command_id, sequence_no, status) + VALUES (910028, 910002, 0, 'applied'); + +-- block 28 (canonical), sequence_no 1, applied: epsilon +INSERT INTO zkapp_commands (id, zkapp_fee_payer_body_id, zkapp_account_updates_ids, memo, hash) + VALUES (910003, 1, '{910005}', 'vk-fixture-seq-one', 'CkpZVkFixtureTx910003'); +INSERT INTO blocks_zkapp_commands (block_id, zkapp_command_id, sequence_no, status) + VALUES (910028, 910003, 1, 'applied'); + +-- block 29 (canonical), sequence_no 0, failed: zeta +INSERT INTO zkapp_commands (id, zkapp_fee_payer_body_id, zkapp_account_updates_ids, memo, hash) + VALUES (910004, 1, '{910006}', 'vk-fixture-failed', 'CkpZVkFixtureTx910004'); +INSERT INTO blocks_zkapp_commands (block_id, zkapp_command_id, sequence_no, status) + VALUES (910029, 910004, 0, 'failed'); + +-- block 29 (canonical), sequence_no 1, applied: eta +INSERT INTO zkapp_commands (id, zkapp_fee_payer_body_id, zkapp_account_updates_ids, memo, hash) + VALUES (910005, 1, '{910007}', 'vk-fixture-precondition', 'CkpZVkFixtureTx910005'); +INSERT INTO blocks_zkapp_commands (block_id, zkapp_command_id, sequence_no, status) + VALUES (910029, 910005, 1, 'applied'); + +-- block 30 (orphaned), sequence_no 0, applied: theta +INSERT INTO zkapp_commands (id, zkapp_fee_payer_body_id, zkapp_account_updates_ids, memo, hash) + VALUES (910006, 1, '{910008}', 'vk-fixture-orphaned', 'CkpZVkFixtureTx910006'); +INSERT INTO blocks_zkapp_commands (block_id, zkapp_command_id, sequence_no, status) + VALUES (910130, 910006, 0, 'applied'); + +-- block 31 (pending), sequence_no 0, applied: iota +INSERT INTO zkapp_commands (id, zkapp_fee_payer_body_id, zkapp_account_updates_ids, memo, hash) + VALUES (910007, 1, '{910009}', 'vk-fixture-pending', 'CkpZVkFixtureTx910007'); +INSERT INTO blocks_zkapp_commands (block_id, zkapp_command_id, sequence_no, status) + VALUES (910031, 910007, 0, 'applied'); + +-- block 32 (pending) and block 32 (pending), sequence_no 0, applied: kappa +INSERT INTO zkapp_commands (id, zkapp_fee_payer_body_id, zkapp_account_updates_ids, memo, hash) + VALUES (910008, 1, '{910010}', 'vk-fixture-both-forks', 'CkpZVkFixtureTx910008'); +INSERT INTO blocks_zkapp_commands (block_id, zkapp_command_id, sequence_no, status) + VALUES (910032, 910008, 0, 'applied'); +INSERT INTO blocks_zkapp_commands (block_id, zkapp_command_id, sequence_no, status) + VALUES (910132, 910008, 0, 'applied'); + +-- block 32 (pending), sequence_no 1, applied: lambda +INSERT INTO zkapp_commands (id, zkapp_fee_payer_body_id, zkapp_account_updates_ids, memo, hash) + VALUES (910009, 1, '{910011}', 'vk-fixture-fork-b-only', 'CkpZVkFixtureTx910009'); +INSERT INTO blocks_zkapp_commands (block_id, zkapp_command_id, sequence_no, status) + VALUES (910132, 910009, 1, 'applied'); + diff --git a/tests/integration/integration.test.ts b/tests/integration/integration.test.ts index 2e4d5dc..c034ed2 100644 --- a/tests/integration/integration.test.ts +++ b/tests/integration/integration.test.ts @@ -18,7 +18,6 @@ import { EventsService } from '../../src/services/events-service/events-service. import { ActionsService } from '../../src/services/actions-service/actions-service.js'; import { NetworkService } from '../../src/services/network-service/network-service.js'; import { BlocksService } from '../../src/services/blocks-service/blocks-service.js'; -import { VerificationKeyUpdatesService } from '../../src/services/verification-key-updates-service/verification-key-updates-service.js'; import { BlockStatusFilter } from '../../src/blockchain/types.js'; import { DEFAULT_TOKEN_ID } from '../../src/blockchain/constants.js'; import { TracingState } from '../../src/tracing/tracer.js'; @@ -400,26 +399,9 @@ describe('ActionsService (integration)', () => { }); }); -// ─── Verification Key Updates Service ─────────────────────────────── - -describe('VerificationKeyUpdatesService (integration)', () => { - test('executes against the archive schema and excludes failed commands', async () => { - const service = new VerificationKeyUpdatesService(client); - const updates = await service.getVerificationKeyUpdates( - { - verificationKeyHash: - '330109536550383627416201330124291596191867681867265169258470531313815097966', - from: 0, - to: 30, - }, - nullOptions - ); - - // The fixture contains this verification key, but all of its zkApp - // commands failed. A failed deployment must never be discoverable. - assert.deepStrictEqual(updates, []); - }); -}); +// The verification-key update tests live in `verification-key-updates.test.ts`. +// They need applied zkApp commands, and every zkApp command in this fixture +// failed, so they run against their own database and their own fixture. // ─── SQL Schema Validation ─────────────────────────────────────────── diff --git a/tests/integration/verification-key-setup.ts b/tests/integration/verification-key-setup.ts new file mode 100644 index 0000000..b291632 --- /dev/null +++ b/tests/integration/verification-key-setup.ts @@ -0,0 +1,80 @@ +/** + * Setup for the verification-key update tests. + * + * These tests use their OWN database, separate from `setup.ts`. The fixture adds + * seven blocks on top of the base dump, and two of them are competing pending + * tips at the same height. That changes the maximum block height, the canonical + * block count and the pending chain — values the other integration tests assert + * on. Keeping the databases separate lets each suite keep exact expectations. + * This mirrors `action-state-setup.ts`. + * + * Requirements are the same as `setup.ts`: a local PostgreSQL and the two + * fixtures under `tests/integration/fixtures/`. + */ +import { execSync } from 'child_process'; +import postgres from 'postgres'; +import path from 'path'; + +const PG_TEST_HOST = process.env.PG_TEST_HOST ?? 'localhost'; +const PG_TEST_PORT = process.env.PG_TEST_PORT ?? '5432'; +const PG_TEST_USER = process.env.PG_TEST_USER ?? 'postgres'; +const PG_TEST_PASSWORD = process.env.PG_TEST_PASSWORD ?? 'postgres'; +const PG_TEST_DB = + process.env.PG_VERIFICATION_KEY_TEST_DB ?? + 'archive_node_api_verification_key_test'; + +const FIXTURE_DIR = path.resolve(process.cwd(), 'tests/integration/fixtures'); +const BASE_DUMP = + process.env.ARCHIVE_DUMP_PATH ?? path.join(FIXTURE_DIR, 'archive_db.sql'); +const VERIFICATION_KEY_FIXTURE = path.join( + FIXTURE_DIR, + 'verification_key_updates.sql' +); + +export const connectionString = `postgres://${PG_TEST_USER}:${PG_TEST_PASSWORD}@${PG_TEST_HOST}:${PG_TEST_PORT}/${PG_TEST_DB}`; + +function adminConnectionString(db = 'postgres') { + return `postgres://${PG_TEST_USER}:${PG_TEST_PASSWORD}@${PG_TEST_HOST}:${PG_TEST_PORT}/${db}`; +} + +function applySqlFile(file: string, { stopOnError }: { stopOnError: boolean }) { + execSync( + `PGPASSWORD=${PG_TEST_PASSWORD} psql ${stopOnError ? '-v ON_ERROR_STOP=1' : ''} ` + + `-h ${PG_TEST_HOST} -p ${PG_TEST_PORT} -U ${PG_TEST_USER} -d ${PG_TEST_DB} -f ${file}`, + { stdio: 'pipe' } + ); +} + +export async function setupTestDatabase(): Promise { + const admin = postgres(adminConnectionString(), { max: 1 }); + try { + await admin.unsafe( + `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '${PG_TEST_DB}' AND pid <> pg_backend_pid()` + ); + await admin.unsafe(`DROP DATABASE IF EXISTS ${PG_TEST_DB}`); + await admin.unsafe(`CREATE DATABASE ${PG_TEST_DB}`); + } finally { + await admin.end(); + } + + // The base dump emits a few benign notices, so it is not run with + // ON_ERROR_STOP. The generated fixture must apply cleanly. + applySqlFile(BASE_DUMP, { stopOnError: false }); + applySqlFile(VERIFICATION_KEY_FIXTURE, { stopOnError: true }); +} + +export async function teardownTestDatabase(): Promise { + const admin = postgres(adminConnectionString(), { max: 1 }); + try { + await admin.unsafe( + `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '${PG_TEST_DB}' AND pid <> pg_backend_pid()` + ); + await admin.unsafe(`DROP DATABASE IF EXISTS ${PG_TEST_DB}`); + } finally { + await admin.end(); + } +} + +export function createTestClient(): postgres.Sql { + return postgres(connectionString, { max: 5 }); +} diff --git a/tests/integration/verification-key-updates.test.ts b/tests/integration/verification-key-updates.test.ts new file mode 100644 index 0000000..fed1205 --- /dev/null +++ b/tests/integration/verification-key-updates.test.ts @@ -0,0 +1,342 @@ +/** + * Integration tests for the `verificationKeyUpdates` query. + * + * WHY THESE EXIST + * --------------- + * The base `archive_db.sql` fixture has 227 `blocks_zkapp_commands` rows and + * every one of them has status `failed`. No input can make this query return a + * row against it, so a test written on the base fixture can only assert the + * empty list — and a query replaced by one that returns nothing at all leaves + * the whole integration suite green. + * + * `fixtures/verification_key_updates.sql` adds blocks 26…32 with applied zkApp + * commands that set verification keys, so these tests can tell a working query + * from a broken one. See `generate-verification-key-fixture.mjs` for what each + * account in the fixture proves. + */ +import { describe, test, before, after } from 'node:test'; +import assert from 'node:assert'; +import { readFileSync } from 'node:fs'; +import path from 'node:path'; +import postgres from 'postgres'; +import { VerificationKeyUpdatesService } from '../../src/services/verification-key-updates-service/verification-key-updates-service.js'; +import { BlockStatusFilter } from '../../src/blockchain/types.js'; +import { TracingState } from '../../src/tracing/tracer.js'; +import { + setupTestDatabase, + teardownTestDatabase, + createTestClient, +} from './verification-key-setup.js'; + +type Occurrence = { + account: string; + accountUpdateId: string; + address: string; + tokenId: string; + height: number; + stateHash: string; + chainStatus: string; + sequenceNumber: number; + position: number; + transactionHash: string; + memo: string; + authorizationKind: string; +}; +type Fixture = { + targetVerificationKeyHash: string; + otherVerificationKeyHash: string; + defaultTokenId: string; + customTokenId: string; + heights: { + anchor: number; + firstCommand: number; + lastPending: number; + afterAll: number; + }; + expected: { canonical: Occurrence[]; pending: Occurrence[] }; + excluded: Record; + excludedAddresses: Record; +}; + +// Read at run time so the expected values can never drift from the generated +// fixture: both come from the same generator. +const fixture: Fixture = JSON.parse( + readFileSync( + path.resolve( + process.cwd(), + 'tests/integration/fixtures/verification_key_updates.json' + ), + 'utf8' + ) +); + +const nullOptions = { tracingState: new TracingState(undefined as never) }; +const WHOLE_FIXTURE = { + from: fixture.heights.anchor, + to: fixture.heights.afterAll, +}; + +let client: postgres.Sql; +let service: VerificationKeyUpdatesService; + +before(async () => { + await setupTestDatabase(); + client = createTestClient(); + service = new VerificationKeyUpdatesService(client); +}, { timeout: 30000 }); + +after(async () => { + await client?.end(); + await teardownTestDatabase(); +}); + +/** The shape the tests compare on: everything the query claims to answer. */ +function shapeOf(update: { + accountUpdateId: string; + address: string; + tokenId: string; + blockInfo: { height: number; stateHash: string; chainStatus: string }; + transactionInfo: { + sequenceNumber: number; + hash: string; + memo: string; + authorizationKind: string; + }; +}) { + return { + accountUpdateId: update.accountUpdateId, + address: update.address, + tokenId: update.tokenId, + height: update.blockInfo.height, + stateHash: update.blockInfo.stateHash, + chainStatus: update.blockInfo.chainStatus, + sequenceNumber: update.transactionInfo.sequenceNumber, + transactionHash: update.transactionInfo.hash, + memo: update.transactionInfo.memo, + authorizationKind: update.transactionInfo.authorizationKind, + }; +} + +function expectedShape(o: Occurrence) { + return { + accountUpdateId: o.accountUpdateId, + address: o.address, + tokenId: o.tokenId, + height: o.height, + stateHash: o.stateHash, + chainStatus: o.chainStatus, + sequenceNumber: o.sequenceNumber, + transactionHash: o.transactionHash, + memo: o.memo, + authorizationKind: o.authorizationKind, + }; +} + +const query = (input: Partial[0]> = {}) => + service.getVerificationKeyUpdates( + { + verificationKeyHash: fixture.targetVerificationKeyHash, + ...WHOLE_FIXTURE, + ...input, + }, + nullOptions + ); + +describe('verificationKeyUpdates (integration)', () => { + test('returns every applied occurrence of the requested key', async () => { + const updates = await query(); + + assert.deepStrictEqual( + updates.map(shapeOf), + [...fixture.expected.canonical, ...fixture.expected.pending].map( + expectedShape + ) + ); + }); + + test('carries the full block metadata for each occurrence', async () => { + const [first] = await query({ status: BlockStatusFilter.canonical }); + + assert.strictEqual( + first.verificationKeyHash, + fixture.targetVerificationKeyHash + ); + // Everything BlockInfo promises must be present and of the right type, not + // just the fields the ordering happens to depend on. + assert.strictEqual(typeof first.blockInfo.parentHash, 'string'); + assert.strictEqual(typeof first.blockInfo.ledgerHash, 'string'); + assert.strictEqual(typeof first.blockInfo.timestamp, 'string'); + assert.strictEqual(typeof first.blockInfo.globalSlotSinceGenesis, 'number'); + assert.strictEqual(typeof first.blockInfo.globalSlotSinceHardfork, 'number'); + assert.strictEqual(typeof first.blockInfo.lastVrfOutput, 'string'); + assert.ok( + first.blockInfo.distanceFromMaxBlockHeight > 0, + 'distanceFromMaxBlockHeight should be measured from the chain tip' + ); + assert.strictEqual(first.transactionInfo.status, 'applied'); + assert.ok(Array.isArray(first.transactionInfo.zkappAccountUpdateIds)); + }); + + test('reads the token of the account, not the default token', async () => { + const updates = await query({ status: BlockStatusFilter.canonical }); + const tokens = updates.map((u) => u.tokenId); + + assert.ok( + tokens.includes(fixture.customTokenId), + 'an account on a custom token must report that token' + ); + assert.ok(tokens.includes(fixture.defaultTokenId)); + }); + + // ─── What must NOT come back ─────────────────────────────────────── + + test('excludes a different verification key, a failed command, a precondition-only update, and an orphaned block', async () => { + const addresses = new Set((await query()).map((u) => u.address)); + + for (const [account, reason] of Object.entries(fixture.excluded)) { + assert.ok( + !addresses.has(fixture.excludedAddresses[account]), + `${account} must be excluded: ${reason}` + ); + } + }); + + test('a verification-key precondition is not a verification-key update', async () => { + // The archive names a verification key in two places on an account update: + // update_id -> zkapp_updates.verification_key_id is the key the update SETS, + // and verification_key_hash_id is the key it REQUIRES. `eta` has the target + // hash only in the second, so a query that reads the wrong column returns it. + const addresses = (await query()).map((u) => u.address); + + assert.ok(!addresses.includes(fixture.excludedAddresses.eta)); + }); + + test('the hash filter selects, it does not just exclude', async () => { + // `beta` sets the other key in the same command as `alpha` and `gamma`. + // Asking for the other key must return beta and nothing else — a query that + // ignored the hash would return all three. + const updates = await query({ + verificationKeyHash: fixture.otherVerificationKeyHash, + status: BlockStatusFilter.canonical, + }); + + assert.deepStrictEqual( + updates.map((u) => u.address), + [fixture.excludedAddresses.beta] + ); + assert.strictEqual( + updates[0].verificationKeyHash, + fixture.otherVerificationKeyHash + ); + }); + + test('returns nothing for a hash no account update ever set', async () => { + const updates = await query({ + verificationKeyHash: 'not-a-verification-key-hash', + }); + + assert.deepStrictEqual(updates, []); + }); + + // ─── Chain status ────────────────────────────────────────────────── + + test('CANONICAL returns only canonical occurrences', async () => { + const updates = await query({ status: BlockStatusFilter.canonical }); + + assert.deepStrictEqual( + updates.map(shapeOf), + fixture.expected.canonical.map(expectedShape) + ); + assert.ok(updates.every((u) => u.blockInfo.chainStatus === 'canonical')); + }); + + test('PENDING returns only occurrences on the best pending chain', async () => { + const updates = await query({ status: BlockStatusFilter.pending }); + + assert.deepStrictEqual( + updates.map(shapeOf), + fixture.expected.pending.map(expectedShape) + ); + assert.ok(updates.every((u) => u.blockInfo.chainStatus === 'pending')); + }); + + test('ALL is the concatenation of canonical and pending, and is the default', async () => { + const explicit = await query({ status: BlockStatusFilter.all }); + const defaulted = await query(); + + assert.deepStrictEqual(defaulted.map(shapeOf), explicit.map(shapeOf)); + assert.strictEqual( + explicit.length, + fixture.expected.canonical.length + fixture.expected.pending.length + ); + }); + + // ─── Ordering ────────────────────────────────────────────────────── + + test('orders competing tips at the same height deterministically', async () => { + // Both pending tips at the maximum height carry the SAME command, so their + // rows agree on height, sequence_no, account-update position and + // zkapp_account_update.id. Only the block separates them. Without a total + // order the two rows come back in whatever order the plan produced. + const atTip = (await query({ status: BlockStatusFilter.pending })).filter( + (u) => u.blockInfo.height === fixture.heights.lastPending + ); + + assert.ok(atTip.length >= 2, 'fixture must have two competing tips'); + const hashes = atTip.map((u) => u.blockInfo.stateHash); + assert.deepStrictEqual( + hashes, + [...hashes].sort(), + 'occurrences at one height must be ordered by state hash' + ); + }); + + test('repeats the same order on every run', async () => { + const runs = await Promise.all([query(), query(), query(), query()]); + const first = JSON.stringify(runs[0].map(shapeOf)); + + for (const run of runs.slice(1)) { + assert.strictEqual(JSON.stringify(run.map(shapeOf)), first); + } + }); + + test('orders account updates inside one command by their position', async () => { + // alpha is at position 1 and gamma at position 3 of the same command, with + // beta between them setting a different key. The gap must not reorder them. + const inFirstBlock = ( + await query({ status: BlockStatusFilter.canonical }) + ).filter((u) => u.blockInfo.height === fixture.heights.firstCommand); + + assert.deepStrictEqual( + inFirstBlock.map((u) => u.address), + fixture.expected.canonical + .filter((o) => o.height === fixture.heights.firstCommand) + .map((o) => o.address) + ); + }); + + // ─── Block range ─────────────────────────────────────────────────── + + test('from is inclusive and to is exclusive', async () => { + const height = fixture.heights.firstCommand; + const inRange = await query({ from: height, to: height + 1 }); + assert.ok(inRange.length > 0); + assert.ok(inRange.every((u) => u.blockInfo.height === height)); + + const next = await query({ from: height + 1, to: height + 2 }); + assert.ok(next.every((u) => u.blockInfo.height === height + 1)); + assert.ok( + !next.some((u) => u.blockInfo.height === height), + 'the from bound must exclude everything below it' + ); + }); + + test('rejects an empty range before it reaches the database', async () => { + const height = fixture.heights.firstCommand; + + await assert.rejects(query({ from: height, to: height }), (error: Error) => { + assert.match(error.message, /to must be greater than from/); + return true; + }); + }); +});