@midnight-ntwrk/midnight-js-protocol 5.0.0-beta.8 → 5.0.0-rc.1

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/dist/engine.js CHANGED
@@ -1,6 +1,15 @@
1
1
  import * as ledgerV9 from '@midnightntwrk/ledger-v9';
2
- import { ComposeOptionError, Ledger8RuntimeInvalidError, DownConvertFailedError, MerkleNotRehashedError, UnknownLedger8AxisError, Ledger8InstanceMismatchError } from './errors.js';
3
- import { a as assembleCallPrototype, e as executeConstructor } from './shared/deploy-D6yyn9P7.js';
2
+ import { ComposeOptionError, PROTOCOL_ERROR_CODES, ComposeFailedError, DownConvertFailedError } from './errors.js';
3
+ import { a as assembleCallPrototype, i as isContractBalance, d as describeValue } from './shared/assemble-call-BB1yZMS6.js';
4
+ import * as Contract from '@midnight-ntwrk/compact-js/effect/Contract';
5
+ import * as ContractConfigurationError from '@midnight-ntwrk/compact-js/effect/ContractConfigurationError';
6
+ import { ContractExecutable, CompiledContract, ZKConfiguration, ZKConfigurationReadError } from '@midnight-ntwrk/compact-js/v8/effect';
7
+ import * as CoinPublicKey from '@midnight-ntwrk/platform-js/effect/CoinPublicKey';
8
+ import * as Configuration from '@midnight-ntwrk/platform-js/effect/Configuration';
9
+ import * as PlatformContractAddress from '@midnight-ntwrk/platform-js/effect/ContractAddress';
10
+ import * as SigningKey from '@midnight-ntwrk/platform-js/effect/SigningKey';
11
+ import * as glue from 'compact-runtime-ledger8';
12
+ import { Effect, Layer, Exit, Cause, Option, Clock } from 'effect';
4
13
  import '@midnight-ntwrk/compact-js';
5
14
 
6
15
  /*
@@ -20,31 +29,16 @@ import '@midnight-ntwrk/compact-js';
20
29
  /**
21
30
  * Re-expresses a retained-era contract's entry points as a CURRENT-era contract state.
22
31
  *
23
- * WHY THIS EXISTS. The fork does not rewrite a contract's stored state: a contract deployed before
24
- * it and dormant across it is served, indefinitely, carrying its retained-era envelope. The current
25
- * ledger's composer cannot deserialize those bytes — it accepts only its own envelope — so a
26
- * keep-state call had nothing to hand it and could not be composed at all.
27
- *
28
- * WHAT THE COMPOSER ACTUALLY WANTS. The `contractState` a composition takes is NOT the state being
29
- * proven. `assembleCallPrototype` uses it for exactly one thing: looking up the `ContractOperation`
30
- * for the circuit, to read its verifier key. The state the call is bound to travels separately, as
31
- * the transcript's `preState`. So what has to cross the era boundary here is the OPERATION
32
- * REGISTRY, not the state.
33
- *
34
- * WHY THIS IS FAITHFUL, NOT A SUBSTITUTION. The verifier keys come from the chain's own state, read
35
- * through the retained decoder, and the caller has already checked its local artifact against them.
36
- * The current ledger models retained-era operations natively — `ContractOperationVersion` carries a
37
- * `'v3'` arm precisely for them — so a key preserved across the fork is a value this ledger is built
38
- * to hold. Nothing is minted here: every key written out is one the chain already holds.
39
- *
40
- * The primary state is deliberately left at its default. A caller that needed the state itself would
41
- * be reading it from {@link ContractStatePojo.state}, not from this registry.
32
+ * What crosses the era boundary here is the OPERATION REGISTRY, not the state. The primary state is
33
+ * deliberately left at its default: a caller that needs the state itself reads it from
34
+ * {@link ContractStatePojo.state}.
42
35
  *
43
36
  * @param entryPoints The entry points a retained-era contract state declared, as the retained
44
37
  * decoder read them.
45
38
  * @returns The serialized current-era contract state, carrying those entry points and their keys.
46
- * @throws ComposeOptionError if no entry point carries a key, since the registry would then answer
47
- * for nothing and the composer's own refusal would name a circuit rather than the empty state.
39
+ * @throws ComposeOptionError if no entry point carries a key.
40
+ * @see {@link CrossEraOperationRegistry} for why re-expressing the operations is faithful rather
41
+ * than a substitution, and why the empty registry is refused here rather than one layer down.
48
42
  */
