Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/deployer/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Binary file not shown.
58 changes: 49 additions & 9 deletions packages/deployer/src/services/maintenance-tx.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down
35 changes: 32 additions & 3 deletions packages/deployer/src/services/maintenance-tx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<VerifierKeyVersion, true>) 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
Expand Down Expand Up @@ -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
Expand Down
Loading