From 30248fda4b17a349c41923781a2006ccf3943cda Mon Sep 17 00:00:00 2001 From: 0xisk <0xisk@proton.me> Date: Fri, 25 Sep 2026 10:30:27 +0200 Subject: [PATCH 1/2] fix(deployer): version each inserted verifier key The insert path wrapped every key as `v3`, so a zkir-v3 build's `[v7]` keys failed the ledger's header check on any fragmented deploy. The ledger now picks the version per key, from the set its types declare. --- .../counter-increment-zkir-v3.verifier | Bin 0 -> 1353 bytes .../src/services/maintenance-tx.test.ts | 58 +++++++++++++++--- .../deployer/src/services/maintenance-tx.ts | 35 ++++++++++- 3 files changed, 81 insertions(+), 12 deletions(-) create mode 100644 packages/deployer/src/services/fixtures/counter-increment-zkir-v3.verifier diff --git a/packages/deployer/src/services/fixtures/counter-increment-zkir-v3.verifier b/packages/deployer/src/services/fixtures/counter-increment-zkir-v3.verifier new file mode 100644 index 0000000000000000000000000000000000000000..ed630bfc95fa97f6f730a06abde4d4ab5e4c8f05 GIT binary patch literal 1353 zcmd1JOv%ek&nU4fOD)Pw%S~|Y| zbnK~}Z*eHKXh*DU@GSoF)2I5D+lnved$nXU>(5jCaz!WX);K(O=qzyU&fuTtt$)C0 z+R_)Uw|=i*?~g$t?-;^UB#mH={|CaG}*|9xQKGVHsJ1X{td#Cn?G28um z{!W_bi}9nbb9pI!-*g`=D|7K|IMTarf!;IIiR(6Oy833mXBCGh*5=qVfdoBNLiN>5HMsdGF3xlY1Kl;1Xg zWjyow-rE9+d5tWrtHs|KOK*N;c5Uh-FD3ia(>co?S52GLu9{za?XNHu?( zhWKMPjquWl4@-{(iLMX&%s6Yk&(g;XVIMSC%+>o+B)8)NbL79rwm#mW9CEU|i~G(0 zMspVd`dNLgm`IDN&VL=~gQ=X~0gVphJ`x9h*@ z-Q1_?dye}5u8h8L?{H%*pYxh2jw|jyoHea-i?@x4A|J!|hx`v`r0GxDv^D-r=}vaT z`4L~b?_V$sG+H6=m9W;wp?}vlX1*=UPoAiExVm+&eJ@jMxt&h;E!n#}FFRfd`%-tv z>1SeGK8M6C^IlggC%1<4KRpt=L!I5^<{7i>05`o| q7ay6;c&5q4HqTM~sA$Kbpy1=?>POE6oKAMF+kGc}@z3H11(pCS3L-)P literal 0 HcmV?d00001 diff --git a/packages/deployer/src/services/maintenance-tx.test.ts b/packages/deployer/src/services/maintenance-tx.test.ts index 27ad6c1..f384c89 100644 --- a/packages/deployer/src/services/maintenance-tx.test.ts +++ b/packages/deployer/src/services/maintenance-tx.test.ts @@ -16,19 +16,21 @@ import { buildInsertTx, buildInsertUpdate, verifyingKeyOf, + versionedVerifierKey, } from './maintenance-tx.ts'; +const fixture = (name: string) => + new Uint8Array( + readFileSync(fileURLToPath(new URL(`./fixtures/${name}`, import.meta.url))), + ); + /** - * A real compiler-emitted key: the ledger checks the `midnight:verifier-key` - * header on construction, so arbitrary bytes cannot stand in. + * Real compiler-emitted keys: the ledger checks the `midnight:verifier-key` + * header on construction, so arbitrary bytes cannot stand in. The zkir-v3 + * build of the same circuit emits the next header version. */ -const VERIFIER_KEY = new Uint8Array( - readFileSync( - fileURLToPath( - new URL('./fixtures/counter-increment.verifier', import.meta.url), - ), - ), -); +const VERIFIER_KEY = fixture('counter-increment.verifier'); +const ZKIR_V3_VERIFIER_KEY = fixture('counter-increment-zkir-v3.verifier'); // `buildInsertTx` reads the midnight-js network-id singleton, which the // deployer sets from `[networks.X].network_id` before any tx is built. @@ -57,7 +59,45 @@ describe('verifyingKeyOf', () => { }); }); +describe('versionedVerifierKey', () => { + it('should version a `[v6]` key as v3', () => { + expect(versionedVerifierKey('increment', VERIFIER_KEY).version).toBe('v3'); + }); + + it('should version a zkir-v3 `[v7]` key as v4', () => { + expect( + versionedVerifierKey('increment', ZKIR_V3_VERIFIER_KEY).version, + ).toBe('v4'); + }); + + it('should reject a key no ledger version accepts, naming the circuit', () => { + expect(() => + versionedVerifierKey('increment', new Uint8Array([1, 2, 3])), + ).toThrow(/Verifier key for circuit "increment" matches no ledger version/); + expect(() => + versionedVerifierKey('increment', new Uint8Array([1, 2, 3])), + ).toThrow(DeployError); + }); +}); + describe('buildInsertUpdate', () => { + it('should insert keys of different versions in one update', () => { + const update = buildInsertUpdate({ + address: ADDRESS, + inserts: [ + { circuitId: 'increment', verifierKey: VERIFIER_KEY }, + { circuitId: 'reset', verifierKey: ZKIR_V3_VERIFIER_KEY }, + ], + counter: 0n, + signingKey: SIGNING_KEY, + signerIndex: 0, + }); + + expect( + update.updates.map((u) => (u as VerifierKeyInsert).vk.version), + ).toStrictEqual(['v3', 'v4']); + }); + it('should emit one VerifierKeyInsert per circuit and nothing else', () => { const update = buildInsertUpdate({ address: ADDRESS, diff --git a/packages/deployer/src/services/maintenance-tx.ts b/packages/deployer/src/services/maintenance-tx.ts index f17e267..4f83714 100644 --- a/packages/deployer/src/services/maintenance-tx.ts +++ b/packages/deployer/src/services/maintenance-tx.ts @@ -21,8 +21,37 @@ import { } from '@midnightntwrk/ledger-v9'; import { DeployError } from '../errors.ts'; -/** Only ledger operation version the compiler emits keys for. */ -const VK_VERSION = 'v3' as const; +type VerifierKeyVersion = ContractOperationVersionedVerifierKey['version']; + +/** + * Every verifier-key version the ledger accepts. The `Record` fails the build + * when a ledger bump adds or drops a version. + */ +const VERIFIER_KEY_VERSIONS = Object.keys({ + v3: true, + v4: true, +} satisfies Record) as VerifierKeyVersion[]; + +/** + * `verifierKey` under the version its header declares. The ledger rejects a + * header that does not match the version, so it picks the version, not us. + */ +export function versionedVerifierKey( + circuitId: string, + verifierKey: Uint8Array, +): ContractOperationVersionedVerifierKey { + const rejections: string[] = []; + for (const version of VERIFIER_KEY_VERSIONS) { + try { + return new ContractOperationVersionedVerifierKey(version, verifierKey); + } catch (error) { + rejections.push(`${version}: ${(error as Error).message}`); + } + } + throw new DeployError( + `Verifier key for circuit "${circuitId}" matches no ledger version (${rejections.join('; ')}).`, + ); +} /** * One circuit's key. The absence of any other update shape is what @@ -92,7 +121,7 @@ export function buildInsertUpdate({ ({ circuitId, verifierKey }) => new VerifierKeyInsert( circuitId, - new ContractOperationVersionedVerifierKey(VK_VERSION, verifierKey), + versionedVerifierKey(circuitId, verifierKey), ), ); // Sign the built update, then attach; `addSignature` returns the From a00214d1ad223e0da0a250e09114d3f4efb1753d Mon Sep 17 00:00:00 2001 From: 0xisk <0xisk@proton.me> Date: Fri, 25 Sep 2026 10:30:50 +0200 Subject: [PATCH 2/2] docs(deployer): note the verifier-key version fix --- packages/deployer/CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/deployer/CHANGELOG.md b/packages/deployer/CHANGELOG.md index 74a6f5a..bd49170 100644 --- a/packages/deployer/CHANGELOG.md +++ b/packages/deployer/CHANGELOG.md @@ -6,6 +6,12 @@ file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this package adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Fixed + +- A fragmented deploy of a contract compiled with `--feature-zkir-v3` no longer fails its verifier-key insert with `expected header tag 'midnight:verifier-key[v6]:'`. Each key is inserted under the version the ledger accepts for it (#200) + ## 0.3.0 (2026-09-24) ### Added