@sig-net/midnight 0.24.0-rc.6 → 0.24.0-rc.9
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 +22 -3
- package/dist/abi-serde.d.ts +103 -92
- package/dist/abi-serde.d.ts.map +1 -1
- package/dist/abi-serde.js +241 -418
- package/dist/abi-serde.js.map +1 -1
- package/dist/compact-descriptors.d.ts.map +1 -1
- package/dist/compact-descriptors.js +0 -4
- package/dist/compact-descriptors.js.map +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/package.json +2 -2
- package/src/Signet.compact +31 -16
- package/src/circuits.compact +13 -13
package/dist/abi-serde.js
CHANGED
|
@@ -1,60 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
// respond path, named after the protocol fields that drive them, and the
|
|
3
|
-
// MPC's rule composing them for an executed EVM transaction:
|
|
4
|
-
//
|
|
5
|
-
// EVM call result --deserializeEvmOutput--> decoded values
|
|
6
|
-
// (outputDeserializationSchema)
|
|
7
|
-
// decoded values --serializeRespondOutput--> RespondBidirectionalEvent bytes
|
|
8
|
-
// (respondSerializationSchema)
|
|
9
|
-
// callTracer top frame --evmTraceOutputFromCallFrame--> the trace
|
|
10
|
-
// executed EVM transaction --executedEvmRespondOutput--> attested bytes
|
|
11
|
-
// (both schemas, isEvmContractCall, the trace)
|
|
12
|
-
//
|
|
13
|
-
// Run by fakenet and the real MPC after the destination-chain transaction
|
|
14
|
-
// confirms, and by Signet clients to independently recompute the respond
|
|
15
|
-
// bytes the MPC attested or to display a decoded result. A client
|
|
16
|
-
// recomputing an attestation calls executedEvmRespondOutput: the MPC decodes
|
|
17
|
-
// and re-packs a contract call's return data, and synthesises the output
|
|
18
|
-
// of a plain transfer or a void call from the respond schema alone.
|
|
19
|
-
//
|
|
20
|
-
// The respond output is UNBOUNDED: this module never pads or clamps it. Any
|
|
21
|
-
// fixed event-field width is the caller's concern. Schemas are the ABI-style
|
|
22
|
-
// JSON carried on chain (NUL-padded fixed-width bytes). Every function
|
|
23
|
-
// accepts the schema in any form: already-parsed fields, a JSON string, or
|
|
24
|
-
// the raw on-chain bytes. An EMPTY output schema (an unset all-NUL field, a
|
|
25
|
-
// blank string, or `[]`) decodes to no values. A respond schema must be
|
|
26
|
-
// non-empty, and its declared capacities are bounded
|
|
27
|
-
// ({@link MAX_RESPOND_PACKED_BYTES}): schemas are requester-authored
|
|
28
|
-
// on-chain data, so the responder never honours a schema demanding a giant
|
|
29
|
-
// allocation.
|
|
30
|
-
//
|
|
31
|
-
// Decode-side type grammar is left FULLY to the ABI library (ethers): this
|
|
32
|
-
// module checks only the schema's shape. Respond-side types are restricted
|
|
33
|
-
// to the Compact-carrier vocabulary below, strictly enforced.
|
|
34
|
-
//
|
|
35
|
-
// The respond byte layout is Compact's builtin serialize<T, N> /
|
|
36
|
-
// deserialize<T, N> (via @sig-net/midnight-serde, pinned against compiled
|
|
37
|
-
// circuits), so a consumer contract reads the payload with ONE
|
|
38
|
-
// deserialize<T, N> call. Per-type mapping (Compact struct field on the
|
|
39
|
-
// right):
|
|
40
|
-
// bool 1 byte Boolean
|
|
41
|
-
// uint8..uint248 bits / 8 bytes LE Uint<bits>
|
|
42
|
-
// (whole-byte widths only: multiples of 8, others are rejected)
|
|
43
|
-
// uint256, field 32 bytes LE, below Fr Field
|
|
44
|
-
// address 32 bytes LE (numeric) Field
|
|
45
|
-
// bytes1..bytes32 N raw bytes Bytes<N>
|
|
46
|
-
// string, bytes 8-byte LE length + payload zero-padded to maxBytes
|
|
47
|
-
// struct { len: Uint<64>; data: Bytes<maxBytes>; }
|
|
48
|
-
// T[] 8-byte LE count + maxItems elements at T's width
|
|
49
|
-
// struct { len: Uint<64>; items: Vector<maxItems, T>; }
|
|
50
|
-
// intN rejected: Compact has no signed integers
|
|
51
|
-
//
|
|
52
|
-
// RANGE TRAP: uint256, address and field all map to Compact `Field`, whose
|
|
53
|
-
// values must lie strictly below the BLS12-381 Fr modulus (just under
|
|
54
|
-
// 2^255). An EVM uint256 at or above Fr cannot be respond-serialised, and
|
|
55
|
-
// `serializeRespondOutput` throws at respond time. Schema authors who need
|
|
56
|
-
// the full 256-bit range carry the value as bytes32.
|
|
57
|
-
import { compactSerialize, compactSerializedSize, } from "@sig-net/midnight-serde";
|
|
1
|
+
import { compactSerialize } from "@sig-net/midnight-serde";
|
|
58
2
|
import { ethers } from "ethers";
|
|
59
3
|
// ---------------------------------------------------------------------------
|
|
60
4
|
// 1. EVM call result -> decoded values (outputDeserializationSchema)
|
|
@@ -64,10 +8,10 @@ import { ethers } from "ethers";
|
|
|
64
8
|
* into named values, driven by the request's outputDeserializationSchema.
|
|
65
9
|
* The decode-side counterpart of {@link serializeRespondOutput}. Type
|
|
66
10
|
* validation is FULLY delegated to ethers: this function checks the schema's
|
|
67
|
-
* shape only
|
|
68
|
-
* decodes
|
|
69
|
-
*
|
|
70
|
-
*
|
|
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.
|
|
71
15
|
*
|
|
72
16
|
* @param schema - The outputDeserializationSchema: parsed, JSON text, or the raw NUL-padded on-chain bytes.
|
|
73
17
|
* @param callResult - The ABI-encoded return data (hex string or bytes).
|
|
@@ -86,54 +30,204 @@ export function deserializeEvmOutput(schema, callResult) {
|
|
|
86
30
|
return output;
|
|
87
31
|
}
|
|
88
32
|
// ---------------------------------------------------------------------------
|
|
89
|
-
// 2. decoded values -> respond bytes (
|
|
33
|
+
// 2. decoded values -> respond bytes (Borsh schema derived from the output schema)
|
|
90
34
|
// ---------------------------------------------------------------------------
|
|
91
35
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
36
|
+
* The ABI output types the MPC attests, and what each maps to. The respond
|
|
37
|
+
* bytes are the Borsh serialisation of a struct with one member per output
|
|
38
|
+
* field, in schema order, typed by this mapping:
|
|
39
|
+
*
|
|
40
|
+
* - {@link EvmOutputTypeKind.Bool}: ABI `bool`, Borsh `bool`, Compact `Boolean` (1 byte).
|
|
41
|
+
* - {@link EvmOutputTypeKind.Uint256}: ABI `uint256`, Borsh `[u8; 32]`
|
|
42
|
+
* holding the value LITTLE-endian, the byte order of every Borsh and
|
|
43
|
+
* Compact integer, Compact `Bytes<32>` (32 bytes). A circuit that wants a
|
|
44
|
+
* number narrows it with `checkedTruncationU128`, which aborts above
|
|
45
|
+
* 2^128 - 1. The ABI wire word is big-endian: the MPC reverses it once,
|
|
46
|
+
* here, so no circuit has to.
|
|
47
|
+
* - {@link EvmOutputTypeKind.Address}: ABI `address`, Borsh `[u8; 20]`
|
|
48
|
+
* holding the address bytes, Compact `Bytes<20>` (20 bytes).
|
|
49
|
+
* - {@link EvmOutputTypeKind.FixedBytes}: ABI `bytes1` to `bytes32`, Borsh
|
|
50
|
+
* `[u8; N]`, Compact `Bytes<N>` (N bytes).
|
|
51
|
+
*
|
|
52
|
+
* Every other ABI type is unsupported: the MPC drops a request whose output
|
|
53
|
+
* schema names one, and {@link deriveRespondSchema} throws on it.
|
|
97
54
|
*/
|
|
98
|
-
export
|
|
55
|
+
export var EvmOutputTypeKind;
|
|
56
|
+
(function (EvmOutputTypeKind) {
|
|
57
|
+
EvmOutputTypeKind["Bool"] = "bool";
|
|
58
|
+
EvmOutputTypeKind["Uint256"] = "uint256";
|
|
59
|
+
EvmOutputTypeKind["Address"] = "address";
|
|
60
|
+
EvmOutputTypeKind["FixedBytes"] = "bytesN";
|
|
61
|
+
})(EvmOutputTypeKind || (EvmOutputTypeKind = {}));
|
|
62
|
+
/** The byte width of an ABI word, and of the Borsh array a `uint256` output maps to. */
|
|
63
|
+
const ABI_WORD_BYTES = 32;
|
|
64
|
+
/** The byte width of an EVM address, and of the Borsh array an `address` output maps to. */
|
|
65
|
+
const EVM_ADDRESS_BYTES = 20;
|
|
99
66
|
/**
|
|
100
|
-
*
|
|
101
|
-
* request's respondSerializationSchema: the bytes a consumer contract reads
|
|
102
|
-
* with `deserialize<T, N>` and the MPC attests. The respond-side counterpart
|
|
103
|
-
* of {@link deserializeEvmOutput}.
|
|
67
|
+
* Classify an ABI type string into the supported output subset.
|
|
104
68
|
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
69
|
+
* @param type - The ABI type string of an output schema field.
|
|
70
|
+
* @returns The field's kind and respond byte width, or `undefined` for an unsupported type.
|
|
71
|
+
*/
|
|
72
|
+
function classifyEvmOutputType(type) {
|
|
73
|
+
if (type === "bool")
|
|
74
|
+
return { kind: EvmOutputTypeKind.Bool, bytes: 1 };
|
|
75
|
+
if (type === "uint256")
|
|
76
|
+
return { kind: EvmOutputTypeKind.Uint256, bytes: ABI_WORD_BYTES };
|
|
77
|
+
if (type === "address")
|
|
78
|
+
return { kind: EvmOutputTypeKind.Address, bytes: EVM_ADDRESS_BYTES };
|
|
79
|
+
const fixedBytes = /^bytes([1-9]|[12][0-9]|3[0-2])$/.exec(type);
|
|
80
|
+
if (fixedBytes !== null) {
|
|
81
|
+
return { kind: EvmOutputTypeKind.FixedBytes, bytes: Number(fixedBytes[1]) };
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* The fields of an outputDeserializationSchema whose ABI type is outside the
|
|
87
|
+
* supported subset ({@link EvmOutputTypeKind}). The check itself never
|
|
88
|
+
* throws on an unsupported type: an empty result means the MPC attests the
|
|
89
|
+
* request, a non-empty one means it drops it.
|
|
110
90
|
*
|
|
111
|
-
* @param schema - The
|
|
112
|
-
* @
|
|
113
|
-
* @
|
|
114
|
-
* @throws {Error} If the schema is empty or malformed, packs to more than
|
|
115
|
-
* {@link MAX_RESPOND_PACKED_BYTES} bytes, a value falls outside its declared
|
|
116
|
-
* range, or a dynamic payload exceeds its capacity.
|
|
91
|
+
* @param schema - The outputDeserializationSchema: parsed, JSON text, or the raw NUL-padded on-chain bytes.
|
|
92
|
+
* @returns The unsupported fields in schema order, empty when every field is supported.
|
|
93
|
+
* @throws {Error} If the schema is not an array of uniquely named fields (its shape, not its types).
|
|
117
94
|
*/
|
|
118
|
-
export function
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
95
|
+
export function unsupportedEvmOutputFields(schema) {
|
|
96
|
+
return parseSchemaShape(schema).filter((field) => classifyEvmOutputType(field.type) === undefined);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Parse an output schema and classify every field, refusing the schema as a
|
|
100
|
+
* whole when any field is unsupported.
|
|
101
|
+
*
|
|
102
|
+
* @param schema - The outputDeserializationSchema: parsed, JSON text, or the raw NUL-padded on-chain bytes.
|
|
103
|
+
* @returns The classified fields in schema order.
|
|
104
|
+
* @throws {Error} If the schema is malformed or names an unsupported ABI type.
|
|
105
|
+
*/
|
|
106
|
+
function classifiedEvmOutputFields(schema) {
|
|
107
|
+
const fields = parseSchemaShape(schema);
|
|
108
|
+
const unsupported = fields.filter((field) => classifyEvmOutputType(field.type) === undefined);
|
|
109
|
+
if (unsupported.length > 0) {
|
|
110
|
+
throw new Error(`respond output: unsupported ABI output type${unsupported.length > 1 ? "s" : ""} ` +
|
|
111
|
+
unsupported.map((field) => `'${field.name}' (${field.type})`).join(", ") +
|
|
112
|
+
": the MPC attests bool, uint256, address and bytes1 to bytes32 only");
|
|
113
|
+
}
|
|
114
|
+
return fields.flatMap((field) => {
|
|
115
|
+
const classified = classifyEvmOutputType(field.type);
|
|
116
|
+
return classified === undefined ? [] : [{ ...field, ...classified }];
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The Borsh schema the respond bytes of an executed call are serialised
|
|
121
|
+
* with: a struct with one member per output field, in schema order, each
|
|
122
|
+
* typed by {@link EvmOutputTypeKind}. An empty output schema derives an
|
|
123
|
+
* empty struct, which serialises to zero bytes.
|
|
124
|
+
*
|
|
125
|
+
* @param schema - The outputDeserializationSchema: parsed, JSON text, or the raw NUL-padded on-chain bytes.
|
|
126
|
+
* @returns The derived Borsh struct schema.
|
|
127
|
+
* @throws {Error} If the schema is malformed or names an unsupported ABI type.
|
|
128
|
+
*/
|
|
129
|
+
export function deriveRespondSchema(schema) {
|
|
130
|
+
return {
|
|
131
|
+
struct: Object.fromEntries(classifiedEvmOutputFields(schema).map((field) => [
|
|
132
|
+
field.name,
|
|
133
|
+
field.kind === EvmOutputTypeKind.Bool
|
|
134
|
+
? "bool"
|
|
135
|
+
: { array: { type: "u8", len: field.bytes } },
|
|
136
|
+
])),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The byte width of the respond bytes an output schema derives: the sum of
|
|
141
|
+
* its fields' widths under {@link EvmOutputTypeKind}, what a settle circuit
|
|
142
|
+
* declares as its `Bytes<N>` output argument.
|
|
143
|
+
*
|
|
144
|
+
* @param schema - The outputDeserializationSchema: parsed, JSON text, or the raw NUL-padded on-chain bytes.
|
|
145
|
+
* @returns The exact serialised width, 0 for an empty schema.
|
|
146
|
+
* @throws {Error} If the schema is malformed or names an unsupported ABI type.
|
|
147
|
+
*/
|
|
148
|
+
export function respondOutputWidth(schema) {
|
|
149
|
+
return classifiedEvmOutputFields(schema).reduce((width, field) => width + field.bytes, 0);
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The 32-byte LITTLE-endian encoding of a `uint256` output value, as ethers
|
|
153
|
+
* decodes it (bigint) or as indexer JSON renders it (a number or a decimal
|
|
154
|
+
* string). This is the ABI wire word reversed: the Borsh integer byte order,
|
|
155
|
+
* which a Compact `Uint<128>` reads directly.
|
|
156
|
+
*
|
|
157
|
+
* @param value - The decoded value.
|
|
158
|
+
* @param name - The field name, for the error message.
|
|
159
|
+
* @returns The value's 32 little-endian bytes.
|
|
160
|
+
* @throws {RangeError} If the value is not an integer in `0 <= value < 2^256`.
|
|
161
|
+
*/
|
|
162
|
+
function uint256Word(value, name) {
|
|
163
|
+
if (typeof value !== "bigint" && typeof value !== "number" && typeof value !== "string") {
|
|
164
|
+
throw new TypeError(`respond output: '${name}' (uint256) expects an integer`);
|
|
165
|
+
}
|
|
166
|
+
if (typeof value === "number" && !Number.isSafeInteger(value)) {
|
|
167
|
+
throw new RangeError(`respond output: '${name}' (uint256) is not a safe integer`);
|
|
127
168
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
169
|
+
if (typeof value === "string" && !/^\s*\d+\s*$/.test(value)) {
|
|
170
|
+
throw new RangeError(`respond output: '${name}' (uint256) is not a decimal integer string`);
|
|
171
|
+
}
|
|
172
|
+
const integer = BigInt(value);
|
|
173
|
+
if (integer < 0n || integer >= 1n << 256n) {
|
|
174
|
+
throw new RangeError(`respond output: '${name}' (uint256) ${String(integer)} is outside uint256`);
|
|
175
|
+
}
|
|
176
|
+
return Array.from(ethers.getBytes(ethers.toBeHex(integer, ABI_WORD_BYTES))).reverse();
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The bytes of an `address` or `bytesN` output value, as ethers decodes it
|
|
180
|
+
* (a hex string, checksummed for an address) or as bytes. Byte order is the
|
|
181
|
+
* wire's: these are byte strings, not numbers.
|
|
182
|
+
*
|
|
183
|
+
* @param value - The decoded value.
|
|
184
|
+
* @param field - The field, for the error message.
|
|
185
|
+
* @returns The value's bytes, length-checked by Borsh against the schema.
|
|
186
|
+
* @throws {TypeError} If the value is neither a hex string nor bytes.
|
|
187
|
+
*/
|
|
188
|
+
function byteString(value, field) {
|
|
189
|
+
if (value instanceof Uint8Array)
|
|
190
|
+
return Array.from(value);
|
|
191
|
+
if (typeof value === "string")
|
|
192
|
+
return Array.from(ethers.getBytes(value));
|
|
193
|
+
throw new TypeError(`respond output: '${field.name}' (${field.type}) expects hex or bytes`);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Serialise ABI-decoded output into the respond bytes the MPC attests: the
|
|
197
|
+
* Borsh encoding of {@link deriveRespondSchema}'s struct over the output's
|
|
198
|
+
* values. A `uint256` is carried whole as 32 little-endian bytes, so no
|
|
199
|
+
* value is narrowed off chain: a circuit that wants a number narrows the
|
|
200
|
+
* `Bytes<32>` with `checkedTruncationU128`.
|
|
201
|
+
*
|
|
202
|
+
* @param schema - The outputDeserializationSchema: parsed, JSON text, or the raw NUL-padded on-chain bytes.
|
|
203
|
+
* @param output - The decoded values keyed by field name, as {@link deserializeEvmOutput} returns them.
|
|
204
|
+
* @returns The unpadded respond bytes, {@link respondOutputWidth} long.
|
|
205
|
+
* @throws {Error} If the schema is malformed or names an unsupported type, a
|
|
206
|
+
* field is missing from `output`, or a value is outside its type.
|
|
207
|
+
*/
|
|
208
|
+
export function serializeRespondOutput(schema, output) {
|
|
209
|
+
const values = {};
|
|
210
|
+
for (const field of classifiedEvmOutputFields(schema)) {
|
|
211
|
+
const value = output[field.name];
|
|
212
|
+
if (value === undefined)
|
|
132
213
|
throw new Error(`respond output: missing value for '${field.name}'`);
|
|
214
|
+
switch (field.kind) {
|
|
215
|
+
case EvmOutputTypeKind.Bool:
|
|
216
|
+
if (typeof value !== "boolean") {
|
|
217
|
+
throw new TypeError(`respond output: '${field.name}' (bool) expects a boolean`);
|
|
218
|
+
}
|
|
219
|
+
values[field.name] = value;
|
|
220
|
+
break;
|
|
221
|
+
case EvmOutputTypeKind.Uint256:
|
|
222
|
+
values[field.name] = uint256Word(value, field.name);
|
|
223
|
+
break;
|
|
224
|
+
case EvmOutputTypeKind.Address:
|
|
225
|
+
case EvmOutputTypeKind.FixedBytes:
|
|
226
|
+
values[field.name] = byteString(value, field);
|
|
227
|
+
break;
|
|
133
228
|
}
|
|
134
|
-
value[field.name] = toCompactValue(raw, field);
|
|
135
229
|
}
|
|
136
|
-
return compactSerialize(
|
|
230
|
+
return compactSerialize(deriveRespondSchema(schema), values);
|
|
137
231
|
}
|
|
138
232
|
// ---------------------------------------------------------------------------
|
|
139
233
|
// 3. executed EVM transaction -> attested respond bytes
|
|
@@ -170,89 +264,62 @@ export var EvmTraceOutputKind;
|
|
|
170
264
|
/** The top call frame carries no string `output` field. */
|
|
171
265
|
EvmTraceOutputKind["NoReturnData"] = "NoReturnData";
|
|
172
266
|
})(EvmTraceOutputKind || (EvmTraceOutputKind = {}));
|
|
173
|
-
/** The string default the MPC synthesises for an execution without call output. */
|
|
174
|
-
const NON_FUNCTION_CALL_SUCCESS = "non_function_call_success";
|
|
175
|
-
/**
|
|
176
|
-
* The per-field values the MPC synthesises for an execution without call
|
|
177
|
-
* output (its `default_value_for_kind`): `true` for a bool field,
|
|
178
|
-
* {@link NON_FUNCTION_CALL_SUCCESS} for a string field.
|
|
179
|
-
*
|
|
180
|
-
* @param respondSchema - The respondSerializationSchema.
|
|
181
|
-
* @returns The synthesised values keyed by field name.
|
|
182
|
-
* @throws {Error} If the schema is one {@link serializeRespondOutput}
|
|
183
|
-
* rejects, or a field is of any other kind.
|
|
184
|
-
*/
|
|
185
|
-
function nonFunctionCallDefaults(respondSchema) {
|
|
186
|
-
const output = {};
|
|
187
|
-
for (const field of normalizeRespondSchema(respondSchema)) {
|
|
188
|
-
if (field.type === "bool") {
|
|
189
|
-
output[field.name] = true;
|
|
190
|
-
}
|
|
191
|
-
else if (field.type === "string") {
|
|
192
|
-
output[field.name] = NON_FUNCTION_CALL_SUCCESS;
|
|
193
|
-
}
|
|
194
|
-
else {
|
|
195
|
-
throw new Error(`respond output: '${field.name}' (${field.type}) has no non-function-call ` +
|
|
196
|
-
"default: only bool and string fields have one");
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
return output;
|
|
200
|
-
}
|
|
201
267
|
/**
|
|
202
|
-
* The exact
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
268
|
+
* The exact respond output the MPC attests for an EVM transaction that
|
|
269
|
+
* executed (the payload of an `OutputKind.executed` attestation), derived
|
|
270
|
+
* from the request's outputDeserializationSchema alone. The schema and the
|
|
271
|
+
* execution's return data MUST agree: an empty schema declares that the
|
|
272
|
+
* execution returns nothing, a non-empty schema that it returns data.
|
|
206
273
|
*
|
|
207
|
-
* - Not a contract call
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
* - A contract call
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
* - A contract call under a non-empty output schema: the return data
|
|
215
|
-
* decoded by {@link deserializeEvmOutput} and packed by
|
|
216
|
-
* {@link serializeRespondOutput}, a throw when the trace has no return
|
|
217
|
-
* data.
|
|
274
|
+
* - Not a contract call (a plain transfer): there is no return data, so the
|
|
275
|
+
* schema must be empty and the output is EMPTY.
|
|
276
|
+
* - A contract call whose trace carries no return data, or empty return
|
|
277
|
+
* data (`0x`): the schema must be empty and the output is EMPTY.
|
|
278
|
+
* - A contract call that returned data: the schema must be non-empty, and
|
|
279
|
+
* the output is the return data decoded by {@link deserializeEvmOutput}
|
|
280
|
+
* and serialised by {@link serializeRespondOutput}.
|
|
218
281
|
*
|
|
219
|
-
*
|
|
220
|
-
*
|
|
282
|
+
* The schema's types are checked on every path, a plain transfer included,
|
|
283
|
+
* so a request the MPC should have dropped never attests anything. A failed
|
|
284
|
+
* or unviable execution is attested over an empty output instead, which
|
|
285
|
+
* needs no call. An empty output under `executed` is therefore ordinary for
|
|
286
|
+
* an empty schema: its settle circuit verifies at width 0 and must route on
|
|
287
|
+
* the verified `outputKind`, never on the width.
|
|
221
288
|
*
|
|
222
|
-
* @param
|
|
289
|
+
* @param schema - The request's outputDeserializationSchema (a request record's field satisfies this).
|
|
223
290
|
* @param isContractCall - Whether the transaction is a contract call, as {@link isEvmContractCall} decides it.
|
|
224
291
|
* @param trace - The transaction's traced return data (`NotTraced` for an untraced plain transfer).
|
|
225
|
-
* @returns The
|
|
292
|
+
* @returns The respond bytes, unpadded, empty for an empty schema.
|
|
226
293
|
* @throws {Error} Exactly where the MPC refuses to attest an execution: a
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
294
|
+
* malformed schema or an unsupported output type, a contract call not
|
|
295
|
+
* traced, return data under an empty schema, no or empty return data
|
|
296
|
+
* under a non-empty schema, return data the schema cannot decode, and
|
|
297
|
+
* every {@link serializeRespondOutput} rejection.
|
|
231
298
|
*/
|
|
232
|
-
export function executedEvmRespondOutput(
|
|
233
|
-
const
|
|
299
|
+
export function executedEvmRespondOutput(schema, isContractCall, trace) {
|
|
300
|
+
const expectsOutput = classifiedEvmOutputFields(schema).length > 0;
|
|
234
301
|
if (!isContractCall) {
|
|
235
|
-
|
|
302
|
+
if (expectsOutput) {
|
|
303
|
+
throw new Error("respond output: a plain transfer returns nothing, but the output schema declares return values");
|
|
304
|
+
}
|
|
305
|
+
return new Uint8Array(0);
|
|
236
306
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
case EvmTraceOutputKind.NotTraced:
|
|
240
|
-
throw new Error("respond output: a contract call's output needs its trace");
|
|
241
|
-
case EvmTraceOutputKind.NoReturnData:
|
|
242
|
-
if (!expectsNoOutput) {
|
|
243
|
-
throw new Error("respond output: the contract call's trace has no return data for a non-empty output schema");
|
|
244
|
-
}
|
|
245
|
-
break;
|
|
246
|
-
case EvmTraceOutputKind.Output:
|
|
247
|
-
if (!expectsNoOutput) {
|
|
248
|
-
return serializeRespondOutput(respondSerializationSchema, deserializeEvmOutput(outputDeserializationSchema, trace.returnData));
|
|
249
|
-
}
|
|
250
|
-
if (ethers.getBytes(trace.returnData).length > 0) {
|
|
251
|
-
throw new Error("respond output: the contract call returned data but its output schema declares no return values");
|
|
252
|
-
}
|
|
253
|
-
break;
|
|
307
|
+
if (trace.kind === EvmTraceOutputKind.NotTraced) {
|
|
308
|
+
throw new Error("respond output: a contract call's output needs its trace");
|
|
254
309
|
}
|
|
255
|
-
|
|
310
|
+
const returnData = trace.kind === EvmTraceOutputKind.Output
|
|
311
|
+
? ethers.getBytes(trace.returnData)
|
|
312
|
+
: new Uint8Array(0);
|
|
313
|
+
if (returnData.length === 0) {
|
|
314
|
+
if (expectsOutput) {
|
|
315
|
+
throw new Error("respond output: the contract call returned no data, but the output schema declares return values");
|
|
316
|
+
}
|
|
317
|
+
return new Uint8Array(0);
|
|
318
|
+
}
|
|
319
|
+
if (!expectsOutput) {
|
|
320
|
+
throw new Error("respond output: the contract call returned data, but the output schema declares no return values");
|
|
321
|
+
}
|
|
322
|
+
return serializeRespondOutput(schema, deserializeEvmOutput(schema, returnData));
|
|
256
323
|
}
|
|
257
324
|
/**
|
|
258
325
|
* Whether a JSON value is an object. A type guard, as `Array.isArray` does
|
|
@@ -321,26 +388,6 @@ export function evmTraceOutputFromCallFrame(frame) {
|
|
|
321
388
|
// Helpers from here down. The exports above are the whole public surface:
|
|
322
389
|
// everything below serves them.
|
|
323
390
|
// ===========================================================================
|
|
324
|
-
// ---------------------------------------------------------------------------
|
|
325
|
-
// Field-kind guards
|
|
326
|
-
// ---------------------------------------------------------------------------
|
|
327
|
-
function isAbiDynamicField(field) {
|
|
328
|
-
return field.type === "string" || field.type === "bytes";
|
|
329
|
-
}
|
|
330
|
-
function isAbiArrayField(field) {
|
|
331
|
-
return field.type.endsWith("[]");
|
|
332
|
-
}
|
|
333
|
-
// ---------------------------------------------------------------------------
|
|
334
|
-
// Decode-side value flattening
|
|
335
|
-
// ---------------------------------------------------------------------------
|
|
336
|
-
/**
|
|
337
|
-
* Flatten ethers `Result` arrays into plain arrays, pass scalars through.
|
|
338
|
-
*
|
|
339
|
-
* @param value - A decoded ABI value, possibly a nested `Result`.
|
|
340
|
-
* @param label - Field path, used in error messages.
|
|
341
|
-
* @returns The value with every `Result` replaced by a plain array.
|
|
342
|
-
* @throws {Error} If the value is of a kind the respond side cannot carry.
|
|
343
|
-
*/
|
|
344
391
|
function toPlainValue(value, label) {
|
|
345
392
|
if (value instanceof ethers.Result) {
|
|
346
393
|
return value.toArray().map((v, i) => toPlainValue(v, `${label}[${String(i)}]`));
|
|
@@ -360,10 +407,6 @@ function toPlainValue(value, label) {
|
|
|
360
407
|
// ---------------------------------------------------------------------------
|
|
361
408
|
// Schema parsing + validation
|
|
362
409
|
// ---------------------------------------------------------------------------
|
|
363
|
-
const ADDRESS_BOUND = 1n << 160n;
|
|
364
|
-
const MAX_UINT_BITS = 248;
|
|
365
|
-
/** Length-prefix width of the dynamic string/bytes/array convention (Uint<64>). */
|
|
366
|
-
const DYN_LEN_BYTES = 8;
|
|
367
410
|
/**
|
|
368
411
|
* Parse schema text as JSON with the parse failure named. Blank text (an
|
|
369
412
|
* unset all-NUL on-chain schema field) is the empty schema.
|
|
@@ -386,10 +429,8 @@ function parseSchemaJson(text) {
|
|
|
386
429
|
/**
|
|
387
430
|
* Parse a schema in any input form and check its SHAPE only: an array of
|
|
388
431
|
* fields with non-empty, unique names and non-empty type strings. An empty
|
|
389
|
-
* schema is valid here:
|
|
390
|
-
*
|
|
391
|
-
* rejects, {@link executedEvmRespondOutput} synthesises defaults for a
|
|
392
|
-
* contract call without return data).
|
|
432
|
+
* schema is valid here: {@link deserializeEvmOutput} decodes nothing from
|
|
433
|
+
* it and {@link executedEvmRespondOutput} attests an empty output.
|
|
393
434
|
*
|
|
394
435
|
* @param schema - The schema as JSON text, packed bytes, or a field array.
|
|
395
436
|
* @returns The schema's fields, names and type strings unvalidated beyond shape.
|
|
@@ -407,7 +448,7 @@ function parseSchemaShape(schema) {
|
|
|
407
448
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
408
449
|
throw new Error(`schema field ${String(i)} is not an object`);
|
|
409
450
|
}
|
|
410
|
-
const { name, type
|
|
451
|
+
const { name, type } = raw;
|
|
411
452
|
if (typeof name !== "string" || name.length === 0) {
|
|
412
453
|
throw new Error(`schema field ${String(i)} needs a non-empty name`);
|
|
413
454
|
}
|
|
@@ -418,36 +459,7 @@ function parseSchemaShape(schema) {
|
|
|
418
459
|
if (typeof type !== "string" || type.length === 0) {
|
|
419
460
|
throw new Error(`schema: '${name}' needs a type`);
|
|
420
461
|
}
|
|
421
|
-
return { name, type
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
/**
|
|
425
|
-
* Enforce the respond-side Compact-carrier vocabulary on a shape-checked
|
|
426
|
-
* schema: every type needs a Compact carrier (no signed ints, uint widths of
|
|
427
|
-
* at most 248 bits or exactly 256, bytesN at most 32) and every dynamic field
|
|
428
|
-
* needs its fixed capacity.
|
|
429
|
-
*
|
|
430
|
-
* @param schema - The schema to normalize.
|
|
431
|
-
* @returns The schema with every field proven to have a Compact carrier.
|
|
432
|
-
* @throws {Error} If a type has no carrier or a dynamic field omits its capacity.
|
|
433
|
-
*/
|
|
434
|
-
function normalizeRespondSchema(schema) {
|
|
435
|
-
const fields = parseSchemaShape(schema);
|
|
436
|
-
if (fields.length === 0) {
|
|
437
|
-
throw new Error("respond schema is empty: a respond serialization schema needs at least one field");
|
|
438
|
-
}
|
|
439
|
-
return fields.map(({ name, type, maxBytes, maxItems }) => {
|
|
440
|
-
if (type === "string" || type === "bytes") {
|
|
441
|
-
assertCapacity(maxBytes, `'${name}' (${type}) maxBytes`);
|
|
442
|
-
return { name, type, maxBytes };
|
|
443
|
-
}
|
|
444
|
-
if (type.endsWith("[]")) {
|
|
445
|
-
classifyFixedType(type.slice(0, -2), name);
|
|
446
|
-
assertCapacity(maxItems, `'${name}' (${type}) maxItems`);
|
|
447
|
-
return { name, type: type, maxItems };
|
|
448
|
-
}
|
|
449
|
-
classifyFixedType(type, name);
|
|
450
|
-
return { name, type: type };
|
|
462
|
+
return { name, type };
|
|
451
463
|
});
|
|
452
464
|
}
|
|
453
465
|
/**
|
|
@@ -461,193 +473,4 @@ function schemaText(schema) {
|
|
|
461
473
|
const nul = raw.indexOf("\0");
|
|
462
474
|
return nul === -1 ? raw : raw.slice(0, nul);
|
|
463
475
|
}
|
|
464
|
-
function assertCapacity(value, label) {
|
|
465
|
-
if (value === undefined) {
|
|
466
|
-
throw new Error(`schema: ${label} is required: Compact types are fixed-size`);
|
|
467
|
-
}
|
|
468
|
-
if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
|
|
469
|
-
throw new Error(`schema: ${label} must be a positive integer`);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
/**
|
|
473
|
-
* Classify a fixed-width respond type into its Compact carrier, validating
|
|
474
|
-
* the respond-side vocabulary in one place: both the schema check
|
|
475
|
-
* ({@link normalizeRespondSchema}) and the descriptor build
|
|
476
|
-
* ({@link respondSchemaToCompactType}) call this, so the grammar cannot
|
|
477
|
-
* drift between them. Respond-side uint widths are restricted to whole-byte
|
|
478
|
-
* widths (multiples of 8 from 8 to 248, packing to bits / 8 bytes), plus
|
|
479
|
-
* uint256 which maps to Field. Throws with the offending field named.
|
|
480
|
-
*
|
|
481
|
-
* @param type - The fixed-width ABI type name.
|
|
482
|
-
* @param fieldName - The field the type belongs to, used in error messages.
|
|
483
|
-
* @returns The Compact descriptor that carries the type.
|
|
484
|
-
* @throws {Error} If the type has no respond-side Compact carrier.
|
|
485
|
-
*/
|
|
486
|
-
function classifyFixedType(type, fieldName) {
|
|
487
|
-
if (type === "bool")
|
|
488
|
-
return { kind: "boolean" };
|
|
489
|
-
if (type === "field" || type === "uint256" || type === "address") {
|
|
490
|
-
return { kind: "field" };
|
|
491
|
-
}
|
|
492
|
-
if (/^int\d+$/.test(type)) {
|
|
493
|
-
throw new Error(`schema: '${fieldName}' (${type}) is unsupported: Compact has no signed integers`);
|
|
494
|
-
}
|
|
495
|
-
const uintMatch = /^uint([1-9]\d*)$/.exec(type);
|
|
496
|
-
if (uintMatch) {
|
|
497
|
-
const bits = Number(uintMatch[1]);
|
|
498
|
-
const wholeByteWidth = bits >= 8 && bits <= MAX_UINT_BITS && bits % 8 === 0;
|
|
499
|
-
if (!wholeByteWidth) {
|
|
500
|
-
throw new Error(`schema: '${fieldName}' (${type}) has no respond carrier: uint widths ` +
|
|
501
|
-
`must be multiples of 8 from 8 to ${String(MAX_UINT_BITS)}, or uint256 (maps to Field)`);
|
|
502
|
-
}
|
|
503
|
-
return { kind: "uint", bits };
|
|
504
|
-
}
|
|
505
|
-
const bytesMatch = /^bytes([1-9]\d*)$/.exec(type);
|
|
506
|
-
if (bytesMatch) {
|
|
507
|
-
const n = Number(bytesMatch[1]);
|
|
508
|
-
if (n < 1 || n > 32) {
|
|
509
|
-
throw new Error(`schema: '${fieldName}' (${type}) is not a valid bytesN type`);
|
|
510
|
-
}
|
|
511
|
-
return { kind: "bytes", length: n };
|
|
512
|
-
}
|
|
513
|
-
throw new Error(`schema: '${fieldName}' has unsupported type '${type}'`);
|
|
514
|
-
}
|
|
515
|
-
// ---------------------------------------------------------------------------
|
|
516
|
-
// Schema -> CompactType descriptor + value coercion
|
|
517
|
-
// ---------------------------------------------------------------------------
|
|
518
|
-
function respondSchemaToCompactType(fields) {
|
|
519
|
-
return {
|
|
520
|
-
kind: "struct",
|
|
521
|
-
fields: fields.map((field) => {
|
|
522
|
-
if (isAbiDynamicField(field)) {
|
|
523
|
-
return {
|
|
524
|
-
name: field.name,
|
|
525
|
-
type: {
|
|
526
|
-
kind: "struct",
|
|
527
|
-
fields: [
|
|
528
|
-
{ name: "len", type: { kind: "uint", bits: DYN_LEN_BYTES * 8 } },
|
|
529
|
-
{ name: "data", type: { kind: "bytes", length: field.maxBytes } },
|
|
530
|
-
],
|
|
531
|
-
},
|
|
532
|
-
};
|
|
533
|
-
}
|
|
534
|
-
if (isAbiArrayField(field)) {
|
|
535
|
-
return {
|
|
536
|
-
name: field.name,
|
|
537
|
-
type: {
|
|
538
|
-
kind: "struct",
|
|
539
|
-
fields: [
|
|
540
|
-
{ name: "len", type: { kind: "uint", bits: DYN_LEN_BYTES * 8 } },
|
|
541
|
-
{
|
|
542
|
-
name: "items",
|
|
543
|
-
type: {
|
|
544
|
-
kind: "vector",
|
|
545
|
-
length: field.maxItems,
|
|
546
|
-
element: classifyFixedType(field.type.slice(0, -2), field.name),
|
|
547
|
-
},
|
|
548
|
-
},
|
|
549
|
-
],
|
|
550
|
-
},
|
|
551
|
-
};
|
|
552
|
-
}
|
|
553
|
-
return { name: field.name, type: classifyFixedType(field.type, field.name) };
|
|
554
|
-
}),
|
|
555
|
-
};
|
|
556
|
-
}
|
|
557
|
-
function toCompactValue(value, field) {
|
|
558
|
-
const { name } = field;
|
|
559
|
-
if (isAbiDynamicField(field)) {
|
|
560
|
-
const payload = field.type === "string"
|
|
561
|
-
? new TextEncoder().encode(asString(value, name))
|
|
562
|
-
: asBytes(value, name);
|
|
563
|
-
if (payload.length > field.maxBytes) {
|
|
564
|
-
throw new Error(`'${name}': payload is ${String(payload.length)} bytes, maxBytes is ${String(field.maxBytes)}`);
|
|
565
|
-
}
|
|
566
|
-
const data = new Uint8Array(field.maxBytes);
|
|
567
|
-
data.set(payload);
|
|
568
|
-
return { len: BigInt(payload.length), data };
|
|
569
|
-
}
|
|
570
|
-
if (isAbiArrayField(field)) {
|
|
571
|
-
if (!Array.isArray(value)) {
|
|
572
|
-
throw new Error(`'${name}' (${field.type}) expects an array`);
|
|
573
|
-
}
|
|
574
|
-
if (value.length > field.maxItems) {
|
|
575
|
-
throw new Error(`'${name}': ${String(value.length)} elements, maxItems is ${String(field.maxItems)}`);
|
|
576
|
-
}
|
|
577
|
-
const elementType = field.type.slice(0, -2);
|
|
578
|
-
const items = value.map((element, i) => fixedCompactValue(element, elementType, `${name}[${String(i)}]`));
|
|
579
|
-
// Unused capacity encodes as zero values of the element type.
|
|
580
|
-
while (items.length < field.maxItems)
|
|
581
|
-
items.push(zeroOf(elementType));
|
|
582
|
-
return { len: BigInt(value.length), items };
|
|
583
|
-
}
|
|
584
|
-
return fixedCompactValue(value, field.type, name);
|
|
585
|
-
}
|
|
586
|
-
function fixedCompactValue(value, type, label) {
|
|
587
|
-
if (type === "bool") {
|
|
588
|
-
if (typeof value !== "boolean") {
|
|
589
|
-
throw new Error(`'${label}' (bool) expects a boolean`);
|
|
590
|
-
}
|
|
591
|
-
return value;
|
|
592
|
-
}
|
|
593
|
-
if (/^bytes\d+$/.test(type)) {
|
|
594
|
-
const raw = asBytes(value, label);
|
|
595
|
-
const expected = Number(type.slice(5));
|
|
596
|
-
if (raw.length !== expected) {
|
|
597
|
-
throw new Error(`'${label}' (${type}) expects exactly ${String(expected)} bytes, got ${String(raw.length)}`);
|
|
598
|
-
}
|
|
599
|
-
return raw;
|
|
600
|
-
}
|
|
601
|
-
// Numeric carriers: uintN, uint256/field and address.
|
|
602
|
-
const n = asBigint(value, label);
|
|
603
|
-
if (type === "address" && n >= ADDRESS_BOUND) {
|
|
604
|
-
throw new Error(`'${label}': value ${String(n)} exceeds an address`);
|
|
605
|
-
}
|
|
606
|
-
// Uint width and Field modulus bounds are enforced by @sig-net/midnight-serde.
|
|
607
|
-
return n;
|
|
608
|
-
}
|
|
609
|
-
function zeroOf(type) {
|
|
610
|
-
if (type === "bool")
|
|
611
|
-
return false;
|
|
612
|
-
if (/^bytes\d+$/.test(type))
|
|
613
|
-
return new Uint8Array(Number(type.slice(5)));
|
|
614
|
-
return 0n;
|
|
615
|
-
}
|
|
616
|
-
// ---------------------------------------------------------------------------
|
|
617
|
-
// Value-form coercions
|
|
618
|
-
// ---------------------------------------------------------------------------
|
|
619
|
-
function asBigint(value, label) {
|
|
620
|
-
if (typeof value === "bigint")
|
|
621
|
-
return value;
|
|
622
|
-
if (typeof value === "number" && Number.isSafeInteger(value))
|
|
623
|
-
return BigInt(value);
|
|
624
|
-
if (typeof value === "string") {
|
|
625
|
-
try {
|
|
626
|
-
return BigInt(value);
|
|
627
|
-
}
|
|
628
|
-
catch {
|
|
629
|
-
throw new Error(`'${label}': cannot parse '${value}' as an integer`);
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
throw new Error(`'${label}': expected an integer-like value`);
|
|
633
|
-
}
|
|
634
|
-
function asBytes(value, label) {
|
|
635
|
-
if (value instanceof Uint8Array)
|
|
636
|
-
return value;
|
|
637
|
-
if (typeof value === "string") {
|
|
638
|
-
try {
|
|
639
|
-
return ethers.getBytes(value);
|
|
640
|
-
}
|
|
641
|
-
catch {
|
|
642
|
-
throw new Error(`'${label}': not a valid 0x hex byte string: "${value}"`);
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
throw new Error(`'${label}': expected bytes (Uint8Array or hex string)`);
|
|
646
|
-
}
|
|
647
|
-
function asString(value, label) {
|
|
648
|
-
if (typeof value !== "string") {
|
|
649
|
-
throw new Error(`'${label}': expected a string`);
|
|
650
|
-
}
|
|
651
|
-
return value;
|
|
652
|
-
}
|
|
653
476
|
//# sourceMappingURL=abi-serde.js.map
|