@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/README.md +18 -5
- package/dist/abi-serde.d.ts +119 -52
- package/dist/abi-serde.d.ts.map +1 -1
- package/dist/abi-serde.js +356 -173
- package/dist/abi-serde.js.map +1 -1
- package/dist/constants.js +1 -1
- package/dist/managed/compiler/contract-info.json +25 -7
- package/dist/managed/compiler/contract-manifest.json +8 -8
- package/dist/managed/contract/index.d.ts +252 -249
- package/dist/managed/contract/index.js +331 -307
- package/dist/managed/contract/index.js.map +1 -1
- package/dist/signet-contract-events.d.ts +11 -1
- package/dist/signet-contract-events.d.ts.map +1 -1
- package/dist/signet-contract-events.js +10 -5
- package/dist/signet-contract-events.js.map +1 -1
- package/dist/signet-request-response-reader.d.ts +8 -0
- package/dist/signet-request-response-reader.d.ts.map +1 -1
- package/dist/signet-request-response-reader.js +3 -1
- package/dist/signet-request-response-reader.js.map +1 -1
- package/package.json +2 -2
- package/src/Signet.compact +34 -15
- package/src/circuits.compact +13 -13
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
|
|
12
|
-
* decodes
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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 (
|
|
32
|
+
// 2. decoded values -> respond bytes (Borsh schema derived from the output schema)
|
|
34
33
|
// ---------------------------------------------------------------------------
|
|
35
34
|
/**
|
|
36
|
-
*
|
|
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
|
-
*
|
|
39
|
-
* @
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
84
|
+
return undefined;
|
|
49
85
|
}
|
|
50
86
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
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 -
|
|
55
|
-
* @
|
|
56
|
-
* @
|
|
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
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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 (
|
|
81
|
-
|
|
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 ("
|
|
90
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
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
|
-
*
|
|
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 -
|
|
112
|
-
* @param output -
|
|
113
|
-
* @returns
|
|
114
|
-
* @throws {Error} If
|
|
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
|
|
118
|
-
|
|
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
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
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
|
|
163
|
-
* @
|
|
164
|
-
* @throws {Error} If the
|
|
165
|
-
*
|
|
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
|
|
168
|
-
|
|
169
|
-
|
|
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
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
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
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
* - A contract call
|
|
194
|
-
*
|
|
195
|
-
*
|
|
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
|
-
*
|
|
202
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
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(
|
|
215
|
-
const
|
|
337
|
+
export function executedEvmRespondOutput(schema, isContractCall, trace) {
|
|
338
|
+
const fields = classifiedEvmOutputFields(schema);
|
|
339
|
+
const expectsOutput = fields.length > 0;
|
|
216
340
|
if (!isContractCall) {
|
|
217
|
-
|
|
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
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
327
|
-
*
|
|
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
|
|
330
|
-
* @returns The
|
|
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
|
|
334
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
-
|
|
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,
|
|
352
|
-
* @returns The schema's fields,
|
|
353
|
-
* @throws {Error} If the
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|