@effectstream/midnight-contracts 0.100.17 → 0.100.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -41,6 +41,8 @@ Returns `{ contractAddress, contractInfo, zkConfigPath }`. Results are cached pe
41
41
 
42
42
  ### Deploy a contract
43
43
 
44
+ Only `contractName` and `contractClass` are required — everything else has a sensible default, so a minimal deploy is just:
45
+
44
46
  ```typescript
45
47
  import {
46
48
  deployMidnightContract,
@@ -48,13 +50,24 @@ import {
48
50
  type NetworkUrls,
49
51
  } from "@effectstream/midnight-contracts/deploy";
50
52
 
53
+ // contractName must match the directory that holds `src/managed`;
54
+ // contractClass is the compiled Contract (e.g. `export * as Counter from "./managed/contract"`).
55
+ const address = await deployMidnightContract({
56
+ contractName: "contract-counter",
57
+ contractClass: Counter.Contract,
58
+ });
59
+ ```
60
+
61
+ Supply the rest only when your contract needs it:
62
+
63
+ ```typescript
51
64
  const config: DeployConfig = {
52
65
  contractName: "contract-counter",
53
- contractFileName: "contract-counter.json",
54
66
  contractClass: Counter.Contract,
55
- witnesses,
56
- privateStateId: "counterPrivateState",
57
- initialPrivateState: { privateCounter: 0 },
67
+ witnesses, // default: {} (no witnesses)
68
+ privateStateId: "counterPrivateState", // default: "privateState"
69
+ initialPrivateState: { privateCounter: 0 }, // default: {} (no private state)
70
+ contractFileName: "contract-counter.json", // default: `${contractName}.json`
58
71
  };
59
72
 
60
73
  const address = await deployMidnightContract(config);
@@ -75,6 +88,25 @@ const address = await deployMidnightContract(config, network);
75
88
 
76
89
  The deploy helper creates and funds a wallet from the genesis mint seed, runs the deployment, and writes the resulting address to `${contractName}.${networkId}.json` so the next `readMidnightContract` call picks it up.
77
90
 
91
+ ### Contracts with many circuits (phased deployment)
92
+
93
+ A standard deploy carries every circuit's verifier key in a single transaction. For contracts with enough circuits, that transaction exceeds the node's per-block limits and fails with `Transaction would exhaust block limits`. Set `phasedVerifierKeys: true` to instead deploy the contract with no verifier keys and then insert each circuit's key in its own transaction:
94
+
95
+ ```typescript
96
+ const address = await deployMidnightContract({
97
+ ...config,
98
+ phasedVerifierKeys: true, // opt-in; default is a single-transaction deploy
99
+ });
100
+ ```
101
+
102
+ Phased mode writes progress to a resume-state file (`deployment-state.json` by default) and removes it on success, so an interrupted run can be re-run to continue from the last inserted circuit instead of redeploying. Tunables:
103
+
104
+ - `phasedVerifierKeys?: boolean` — enable phased deployment (default `false`).
105
+ - `vkInsertRetries?: number` — per-circuit retry count for key insertion (default `3`).
106
+ - `phasedStateFile?: string` — resume-state file path (default `deployment-state.json` in the CWD).
107
+
108
+ The circuit list is enumerated automatically from the contract's compiled `keys/` directory, so no per-contract configuration is required.
109
+
78
110
  ## Inside Effectstream
79
111
 
80
112
  `@effectstream/midnight-contracts` is the seam between Midnight contract artifacts on disk and the code that reads or deploys them. Templates that target Midnight call `readMidnightContract` from their node startup to resolve the on-chain address; the orchestrator's deploy step calls `deployMidnightContract` to put one there in the first place. On the sync side, `@effectstream/sync`'s `MidnightFetcher` consumes the node behind both calls.
@@ -87,7 +119,8 @@ The deploy helper creates and funds a wallet from the genesis mint seed, runs th
87
119
 
88
120
  `@effectstream/midnight-contracts/deploy`:
89
121
 
90
- - `deployMidnightContract(config, networkUrls?)`: deploys and returns the address. Persists the address to a JSON file.
122
+ - `deployMidnightContract(config, networkUrls?)`: deploys and returns the address. Persists the address to a JSON file. Set `config.phasedVerifierKeys` for contracts whose circuits don't fit in a single deploy transaction.
123
+ - `deployMidnightContractPhased(...)`: the phased deploy routine `deployMidnightContract` delegates to when `phasedVerifierKeys` is set. Exported for advanced callers that already have built providers and a wallet.
91
124
  - `DeployConfig`, `NetworkUrls`: input types.
92
125
 
93
126
  ## Examples
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effectstream/midnight-contracts",
3
- "version": "0.100.17",
3
+ "version": "0.100.19",
4
4
  "license": "MIT",
5
5
  "homepage": "https://effectstream.github.io/docs/",
6
6
  "repository": {
@@ -20,7 +20,7 @@
20
20
  "./types": "./src/types.ts"
21
21
  },
22
22
  "dependencies": {
23
- "@effectstream/utils": "0.100.17",
23
+ "@effectstream/utils": "0.100.19",
24
24
  "@scure/bip39": "^2.0.1",
25
25
  "dotenv": "^16.4.7",
26
26
  "@midnight-ntwrk/wallet": "5.0.0",
@@ -0,0 +1,284 @@
1
+ // ============================================================================
2
+ // Phased Midnight contract deployment
3
+ // ============================================================================
4
+ //
5
+ // Some contracts define so many circuits that a single deploy transaction —
6
+ // which carries every circuit's verifier key (VK) — exceeds the node's
7
+ // per-block transaction limits ("Transaction would exhaust block limits").
8
+ //
9
+ // This module deploys such contracts in phases:
10
+ // 1. Deploy the contract with NO verifier keys (a "stripped" contract state
11
+ // that keeps only the initial data and maintenance authority).
12
+ // 2. Insert each circuit's verifier key in its own transaction, using the
13
+ // official `submitInsertVerifierKeyTx` maintenance helper.
14
+ //
15
+ // Progress is tracked in a small resume-state file so an interrupted run can be
16
+ // continued without redeploying or re-inserting already-applied keys.
17
+ //
18
+ // This path is opt-in via `DeployConfig.phasedVerifierKeys`; the default
19
+ // single-transaction deploy in `deploy.ts` is unaffected.
20
+
21
+ import { readFileSync, writeFileSync, rmSync, readdirSync } from "node:fs";
22
+ import * as path from "node:path";
23
+ import { getNetworkId } from "@midnight-ntwrk/midnight-js-network-id";
24
+ import { ContractExecutable } from "@midnight-ntwrk/compact-js";
25
+ import {
26
+ asContractAddress,
27
+ exitResultOrError,
28
+ makeContractExecutableRuntime,
29
+ SucceedEntirely,
30
+ type MidnightProviders,
31
+ } from "@midnight-ntwrk/midnight-js-types";
32
+ import { submitInsertVerifierKeyTx } from "@midnight-ntwrk/midnight-js-contracts";
33
+ import {
34
+ ContractDeploy,
35
+ ContractState,
36
+ Intent,
37
+ sampleSigningKey,
38
+ Transaction,
39
+ } from "@midnight-ntwrk/ledger-v8";
40
+
41
+ import { CONSTANTS } from "./constants.ts";
42
+ import type { DeployConfig, WalletResult } from "./types.ts";
43
+
44
+ const log = console;
45
+
46
+ const DEFAULT_VK_INSERT_RETRIES = 3;
47
+ const DEFAULT_STATE_FILE = "deployment-state.json";
48
+
49
+ interface PhasedDeploymentState {
50
+ contractAddress: string;
51
+ deployedCircuits: string[];
52
+ }
53
+
54
+ function createTtl(): Date {
55
+ return new Date(Date.now() + CONSTANTS.TTL_DURATION_MS);
56
+ }
57
+
58
+ /**
59
+ * Enumerate circuit IDs generically from the compiled `keys/` directory.
60
+ * Each circuit has a `<circuitId>.verifier` artifact; this avoids hardcoding a
61
+ * per-contract circuit list.
62
+ */
63
+ function listCircuitIds(zkConfigPath: string): string[] {
64
+ const keysDir = path.join(zkConfigPath, "keys");
65
+ let entries: string[];
66
+ try {
67
+ entries = readdirSync(keysDir);
68
+ } catch (error) {
69
+ throw new Error(
70
+ `Could not read verifier keys directory '${keysDir}': ${
71
+ error instanceof Error ? error.message : String(error)
72
+ }`,
73
+ );
74
+ }
75
+ const circuitIds = entries
76
+ .filter((name) => name.endsWith(".verifier"))
77
+ .map((name) => name.slice(0, -".verifier".length))
78
+ .sort();
79
+ if (circuitIds.length === 0) {
80
+ throw new Error(`No '*.verifier' artifacts found in '${keysDir}'.`);
81
+ }
82
+ return circuitIds;
83
+ }
84
+
85
+ function loadState(stateFilePath: string): PhasedDeploymentState {
86
+ try {
87
+ const parsed = JSON.parse(
88
+ readFileSync(stateFilePath, "utf8"),
89
+ ) as PhasedDeploymentState;
90
+ log.info(
91
+ `Found existing deployment state. Resuming deployment for contract: ${parsed.contractAddress}`,
92
+ );
93
+ return {
94
+ contractAddress: parsed.contractAddress ?? "",
95
+ deployedCircuits: parsed.deployedCircuits ?? [],
96
+ };
97
+ } catch {
98
+ return { contractAddress: "", deployedCircuits: [] };
99
+ }
100
+ }
101
+
102
+ function saveState(stateFilePath: string, state: PhasedDeploymentState): void {
103
+ writeFileSync(stateFilePath, JSON.stringify(state, null, 2));
104
+ }
105
+
106
+ /**
107
+ * Deploy a Midnight contract in phases: empty deploy, then per-circuit verifier
108
+ * key insertion. Returns the deployed contract address.
109
+ */
110
+ export async function deployMidnightContractPhased(
111
+ providers: MidnightProviders,
112
+ // deno-lint-ignore no-explicit-any
113
+ compiledContract: any,
114
+ config: DeployConfig,
115
+ // deno-lint-ignore no-explicit-any
116
+ deployArgs: any[] | undefined,
117
+ walletResult: WalletResult,
118
+ zkConfigPath: string,
119
+ ): Promise<string> {
120
+ const stateFilePath = path.resolve(
121
+ config.phasedStateFile ?? path.join(process.cwd(), DEFAULT_STATE_FILE),
122
+ );
123
+ const maxRetries = config.vkInsertRetries ?? DEFAULT_VK_INSERT_RETRIES;
124
+
125
+ const state = loadState(stateFilePath);
126
+ let contractAddress = state.contractAddress;
127
+
128
+ log.info(
129
+ `Phased deploy: contract=${config.contractName} resuming=${Boolean(
130
+ contractAddress,
131
+ )}`,
132
+ );
133
+
134
+ // ── Phase 1: deploy the contract with no verifier keys ────────────────────
135
+ if (!contractAddress) {
136
+ const signingKey = sampleSigningKey();
137
+ const coinPublicKey = providers.walletProvider.getCoinPublicKey().toString();
138
+
139
+ const contractExec = ContractExecutable.make(compiledContract);
140
+ const contractRuntime = makeContractExecutableRuntime(
141
+ providers.zkConfigProvider,
142
+ { coinPublicKey, signingKey },
143
+ );
144
+
145
+ log.info("Running contract initialization to derive initial state...");
146
+ const exitResult = await contractRuntime.runPromiseExit(
147
+ (contractExec as any).initialize(
148
+ config.initialPrivateState ?? undefined,
149
+ ...(deployArgs ?? []),
150
+ ),
151
+ );
152
+ const initResult = exitResultOrError(exitResult as any) as any;
153
+
154
+ const privateState = initResult.private.privateState;
155
+ const derivedSigningKey = initResult.private.signingKey;
156
+ const fullContractState = initResult.public.contractState;
157
+
158
+ // Convert the compact-runtime contract state to a ledger-v8 contract state,
159
+ // then build a stripped copy that keeps the initial data + maintenance
160
+ // authority but drops every circuit operation (i.e. no verifier keys).
161
+ const fullLedgerState = ContractState.deserialize(
162
+ fullContractState.serialize(),
163
+ );
164
+ const strippedState = new ContractState();
165
+ strippedState.data = fullLedgerState.data;
166
+ strippedState.maintenanceAuthority = fullLedgerState.maintenanceAuthority;
167
+ log.info("Created stripped contract state (no verifier keys).");
168
+
169
+ const contractDeploy = new ContractDeploy(strippedState);
170
+ contractAddress = contractDeploy.address;
171
+
172
+ const unprovenTx = Transaction.fromParts(
173
+ getNetworkId(),
174
+ undefined,
175
+ undefined,
176
+ Intent.new(createTtl()).addDeploy(contractDeploy),
177
+ );
178
+ log.info(`Deploying empty contract at address: ${contractAddress}`);
179
+
180
+ const recipe = await walletResult.wallet.balanceUnprovenTransaction(
181
+ unprovenTx as any,
182
+ {
183
+ shieldedSecretKeys: walletResult.walletZswapSecretKeys as any,
184
+ dustSecretKey: walletResult.walletDustSecretKey as any,
185
+ },
186
+ { ttl: createTtl() },
187
+ );
188
+ const signedRecipe = await walletResult.wallet.signRecipe(
189
+ recipe,
190
+ (payload) => walletResult.unshieldedKeystore.signData(payload),
191
+ );
192
+ const finalizedTx = await walletResult.wallet.finalizeRecipe(signedRecipe);
193
+ const txId = await walletResult.wallet.submitTransaction(finalizedTx);
194
+
195
+ const txData = await providers.publicDataProvider.watchForTxData(txId);
196
+ if (txData.status !== SucceedEntirely) {
197
+ throw new Error(`Empty deploy failed with status ${txData.status}`);
198
+ }
199
+ log.info(
200
+ `Empty deploy succeeded: address=${contractAddress} (no verifier keys).`,
201
+ );
202
+
203
+ // Persist the contract address, private state and signing key so the VK
204
+ // insertion phase (and any resumed run) can find the maintenance authority.
205
+ state.contractAddress = contractAddress;
206
+ saveState(stateFilePath, state);
207
+
208
+ (providers.privateStateProvider as any).setContractAddress?.(contractAddress);
209
+ if (config.privateStateId) {
210
+ await providers.privateStateProvider.set(
211
+ config.privateStateId as any,
212
+ privateState,
213
+ );
214
+ }
215
+ await providers.privateStateProvider.setSigningKey(
216
+ contractAddress,
217
+ derivedSigningKey,
218
+ );
219
+ }
220
+
221
+ // ── Phase 2: insert each circuit's verifier key individually ──────────────
222
+ const circuitIds = listCircuitIds(zkConfigPath);
223
+ const verifierKeys = await providers.zkConfigProvider.getVerifierKeys(
224
+ circuitIds,
225
+ );
226
+ log.info(
227
+ `Inserting ${verifierKeys.length} verifier keys one-by-one: [${circuitIds.join(
228
+ ", ",
229
+ )}]`,
230
+ );
231
+
232
+ for (const [circuitId, verifierKey] of verifierKeys) {
233
+ if (state.deployedCircuits.includes(circuitId)) {
234
+ log.info(`Skipping already deployed circuit: ${circuitId}`);
235
+ continue;
236
+ }
237
+
238
+ log.info(`Inserting verifier key for circuit: ${circuitId}`);
239
+ for (let attempt = 1; ; attempt++) {
240
+ try {
241
+ const result = await submitInsertVerifierKeyTx(
242
+ providers,
243
+ compiledContract,
244
+ asContractAddress(contractAddress),
245
+ circuitId as any,
246
+ verifierKey,
247
+ );
248
+ if (result.status !== SucceedEntirely) {
249
+ throw new Error(
250
+ `Insert verifier key for ${circuitId} returned status ${result.status}`,
251
+ );
252
+ }
253
+ break;
254
+ } catch (error) {
255
+ if (attempt >= maxRetries) throw error;
256
+ log.warn(
257
+ `Retry inserting ${circuitId} (${attempt}/${maxRetries}) due to: ${
258
+ error instanceof Error ? error.message : String(error)
259
+ }`,
260
+ );
261
+ await new Promise((r) => setTimeout(r, 2000 * attempt));
262
+ }
263
+ }
264
+
265
+ state.deployedCircuits.push(circuitId);
266
+ saveState(stateFilePath, state);
267
+ log.info(
268
+ `Verifier key inserted: circuit=${circuitId} (${state.deployedCircuits.length}/${verifierKeys.length})`,
269
+ );
270
+ }
271
+
272
+ log.info(
273
+ `Phased deploy complete: address=${contractAddress} circuits=[${state.deployedCircuits.join(
274
+ ", ",
275
+ )}]`,
276
+ );
277
+ try {
278
+ rmSync(stateFilePath);
279
+ } catch {
280
+ // Resume-state file already gone — nothing to clean up.
281
+ }
282
+
283
+ return contractAddress;
284
+ }
package/src/deploy.ts CHANGED
@@ -19,6 +19,7 @@ import { mnemonicToSeed } from "./mnemonicToSeed.ts";
19
19
  import type { NetworkUrls, DeployConfig, WalletResult } from "./types.ts";
20
20
  import { buildWalletAndWaitForFunds, extractInitialOwnerFromWallet } from "./build-wallet.ts";
21
21
  import { configureMidnightNodeProviders } from "./providers.ts";
22
+ import { deployMidnightContractPhased } from "./deploy-phased.ts";
22
23
  import { midnightNetworkConfig } from "./midnight-env.ts";
23
24
 
24
25
  function checkEnvVariables(): void {
@@ -209,45 +210,71 @@ export async function deployMidnightContract(
209
210
 
210
211
  log.info("Deploying contract...");
211
212
 
213
+ // Apply defaults so callers only pass what their contract needs. Only
214
+ // contractName and contractClass are truly required; everything else has a
215
+ // sensible default.
216
+ const resolvedConfig: DeployConfig = {
217
+ ...config,
218
+ contractFileName: config.contractFileName ?? `${config.contractName}.json`,
219
+ witnesses: config.witnesses ?? {},
220
+ privateStateId: config.privateStateId ?? "privateState",
221
+ initialPrivateState: config.initialPrivateState ?? {},
222
+ };
223
+
212
224
  // First, create the compiled contract
213
225
  const MyCompiledContract = CompiledContract.make(
214
- config.contractName,
215
- config.contractClass,
226
+ resolvedConfig.contractName,
227
+ resolvedConfig.contractClass,
216
228
  ).pipe(
217
- CompiledContract.withWitnesses(config.witnesses as never),
229
+ CompiledContract.withWitnesses(resolvedConfig.witnesses as never),
218
230
  CompiledContract.withCompiledFileAssets(managedDir),
219
231
  );
220
232
 
221
- const deployOptions: {
222
- compiledContract: CompiledContract.CompiledContract<
223
- Contract<undefined, Witnesses<undefined>>,
224
- undefined,
225
- never
226
- >;
227
- privateStateId: PrivateStateId;
228
- initialPrivateState: Contract.PrivateState<any>;
229
- signingKey?: SigningKey;
230
- args: Contract.InitializeParameters<any>;
231
- } = {
232
- compiledContract: MyCompiledContract as any,
233
- privateStateId: config.privateStateId as PrivateStateId,
234
- initialPrivateState:
235
- config.initialPrivateState as Contract.PrivateState<any>,
236
- args: (deployArgs && deployArgs.length > 0
237
- ? deployArgs
238
- : []) as Contract.InitializeParameters<any>,
239
- signingKey: undefined,
240
- };
241
-
242
- const deployedContract = await deployContract(providers, deployOptions);
243
- log.info("Contract deployed.");
244
-
245
- const contractAddress =
246
- deployedContract.deployTxData.public.contractAddress;
233
+ let contractAddress: string;
234
+ if (resolvedConfig.phasedVerifierKeys) {
235
+ // Opt-in path for contracts with too many circuits to deploy in a single
236
+ // transaction: deploy with no verifier keys, then insert each circuit's
237
+ // key one transaction at a time.
238
+ log.info("Phased verifier-key deployment enabled.");
239
+ contractAddress = await deployMidnightContractPhased(
240
+ providers,
241
+ MyCompiledContract,
242
+ resolvedConfig,
243
+ deployArgs,
244
+ walletResult,
245
+ zkConfigPath,
246
+ );
247
+ log.info("Contract deployed (phased).");
248
+ } else {
249
+ const deployOptions: {
250
+ compiledContract: CompiledContract.CompiledContract<
251
+ Contract<undefined, Witnesses<undefined>>,
252
+ undefined,
253
+ never
254
+ >;
255
+ privateStateId: PrivateStateId;
256
+ initialPrivateState: Contract.PrivateState<any>;
257
+ signingKey?: SigningKey;
258
+ args: Contract.InitializeParameters<any>;
259
+ } = {
260
+ compiledContract: MyCompiledContract as any,
261
+ privateStateId: resolvedConfig.privateStateId as PrivateStateId,
262
+ initialPrivateState:
263
+ resolvedConfig.initialPrivateState as Contract.PrivateState<any>,
264
+ args: (deployArgs && deployArgs.length > 0
265
+ ? deployArgs
266
+ : []) as Contract.InitializeParameters<any>,
267
+ signingKey: undefined,
268
+ };
269
+
270
+ const deployedContract = await deployContract(providers, deployOptions);
271
+ log.info("Contract deployed.");
272
+ contractAddress = deployedContract.deployTxData.public.contractAddress;
273
+ }
247
274
  log.info(`Contract address: ${contractAddress}`);
248
275
 
249
276
  const baseContractFileName =
250
- config.contractFileName ?? `${config.contractName}.json`;
277
+ resolvedConfig.contractFileName ?? `${config.contractName}.json`;
251
278
  const {
252
279
  dir: contractFileDir,
253
280
  name: contractFileBaseName,
package/src/mod.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export { deployMidnightContract } from "./deploy.ts";
2
+ export { deployMidnightContractPhased } from "./deploy-phased.ts";
2
3
  export type {
3
4
  DeployConfig,
4
5
  NetworkUrls,
package/src/types.ts CHANGED
@@ -11,21 +11,21 @@ import type { UnshieldedKeystore } from "@midnight-ntwrk/wallet-sdk-unshielded-w
11
11
  * Configuration for deploying a Midnight contract
12
12
  */
13
13
  export interface DeployConfig {
14
- /** Name of the contract directory (e.g., "contract-counter", "contract-eip-20") */
14
+ /** Name of the contract directory containing `src/managed` (e.g., "contract-counter", "contract-eip-20"). Required. */
15
15
  contractName: string;
16
- /** Base filename for contract address (e.g., "contract-counter.json"); a network suffix is appended */
17
- contractFileName: string;
18
- /** The Contract class to deploy */
16
+ /** The compiled Contract class to deploy (e.g. `Foo.Contract`). Required. */
19
17
  // deno-lint-ignore no-explicit-any
20
18
  contractClass: any;
21
- /** Witness definitions */
19
+ /** Base filename the deployed address is written to; a network suffix is appended. Defaults to `${contractName}.json`. */
20
+ contractFileName?: string;
21
+ /** Witness definitions. Defaults to `{}` (no witnesses). */
22
22
  // deno-lint-ignore no-explicit-any
23
- witnesses: any;
24
- /** On-chain private state ID */
25
- privateStateId: string;
26
- /** Initial private state object */
23
+ witnesses?: any;
24
+ /** On-chain private state ID. Defaults to `"privateState"`. */
25
+ privateStateId?: string;
26
+ /** Initial private state object. Defaults to `{}` (for contracts with no private state). */
27
27
  // deno-lint-ignore no-explicit-any
28
- initialPrivateState: any;
28
+ initialPrivateState?: any;
29
29
  /** Optional deployment arguments array */
30
30
  // deno-lint-ignore no-explicit-any
31
31
  deployArgs?: any[];
@@ -35,6 +35,25 @@ export interface DeployConfig {
35
35
  baseDir?: string;
36
36
  /** Optional flag to extract wallet address info (for contracts that need initialOwner) */
37
37
  extractWalletAddress?: boolean;
38
+ /**
39
+ * Deploy in phases instead of a single transaction.
40
+ *
41
+ * When `true`, the contract is first deployed with NO verifier keys, then each
42
+ * circuit's verifier key is inserted in its own transaction. Use this for
43
+ * contracts with many circuits whose combined verifier keys would otherwise
44
+ * exceed the node's per-block transaction limits ("Transaction would exhaust
45
+ * block limits"). Defaults to `false` (single-transaction deploy).
46
+ */
47
+ phasedVerifierKeys?: boolean;
48
+ /** Per-circuit retry count when inserting verifier keys in phased mode (default 3). */
49
+ vkInsertRetries?: number;
50
+ /**
51
+ * Path to the resume-state file used by phased mode to track progress so an
52
+ * interrupted deployment can be resumed. Defaults to `deployment-state.json`
53
+ * in the current working directory. This file is removed once the phased
54
+ * deployment completes successfully.
55
+ */
56
+ phasedStateFile?: string;
38
57
  }
39
58
 
40
59
  /**