@sig-net/midnight 0.24.0-rc.8 → 0.24.0

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/abi-serde.js CHANGED
@@ -8,10 +8,10 @@ import { ethers } from "ethers";
8
8
  * into named values, driven by the request's outputDeserializationSchema.
9
9
  * The decode-side counterpart of {@link serializeRespondOutput}. Type
10
10
  * validation is FULLY delegated to ethers: this function checks the schema's
11
- * shape only. An empty schema decodes to an empty object. The MPC never
12
- * decodes under an empty schema: {@link executedEvmRespondOutput} carries
13
- * its rule for that case. Returns a plain object (never an ethers
14
- * `Result`), so it survives JSON round-trips and structural comparison.
11
+ * shape only, so it decodes ABI types the respond side refuses. An empty
12
+ * schema decodes to an empty object. Returns a plain object (never an
13
+ * ethers `Result`), so it survives JSON round-trips and structural
14
+ * comparison.
15
15
  *
16
16
  * @param schema - The outputDeserializationSchema: parsed, JSON text, or the raw NUL-padded on-chain bytes.
17
17
  * @param callResult - The ABI-encoded return data (hex string or bytes).
@@ -23,99 +23,211 @@ export function deserializeEvmOutput(schema, callResult) {
23
23
  return {};
24
24
  }
25
25
  const decoded = ethers.AbiCoder.defaultAbiCoder().decode(fields.map((field) => field.type), callResult);
26
- const output = {};
27
- fields.forEach((field, i) => {
28
- output[field.name] = toPlainValue(decoded[i], field.name);
29
- });
30
- return output;
26
+ return Object.fromEntries(fields.map((field, i) => [
27
+ field.name,
28
+ toPlainValue(decoded[i], field.name),
29
+ ]));
31
30
  }
32
31
  // ---------------------------------------------------------------------------
33
- // 2. decoded values -> respond bytes (respondSerializationSchema)
32
+ // 2. decoded values -> respond bytes (Borsh schema derived from the output schema)
34
33
  // ---------------------------------------------------------------------------
35
34
  /**
36
- * Parse native Borsh schema JSON from an on-chain field.
35
+ * The ABI output types the MPC attests, and what each maps to. The respond
36
+ * bytes are the Borsh serialisation of a struct with one member per output
37
+ * field, in schema order, typed by this mapping:
38
+ *
39
+ * - {@link EvmOutputTypeKind.Bool}: ABI `bool`, Borsh `bool`, Compact `Boolean` (1 byte).
40
+ * - {@link EvmOutputTypeKind.Uint256}: ABI `uint256`, Borsh `[u8; 32]`
41
+ * holding the value LITTLE-endian, the byte order of every Borsh and
42
+ * Compact integer, Compact `Bytes<32>` (32 bytes). A circuit that wants a
43
+ * number narrows it with `checkedTruncationU128`, which aborts above
44
+ * 2^128 - 1. The ABI wire word is big-endian: the MPC reverses it once,
45
+ * here, so no circuit has to.
46
+ * - {@link EvmOutputTypeKind.Address}: ABI `address`, Borsh `[u8; 20]`
47
+ * holding the address bytes, Compact `Bytes<20>` (20 bytes).
48
+ * - {@link EvmOutputTypeKind.FixedBytes}: ABI `bytes1` to `bytes32`, Borsh
49
+ * `[u8; N]`, Compact `Bytes<N>` (N bytes).
37
50
  *
38
- * @param schema - Native schema, JSON text, or NUL-padded bytes.
39
- * @returns The native Borsh schema.
40
- * @throws {Error} If schema JSON is malformed.
51
+ * Every other ABI type is unsupported: the MPC drops a request whose output
52
+ * schema names one, and {@link deriveRespondSchema} throws on it.
41
53
  */