49
43
  const reexpressOperationsForCurrentEra = (entryPoints) => {
50
44
  const state = new ledgerV9.ContractState();
@@ -83,8 +77,8 @@ const reexpressOperationsForCurrentEra = (entryPoints) => {
83
77
  * limitations under the License.
84
78
  */
85
79
  /**
86
- * Wraps a {@link TranscriptPojo} — the output of {@link executeCircuit}
87
- * (`./execute.ts`) — into a v9-native `ContractCallPrototype`, ready for
80
+ * Wraps a {@link TranscriptPojo} — the output of {@link runRetainedCircuit}
81
+ * (`../v8/executable.ts`) — into a v9-native `ContractCallPrototype`, ready for
88
82
  * `Intent.new(ttl).addCall(...)`, via {@link assembleCallPrototype}
89
83
  * (`./assemble-call.ts`) against the ledger-v9 module.
90
84
  *
@@ -104,14 +98,14 @@ const reexpressOperationsForCurrentEra = (entryPoints) => {
104
98
  */
105
99
  const wrapKeepStateCall = (options) => {
106
100
  const { transcript, contractAddress, contractState, ledgerParameters } = options;
107
- return assembleCallPrototype(ledgerV9, {
101
+ const { prototype } = assembleCallPrototype(ledgerV9, {
108
102
  circuitId: transcript.circuitId,
109
103
  contractAddress,
110
104
  // The retained execution leg partitions nothing: it always submits the raw
111
105
  // op sequence as `'unpartitioned'` -- see RetainedEraExecution.
112
106
  transcript: {
113
107
  kind: 'unpartitioned',
114
- preState: transcript.preContractState.data.state.encode(),
108
+ preState: transcript.preContractState.encode(),
115
109
  publicTranscript: transcript.publicTranscript,
116
110
  partitionContext: transcript.partitionContext
117
111
  },
@@ -123,6 +117,7 @@ const wrapKeepStateCall = (options) => {
123
117
  stage: 'wrap-call',
124
118
  version: 'v9'
125
119
  });
120
+ return prototype;
126
121
  };
127
122
 
128
123
  /*
@@ -140,16 +135,190 @@ const wrapKeepStateCall = (options) => {
140
135
  * limitations under the License.
141
136
  */
142
137
  /**
143
- * Structural equality over the `EncodedStateValue` algebra — plain objects,
138
+ * Retained-era (ledger 8) execution, on compact-js's own era-pinned entries.
139
+ *
140
+ * This module replaced a hand-maintained execution layer — `execute.ts`, which
141
+ * drove `compact-runtime@0.16` directly, and `down-convert.ts`, which built the
142
+ * state it ran against. Both are gone; see
143
+ * `docs/adr/0015-retained-era-execution-on-compact-js.md` for why.
144
+ *
145
+ * Everything crossing this module's surface is PLAIN DATA or an era-tagged
146
+ * handle the caller already holds. No `Effect` value leaves it: the effects
147
+ * compact-js returns are discharged here with `Effect.runPromise`, which is
148
+ * also why every entry point is asynchronous — `ContractExecutable` builds its
149
+ * work on `Effect.tryPromise`, so `runSync` cannot discharge it.
150
+ *
151
+ * @see {@link RetainedEraExecution}
152
+ */
153
+ /**
154
+ * The shape the retained runtime reads a signing key in: 32 bytes written in
155
+ * hex, either case.
156
+ *
157
+ * MEASURED against the pinned runtime rather than guessed. `sampleSigningKey()`
158
+ * answers 64 lowercase hex characters; an uppercase key of the same length is
159
+ * accepted, a shorter or longer one fails with `failed to fill whole buffer`
160
+ * and a non-hex one with `Invalid character 'z' at position 0`. The value being
161
+ * a valid Schnorr scalar is a cryptographic property this cannot check, and the
162
+ * runtime's own `Malformed Schnorr signing key` already names it.
163
+ */
164
+ const LEDGER8_SIGNING_KEY_PATTERN = /^[0-9a-fA-F]{64}$/;
165
+ /**
166
+ * The signature kind this era's maintenance authority accepts.
167
+ *
168
+ * MEASURED, not chosen: the era reports `supportsCmaSignatureKind` as
169
+ * `schnorr=true, ecdsa=false`, and `Ledger.fromPlatformSigningKey` answers
170
+ * `Left(ContractConfigurationError)` for an ecdsa key. Naming it here keeps the
171
+ * bare hex string this era's callers hold assignable to platform-js's tagged
172
+ * `SigningKey`, which is what `Configuration.Keys` serves.
173
+ */
174
+ const LEDGER8_SIGNATURE_KIND = 'schnorr';
175
+ /**
176
+ * The tag compact-js reports a retained contract under.
177
+ *
178
+ * It reaches nothing but error messages: the container's tag is chosen by
179
+ * whoever builds it, and this era's callers pass an instance that carries no
180
+ * name of its own.
181
+ */
182
+ const RETAINED_CONTRACT_TAG = 'retained-era-contract';
183
+ /**
184
+ * Wraps a constructed retained contract in the container compact-js executes.
185
+ *
186
+ * `ContractExecutable` resolves the contract through `new ctor(witnesses)`, so
187
+ * it needs a constructor. The caller has an instance, and its witnesses are
188
+ * already bound to it — so the constructor hands that same instance back and
189
+ * the witnesses arrive with it. Returning an object from a constructor is
190
+ * ordinary JavaScript; the assertion is needed only because TypeScript types a
191
+ * class expression by its declaration rather than by what it returns.
192
+ *
193
+ * Reusing one instance across calls is what this era already did before
194
+ * compact-js served it, so this is the existing lifetime, not a new one.
195
+ *
196
+ * The compiled-assets path is a placeholder: it is read only by compact-js's
197
+ * own `ZKConfiguration` reader, and {@link zkConfigurationLayer} replaces that
198
+ * service outright, so nothing ever resolves it.
199
+ */
200
+ const containerFor = (contract) => CompiledContract.make(RETAINED_CONTRACT_TAG,
201
+ // The single assertion this seam needs, and it is here rather than at any
202
+ // caller. A retained artifact satisfies compact-js's `Contract` structurally
203
+ // -- same four circuit maps, and a synchronous `initialState` satisfies its
204
+ // `Awaitable` return -- but the two declare their members over different
205
+ // private-state parameters, so TypeScript will not see it. Narrowing what
206
+ // this module ASKS for (see `RetainedContract`) is what keeps the assertion
207
+ // to one line instead of pushing a cast onto every consumer.
208
+ class {
209
+ constructor() {
210
+ return contract;
211
+ }
212
+ }).pipe(CompiledContract.withVacantWitnesses, CompiledContract.withCompiledFileAssets('.'));
213
+ /**
214
+ * Serves the coin public key and signing key compact-js reads off
215
+ * `Configuration.Keys`.
216
+ *
217
+ * Built with `Layer.succeed` over the values the caller already holds, rather
218
+ * than through platform-js's own `Configuration.layer`, which sources them from
219
+ * a `ConfigProvider`. The service is a plain `Context.Tag` with two members, so
220
+ * there is nothing a config round trip would add except a second place for the
221
+ * values to disagree.
222
+ */
223
+ const keysLayer = (coinPk, signingKey) => Layer.succeed(Configuration.Keys, Configuration.Keys.of({
224
+ coinPublicKey: CoinPublicKey.make(coinPk),
225
+ getSigningKey: () => signingKey === undefined
226
+ ? Option.none()
227
+ : Option.some(SigningKey.make(signingKey, LEDGER8_SIGNATURE_KIND))
228
+ }));
229
+ /**
230
+ * Pins the execution clock, or leaves the ambient one in place.
231
+ *
232
+ * `Effect.withClock`, NOT a `Layer` over `Clock.Clock`. The clock is one of
233
+ * Effect's DEFAULT services: it lives on the fiber rather than in the context,
234
+ * so providing a layer for it type-checks, runs, and is silently ignored — the
235
+ * wall clock still wins. That was measured here, by a test that asserted a
236
+ * pinned second and got `Date.now()`.
237
+ *
238
+ * Only `currentTimeMillis` and its unsafe twin reach an execution — compact-js
239
+ * converts to seconds at the call site — but the whole interface is pinned so a
240
+ * future read cannot quietly fall back to the wall clock.
241
+ */
242
+ const pinnedClock = (nowSeconds) => {
243
+ // BEFORE the rounding, because `Math.round` carries `NaN` and both infinities
244
+ // straight through to `BigInt(millis)`, which refuses them with a bare
245
+ // `RangeError` naming neither the option nor the era. `nowSeconds` is a plain
246
+ // `number` on the published option, and `Number(process.env.X)` with the
247
+ // variable unset is the obvious way to produce one.
248
+ if (!Number.isFinite(nowSeconds)) {
249
+ throw new Error(`the retained era cannot pin its execution clock to '${String(nowSeconds)}'. ` +
250
+ '`nowSeconds` must be a finite number of seconds since the epoch.');
251
+ }
252
+ // ROUNDED, not used raw. `nowSeconds` is a plain `number` on the published
253
+ // option and `Date.now() / 1000` -- the expression a consumer reaches for --
254
+ // is fractional, which makes `BigInt(millis)` throw `RangeError: ... not an
255
+ // integer` from inside the clock builder, naming neither the option nor the
256
+ // era. The millisecond is the finest unit this interface carries and
257
+ // compact-js divides it back down to seconds, so rounding loses nothing an
258
+ // execution can observe.
259
+ const millis = Math.round(nowSeconds * 1_000);
260
+ const nanos = BigInt(millis) * 1000000n;
261
+ // `Object.assign` onto the real clock, NOT a spread of it. `Clock.make()`
262
+ // returns a class instance whose `sleep` and `scheduler` live on the
263
+ // prototype; a spread copies own enumerable properties only and silently
264
+ // drops both, leaving a `Clock` that throws `clock.sleep is not a function`
265
+ // the first time anything schedules work. TypeScript cannot see it, because a
266
+ // spread type keeps the declared members.
267
+ return Object.assign(Clock.make(), {
268
+ unsafeCurrentTimeMillis: () => millis,
269
+ currentTimeMillis: Effect.succeed(millis),
270
+ unsafeCurrentTimeNanos: () => nanos,
271
+ currentTimeNanos: Effect.succeed(nanos)
272
+ });
273
+ };
274
+ const withPinnedClock = (nowSeconds, effect) => nowSeconds === undefined ? effect : Effect.withClock(pinnedClock(nowSeconds))(effect);
275
+ /**
276
+ * Adapts a {@link VerifierKeyReader} into the service compact-js's
277
+ * `initialize` reads keys through.
278
+ *
279
+ * A key the reader does not answer for becomes `Option.none()`, and compact-js
280
+ * REFUSES it: `initialize` fails with a `ContractConfigurationError` naming the
281
+ * circuit. That is measured, not assumed, and it is stricter than the leg this
282
+ * replaced — a retained-era constructor used to leave every slot blank and the
283
+ * deploy composition was the only thing that checked coverage. It now fails
284
+ * before a state is built at all.
285
+ */
286
+ const zkConfigurationLayer = (read) => Layer.succeed(ZKConfiguration.ZKConfiguration, ZKConfiguration.ZKConfiguration.of({
287
+ createReader: (compiledContract) => Effect.sync(() => {
288
+ const getVerifierKey = (provableCircuitId) => Effect.tryPromise({
289
+ try: () => read(provableCircuitId).then((key) => key === undefined ? Option.none() : Option.some(Contract.VerifierKey(key))),
290
+ catch: (cause) => ZKConfigurationReadError.make(compiledContract.tag, provableCircuitId, 'verifier-key', cause)
291
+ });
292
+ return {
293
+ getVerifierKey,
294
+ getVerifierKeys: (provableCircuitIds) => Effect.forEach(provableCircuitIds, (provableCircuitId) => getVerifierKey(provableCircuitId).pipe(Effect.map((verifierKey) => [provableCircuitId, verifierKey])), { concurrency: 'unbounded', discard: false })
295
+ };
296
+ })
297
+ }));
298
+ /**
299
+ * The verifier-key reader a CIRCUIT call is given.
300
+ *
301
+ * `ContractExecutable` declares `ZKConfiguration` in its context for every
302
+ * method, but only `initialize` and the maintenance operations read a key — a
303
+ * circuit call needs none, which is measured, not assumed. Providing a reader
304
+ * that refuses keeps that fact enforced: were a future version to read a key
305
+ * during a circuit call, this fails by name instead of silently serving
306
+ * `Option.none()` and composing a call with a blank key slot.
307
+ */
308
+ const refuseVerifierKeyRead = (provableCircuitId) => Promise.reject(new Error(`a retained-era circuit call read the verifier key for '${provableCircuitId}'. Circuit calls are ` +
309
+ 'not supposed to read ZK configuration; only deployment and maintenance are.'));
310
+ /**
311
+ * Structural equality over the `EncodedStateValue` algebra -- plain objects,
144
312
  * arrays, `Map`s, `Uint8Array`s and primitives.
145
313
  *
146
314
  * `Map`s are compared pairwise in iteration order, deliberately, and that order
147
- * is not ascending by key — do not reason about it as sorted.
315
+ * is not ascending by key -- do not reason about it as sorted. The
316
+ * order-sensitivity is what makes {@link decodeExecutableStateValue}'s
317
+ * comparison exact; a `get()`-based rewrite would silently drop it.
148
318
  *
149
319
  * @param a One value in the algebra.
150
320
  * @param b The value to compare it against.
151
321
  * @returns `true` when the two are structurally equal.
152
- * @see {@link RetainedEraExecution}
153
322
  */
154
323
  const structurallyEqual = (a, b) => {
155
324
  if (a === b) {
@@ -172,280 +341,347 @@ const structurallyEqual = (a, b) => {
172
341
  });
173
342
  }
174
343
  if (Array.isArray(a) || Array.isArray(b)) {
175
- return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, i) => structurallyEqual(item, b[i]));
344
+ return (Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, i) => structurallyEqual(item, b[i])));
176
345
  }
177
346
  const aRecord = { ...a };
178
347
  const bRecord = { ...b };
179
348
  const aKeys = Object.keys(aRecord);
180
- // `key in bRecord`, not just a matching key count, and a partial guard only --
181
- // see RetainedEraExecution and SharedTableDiscipline.
349
+ // `key in bRecord`, not just a matching key count: looking each of a's keys
350
+ // up in b without checking it exists reads `undefined` on both sides and
351
+ // short-circuits to equal.
182
352
  return (aKeys.length === Object.keys(bRecord).length &&
183
353
  aKeys.every((key) => key in bRecord && structurallyEqual(aRecord[key], bRecord[key])));
184
354
  };
