@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/README.md +32 -11
- package/dist/compact-js-v8-effect.d.ts +1 -0
- package/dist/compact-js-v8-effect.js +2 -0
- package/dist/compact-js-v8-effect.js.map +1 -0
- package/dist/engine.d.ts +214 -224
- package/dist/engine.js +516 -304
- package/dist/engine.js.map +1 -1
- package/dist/errors.d.ts +64 -38
- package/dist/errors.js +50 -33
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +309 -309
- package/dist/index.js +173 -121
- package/dist/index.js.map +1 -1
- package/dist/prove.d.ts +7 -5
- package/dist/prove.js +6 -4
- package/dist/prove.js.map +1 -1
- package/dist/shared/{deploy-D6yyn9P7.js → assemble-call-BB1yZMS6.js} +254 -252
- package/dist/shared/assemble-call-BB1yZMS6.js.map +1 -0
- package/package.json +11 -7
- package/dist/shared/deploy-D6yyn9P7.js.map +0 -1
package/dist/engine.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import * as ledgerV9 from '@midnightntwrk/ledger-v9';
|
|
2
|
-
import { ComposeOptionError,
|
|
3
|
-
import { a as assembleCallPrototype,
|
|
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
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
|
47
|
-
*
|
|
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
|
|
87
|
-
* (
|
|
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
|
-
|
|
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.
|
|
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
|
-
*
|
|
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
|
|
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
|
|
181
|
-
//
|
|
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
|
-
*
|
|
187
|
-
*
|
|
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
|
-
*
|
|
190
|
-
*
|
|
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
|
|
193
|
-
* @returns The
|
|
194
|
-
* @throws
|
|
195
|
-
*
|
|
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
|
|
199
|
-
let root;
|
|
382
|
+
const decodeExecutableStateValue = (state) => {
|
|
200
383
|
try {
|
|
201
|
-
|
|
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
|
-
|
|
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
|
-
|
|
207
|
-
|
|
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
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
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
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
* @
|
|
271
|
-
* @
|
|
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
|
|
282
|
-
if (
|
|
283
|
-
|
|
488
|
+
const isCodedProtocolError = (error) => {
|
|
489
|
+
if (!(error instanceof Error) || !('code' in error)) {
|
|
490
|
+
return false;
|
|
284
491
|
}
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
*
|
|
306
|
-
*
|
|
307
|
-
*
|
|
308
|
-
*
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
312
|
-
*
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
316
|
-
*
|
|
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
|
|
320
|
-
*
|
|
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
|
-
*
|
|
324
|
-
*
|
|
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
|
|
328
|
-
*
|
|
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
|
|
337
|
-
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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:
|
|
353
|
-
input:
|
|
354
|
-
output:
|
|
355
|
-
publicTranscript:
|
|
356
|
-
privateTranscriptOutputs:
|
|
357
|
-
preContractState: state,
|
|
358
|
-
postContractState:
|
|
359
|
-
|
|
360
|
-
|
|
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:
|
|
367
|
-
effects:
|
|
368
|
-
comIndices:
|
|
613
|
+
block: only.public.partitionInputs.block,
|
|
614
|
+
effects: only.public.partitionInputs.effects,
|
|
615
|
+
comIndices: only.public.partitionInputs.comIndices
|
|
369
616
|
},
|
|
370
|
-
zswapLocalState:
|
|
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
|
-
*
|
|
390
|
-
*
|
|
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
|
-
*
|
|
393
|
-
*
|
|
394
|
-
*
|
|
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
|
-
* @
|
|
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
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
*
|
|
404
|
-
*
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
* @
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
* @
|
|
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
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
-
*
|
|
467
|
-
*
|
|
468
|
-
*
|
|
469
|
-
*
|
|
470
|
-
*
|
|
471
|
-
* `
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
477
|
-
*
|
|
478
|
-
*
|
|
479
|
-
*
|
|
480
|
-
*
|
|
481
|
-
*
|
|
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
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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
|