42
- function respondSchema(schema) {
43
- if (schema instanceof Uint8Array)
44
- return JSON.parse(schemaText(schema));
45
- if (typeof schema === "string" && /^\s*[{["]/.test(schema)) {
46
- return JSON.parse(schema);
54
+ export var EvmOutputTypeKind;
55
+ (function (EvmOutputTypeKind) {
56
+ EvmOutputTypeKind["Bool"] = "bool";
57
+ EvmOutputTypeKind["Uint256"] = "uint256";
58
+ EvmOutputTypeKind["Address"] = "address";
59
+ EvmOutputTypeKind["FixedBytes"] = "bytesN";
60
+ })(EvmOutputTypeKind || (EvmOutputTypeKind = {}));
61
+ /** The byte width of an ABI word, and of the Borsh array a `uint256` output maps to. */
62
+ const ABI_WORD_BYTES = 32;
63
+ /** The byte width of an EVM address, and of the Borsh array an `address` output maps to. */
64
+ const EVM_ADDRESS_BYTES = 20;
65
+ /**
66
+ * Classify an ABI type string into the supported output subset. The match is
67
+ * on the raw string, so `uint` is unsupported although the ABI library reads
68
+ * it as `uint256`: the MPC matches the same raw string.
69
+ *
70
+ * @param type - The ABI type string of an output schema field.
71
+ * @returns The field's kind and respond byte width, or `undefined` for an unsupported type.
72
+ */
73
+ function classifyEvmOutputType(type) {
74
+ if (type === "bool")
75
+ return { kind: EvmOutputTypeKind.Bool, bytes: 1 };
76
+ if (type === "uint256")
77
+ return { kind: EvmOutputTypeKind.Uint256, bytes: ABI_WORD_BYTES };
78
+ if (type === "address")
79
+ return { kind: EvmOutputTypeKind.Address, bytes: EVM_ADDRESS_BYTES };
80
+ const fixedBytes = /^bytes([1-9]|[12][0-9]|3[0-2])$/.exec(type);
81
+ if (fixedBytes !== null) {
82
+ return { kind: EvmOutputTypeKind.FixedBytes, bytes: Number(fixedBytes[1]) };
47
83
  }
48
- return schema;
84
+ return undefined;
49
85
  }
50
86
  /**
51
- * Convert ABI numeric and byte representations into native Borsh values.
52
- * Integer narrowing must fail before the value reaches Borsh's truncating writes.
87
+ * The fields of an outputDeserializationSchema whose ABI type is outside the
88
+ * supported subset ({@link EvmOutputTypeKind}). The check itself never
89
+ * throws on an unsupported type: an empty result means the MPC attests the
90
+ * request, a non-empty one means it drops it.
53
91
  *
54
- * @param schema - Native Borsh schema.
55
- * @param value - Decoded EVM value.
56
- * @returns Value in the Borsh representation.
57
- * @throws {RangeError} If an integer cannot be represented without loss.
92
+ * @param schema - The outputDeserializationSchema: parsed, JSON text, or the raw NUL-padded on-chain bytes.
93
+ * @returns The unsupported fields in schema order, empty when every field is supported.
94
+ * @throws {Error} If the schema is not an array of uniquely named fields (its shape, not its types).
58
95
  */
59
- function toBorshValue(schema, value) {
60
- if (typeof schema === "string") {
61
- const integer = /^([ui])(8|16|32|64|128)$/.exec(schema);
62
- if (integer) {
63
- if (typeof value !== "bigint" && typeof value !== "number" && typeof value !== "string") {
64
- throw new TypeError(`Expected an integer for ${schema}`);
65
- }
66
- if (typeof value === "number" && !Number.isSafeInteger(value)) {
67
- throw new RangeError(`Unsafe integer for ${schema}`);
68
- }
69
- const number = BigInt(value);
70
- const bits = Number(integer[2]);
71
- const signed = integer[1] === "i";
72
- const bound = 1n << BigInt(signed ? bits - 1 : bits);
73
- if (number < (signed ? -bound : 0n) || number >= bound) {
74
- throw new RangeError(`EVM integer ${String(number)} does not fit Borsh ${schema}`);
75
- }
76
- return bits <= 32 ? Number(number) : number;
77
- }
78
- return value;
96
+ export function unsupportedEvmOutputFields(schema) {
97
+ return parseSchemaShape(schema).filter((field) => classifyEvmOutputType(field.type) === undefined);
98
+ }
99
+ /**
100
+ * Parse an output schema and classify every field, refusing the schema as a
101
+ * whole when any field is unsupported.
102
+ *
103
+ * @param schema - The outputDeserializationSchema: parsed, JSON text, or the raw NUL-padded on-chain bytes.
104
+ * @returns The classified fields in schema order.
105
+ * @throws {Error} If the schema is malformed or names an unsupported ABI type.
106
+ */
107
+ function classifiedEvmOutputFields(schema) {
108
+ const fields = parseSchemaShape(schema);
109
+ const unsupported = fields.filter((field) => classifyEvmOutputType(field.type) === undefined);
110
+ if (unsupported.length > 0) {
111
+ throw new Error(`respond output: unsupported ABI output type${unsupported.length > 1 ? "s" : ""} ` +
112
+ unsupported.map((field) => `'${field.name}' (${field.type})`).join(", ") +
113
+ ": the MPC attests bool, uint256, address and bytes1 to bytes32 only");
114
+ }
115
+ return fields.flatMap((field) => {
116
+ const classified = classifyEvmOutputType(field.type);
117
+ return classified === undefined ? [] : [{ ...field, ...classified }];
118
+ });
119
+ }
120
+ /**
121
+ * The Borsh schema the respond bytes of an executed call are serialised
122
+ * with: a struct with one member per output field, in schema order, each
123
+ * typed by {@link EvmOutputTypeKind}. An empty output schema derives an
124
+ * empty struct, which serialises to zero bytes. Borsh writes struct members
125
+ * in the struct object's key order, which equals schema order because field
126
+ * names are Solidity identifiers (see {@link parseSchemaShape}).
127
+ *
128
+ * @param schema - The outputDeserializationSchema: parsed, JSON text, or the raw NUL-padded on-chain bytes.
129
+ * @returns The derived Borsh struct schema.
130
+ * @throws {Error} If the schema is malformed or names an unsupported ABI type.
131
+ */
132
+ export function deriveRespondSchema(schema) {
133
+ return {
134
+ struct: Object.fromEntries(classifiedEvmOutputFields(schema).map((field) => [
135
+ field.name,
136
+ field.kind === EvmOutputTypeKind.Bool
137
+ ? "bool"
138
+ : { array: { type: "u8", len: field.bytes } },
139
+ ])),
140
+ };
141
+ }
142
+ /**
143
+ * The byte width of the respond bytes an output schema derives: the sum of
144
+ * its fields' widths under {@link EvmOutputTypeKind}, what a settle circuit
145
+ * declares as its `Bytes<N>` output argument.
146
+ *
147
+ * @param schema - The outputDeserializationSchema: parsed, JSON text, or the raw NUL-padded on-chain bytes.
148
+ * @returns The exact serialised width, 0 for an empty schema.
149
+ * @throws {Error} If the schema is malformed or names an unsupported ABI type.
150
+ */
151
+ export function respondOutputWidth(schema) {
152
+ return classifiedEvmOutputFields(schema).reduce((width, field) => width + field.bytes, 0);
153
+ }
154
+ /**
155
+ * The 32-byte LITTLE-endian encoding of a `uint256` output value, as ethers
156
+ * decodes it (bigint) or as indexer JSON renders it (a number or a decimal
157
+ * string). This is the ABI wire word reversed: the Borsh integer byte order,
158
+ * which a Compact `Uint<128>` reads directly.
159
+ *
160
+ * @param value - The decoded value.
161
+ * @param name - The field name, for the error message.
162
+ * @returns The value's 32 little-endian bytes.
163
+ * @throws {RangeError} If the value is not an integer in `0 <= value < 2^256`.
164
+ */
165
+ function uint256Word(value, name) {
166
+ if (typeof value !== "bigint" && typeof value !== "number" && typeof value !== "string") {
167
+ throw new TypeError(`respond output: '${name}' (uint256) expects an integer`);
79
168
  }
80
- if ("struct" in schema && typeof value === "object" && value !== null) {
81
- const record = value;
82
- return Object.fromEntries(Object.entries(schema.struct).map(([name, child]) => {
83
- const field = record[name];
84
- if (field === undefined)
85
- throw new Error(`Missing response field '${name}'`);
86
- return [name, toBorshValue(child, field)];
87
- }));
169
+ if (typeof value === "number" && !Number.isSafeInteger(value)) {
170
+ throw new RangeError(`respond output: '${name}' (uint256) is not a safe integer`);
88
171
  }
89
- if ("array" in schema) {
90
- const array = typeof value === "string" && schema.array.type === "u8"
91
- ? Array.from(ethers.getBytes(value))
92
- : value instanceof Uint8Array
93
- ? Array.from(value)
94
- : value;
95
- if (Array.isArray(array)) {
96
- return array.map((element) => toBorshValue(schema.array.type, element));
97
- }
172
+ if (typeof value === "string" && !/^\s*\d+\s*$/.test(value)) {
173
+ throw new RangeError(`respond output: '${name}' (uint256) is not a decimal integer string`);
98
174
  }
99
- if ("option" in schema && value !== null)
100
- return toBorshValue(schema.option, value);
101
- if ("enum" in schema && typeof value === "object" && value !== null) {
102
- const variant = schema.enum.find((item) => Object.keys(item.struct).some((key) => Object.hasOwn(value, key)));
103
- if (variant !== undefined)
104
- return toBorshValue(variant, value);
175
+ const integer = BigInt(value);
176
+ if (integer < 0n || integer >= 1n << 256n) {
177
+ throw new RangeError(`respond output: '${name}' (uint256) ${String(integer)} is outside uint256`);
105
178
  }
106
- return value;
179
+ return Array.from(ethers.getBytes(ethers.toBeHex(integer, ABI_WORD_BYTES))).reverse();
180
+ }
181
+ /**
182
+ * The bytes of an `address` or `bytesN` output value, as ethers decodes it
183
+ * (a hex string, checksummed for an address) or as bytes. Byte order is the
184
+ * wire's: these are byte strings, not numbers.
185
+ *
186
+ * @param value - The decoded value.
187
+ * @param field - The field, for the error message.
188
+ * @returns The value's bytes, length-checked by Borsh against the schema.
189
+ * @throws {TypeError} If the value is neither a hex string nor bytes.
190
+ */
191
+ function byteString(value, field) {
192
+ if (value instanceof Uint8Array)
193
+ return Array.from(value);
194
+ if (typeof value === "string")
195
+ return Array.from(ethers.getBytes(value));
196
+ throw new TypeError(`respond output: '${field.name}' (${field.type}) expects hex or bytes`);
107
197
  }
108
198
  /**
109
- * Encode ABI-decoded output using the request's native Borsh response schema.
199
+ * Serialise ABI-decoded output into the respond bytes the MPC attests: the
200
+ * Borsh encoding of {@link deriveRespondSchema}'s struct over the output's
201
+ * values. A `uint256` is carried whole as 32 little-endian bytes, so no
202
+ * value is narrowed off chain: a circuit that wants a number narrows the
203
+ * `Bytes<32>` with `checkedTruncationU128`.
110
204
  *
111
- * @param schema - Native schema, JSON text, or NUL-padded on-chain JSON.
112
- * @param output - Named decoded EVM values.
113
- * @returns Unpadded Borsh bytes.
114
- * @throws {Error} If conversion loses integer precision or Borsh rejects the input.
205
+ * @param schema - The outputDeserializationSchema: parsed, JSON text, or the raw NUL-padded on-chain bytes.
206
+ * @param output - The decoded values keyed by field name, as {@link deserializeEvmOutput} returns them.
207
+ * @returns The unpadded respond bytes, {@link respondOutputWidth} long.
208
+ * @throws {Error} If the schema is malformed or names an unsupported type, a
209
+ * field is missing from `output`, or a value is outside its type.
115
210
  */
116
211
  export function serializeRespondOutput(schema, output) {
117
- const parsed = respondSchema(schema);
118
- return compactSerialize(parsed, toBorshValue(parsed, output));
212
+ const values = Object.fromEntries(classifiedEvmOutputFields(schema).map((field) => {
213
+ const value = output[field.name];
214
+ if (value === undefined) {
215
+ throw new Error(`respond output: missing value for '${field.name}'`);
216
+ }
217
+ switch (field.kind) {
218
+ case EvmOutputTypeKind.Bool:
219
+ if (typeof value !== "boolean") {
220
+ throw new TypeError(`respond output: '${field.name}' (bool) expects a boolean`);
221
+ }
222
+ return [field.name, value];
223
+ case EvmOutputTypeKind.Uint256:
224
+ return [field.name, uint256Word(value, field.name)];
225
+ case EvmOutputTypeKind.Address:
226
+ case EvmOutputTypeKind.FixedBytes:
227
+ return [field.name, byteString(value, field)];
228
+ }
229
+ }));
230
+ return compactSerialize(deriveRespondSchema(schema), values);
119
231
  }
120
232
  // ---------------------------------------------------------------------------
121
233
  // 3. executed EVM transaction -> attested respond bytes
@@ -152,89 +264,102 @@ export var EvmTraceOutputKind;
152
264
  /** The top call frame carries no string `output` field. */
153
265
  EvmTraceOutputKind["NoReturnData"] = "NoReturnData";
154
266
  })(EvmTraceOutputKind || (EvmTraceOutputKind = {}));
155
- /** The string default the MPC synthesises for an execution without call output. */
156
- const NON_FUNCTION_CALL_SUCCESS = "non_function_call_success";
157
267
  /**
158
- * The per-field values the MPC synthesises for an execution without call
159
- * output (its `default_value_for_kind`): `true` for a bool field,
160
- * {@link NON_FUNCTION_CALL_SUCCESS} for a string field.
268
+ * Refuse return data that is not canonical ABI before it is decoded: whole
269
+ * 32-byte words, at least one per declared field, a `bool` word of 0 or 1,
270
+ * an `address` word with zero high bytes and a `bytesN` word zero-padded
271
+ * after its N bytes. Words past the declared fields are not checked. The ABI
272
+ * library decodes such data leniently (a `bool` word of 2 reads as true), so
273
+ * the check sits here, where the MPC decides whether to attest.
161
274
  *
162
- * @param schema - The respondSerializationSchema.
163
- * @returns The synthesised values keyed by field name.
164
- * @throws {Error} If the schema is one {@link serializeRespondOutput}
165
- * rejects, or a field is of any other kind.
275
+ * @param fields - The schema's classified fields, in order.
276
+ * @param returnData - The traced return data, non-empty.
277
+ * @throws {Error} If the data is not whole words, holds fewer words than
278
+ * fields, or a field's word is not canonical for its type.
166
279
  */
167
- function nonFunctionCallDefaults(schema) {
168
- const parsed = respondSchema(schema);
169
- if (typeof parsed === "string" || !("struct" in parsed)) {
170
- throw new Error("Non-function-call defaults require a Borsh struct");
280
+ function checkCanonicalReturnData(fields, returnData) {
281
+ if (returnData.length % ABI_WORD_BYTES !== 0) {
282
+ throw new Error(`respond output: return data of ${String(returnData.length)} bytes is not whole ABI words`);
171
283
  }
172
- const output = {};
173
- for (const [name, type] of Object.entries(parsed.struct)) {
174
- if (type === "bool")
175
- output[name] = true;
176
- else if (type === "string")
177
- output[name] = NON_FUNCTION_CALL_SUCCESS;
178
- else
179
- throw new Error(`Response field '${name}' has no non-function-call default`);
284
+ const words = returnData.length / ABI_WORD_BYTES;
285
+ if (words < fields.length) {
286
+ throw new Error(`respond output: return data holds ${String(words)} word${words === 1 ? "" : "s"} but the output schema declares ${String(fields.length)} fields`);
180
287
  }
181
- return output;
288
+ const isZero = (bytes) => bytes.every((byte) => byte === 0);
289
+ fields.forEach((field, i) => {
290
+ const word = returnData.subarray(i * ABI_WORD_BYTES, (i + 1) * ABI_WORD_BYTES);
291
+ const last = word[ABI_WORD_BYTES - 1];
292
+ const canonical = field.kind === EvmOutputTypeKind.Bool
293
+ ? isZero(word.subarray(0, ABI_WORD_BYTES - 1)) && (last === 0 || last === 1)
294
+ : field.kind === EvmOutputTypeKind.Address
295
+ ? isZero(word.subarray(0, ABI_WORD_BYTES - EVM_ADDRESS_BYTES))
296
+ : field.kind === EvmOutputTypeKind.FixedBytes
297
+ ? isZero(word.subarray(field.bytes))
298
+ : true;
299
+ if (!canonical) {
300
+ throw new Error(`respond output: '${field.name}' (${field.type}) word 0x${ethers.hexlify(word).slice(2)} is not canonical ABI`);
301
+ }
302
+ });
182
303
  }
183
304
  /**
184
- * The exact serialised respond output the MPC attests for an EVM transaction
185
- * that executed (the payload of an `OutputKind.executed` attestation),
186
- * mirroring the MPC's `build_serialized_output` under the Midnight respond
187
- * format:
305
+ * The exact respond output the MPC attests for an EVM transaction that
306
+ * executed (the payload of an `OutputKind.executed` attestation), derived
307
+ * from the request's outputDeserializationSchema alone. The schema and the
308
+ * execution's return data MUST agree: an empty schema declares that the
309
+ * execution returns nothing, a non-empty schema that it returns data.
188
310
  *
189
- * - Not a contract call: the output schema and the trace are ignored, and
190
- * the respond fields get synthesised defaults (bool `true`, string
191
- * `"non_function_call_success"`, any other kind
192
- * throws).
193
- * - A contract call under an EMPTY output schema (a void call): the same
194
- * defaults when the trace has no or empty return data, a throw when it
195
- * returned data.
196
- * - A contract call under a non-empty output schema: the return data
197
- * decoded by {@link deserializeEvmOutput} and packed by
198
- * {@link serializeRespondOutput}, a throw when the trace has no return
199
- * data.
311
+ * - Not a contract call (a plain transfer): there is no return data, so the
312
+ * schema must be empty and the output is EMPTY.
313
+ * - A contract call whose trace carries no return data, or empty return
314
+ * data (`0x`): the schema must be empty and the output is EMPTY.
315
+ * - A contract call that returned data: the schema must be non-empty, and
316
+ * the output is the return data decoded by {@link deserializeEvmOutput}
317
+ * and serialised by {@link serializeRespondOutput}.
200
318
  *
201
- * A contract call that was not traced always throws. A failed or unviable
202
- * execution is attested over an empty output instead, which needs no call.
319
+ * The schema's types are checked on every path, a plain transfer included,
320
+ * so a request the MPC should have dropped never attests anything. A failed
321
+ * or unviable execution is attested over an empty output instead, which
322
+ * needs no call. An empty output under `executed` is therefore ordinary for
323
+ * an empty schema: its settle circuit verifies at width 0 and must route on
324
+ * the verified `outputKind`, never on the width.
203
325
  *
204
- * @param schemas - The request's two schemas (a request record satisfies this).
326
+ * @param schema - The request's outputDeserializationSchema (a request record's field satisfies this).
205
327
  * @param isContractCall - Whether the transaction is a contract call, as {@link isEvmContractCall} decides it.
206
328
  * @param trace - The transaction's traced return data (`NotTraced` for an untraced plain transfer).
207
- * @returns The packed respond bytes, unpadded.
329
+ * @returns The respond bytes, unpadded, empty for an empty schema.
208
330
  * @throws {Error} Exactly where the MPC refuses to attest an execution: a
209
- * contract call not traced, an empty output schema with return data, a
210
- * non-empty output schema with no or undecodable return data, a malformed
211
- * output schema on a contract call, a respond field without a synthesised
212
- * default, and every {@link serializeRespondOutput} rejection.
331
+ * malformed schema or an unsupported output type, a contract call not
332
+ * traced, return data under an empty schema, no or empty return data
333
+ * under a non-empty schema, return data that is not canonical ABI
334
+ * ({@link checkCanonicalReturnData}) or that the schema cannot decode, and
335
+ * every {@link serializeRespondOutput} rejection.
213
336
  */
214
- export function executedEvmRespondOutput(schemas, isContractCall, trace) {
215
- const { outputDeserializationSchema, respondSerializationSchema } = schemas;
337
+ export function executedEvmRespondOutput(schema, isContractCall, trace) {
338
+ const fields = classifiedEvmOutputFields(schema);
339
+ const expectsOutput = fields.length > 0;
216
340
  if (!isContractCall) {
217
- return serializeRespondOutput(respondSerializationSchema, nonFunctionCallDefaults(respondSerializationSchema));
341
+ if (expectsOutput) {
342
+ throw new Error("respond output: a plain transfer returns nothing, but the output schema declares return values");
343
+ }
344
+ return new Uint8Array(0);
345
+ }
346
+ if (trace.kind === EvmTraceOutputKind.NotTraced) {
347
+ throw new Error("respond output: a contract call's output needs its trace");
218
348
  }
219
- const expectsNoOutput = parseSchemaShape(outputDeserializationSchema).length === 0;
220
- switch (trace.kind) {
221
- case EvmTraceOutputKind.NotTraced:
222
- throw new Error("respond output: a contract call's output needs its trace");
223
- case EvmTraceOutputKind.NoReturnData:
224
- if (!expectsNoOutput) {
225
- throw new Error("respond output: the contract call's trace has no return data for a non-empty output schema");
226
- }
227
- break;
228
- case EvmTraceOutputKind.Output:
229
- if (!expectsNoOutput) {
230
- return serializeRespondOutput(respondSerializationSchema, deserializeEvmOutput(outputDeserializationSchema, trace.returnData));
231
- }
232
- if (ethers.getBytes(trace.returnData).length > 0) {
233
- throw new Error("respond output: the contract call returned data but its output schema declares no return values");
234
- }
235
- break;
349
+ const returnData = trace.kind === EvmTraceOutputKind.Output
350
+ ? ethers.getBytes(trace.returnData)
351
+ : new Uint8Array(0);
352
+ if (returnData.length === 0) {
353
+ if (expectsOutput) {
354
+ throw new Error("respond output: the contract call returned no data, but the output schema declares return values");
355
+ }
356
+ return new Uint8Array(0);
357
+ }
358
+ if (!expectsOutput) {
359
+ throw new Error("respond output: the contract call returned data, but the output schema declares no return values");
236
360
  }
237
- return serializeRespondOutput(respondSerializationSchema, nonFunctionCallDefaults(respondSerializationSchema));
361
+ checkCanonicalReturnData(fields, returnData);
362
+ return serializeRespondOutput(schema, deserializeEvmOutput(schema, returnData));
238
363
  }
239
364
  /**
240
365
  * Whether a JSON value is an object. A type guard, as `Array.isArray` does
@@ -323,39 +448,96 @@ function toPlainValue(value, label) {
323
448
  // Schema parsing + validation
324
449
  // ---------------------------------------------------------------------------
325
450
  /**
326
- * Parse schema text as JSON with the parse failure named. Blank text (an
327
- * unset all-NUL on-chain schema field) is the empty schema.
451
+ * The canonical text of an output schema: `JSON.stringify` of its fields
452
+ * reduced to `{name, type}` in schema order, with no whitespace. A schema's
453
+ * on-chain bytes must be exactly this text followed by NUL padding, so one
454
+ * schema has exactly one byte form. Write a contract's schema literal with
455
+ * this function's output.
328
456
  *
329
- * @param text - The schema text, NUL-trimmed.
330
- * @returns The parsed JSON value, `[]` for blank text.
331
- * @throws {Error} If non-blank text is not valid JSON.
457
+ * @param fields - The schema's fields in order.
458
+ * @returns The canonical JSON text, `[]` for an empty schema.
332
459
  */
333
- function parseSchemaJson(text) {
334
- if (text.trim() === "") {
460
+ export function canonicalSchemaText(fields) {
461
+ return JSON.stringify(fields.map(({ name, type }) => ({ name, type })));
462
+ }
463
+ /**
464
+ * Parse a schema's bytes, requiring the canonical form: the bytes before the
465
+ * first NUL are exactly {@link canonicalSchemaText} of the fields they
466
+ * encode, every byte from the first NUL on is NUL, and an empty schema is
467
+ * `[]` or no bytes at all (an unset all-NUL field).
468
+ *
469
+ * @param bytes - The schema text's UTF-8 bytes, NUL-padded or not.
470
+ * @returns The schema's fields.
471
+ * @throws {Error} If the padding holds a non-NUL byte, the text is not JSON,
472
+ * the fields fail {@link checkSchemaFields}, or the text is not canonical.
473
+ */
474
+ function parseCanonicalSchema(bytes) {
475
+ const nul = bytes.indexOf(0);
476
+ const body = nul === -1 ? bytes : bytes.subarray(0, nul);
477
+ if (nul !== -1 && bytes.subarray(nul).some((byte) => byte !== 0)) {
478
+ throw new Error("schema: every byte from the first NUL on must be NUL");
479
+ }
480
+ if (body.length === 0) {
335
481
  return [];
336
482
  }
483
+ let parsed;
337
484
  try {
338
- return JSON.parse(text);
485
+ parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(body));
339
486
  }
340
487
  catch (error) {
341
488
  throw new Error(`schema is not valid JSON (${String(error)})`, { cause: error });
342
489
  }
490
+ const fields = checkSchemaFields(parsed);
491
+ const canonical = new TextEncoder().encode(canonicalSchemaText(fields));
492
+ if (canonical.length !== body.length || canonical.some((byte, i) => byte !== body[i])) {
493
+ throw new Error(`schema is not canonical: expected exactly ${canonicalSchemaText(fields)}`);
494
+ }
495
+ return fields;
343
496
  }
344
497
  /**
345
- * Parse a schema in any input form and check its SHAPE only: an array of
346
- * fields with non-empty, unique names and non-empty type strings. An empty
347
- * schema is valid here: emptiness policy belongs to the callers
348
- * ({@link deserializeEvmOutput} decodes nothing and
349
- * {@link executedEvmRespondOutput} synthesises defaults for a void call).
498
+ * A Solidity identifier, the only form a field name takes. A name outside
499
+ * this grammar can start with a digit, and a JavaScript object orders
500
+ * integer-like keys first, which would move the derived Borsh struct's
501
+ * members out of schema order.
502
+ */
503
+ const SOLIDITY_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
504
+ /**
505
+ * The one identifier no field may use: assigning it on a JavaScript object
506
+ * reaches the prototype accessor, so an output object cannot carry it.
507
+ */
508
+ const PROTOTYPE_ACCESSOR_NAME = "__proto__";
509
+ /**
510
+ * Parse a schema in any input form and check its SHAPE only: text and bytes
511
+ * must be the canonical form ({@link parseCanonicalSchema}), and every form
512
+ * passes {@link checkSchemaFields}. Whether a type is in the attested subset
513
+ * is decided later, by {@link classifyEvmOutputType}. An empty schema is
514
+ * valid here: {@link deserializeEvmOutput} decodes nothing from it and
515
+ * {@link executedEvmRespondOutput} attests an empty output.
350
516
  *
351
- * @param schema - The schema as JSON text, packed bytes, or a field array.
352
- * @returns The schema's fields, names and type strings unvalidated beyond shape.
353
- * @throws {Error} If the schema is not an array of uniquely named fields.
517
+ * @param schema - The schema as canonical JSON text, NUL-padded bytes, or a field array.
518
+ * @returns The schema's fields, type strings as written.
519
+ * @throws {Error} If the text or bytes are not canonical, or the fields fail {@link checkSchemaFields}.
354
520
  */
355
521
  function parseSchemaShape(schema) {
356
- const parsed = typeof schema === "string" || schema instanceof Uint8Array
357
- ? parseSchemaJson(schemaText(schema))
358
- : schema;
522
+ if (typeof schema === "string") {
523
+ return parseCanonicalSchema(new TextEncoder().encode(schema));
524
+ }
525
+ if (schema instanceof Uint8Array) {
526
+ return parseCanonicalSchema(schema);
527
+ }
528
+ return checkSchemaFields(schema);
529
+ }
530
+ /**
531
+ * Check a parsed schema value's shape: an array of fields with unique
532
+ * Solidity-identifier names and ABI type strings `ethers.ParamType` accepts.
533
+ *
534
+ * @param parsed - The schema as parsed JSON or a field array.
535
+ * @returns The schema's fields, type strings as written.
536
+ * @throws {Error} If the value is not an array of fields, a name is not a
537
+ * Solidity identifier or is `__proto__`, a name repeats, or a type is
538
+ * missing or not an ABI type.
539
+ */
540
+ function checkSchemaFields(parsed) {
359
541
  if (!Array.isArray(parsed)) {
360
542
  throw new Error("schema must be a JSON array of fields");
361
543
  }
@@ -368,6 +550,12 @@ function parseSchemaShape(schema) {
368
550
  if (typeof name !== "string" || name.length === 0) {
369
551
  throw new Error(`schema field ${String(i)} needs a non-empty name`);
370
552
  }
553
+ if (!SOLIDITY_IDENTIFIER.test(name)) {
554
+ throw new Error(`schema: field name '${name}' is not a Solidity identifier`);
555
+ }
556
+ if (name === PROTOTYPE_ACCESSOR_NAME) {
557
+ throw new Error(`schema: field name '${name}' is refused`);
558
+ }
371
559
  if (seen.has(name)) {
372
560
  throw new Error(`schema: duplicate field name '${name}'`);
373
561
  }
@@ -375,18 +563,13 @@ function parseSchemaShape(schema) {
375
563
  if (typeof type !== "string" || type.length === 0) {
376
564
  throw new Error(`schema: '${name}' needs a type`);
377
565
  }
566
+ try {
567
+ ethers.ParamType.from(type);
568
+ }
569
+ catch (error) {
570
+ throw new Error(`schema: '${name}' has an invalid ABI type '${type}'`, { cause: error });
571
+ }
378
572
  return { name, type };
379
573
  });
380
574
  }
381
- /**
382
- * Cut a NUL-padded on-chain schema at the first NUL and decode to text.
383
- *
384
- * @param schema - Schema text, or the NUL-padded bytes read from the ledger.
385
- * @returns The schema text with the padding removed.
386
- */
387
- function schemaText(schema) {
388
- const raw = typeof schema === "string" ? schema : new TextDecoder().decode(schema);
389
- const nul = raw.indexOf("\0");
390
- return nul === -1 ? raw : raw.slice(0, nul);
391
- }
392
575
  //# sourceMappingURL=abi-serde.js.map