185
355
  /**
186
- * Reads a bounded Merkle tree's root, failing with
187
- * {@link MerkleNotRehashedError} when the tree has not been rehashed.
356
+ * Decodes the primary state a circuit executes against, refusing a value that
357
+ * does not re-encode to the bytes it came from.
358
+ *
359
+ * The comparison is CROSS-CODEC in production, which is what it is for. A
360
+ * contract an earlier post-fork call migrated carries a current-era envelope,
361
+ * so the pipeline reads it with the HEAD era's reader
362
+ * (`readLedger8Snapshot`) -- the state is then encoded by ledger-v9 and
363
+ * decoded here by the retained runtime. A retained-era envelope is encoded by
364
+ * ledger-v8 and decoded by the same retained runtime, which is still a
365
+ * different physical copy. Either way a shape one side writes and the other
366
+ * merely tolerates would decode without complaint and execute a circuit
367
+ * against a state that is not the chain's, with every guard green.
188
368
  *
189
- * Three shapes of "no root" are treated alike: `undefined`, `null`, and a
190
- * throw out of the wasm-bindgen shim for `root()`.
369
+ * Reachable with the REAL decoder, not only an injected one: the seam takes a
370
+ * bare {@link EncodedStateValue} a caller can assemble by hand, and the
371
+ * retained runtime canonicalises map order and drops members it does not
372
+ * declare. Both are refused here rather than executed on.
191
373
  *
192
- * @param tree The bounded Merkle tree to read.
193
- * @returns The tree's root.
194
- * @throws MerkleNotRehashedError If the tree has no readable root, in any of
195
- * those three shapes.
374
+ * @param state The era-neutral encoded primary state.
375
+ * @returns The decoded state value.
376
+ * @throws DownConvertFailedError at stage `'state down-convert'` when the
377
+ * decoded value does not re-encode to its source, with the mismatch on
378
+ * `cause`.
196
379
  * @see {@link RetainedEraExecution}
380
+ * @see {@link FailClosedDecoding}
197
381
  */
198
- const checkRoot = (tree) => {
199
- let root;
382
+ const decodeExecutableStateValue = (state) => {
200
383
  try {
201
- root = tree.root();
384
+ const decoded = glue.StateValue.decode(state);
385
+ if (!structurallyEqual(decoded.encode(), state)) {
386
+ throw new DownConvertFailedError('state down-convert', new Error(`decoded StateValue did not re-encode to its source (source tag '${state.tag}')`));
387
+ }
388
+ return decoded;
202
389
  }
203
390
  catch (cause) {
204
- throw new MerkleNotRehashedError(cause);
391
+ // The decoder and the re-encoder both raise BARE vendor errors -- `missing
392
+ // field \`tag\``, `unknown variant \`x\`` -- carrying neither `code` nor
393
+ // `stage`. Coding them here is what the retired `downConvertForExecution`
394
+ // did, and what `fail-closed-decoding.md` still promises a caller.
395
+ throw cause instanceof DownConvertFailedError ? cause : new DownConvertFailedError('state down-convert', cause);
205
396
  }
206
- if (root == null) {
207
- throw new MerkleNotRehashedError();
397
+ };
398
+ /**
399
+ * Builds the retained-era `ContractState` a circuit executes against, from the
400
+ * era-neutral state the envelope's own reader produced.
401
+ *
402
+ * `StateValue` and `ChargedState` come from the retained glue rather than from
403
+ * compact-js: its `CompactRuntime` binding re-exports neither as a VALUE
404
+ * (midnightntwrk/midnight-sdk, spec finding E9), and building them through
405
+ * ledger-v8's own same-named classes instead would be the dual instantiation
406
+ * this era must not perform. `ContractState` is the glue's as well, so all
407
+ * three come from one module.
408
+ *
409
+ * A WHOLE `ContractState` rather than the bare `ChargedState` its predecessor
410
+ * built: the runtime reads `block.balance` off a contract state and substitutes
411
+ * an empty map for anything less, so writing the balance here is what makes it
412
+ * reach the circuit at all. It used to be injected into the query context after
413
+ * the fact, which is what #1345 was.
414
+ *
415
+ * @param contractState The decoded state and the balances beside it.
416
+ * @returns The state the retained runtime executes against.
417
+ */
418
+ const executableStateFrom = (contractState) => {
419
+ // Read ONCE, then guarded. Not defaulted and not trusted: `new Map(...)`
420
+ // turns an absent balance, a `null`, an empty array and an empty `Set` alike
421
+ // into an EMPTY map, which is indistinguishable from a contract that holds
422
+ // nothing -- the circuit then reads zero where the chain says otherwise and
423
+ // the node refuses the resulting transcript, with every guard green. That is
424
+ // #1345. `tsc` reaches neither an untyped JavaScript caller nor an options
425
+ // object assembled dynamically, and both `RunRetainedCircuitOptions` and
426
+ // `ContractStatePojo` are on the published surface.
427
+ // @see FailClosedDecoding
428
+ const { balance } = contractState;
429
+ if (!isContractBalance(balance)) {
430
+ throw new DownConvertFailedError('state down-convert', new Error(`a contract state carries no usable balance (received ${describeValue(balance)}), so it cannot be ` +
431
+ 'executed against. Read it from a contract-state snapshot rather than assembling one; a contract ' +
432
+ 'that holds nothing declares an empty Map.'));
433
+ }
434
+ try {
435
+ const state = new glue.ContractState();
436
+ state.data = new glue.ChargedState(decodeExecutableStateValue(contractState.state));
437
+ // Copied rather than shared, so an entry the caller adds or removes afterwards
438
+ // cannot reach a running circuit.
439
+ state.balance = new Map(balance);
440
+ return state;
441
+ }
442
+ catch (cause) {
443
+ // Both setters are WASM, and both raise bare errors: `balance` refuses a
444
+ // colour `isContractBalance` admits by name but the runtime cannot read,
445
+ // and `data` refuses a `ChargedState` from another physical copy. Neither
446
+ // carries a `code` a caller can branch on.
447
+ throw cause instanceof DownConvertFailedError ? cause : new DownConvertFailedError('state down-convert', cause);
208
448
  }
209
- return root;
210
449
  };
211
450
  /**
212
- * Asserts that every bounded Merkle tree in a `StateValue` tree already has
213
- * its node hashes computed, recursing through the only two container variants
214
- * in the algebra (`array` and `map`) and ignoring the leaf variants (`cell`,
215
- * `null`, and `boundedMerkleTree`'s own contents).
216
- *
217
- * Does not mutate or rebuild the state: a rootless tree is refused, never
218
- * repaired.
219
- *
220
- * @param sv The `StateValue` tree to walk.
221
- * @throws MerkleNotRehashedError On the first tree without a readable root,
222
- * via {@link checkRoot}.
223
- * @throws DownConvertFailedError If the walk meets a `StateValue` variant the
224
- * pinned typings do not declare.
225
- * @see {@link RetainedEraExecution}
451
+ * Every message in a failure's cause chain, outermost first.
452
+ *
453
+ * compact-js reports an execution failure as `Error executing circuit 'x'` and
454
+ * hangs the runtime's own diagnostic -- `Block time is <= time`, an out-of-gas,
455
+ * a failed assertion -- on `cause`. Only the outer message reaches a caller who
456
+ * reads `error.message`, so the reason the circuit ACTUALLY failed is lost at
457
+ * exactly the moment someone needs it.
226
458
  */
227
- const assertMerkleTreesRehashed = (sv) => {
228
- const variant = sv.type();
229
- switch (variant) {
230
- case 'boundedMerkleTree':
231
- checkRoot(sv.asBoundedMerkleTree());
232
- return;
233
- case 'array':
234
- sv.asArray().forEach((element) => assertMerkleTreesRehashed(element));
235
- return;
236
- case 'map': {
237
- const map = sv.asMap();
238
- map.keys().forEach((key) => assertMerkleTreesRehashed(map.get(key)));
239
- return;
240
- }
241
- case 'cell':
242
- case 'null':
243
- return;
244
- default: {
245
- // Compile-time exhaustiveness plus a runtime throw; neither is redundant
246
- // with the other -- see SharedTableDiscipline.
247
- const unhandled = variant;
248
- throw new DownConvertFailedError('state down-convert', new Error(`unhandled StateValue variant '${String(unhandled)}' in the Merkle rehash walk`));
249
- }
459
+ const causeChain = (error, seen = new Set()) => {
460
+ if (typeof error !== 'object' || error === null || seen.has(error)) {
461
+ return typeof error === 'string' ? [error] : [];
250
462
  }
463
+ seen.add(error);
464
+ const message = 'message' in error && typeof error.message === 'string' ? [error.message] : [];
465
+ return 'cause' in error ? [...message, ...causeChain(error.cause, seen)] : message;
251
466
  };
252
467
  /**
253
- * Down-converts an already-extracted (post-fork) {@link EncodedStateValue}
254
- * into the pre-fork {@link DownConvertedState} a v8-era circuit executes
255
- * against, decoding it through `ledger8Runtime` and asserting (via
256
- * {@link assertMerkleTreesRehashed}) that every bounded Merkle tree it
257
- * contains already has its hashes computed.
258
- *
259
- * Never returns a silently empty or partial state: every failure leaves as a
260
- * coded error. Structural integrity is checked by re-encoding the decoded value
261
- * and comparing it to the input, which costs one extra encode plus a deep
262
- * comparison per call.
263
- *
264
- * `ledger8Runtime` is checked before anything is decoded. Only the two members
265
- * this function actually calls are checked; `ContractState` is on
266
- * {@link Ledger8CompactRuntime} to pin the era, and is never dereferenced here.
267
- *
268
- * @param state The already-extracted post-fork encoded state value.
269
- * @param ledger8Runtime The injected pre-fork runtime slice.
270
- * @returns The pre-fork state a v8-era circuit executes against.
271
- * @throws Ledger8RuntimeInvalidError If `ledger8Runtime` is missing
272
- * `StateValue.decode` or `ChargedState`.
273
- * @throws DownConvertFailedError If the state cannot be decoded, does not
274
- * re-encode to its source, or the Merkle walk meets an undeclared variant —
275
- * carrying the underlying failure on `cause`.
276
- * @throws MerkleNotRehashedError If any bounded Merkle tree in the state has no
277
- * readable root.
278
- * @see {@link RetainedEraExecution}
279
- * @see {@link FailClosedDecoding}
468
+ * Runs an effect and rethrows its failure with the whole cause chain in the
469
+ * message.
470
+ *
471
+ * `Effect.runPromise` alone rejects with a `FiberFailure`, which carries neither
472
+ * the failure's class nor its `cause` — so a caller loses both the error it
473
+ * could have discriminated on and the diagnostic it needed. `runPromiseExit`
474
+ * plus `Cause.squash` recovers the real error, which is then rethrown with the
475
+ * underlying reason spelled out and the original on `cause`.
476
+ */
477
+ const PROTOCOL_ERROR_CODE_VALUES = new Set(Object.values(PROTOCOL_ERROR_CODES));
478
+ /**
479
+ * Whether a value is one of this package's own coded errors.
480
+ *
481
+ * The registry is the test rather than `instanceof` against a list: every class
482
+ * in `errors.ts` carries a `code` drawn from {@link PROTOCOL_ERROR_CODES}, so a
483
+ * class added later is recognised without this predicate being touched.
484
+ *
485
+ * @param error The value to test.
486
+ * @returns `true` when the value carries a registered protocol error code.
280
487
  */
281
- const downConvertForExecution = (state, ledger8Runtime) => {
282
- if (typeof ledger8Runtime?.StateValue?.decode !== 'function') {
283
- throw new Ledger8RuntimeInvalidError('StateValue.decode');
488
+ const isCodedProtocolError = (error) => {
489
+ if (!(error instanceof Error) || !('code' in error)) {
490
+ return false;
284
491
  }
285
- if (typeof ledger8Runtime.ChargedState !== 'function') {
286
- throw new Ledger8RuntimeInvalidError('ChargedState');
492
+ const { code } = error;
493
+ return typeof code === 'string' && PROTOCOL_ERROR_CODE_VALUES.has(code);
494
+ };
495
+ const runOrRethrow = async (effect) => {
496
+ const exit = await Effect.runPromiseExit(effect);
497
+ if (Exit.isSuccess(exit)) {
498
+ return exit.value;
287
499
  }
288
- try {
289
- const decoded = ledger8Runtime.StateValue.decode(state);
290
- if (!structurallyEqual(decoded.encode(), state)) {
291
- throw new DownConvertFailedError('state down-convert', new Error(`decoded StateValue did not re-encode to its source (source tag '${state.tag}')`));
292
- }
293
- assertMerkleTreesRehashed(decoded);
294
- return { data: new ledger8Runtime.ChargedState(decoded) };
500
+ // EVERY failure, not just the squashed first one. Verifier keys are read with
501
+ // `concurrency: 'unbounded'`, so a deploy missing three circuits' artifacts
502
+ // fails three times at once and `Cause.squash` answers with one -- sending the
503
+ // caller round the fix-and-rerun loop once per missing key.
504
+ const failures = Array.from(Cause.failures(exit.cause));
505
+ const error = failures.length > 0 ? failures[0] : Cause.squash(exit.cause);
506
+ // A coded error leaves with its CLASS intact, however many failures arrived
507
+ // beside it. Flattening one erases the `code` and `stage` a caller
508
+ // discriminates on, and this boundary sees errors this package raised itself
509
+ // on the way through, not only compact-js's. When several fail at once the
510
+ // coded one wins: a caller can branch on a code but not on a joined message,
511
+ // and the rest stay readable on the cause chain.
512
+ const coded = failures.find(isCodedProtocolError);
513
+ if (coded !== undefined) {
514
+ throw coded;
295
515
  }
296
- catch (cause) {
297
- if (cause instanceof DownConvertFailedError || cause instanceof MerkleNotRehashedError) {
298
- throw cause;
299
- }
300
- throw new DownConvertFailedError('state down-convert', cause);
516
+ const describe = (failure) => {
517
+ const chain = causeChain(failure);
518
+ return chain.length > 0 ? chain.join(': ') : String(failure);
519
+ };
520
+ throw new Error(failures.length > 1 ? failures.map(describe).join('; ') : describe(error), { cause: error });
521
+ };
522
+ /**
523
+ * Reads the ONE contract call a retained-era circuit produces.
524
+ *
525
+ * The retained era has no `crossContractCall`, so its execution adapter
526
+ * synthesises a single-entry trace and a one-element list is complete by
527
+ * construction. The list is still typed as a list, so the emptiness this era
528
+ * cannot produce is refused by name rather than read as `undefined` and carried
529
+ * into a composition.
530
+ *
531
+ * @param calls The calls the execution reported.
532
+ * @param circuitId The circuit they were produced for, for the message.
533
+ * @returns The sole call.
534
+ * @throws Error If the list is empty.
535
+ */
536
+ const soleCall = (calls, circuitId) => {
537
+ const [only] = calls;
538
+ if (only === undefined) {
539
+ throw new Error(`circuit '${circuitId}' produced no contract call. The retained era cannot make a ` +
540
+ 'cross-contract call, so exactly one is expected.');
301
541
  }
542
+ return only;
302
543
  };
303
-
304
- /*
305
- * This file is part of midnight-js.
306
- * Copyright (C) Midnight Foundation
307
- * SPDX-License-Identifier: Apache-2.0
308
- * Licensed under the Apache License, Version 2.0 (the "License");
309
- * You may not use this file except in compliance with the License.
310
- * You may obtain a copy of the License at
311
- * http://www.apache.org/licenses/LICENSE-2.0
312
- * Unless required by applicable law or agreed to in writing, software
313
- * distributed under the License is distributed on an "AS IS" BASIS,
314
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
315
- * See the License for the specific language governing permissions and
316
- * limitations under the License.
544
+ /**
545
+ * Refuses a circuit id the contract does not declare, before compact-js
546
+ * dispatches on it.
547
+ *
548
+ * `Object.hasOwn`, NOT a bare truthiness check. A 0.16 artifact assigns
549
+ * `provableCircuits` a plain object literal, so its prototype is live, and
550
+ * compact-js resolves a circuit with `contract.provableCircuits[id]` guarded
551
+ * only by `if (!circuit)`. `'toString'`, `'valueOf'` and `'constructor'` all
552
+ * satisfy that guard: the prototype member is CALLED with the runtime context
553
+ * and its return value fed to `readExecution`. The id reaches here from the
554
+ * caller, so it is refused by name, the way the retired `executeCircuit` did.
555
+ *
556
+ * @param contract The contract the call names.
557
+ * @param circuitId The circuit the caller asked for.
558
+ * @throws Error If the contract declares no such circuit.
317
559
  */
560
+ const assertDeclaredCircuit = (contract, circuitId) => {
561
+ const circuit = Object.hasOwn(contract.provableCircuits, circuitId)
562
+ ? contract.provableCircuits[circuitId]
563
+ : undefined;
564
+ if (typeof circuit !== 'function') {
565
+ throw new Error(`No circuit named '${circuitId}' on this retained-era contract instance. ` +
566
+ `Available circuits: ${Object.keys(contract.provableCircuits).sort().join(', ')}.`);
567
+ }
568
+ };
318
569
  /**
319
- * Runs one impure circuit on a pre-fork (`compact-runtime@0.16`) contract
320
- * instance and packages every artifact {@link wrapKeepStateCall} needs into a
321
- * {@link TranscriptPojo}.
570
+ * Runs one circuit on a retained-era contract and packages every artifact a
571
+ * v9-native call prototype needs.
322
572
  *
323
- * A circuit that moved coins runs like any other: its post-call Zswap local
324
- * state leaves on {@link TranscriptPojo}, decoded through the injected runtime,
325
- * for the caller to turn into the transaction's Zswap offer.
573
+ * The balances the contract holds travel on the state it is given — see
574
+ * {@link RunRetainedCircuitOptions.contractState}.
326
575
  *
327
- * @param options The contract module, circuit id, arguments, down-converted
328
- * state, contract address, coin public key and private state.
329
- * @param ledger8Runtime The injected pre-fork glue slice.
576
+ * @param options The retained contract, circuit id, arguments, decoded state,
577
+ * address, coin public key, private state and optional pinned clock.
330
578
  * @returns Every artifact a v9-native call prototype needs.
331
- * @throws Error A plain `Error` — not a {@link PROTOCOL_ERROR_CODES}-carrying
332
- * class — when `circuitId` names no entry point on
333
- * `contract.impureCircuits`.
334
579
  * @see {@link RetainedEraExecution}
335
580
  */
336
- const executeCircuit = (options, ledger8Runtime) => {
337
- const { contract, circuitId, args, state, address, coinPk, privateState } = options;
338
- const circuit = Object.hasOwn(contract.impureCircuits, circuitId) ? contract.impureCircuits[circuitId] : undefined;
339
- if (typeof circuit !== 'function') {
340
- throw new Error(`No impure circuit named '${circuitId}' on this pre-fork contract instance. ` +
341
- `Available circuits: ${Object.keys(contract.impureCircuits).join(', ') || '(none)'}.`);
342
- }
343
- const ctx = ledger8Runtime.createCircuitContext(address, coinPk, state.data, privateState, undefined, ledger8Runtime.CostModel.initialCostModel());
344
- // Read BEFORE the circuit runs. The glue swaps `currentQueryContext` for a
345
- // new context on every coin it registers, so after the call this object no
346
- // longer answers for the context the call started from.
347
- const preCallBlock = ctx.currentQueryContext.block;
348
- const preCallEffects = ctx.currentQueryContext.effects;
349
- const res = circuit(ctx, ...args);
581
+ const runRetainedCircuit = async (options) => {
582
+ assertDeclaredCircuit(options.contract, options.circuitId);
583
+ const executable = ContractExecutable.make(containerFor(options.contract));
584
+ const contractState = executableStateFrom(options.contractState);
585
+ const program = Effect.gen(function* () {
586
+ return yield* executable.circuit(Contract.ProvableCircuitId(options.circuitId), {
587
+ address: PlatformContractAddress.ContractAddress(options.address),
588
+ contractState,
589
+ privateState: options.privateState
590
+ },
591
+ // Asserted for the same reason `createCallTxOptions` asserts it in
592
+ // `packages/contracts/src/tx-interfaces.ts`: resolved through a GENERIC
593
+ // `C`, `CircuitParameters` degrades to `unknown[]` because TypeScript
594
+ // indexes `provableCircuits` against the constraint rather than the
595
+ // instantiated contract. The tuple is checked where `C` is concrete, at
596
+ // the caller's own call site.
597
+ ...options.args);
598
+ });
599
+ const call = await runOrRethrow(withPinnedClock(options.nowSeconds, program.pipe(Effect.provide(Layer.mergeAll(keysLayer(options.coinPk, undefined), zkConfigurationLayer(refuseVerifierKeyRead))))));
600
+ const only = soleCall(call.calls, options.circuitId);
350
601
  return {
351
- circuitId,
352
- result: res.result,
353
- input: res.proofData.input,
354
- output: res.proofData.output,
355
- publicTranscript: res.proofData.publicTranscript,
356
- privateTranscriptOutputs: res.proofData.privateTranscriptOutputs,
357
- preContractState: state,
358
- postContractState: { data: res.context.currentQueryContext.state },
359
- // Encoded HERE, where the retained runtime is in scope, rather than by the
360
- // caller: `.encode()` is on the pre-fork `StateValue` this module already
361
- // holds, and a caller reaching for it would be reaching through a handle
362
- // whose type it cannot name.
363
- postContractStateEncoded: res.context.currentQueryContext.state.state.encode(),
364
- privateStateAfter: res.context.currentPrivateState,
602
+ circuitId: only.circuitId,
603
+ result: call.result,
604
+ input: only.private.input,
605
+ output: only.private.output,
606
+ publicTranscript: only.public.publicTranscript,
607
+ privateTranscriptOutputs: only.private.privateTranscriptOutputs,
608
+ preContractState: only.public.partitionInputs.state,
609
+ postContractState: only.public.contractState,
610
+ postContractStateEncoded: only.public.contractState.encode(),
611
+ privateStateAfter: call.privateState,
365
612
  partitionContext: {
366
- block: preCallBlock,
367
- effects: preCallEffects,
368
- comIndices: res.context.currentQueryContext.comIndices
613
+ block: only.public.partitionInputs.block,
614
+ effects: only.public.partitionInputs.effects,
615
+ comIndices: only.public.partitionInputs.comIndices
369
616
  },
370
- zswapLocalState: ledger8Runtime.decodeZswapLocalState(res.context.currentZswapLocalState)
617
+ zswapLocalState: call.zswapLocalState
371
618
  };
372
619
  };
373
-
374
- /*
375
- * This file is part of midnight-js.
376
- * Copyright (C) Midnight Foundation
377
- * SPDX-License-Identifier: Apache-2.0
378
- * Licensed under the Apache License, Version 2.0 (the "License");
379
- * You may not use this file except in compliance with the License.
380
- * You may obtain a copy of the License at
381
- * http://www.apache.org/licenses/LICENSE-2.0
382
- * Unless required by applicable law or agreed to in writing, software
383
- * distributed under the License is distributed on an "AS IS" BASIS,
384
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
385
- * See the License for the specific language governing permissions and
386
- * limitations under the License.
387
- */
388
620
  /**
389
- * The closed set of axes this guard will run on, as a lookup rather than a
390
- * comparison chain.
621
+ * Re-codes compact-js's refusal of a verifier-key blob as this package's own
622
+ * {@link ComposeFailedError}.
623
+ *
624
+ * compact-js writes the keys itself inside `initialize`, so it is the first
625
+ * thing to touch the bytes and the retained composer -- which raised the coded
626
+ * refusal before this move -- is never reached. Without this the same fault is
627
+ * reported as a coded error on the current era and an uncoded one on the
628
+ * retained era.
391
629
  *
392
- * Kept here rather than exported from `errors.ts`, which is a build entry:
393
- * nothing outside this module needs to test an axis, and the engine's whole
394
- * design is to stay off the public surface.
630
+ * The `cause` is what separates a rejected blob from the other configuration
631
+ * refusals: compact-js attaches the ledger's own error only when a key it was
632
+ * handed was refused, and leaves it absent for a key that was never served.
395
633
  *
396
- * @see {@link SharedTableDiscipline}
634
+ * @param error The failure compact-js reported.
635
+ * @returns A {@link ComposeFailedError} for a rejected blob; the error
636
+ * unchanged for anything else.
397
637
  */
398
- const KNOWN_AXES = Object.freeze(Object.assign(Object.create(null), {
399
- 'onchain-runtime-v3': true
400
- }));
401
- const isKnownAxis = (axis) => KNOWN_AXES[axis] === true;
638
+ const recodeVerifierKeyBlobFailure = (error) => {
639
+ if (!ContractConfigurationError.isConfigurationError(error) || error.cause === undefined) {
640
+ return error;
641
+ }
642
+ const circuitId = /verifier key for circuit '([^']+)'/.exec(error.message)?.[1];
643
+ return circuitId === undefined
644
+ ? error
645
+ : new ComposeFailedError('v8', 'deploy-verifier-key-blob', circuitId, error.cause);
646
+ };
402
647
  /**
403
- * Fails fast on a dual-instantiation of a WASM package the down-convert
404
- * engine depends on, along one named `axis`.
405
- *
406
- * Pass a **shared binding** — a class the package exports, such as
407
- * `ChargedState` or `StateValue` — never a module namespace object.
408
- *
409
- * The two probes are compared symmetrically, so they are interchangeable, but
410
- * they must be obtained from **two different acquisition paths** (two distinct
411
- * import specifiers, or an import versus a caller-supplied module). Passing the
412
- * same binding twice satisfies the check trivially and proves nothing; the
413
- * caller owns that, because nothing in the signature can enforce it.
414
- *
415
- * @param axis The physical-copy axis to check, validated against
416
- * {@link Ledger8InstanceAxis} before either probe is looked at.
417
- * @param probeA A shared binding, obtained through one acquisition path.
418
- * @param probeB The same binding, obtained through a different acquisition
419
- * path.
420
- * @throws UnknownLedger8AxisError If `axis` is not a member of
421
- * {@link Ledger8InstanceAxis}.
422
- * @throws Ledger8RuntimeInvalidError If either probe is `null` or `undefined`.
423
- * A missing probe is a binding the caller did not hand over, so it is not
424
- * reported as a mismatch.
425
- * @throws Ledger8InstanceMismatchError If the two probes are not
426
- * reference-equal, i.e. the package resolved to two physically distinct
427
- * copies in this process.
428
- * @example
429
- * ```typescript
430
- * const [glue, ocrt3] = await Promise.all([
431
- * import('compact-runtime-ledger8'),
432
- * import('@midnight-ntwrk/onchain-runtime-v3')
433
- * ]);
434
- * // Two acquisition paths to the same binding, never a namespace object.
435
- * assertSharedLedger8Instance('onchain-runtime-v3', ocrt3.ChargedState, glue.ChargedState);
436
- * ```
437
- * @see {@link DualInstantiationGuard}
648
+ * Runs a retained-era contract's constructor and packages the result.
649
+ *
650
+ * compact-js's `initialize` also writes the maintenance authority and registers
651
+ * each entry point's verifier key onto the state, both of which this era's
652
+ * deploy leg previously did for itself. The authority matters: the retained
653
+ * constructor leaves an EMPTY committee with a threshold of one — one signature
654
+ * from a set of zero keys, which nothing can satisfy — so a deployment left on
655
+ * it could never have a verifier key inserted, removed or replaced.
656
+ *
657
+ * @param options The compiled contract, constructor arguments, private state,
658
+ * coin public key, optional signing key and verifier-key reader.
659
+ * @throws ComposeOptionError naming option `'signingKey'` when a supplied key
660
+ * is not the 32 bytes of hex the retained runtime reads, raised before the
661
+ * constructor runs.
662
+ * @returns The freshly built state, the resulting private state, the Zswap
663
+ * local state the constructor ended on and the signing key the authority was
664
+ * built from.
665
+ * @see {@link RetainedEraExecution}
438
666
  */
439
- const assertSharedLedger8Instance = (axis, probeA, probeB) => {
440
- if (!isKnownAxis(axis)) {
441
- throw new UnknownLedger8AxisError(String(axis));
442
- }
443
- if (probeA == null || probeB == null) {
444
- throw new Ledger8RuntimeInvalidError(`${axis} instance probe`);
445
- }
446
- if (probeA !== probeB) {
447
- throw new Ledger8InstanceMismatchError(axis);
667
+ const runRetainedConstructor = async (options) => {
668
+ // BEFORE the constructor runs, so nothing is executed against a key the
669
+ // maintenance authority could never have been built from, and the refusal
670
+ // names the option rather than leaving the runtime's own buffer message to
671
+ // stand for it.
672
+ if (options.signingKey !== undefined && !LEDGER8_SIGNING_KEY_PATTERN.test(options.signingKey)) {
673
+ throw new ComposeOptionError('v8', 'signingKey');
448
674
  }
675
+ const executable = ContractExecutable.make(containerFor(options.contract));
676
+ const deployed = await runOrRethrow(executable
677
+ .initialize(options.privateState, ...options.args)
678
+ .pipe(Effect.mapError(recodeVerifierKeyBlobFailure), Effect.provide(Layer.mergeAll(keysLayer(options.coinPk, options.signingKey), zkConfigurationLayer(options.verifierKeys)))));
679
+ return {
680
+ contractState: deployed.public.contractState,
681
+ privateState: deployed.private.privateState,
682
+ zswapLocalState: deployed.private.zswapLocalState,
683
+ signingKey: deployed.private.signingKey.value
684
+ };
449
685
  };
450
686
 
451
687
  /*
@@ -463,57 +699,33 @@ const assertSharedLedger8Instance = (axis, probeA, probeB) => {
463
699
  * limitations under the License.
464
700
  */
465
701
  /**
466
- * Acquires the retained pre-fork toolchain — the `compact-runtime@0.16` glue
467
- * and `@midnight-ntwrk/onchain-runtime-v3` — and builds a {@link Ledger8Engine}
468
- * bound to it.
469
- *
470
- * Runs {@link assertSharedLedger8Instance} exactly once, on the
471
- * `onchain-runtime-v3` axis. Any acquisition failure surfaces through the
472
- * facade (`lib/v8/load-engine.ts`) as `Ledger8RuntimeMissingError`
473
- * (`../../errors.ts`).
474
- *
475
- * Does NOT acquire the v8 ledger module: a consumer that only executes
476
- * circuits and binds them onto v9 never instantiates the multi-megabyte v8
477
- * WASM, and never hard-depends on ledger-v8 resolving.
478
- *
479
- * @returns The engine surface, with the acquired runtime captured in closure.
480
- * @throws Ledger8InstanceMismatchError If `onchain-runtime-v3` resolved to two
481
- * physically distinct copies in this process.
702
+ * Builds a {@link Ledger8Engine}.
703
+ *
704
+ * The retained toolchain is no longer acquired here: compact-js's era-pinned
705
+ * ledger-8 entries own it, and `lib/v8/executable.ts` reaches them. What this
706
+ * function still does is sit behind the dynamic `import('../../engine.js')` in
707
+ * `lib/v8/load-engine.ts`, so importing the package root never pulls the
708
+ * multi-megabyte retained WASM onto the module graph.
709
+ *
710
+ * It no longer runs a dual-instantiation guard either. That guard compared
711
+ * `onchain-runtime-v3` against the 0.16 glue at construction time. Both
712
+ * acquisition paths are still live — `lib/v8/executable.ts` imports the glue
713
+ * directly, and compact-js resolves the same specifier for itself — so the axis
714
+ * did not go away; only the runtime check did. The invariant is held at install
715
+ * time instead, by `src/test/single-instance.test.ts`, which pins one resolved
716
+ * copy of `onchain-runtime-v3` and of each compact-runtime era line. That is a
717
+ * check on THIS repo's lockfile, not on a consumer's process.
718
+ *
719
+ * @returns The engine surface.
482
720
  * @see {@link EraSeam}
483
- * @see {@link DualInstantiationGuard}
484
721
  * @see {@link ModuleGraphAndLazyLoading}
485
722
  */
486
- const createLedger8Engine = async () => {
487
- const [glue, ocrt3] = await Promise.all([
488
- import('compact-runtime-ledger8'),
489
- import('@midnight-ntwrk/onchain-runtime-v3')
490
- ]);
491
- assertSharedLedger8Instance('onchain-runtime-v3', ocrt3.ChargedState, glue.ChargedState);
492
- // `ContractState` from ocrt3, the other two from the glue: sound only because
493
- // of the assertion above -- see DualInstantiationGuard. The member is what
494
- // pins this object to the pre-fork era -- see `Ledger8CompactRuntime`.
495
- const ledger8CompactRuntime = {
496
- ContractState: ocrt3.ContractState,
497
- StateValue: glue.StateValue,
498
- ChargedState: glue.ChargedState
499
- };
500
- const ledger8ExecutionRuntime = {
501
- decodeZswapLocalState: glue.decodeZswapLocalState,
502
- createCircuitContext: glue.createCircuitContext,
503
- CostModel: glue.CostModel
504
- };
505
- const ledger8ConstructorRuntime = {
506
- createConstructorContext: glue.createConstructorContext,
507
- decodeZswapLocalState: glue.decodeZswapLocalState
508
- };
509
- return {
510
- downConvertForExecution: (state) => downConvertForExecution(state, ledger8CompactRuntime),
511
- executeCircuit: (options) => executeCircuit(options, ledger8ExecutionRuntime),
512
- wrapKeepStateCall,
513
- reexpressOperationsForCurrentEra,
514
- executeConstructor: (options) => executeConstructor(options, ledger8ConstructorRuntime)
515
- };
516
- };
723
+ const createLedger8Engine = async () => Promise.resolve({
724
+ executeCircuit: runRetainedCircuit,
725
+ executeConstructor: runRetainedConstructor,
726
+ wrapKeepStateCall,
727
+ reexpressOperationsForCurrentEra
728
+ });
517
729
 
518
730
  export { createLedger8Engine };
519
731
  //# sourceMappingURL=engine.js.map