@sig-net/midnight 0.23.0 → 0.24.0-rc.10
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 +52 -24
- package/dist/abi-serde.d.ts +198 -57
- package/dist/abi-serde.d.ts.map +1 -1
- package/dist/abi-serde.js +478 -367
- package/dist/abi-serde.js.map +1 -1
- package/dist/compact-descriptors.d.ts +25 -8
- package/dist/compact-descriptors.d.ts.map +1 -1
- package/dist/compact-descriptors.js +42 -25
- package/dist/compact-descriptors.js.map +1 -1
- package/dist/constants.d.ts +3 -22
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +3 -25
- package/dist/constants.js.map +1 -1
- package/dist/ecdsa-attestation.d.ts +44 -14
- package/dist/ecdsa-attestation.d.ts.map +1 -1
- package/dist/ecdsa-attestation.js +50 -15
- package/dist/ecdsa-attestation.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/managed/compiler/contract-info.json +1808 -48
- package/dist/managed/compiler/contract-manifest.json +8 -8
- package/dist/managed/contract/index.d.ts +424 -5
- package/dist/managed/contract/index.js +1311 -171
- package/dist/managed/contract/index.js.map +1 -1
- package/dist/mpc-output-cache.js +2 -2
- package/dist/mpc-output-cache.js.map +1 -1
- package/dist/signature-requests-state-reader.d.ts +6 -13
- package/dist/signature-requests-state-reader.d.ts.map +1 -1
- package/dist/signature-requests-state-reader.js +10 -16
- package/dist/signature-requests-state-reader.js.map +1 -1
- package/dist/signet-contract-events.d.ts +52 -12
- package/dist/signet-contract-events.d.ts.map +1 -1
- package/dist/signet-contract-events.js +89 -34
- package/dist/signet-contract-events.js.map +1 -1
- package/dist/signet-evtype2tx-record-decoding.d.ts.map +1 -1
- package/dist/signet-evtype2tx-record-decoding.js +7 -6
- package/dist/signet-evtype2tx-record-decoding.js.map +1 -1
- package/dist/signet-evtype2tx-requests.d.ts +17 -12
- package/dist/signet-evtype2tx-requests.d.ts.map +1 -1
- package/dist/signet-evtype2tx-requests.js +92 -16
- package/dist/signet-evtype2tx-requests.js.map +1 -1
- package/dist/signet-request-id.d.ts +5 -2
- package/dist/signet-request-id.d.ts.map +1 -1
- package/dist/signet-request-id.js +27 -9
- package/dist/signet-request-id.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 +8 -7
- package/dist/signet-request-response-reader.js.map +1 -1
- package/dist/signet-requests.d.ts +48 -19
- package/dist/signet-requests.d.ts.map +1 -1
- package/dist/signet-requests.js +19 -8
- package/dist/signet-requests.js.map +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +3 -8
- package/dist/testing.js.map +1 -1
- package/package.json +2 -2
- package/src/Signet.compact +299 -101
- package/src/circuits.compact +92 -12
package/src/Signet.compact
CHANGED
|
@@ -18,16 +18,22 @@
|
|
|
18
18
|
// import "@sig-net/midnight/src/Signet";
|
|
19
19
|
//
|
|
20
20
|
// Key types at a glance (each declaration below carries the full contract):
|
|
21
|
-
// -
|
|
22
|
-
// - RequestId: 32-byte id
|
|
23
|
-
// -
|
|
21
|
+
// - SignBidirectionalEventV1: a client contract's structured request for a signature on a transaction.
|
|
22
|
+
// - RequestId: 32-byte id derived by calculateRequestIdV1.
|
|
23
|
+
// - SignBidirectionalEventMapV1: the RequestId -> SignBidirectionalEventV1 map a client contract
|
|
24
24
|
// declares in its own ledger for the MPC to read requests from.
|
|
25
25
|
// - SignetSigner: the contract interface client contracts call to notify the Signet singleton.
|
|
26
|
-
// - SignBidirectionalEventNotification: the
|
|
27
|
-
//
|
|
28
|
-
// -
|
|
26
|
+
// - SignBidirectionalEventNotification: the notification delivered through that call, versioned by
|
|
27
|
+
// the version byte it carries.
|
|
28
|
+
// - SignatureRespondedEventV1: the MPC's posted signature answering a request.
|
|
29
|
+
// - RespondBidirectionalEventV1: the MPC's posted attestation that the foreign chain executed the
|
|
29
30
|
// signed transaction.
|
|
30
31
|
//
|
|
32
|
+
// The V1 suffix marks the wire and hash-domain artefacts of protocol version 1 (the structs the
|
|
33
|
+
// ledger stores and the singleton emits, and the circuits that hash or verify them). Vocabulary
|
|
34
|
+
// shared across versions (RequestId, OutputKind, Signature, the enums, the transaction parameter
|
|
35
|
+
// structs, which TxParamType tags) and the version-tagged notification carry no suffix.
|
|
36
|
+
//
|
|
31
37
|
// These key types are best understood within the sign bidirectional flow that they enable:
|
|
32
38
|
// 1. Client contract stores a SignBidirectionalEvent in a SignBidirectionalEventMap in its own ledger
|
|
33
39
|
// (against the calculated RequestId) and requests a signature from the MPC via a cross contract call
|
|
@@ -42,8 +48,8 @@
|
|
|
42
48
|
// the requested signing key. Then it broadcasts the signed txn to the destination chain.
|
|
43
49
|
// 4. MPC observes txn execution on destination chain and posts a RespondBidirectionalEvent attestation
|
|
44
50
|
// through the Signet singleton, which emits it as a contract event beside the request id.
|
|
45
|
-
// The client polls its request's posts and takes the first one that verifies over the request id
|
|
46
|
-
// and output. The client obtains the execution output off chain (for EVM: the mined
|
|
51
|
+
// The client polls its request's posts and takes the first one that verifies over the request id,
|
|
52
|
+
// block height, output kind and output. The client obtains the execution output off chain (for EVM: the mined
|
|
47
53
|
// call's return data via debug_traceTransaction, or the exact attested bytes from the
|
|
48
54
|
// MPC's output cache) and hands output plus attestation to its verify circuit. See
|
|
49
55
|
// "Output Recovery" in the repository README.
|
|
@@ -59,6 +65,16 @@
|
|
|
59
65
|
module Signet {
|
|
60
66
|
import CompactStandardLibrary;
|
|
61
67
|
|
|
68
|
+
// Hash-domain indices are protocol u8 values. Append variants only.
|
|
69
|
+
export enum HashDomain {
|
|
70
|
+
requestId,
|
|
71
|
+
attestationDigest,
|
|
72
|
+
evmType2TxHeader,
|
|
73
|
+
evmType2TxWord,
|
|
74
|
+
evmType2TxAccessEntry,
|
|
75
|
+
evmType2TxStorageKey,
|
|
76
|
+
}
|
|
77
|
+
|
|
62
78
|
// ---- SignBidirectionalEvent ------------------------------------------------------------------------
|
|
63
79
|
|
|
64
80
|
// Structured request for a signature on a transaction. Generically configured by client contracts
|
|
@@ -66,49 +82,57 @@ module Signet {
|
|
|
66
82
|
//
|
|
67
83
|
// KEY: texual order of the fields in this structure must remain fixed as is as their order
|
|
68
84
|
// is relied upon for consistent hashing and decoding from ledger state.
|
|
69
|
-
export struct
|
|
70
|
-
//
|
|
71
|
-
// Set with kernel.self().
|
|
72
|
-
sender: ContractAddress;
|
|
73
|
-
|
|
74
|
-
// Optional nonce, set by client contracts to make otherwise-identical requests unique.
|
|
75
|
-
requestNonce: Uint<64>;
|
|
85
|
+
export struct SignBidirectionalEventV1<TxParams, #LenOutputDeserialization, #LenRespondSerialization> {
|
|
86
|
+
// ---- RequestIdPreimageV1 fields: hashed into the request id, in this order ----
|
|
76
87
|
|
|
77
88
|
// MPC Key Derivation Parameters.
|
|
78
89
|
// Every key the MPC signs with is scoped by the requesting contract:
|
|
79
90
|
// derivedSigningKey = f(mpcRootKey[keyVersion], caip2ChainId, sender, hex::encode(path))
|
|
80
91
|
// where caip2ChainId is the CAIP-2 id of the ORIGINATING chain (the Midnight
|
|
81
92
|
// variant for requests made from Midnight contracts), not this record's target
|
|
82
|
-
//
|
|
93
|
+
// executionDest. The path is 32 opaque bytes of the client contract's choosing (e.g. a
|
|
83
94
|
// fixed literal for a contract-owned account or a hash of a caller's secret for
|
|
84
95
|
// per-user accounts): no format is enforced, any 32 bytes are valid.
|
|
85
96
|
// CRITICAL: the MPC renders the path as hex::encode(path) for key derivation (lowercase hex
|
|
86
97
|
// of the full 32 bytes, no trimming, no 0x prefix). The sender is always part
|
|
87
98
|
// of the derivation, so no contract can reach another contract's derived keys.
|
|
88
|
-
keyVersion: Uint<8>;
|
|
89
|
-
|
|
99
|
+
keyVersion: Uint<8>; // MPC root-key version to derive from. Currently 1.
|
|
100
|
+
sender: ContractAddress; // Address of the client contract that stores and notifies of this
|
|
101
|
+
// request. Set with kernel.self().
|
|
102
|
+
path: Bytes<32>; // Key derivation path: 32 opaque bytes, client contract's choice.
|
|
90
103
|
|
|
91
104
|
// MPC Parameters.
|
|
92
|
-
algo: MPCSignatureAlgorithm;
|
|
93
|
-
dest: MPCDestination; // Destination field reserved for future use. Set to unused.
|
|
94
|
-
params: Bytes<64>; // Extra MPC parameters reserved for future use. Zero-fill.
|
|
105
|
+
algo: MPCSignatureAlgorithm; // Signature algorithm scheme, e.g. ecdsa
|
|
95
106
|
|
|
96
107
|
// Specific destination chain Transaction parameters.
|
|
97
|
-
txParamType: TxParamType;
|
|
98
|
-
txParams: TxParams;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
//
|
|
110
|
-
|
|
108
|
+
txParamType: TxParamType; // Off-chain tag indicating what type txParams is set to.
|
|
109
|
+
txParams: TxParams; // The generic transaction-param payload
|
|
110
|
+
executionDest: Bytes<32>; // The MPC's routing key for the target chain, in CAIP-2 form
|
|
111
|
+
// (https://chainagnostic.org/CAIPs/caip-2). An Ethereum target
|
|
112
|
+
// (mainnet, Sepolia or a local anvil) uses ethereumCaip2Id():
|
|
113
|
+
// the network itself is txParams' chainId.
|
|
114
|
+
|
|
115
|
+
// ---- Protocol-only fields: outside the request id ----
|
|
116
|
+
|
|
117
|
+
signatureDest: MPCDestination; // Destination field reserved for future use. Set to unused.
|
|
118
|
+
params: Bytes<64>; // Extra MPC parameters reserved for future use. Zero-fill.
|
|
119
|
+
|
|
120
|
+
// ---- Serialisation Schemas ----
|
|
121
|
+
|
|
122
|
+
// outputDeserializationSchema tells the MPC how to decode the destination chain's
|
|
123
|
+
// execution output (destination chain -> MPC).
|
|
124
|
+
// The value is execution chain specific and sized per client contract:
|
|
125
|
+
// - Ethereum: an ABI JSON schema string (see https://docs.soliditylang.org/en/v0.8.30/abi-spec.html#json)
|
|
126
|
+
// in canonical form: `[{"name":"<identifier>","type":"<abi type>"},...]` with no
|
|
127
|
+
// whitespace, NUL-padded to LenOutputDeserialization (see the README's "Output
|
|
128
|
+
// Recovery and Serialisation"). The MPC drops a request whose bytes differ.
|
|
111
129
|
outputDeserializationSchema: Bytes<LenOutputDeserialization>;
|
|
130
|
+
|
|
131
|
+
// respondSerializationSchema is reserved for future use and IGNORED by the MPC. The
|
|
132
|
+
// MPC serialises the decoded output with Borsh against a schema it derives from the
|
|
133
|
+
// output types itself (see the README's "Output Recovery and Serialisation"). Leave it
|
|
134
|
+
// empty: constructSignBidirectionalEventV1 pins it to Bytes<0>, and a request built by
|
|
135
|
+
// hand must do the same.
|
|
112
136
|
respondSerializationSchema: Bytes<LenRespondSerialization>;
|
|
113
137
|
}
|
|
114
138
|
|
|
@@ -133,61 +157,74 @@ module Signet {
|
|
|
133
157
|
reserved, // Reserved for future use. Exists to keep enum >= 2 (proof server bug).
|
|
134
158
|
}
|
|
135
159
|
|
|
136
|
-
// Construction helper for the canonical Signet
|
|
137
|
-
export pure circuit
|
|
138
|
-
sender: ContractAddress,
|
|
139
|
-
requestNonce: Uint<64>,
|
|
160
|
+
// Construction helper for the canonical Signet SignBidirectionalEventV1.
|
|
161
|
+
export pure circuit constructSignBidirectionalEventV1<TxParams, #LenOutputDeserialization>(
|
|
140
162
|
keyVersion: Uint<8>,
|
|
163
|
+
sender: ContractAddress,
|
|
141
164
|
path: Bytes<32>,
|
|
142
165
|
algo: MPCSignatureAlgorithm,
|
|
143
|
-
dest: MPCDestination,
|
|
144
|
-
params: Bytes<64>,
|
|
145
166
|
txParamType: TxParamType,
|
|
146
167
|
txParams: TxParams,
|
|
147
|
-
|
|
168
|
+
executionDest: Bytes<32>,
|
|
169
|
+
signatureDest: MPCDestination,
|
|
170
|
+
params: Bytes<64>,
|
|
148
171
|
outputDeserializationSchema: Bytes<LenOutputDeserialization>,
|
|
149
|
-
|
|
150
|
-
): SignBidirectionalEvent<TxParams, LenOutputDeserialization, LenRespondSerialization> {
|
|
172
|
+
): SignBidirectionalEventV1<TxParams, LenOutputDeserialization, 0> {
|
|
151
173
|
assert(keyVersion >= 1, "keyVersion must be >= 1");
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
174
|
+
assert(signatureDest == MPCDestination.unused, "signatureDest must be unused");
|
|
175
|
+
assert(params == default<Bytes<64>>, "params must be zero");
|
|
176
|
+
// The protocol reserves the "midnight response key" path for the MPC's
|
|
177
|
+
// attestation key of this contract (see RespondBidirectionalEventV1), and the
|
|
178
|
+
// MPC refuses a request that names it.
|
|
179
|
+
assert(path != pad(32, "midnight response key"), "path is reserved for the MPC response key");
|
|
180
|
+
|
|
181
|
+
// Shorthand struct fields bind positionally: this list follows the declaration order.
|
|
182
|
+
return SignBidirectionalEventV1<TxParams, LenOutputDeserialization, 0> {
|
|
156
183
|
keyVersion,
|
|
184
|
+
sender,
|
|
157
185
|
path,
|
|
158
186
|
algo,
|
|
159
|
-
dest,
|
|
160
|
-
params,
|
|
161
187
|
txParamType,
|
|
162
188
|
txParams,
|
|
163
|
-
|
|
189
|
+
executionDest,
|
|
190
|
+
signatureDest,
|
|
191
|
+
params,
|
|
164
192
|
outputDeserializationSchema,
|
|
165
|
-
|
|
193
|
+
default<Bytes<0>>,
|
|
166
194
|
};
|
|
167
195
|
}
|
|
168
196
|
|
|
169
197
|
// ---- SignBidirectionalEvent RequestId --------------------------------------------------------------
|
|
170
198
|
|
|
171
|
-
// 32
|
|
172
|
-
// SignBidirectionalEvent record (see calculateRequestId).
|
|
199
|
+
// 32-byte chain-agnostic nominal id derived by calculateRequestIdV1.
|
|
173
200
|
export new type RequestId = Bytes<32>;
|
|
174
201
|
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
202
|
+
// The fields of a SignBidirectionalEventV1 that name one execution: the signing key
|
|
203
|
+
// (keyVersion, path, algo), the requesting contract, the transaction and its execution
|
|
204
|
+
// destination. Fields the execution does not depend on (the serialisation schemas and
|
|
205
|
+
// the reserved MPC parameters signatureDest and params) stay out. The transaction
|
|
206
|
+
// enters as a digest computed per TxParamType over its USED entries only
|
|
207
|
+
// (calculateEvmType2TxParamsDigestV1 for evmType2), so one execution has one id
|
|
208
|
+
// whatever capacities the requester compiled with and whatever sits in unused slots.
|
|
179
209
|
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
210
|
+
// KEY: the field order follows SignBidirectionalEventV1's and is relied upon for consistent
|
|
211
|
+
// hashing. The TS twin (requestIdPreimageDescriptor in signet-requests.ts) mirrors it
|
|
212
|
+
// field by field.
|
|
213
|
+
export struct RequestIdPreimageV1 {
|
|
214
|
+
keyVersion: Uint<8>;
|
|
215
|
+
sender: ContractAddress;
|
|
216
|
+
path: Bytes<32>;
|
|
217
|
+
algo: MPCSignatureAlgorithm;
|
|
218
|
+
txParamType: TxParamType;
|
|
219
|
+
txParamsDigest: Bytes<32>;
|
|
220
|
+
executionDest: Bytes<32>;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Off-chain recomputation must prefix the preimage with HashDomain.requestId
|
|
224
|
+
// and reproduce its field-aligned layout exactly. A ledger-era hash change
|
|
225
|
+
// requires draining pending requests before the fork.
|
|
226
|
+
export pure circuit calculateRequestIdV1(preimage: RequestIdPreimageV1): RequestId {
|
|
227
|
+
return upgradeFromTransient(transientHash<[HashDomain, RequestIdPreimageV1]>([HashDomain.requestId, preimage])) as RequestId;
|
|
191
228
|
}
|
|
192
229
|
|
|
193
230
|
// ---- Client Contract: SignBidirectionalEvent Map ---------------------------------------------------
|
|
@@ -196,7 +233,7 @@ module Signet {
|
|
|
196
233
|
// in their ledger. Client contract tells MPC field location of this map when making a cross
|
|
197
234
|
// contract call, via SignetSigner, to the Signet singleton. The MPC looks here to find
|
|
198
235
|
// SignBidirectionalEvents.
|
|
199
|
-
export new type
|
|
236
|
+
export new type SignBidirectionalEventMapV1<TxParams, #LenOutputDeserialization> = Map<RequestId, SignBidirectionalEventV1<TxParams, LenOutputDeserialization, 0>>;
|
|
200
237
|
|
|
201
238
|
// ---- Cross Contract SignBidirectionalEventNotification ---------------------------------------------
|
|
202
239
|
|
|
@@ -205,7 +242,7 @@ module Signet {
|
|
|
205
242
|
export contract SignetSigner {
|
|
206
243
|
// Called by client contracts to notify the MPC of a SignBidirectionalEvent.
|
|
207
244
|
circuit signBidirectional(
|
|
208
|
-
// Canonical id of the SignBidirectionalEvent (
|
|
245
|
+
// Canonical id of the SignBidirectionalEvent (calculateEvmType2RequestIdV1 for evmType2).
|
|
209
246
|
// Disclosed in the emitted notification event: the MPC looks this id up in the
|
|
210
247
|
// caller's own request map.
|
|
211
248
|
requestId: RequestId,
|
|
@@ -228,7 +265,7 @@ module Signet {
|
|
|
228
265
|
// Construction helper for the V1 SignBidirectionalEventNotification.
|
|
229
266
|
//
|
|
230
267
|
// The payload is a pointer for the MPC: the contract that holds the
|
|
231
|
-
//
|
|
268
|
+
// SignBidirectionalEventMapV1 (callerAddress) and the position of that map in
|
|
232
269
|
// the contract's raw ledger state tree (requestsPathDepth + requestsPath).
|
|
233
270
|
//
|
|
234
271
|
// Read the path from your contract's compiled artifacts, never derive it by
|
|
@@ -244,7 +281,7 @@ module Signet {
|
|
|
244
281
|
// Number of meaningful entries in requestsPath: 1 for a flat (15 or fewer
|
|
245
282
|
// field) contract, one more per chunk level after that (max 4).
|
|
246
283
|
requestsPathDepth: Uint<8>,
|
|
247
|
-
// Resolved ledger-tree path of the caller's
|
|
284
|
+
// Resolved ledger-tree path of the caller's SignBidirectionalEventMapV1, from
|
|
248
285
|
// its contract-info.json "index". Entries at and beyond requestsPathDepth
|
|
249
286
|
// are zero.
|
|
250
287
|
requestsPath: Vector<4, Uint<8>>
|
|
@@ -268,10 +305,10 @@ module Signet {
|
|
|
268
305
|
y: Bytes<32>;
|
|
269
306
|
}
|
|
270
307
|
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
308
|
+
// recoveryId (0 or 1) recovers the signer with the supplied s. Converting
|
|
309
|
+
// high-s to low-s requires replacing s with n - s and flipping recoveryId,
|
|
310
|
+
// where n is the curve's group order, even when bigR is kept unchanged.
|
|
311
|
+
// Point coordinates and s are stored as BIG-endian SEC1 bytes.
|
|
275
312
|
export struct Signature {
|
|
276
313
|
bigR: AffinePoint;
|
|
277
314
|
s: Bytes<32>;
|
|
@@ -281,7 +318,10 @@ module Signet {
|
|
|
281
318
|
// Counterpart to SignBidirectionalEvent: the MPC generated signature a request asked for.
|
|
282
319
|
// Posted by the MPC through the Signet singleton for clients to extract and build a
|
|
283
320
|
// signed transaction from.
|
|
284
|
-
export struct
|
|
321
|
+
export struct SignatureRespondedEventV1 {
|
|
322
|
+
// The request this signature answers. Routing data: the singleton emits it
|
|
323
|
+
// unverified, and a reader proves the signature against the request's own key.
|
|
324
|
+
requestId: RequestId;
|
|
285
325
|
// The requested signature over the transaction the request describes.
|
|
286
326
|
signature: Signature;
|
|
287
327
|
}
|
|
@@ -289,7 +329,7 @@ module Signet {
|
|
|
289
329
|
// RespondBidirectionalEvent: the MPC's attestation of a foreign execution, the
|
|
290
330
|
// final step of the sign bidirectional flow.
|
|
291
331
|
//
|
|
292
|
-
// attestationDigest =
|
|
332
|
+
// attestationDigest = calculateSignetAttestationDigestV1(requestId, blockHeight, outputKind, serializedOutput)
|
|
293
333
|
// signature = signSECP256k1(mpcResponseKey, attestationDigest)
|
|
294
334
|
//
|
|
295
335
|
// mpcResponseKey is derived like request signing keys, with the path set to the
|
|
@@ -298,46 +338,83 @@ module Signet {
|
|
|
298
338
|
// mpcResponseKey = f(mpcRootKey[keyVersion], caip2ChainId, sender, "midnight response key")
|
|
299
339
|
//
|
|
300
340
|
// The output travels off chain and the digest is recomputed at verification
|
|
301
|
-
// time (
|
|
341
|
+
// time (verifyRespondBidirectionalEventV1). The signature is the ONLY thing that
|
|
302
342
|
// distinguishes a genuine post from garbage: the singleton emits every post
|
|
303
343
|
// unverified.
|
|
304
|
-
export struct
|
|
344
|
+
export struct RespondBidirectionalEventV1 {
|
|
345
|
+
// The request this attestation answers. Signed into the digest: a verified
|
|
346
|
+
// post settles exactly the request it names, so a client consumes this id
|
|
347
|
+
// and never one taken from elsewhere.
|
|
348
|
+
requestId: RequestId;
|
|
349
|
+
// Height of the finalised destination block holding the transaction the
|
|
350
|
+
// attestation describes, in the destination chain's own numbering (a slot
|
|
351
|
+
// on Solana). Signed into the digest.
|
|
352
|
+
blockHeight: Uint<64>;
|
|
353
|
+
// The MPC's verdict on the execution, signed into the digest.
|
|
354
|
+
outputKind: OutputKind;
|
|
355
|
+
// Byte width of the serialised output the digest commits to (0 for a failed
|
|
356
|
+
// or unviable execution), so a client holding no output yet knows which
|
|
357
|
+
// width to settle at.
|
|
358
|
+
serializedOutputLength: Uint<64>;
|
|
359
|
+
// The attestation digest the signature is over (calculateSignetAttestationDigestV1),
|
|
360
|
+
// so a reader holding no output can check the signature. The verifying
|
|
361
|
+
// circuit recomputes it from the output and never reads this copy.
|
|
362
|
+
digest: Bytes<32>;
|
|
305
363
|
// The MPC's ECDSA signature over the attestation digest.
|
|
306
364
|
signature: Signature;
|
|
307
365
|
}
|
|
308
366
|
|
|
367
|
+
// OutputKind is the MPC's verdict on a request's execution, the kind half of
|
|
368
|
+
// the protocol's outcome (kind, data). The block height the attestation
|
|
369
|
+
// commits to is the finalised destination block that settled the verdict.
|
|
370
|
+
export enum OutputKind {
|
|
371
|
+
executed, // finalised and succeeded: the attested output is the decoded return data, Borsh-serialised by the MPC (empty when the call returns nothing)
|
|
372
|
+
failed, // finalised and reverted: the attested output is empty (Bytes<0>)
|
|
373
|
+
unviable, // a finalised transaction carrying other bytes took the nonce: the attested output is empty (Bytes<0>)
|
|
374
|
+
}
|
|
375
|
+
|
|
309
376
|
// The 32-byte attestation digest of a remote execution output:
|
|
310
|
-
// attestationDigest = upgradeFromTransient(transientHash([requestId, serializedOutputLength, serializedOutput]))
|
|
377
|
+
// attestationDigest = upgradeFromTransient(transientHash([HashDomain.attestationDigest, requestId, blockHeight, outputKind, serializedOutputLength, serializedOutput]))
|
|
378
|
+
// blockHeight is the height of the finalised destination block holding the
|
|
379
|
+
// transaction the attestation describes, in the destination chain's own
|
|
380
|
+
// numbering (a slot on Solana).
|
|
311
381
|
// Off-chain code uses the TypeScript twin:
|
|
312
|
-
// import { calculateSignetAttestationDigest } from "@sig-net/midnight";
|
|
313
|
-
export pure circuit
|
|
382
|
+
// import { calculateSignetAttestationDigest } from "@sig-net/midnight/testing";
|
|
383
|
+
export pure circuit calculateSignetAttestationDigestV1<#serializedOutputLength>(
|
|
314
384
|
requestId: RequestId,
|
|
385
|
+
blockHeight: Uint<64>,
|
|
386
|
+
outputKind: OutputKind,
|
|
315
387
|
serializedOutput: Bytes<serializedOutputLength>,
|
|
316
388
|
): Bytes<32> {
|
|
317
|
-
return upgradeFromTransient(transientHash<[RequestId, Uint<64>, Bytes<serializedOutputLength>]>([requestId, serializedOutputLength, serializedOutput]));
|
|
389
|
+
return upgradeFromTransient(transientHash<[HashDomain, RequestId, Uint<64>, OutputKind, Uint<64>, Bytes<serializedOutputLength>]>([HashDomain.attestationDigest, requestId, blockHeight, outputKind, serializedOutputLength, serializedOutput]));
|
|
318
390
|
}
|
|
319
391
|
|
|
320
392
|
// Verifies a RespondBidirectionalEvent against its execution output and MPC
|
|
321
|
-
// response key: recomputes the
|
|
322
|
-
//
|
|
323
|
-
//
|
|
393
|
+
// response key: recomputes the digest over the request id, block height,
|
|
394
|
+
// output kind, output length and caller's output, then verifies its signature. Client
|
|
395
|
+
// contracts run this before consuming outputs, as the singleton emits
|
|
396
|
+
// responses unverified. The caller supplies
|
|
324
397
|
// serializedOutput at its exact unpadded width, and the event record with
|
|
325
398
|
// signature.bigR.x and signature.s byte-reversed into LITTLE-endian: the
|
|
326
399
|
// scalar casts below read little-endian bytes, and the wire/ledger records
|
|
327
400
|
// are big-endian, so the reversal happens off-chain where it costs nothing
|
|
328
401
|
// (the TS SDK's respondBidirectionalEventToCircuitInput does it). A wrong
|
|
329
|
-
// byte order fails verification, never a false accept.
|
|
330
|
-
//
|
|
402
|
+
// byte order fails verification, never a false accept. Of the event, requestId,
|
|
403
|
+
// blockHeight, outputKind, signature.bigR.x and signature.s enter verification:
|
|
404
|
+
// serializedOutputLength and digest serve off-chain readers. The digest binds
|
|
405
|
+
// the event's requestId, so a client MUST consume that request and no other. An s
|
|
406
|
+
// that reduces to 0 mod the curve order aborts the
|
|
331
407
|
// circuit rather than returning false, so sift candidate posts off chain
|
|
332
408
|
// first (verifyRespondBidirectionalSignature).
|
|
333
|
-
export pure circuit
|
|
334
|
-
requestId: RequestId,
|
|
409
|
+
export pure circuit verifyRespondBidirectionalEventV1<#serializedOutputLength>(
|
|
335
410
|
serializedOutput: Bytes<serializedOutputLength>,
|
|
336
|
-
respondBidirectionalEvent:
|
|
411
|
+
respondBidirectionalEvent: RespondBidirectionalEventV1,
|
|
337
412
|
mpcResponseKey: Secp256k1Point
|
|
338
413
|
): Boolean {
|
|
339
|
-
const digest =
|
|
340
|
-
requestId,
|
|
414
|
+
const digest = calculateSignetAttestationDigestV1<serializedOutputLength>(
|
|
415
|
+
respondBidirectionalEvent.requestId,
|
|
416
|
+
respondBidirectionalEvent.blockHeight,
|
|
417
|
+
respondBidirectionalEvent.outputKind,
|
|
341
418
|
serializedOutput,
|
|
342
419
|
);
|
|
343
420
|
|
|
@@ -371,10 +448,11 @@ module Signet {
|
|
|
371
448
|
//
|
|
372
449
|
// Every variable-length part is a compile-time CAPACITY (a generic size
|
|
373
450
|
// parameter the requesting contract fixes) plus a runtime count of how many
|
|
374
|
-
// leading slots are real. Unused capacity
|
|
375
|
-
//
|
|
376
|
-
// the
|
|
377
|
-
//
|
|
451
|
+
// leading slots are real. Unused capacity sits in the ledger record but
|
|
452
|
+
// reaches neither the request id (calculateEvmType2TxParamsDigestV1 walks
|
|
453
|
+
// the used slots only) nor the assembled transaction. Field order is the
|
|
454
|
+
// canonical EIP-1559 RLP order and the digest order: part of the wire
|
|
455
|
+
// contract, each count directly before the slot it bounds.
|
|
378
456
|
//
|
|
379
457
|
// KEY: texual order of the fields in this structure must remain fixed as is as their order
|
|
380
458
|
// is relied upon for consistent hashing and decoding from ledger state.
|
|
@@ -396,7 +474,7 @@ module Signet {
|
|
|
396
474
|
// raw 32-byte values, copied verbatim into the RLP encoding.
|
|
397
475
|
//
|
|
398
476
|
// KEY: texual order of the fields in this structure must remain fixed as is as their order
|
|
399
|
-
// is relied upon for consistent hashing and decoding from ledger state.
|
|
477
|
+
// is relied upon for consistent hashing and decoding from ledger state.
|
|
400
478
|
export struct EvmAccessListEntry<#maxStorageKeys> {
|
|
401
479
|
address: Bytes<20>;
|
|
402
480
|
storageKeyCount: Uint<8>; // how many leading storageKeys are real
|
|
@@ -411,13 +489,122 @@ module Signet {
|
|
|
411
489
|
// canonical ABI words for common types.
|
|
412
490
|
//
|
|
413
491
|
// KEY: texual order of the fields in this structure must remain fixed as is as their order
|
|
414
|
-
// is relied upon for consistent hashing and decoding from ledger state.
|
|
492
|
+
// is relied upon for consistent hashing and decoding from ledger state.
|
|
415
493
|
export struct EvmCalldata<#maxWords> {
|
|
416
494
|
selector: Bytes<4>; // the literal first 4 calldata bytes
|
|
417
495
|
noWords: Uint<16>; // No. of used words in the words list
|
|
418
496
|
words: Vector<maxWords, Bytes<32>>; // canonical ABI words (big-endian)
|
|
419
497
|
}
|
|
420
498
|
|
|
499
|
+
// Digest of an EvmType2TxParams over the bytes that reach the signed transaction:
|
|
500
|
+
// the scalar fields, then the calldata words, access-list entries and storage keys
|
|
501
|
+
// up to their declared counts, each count hashed beside its entries so the encoding
|
|
502
|
+
// commits to its length. Slots past a count are skipped, and an absent calldata
|
|
503
|
+
// contributes only its absence, so one transaction has one digest whatever
|
|
504
|
+
// capacities the requester compiled its struct with and whatever bytes sit in
|
|
505
|
+
// unused slots. The counts must fit their capacities. Off-chain code uses the
|
|
506
|
+
// TypeScript twin calculateEvmType2TxParamsDigest in @sig-net/midnight.
|
|
507
|
+
//
|
|
508
|
+
// The slot walks are folds carrying the slot index and a running hash: a `for`
|
|
509
|
+
// over `0..n` has no index type at n = 0, and a zero capacity is the common case.
|
|
510
|
+
export pure circuit calculateEvmType2TxParamsDigestV1<#maxCalldataWords, #maxAccessListEntries, #maxStorageKeysPerEntry>(
|
|
511
|
+
txParams: EvmType2TxParams<maxCalldataWords, maxAccessListEntries, maxStorageKeysPerEntry>
|
|
512
|
+
): Bytes<32> {
|
|
513
|
+
// An absent calldata contributes is_some = false, a zero selector and a zero word count.
|
|
514
|
+
const calldata = txParams.calldata;
|
|
515
|
+
const noWords = calldata.is_some ? calldata.value.noWords : 0 as Uint<16>;
|
|
516
|
+
const selector = calldata.is_some ? calldata.value.selector : default<Bytes<4>>;
|
|
517
|
+
|
|
518
|
+
assert(noWords <= maxCalldataWords, "calldata noWords exceeds capacity");
|
|
519
|
+
assert(txParams.accessListEntryCount <= maxAccessListEntries, "accessListEntryCount exceeds capacity");
|
|
520
|
+
|
|
521
|
+
// For every used access-list entry, confirm its storageKeyCount fits
|
|
522
|
+
// maxStorageKeysPerEntry. A Compact loop needs a compile-time bound, so the walk
|
|
523
|
+
// is a fold over every slot carrying [slot index, all fit so far], and the index
|
|
524
|
+
// picks out the used entries.
|
|
525
|
+
const keyCountsFit = fold(
|
|
526
|
+
(acc: [Uint<8>, Boolean], entry: EvmAccessListEntry<maxStorageKeysPerEntry>): [Uint<8>, Boolean] =>
|
|
527
|
+
[(acc[0] + 1) as Uint<8>, acc[1] && (acc[0] >= txParams.accessListEntryCount || entry.storageKeyCount <= maxStorageKeysPerEntry)],
|
|
528
|
+
[0 as Uint<8>, true],
|
|
529
|
+
txParams.accessList
|
|
530
|
+
);
|
|
531
|
+
assert(keyCountsFit[1], "a used entry's storageKeyCount exceeds capacity");
|
|
532
|
+
|
|
533
|
+
// The scalar fields and the counts, hashed as one field-aligned tuple.
|
|
534
|
+
const head = transientHash<[HashDomain, Uint<64>, Uint<64>, Uint<128>, Uint<128>, Uint<64>, Bytes<20>, Uint<128>, Boolean, Bytes<4>, Uint<16>, Uint<8>]>([
|
|
535
|
+
HashDomain.evmType2TxHeader,
|
|
536
|
+
txParams.chainId,
|
|
537
|
+
txParams.nonce,
|
|
538
|
+
txParams.maxPriorityFeePerGas,
|
|
539
|
+
txParams.maxFeePerGas,
|
|
540
|
+
txParams.gasLimit,
|
|
541
|
+
txParams.to,
|
|
542
|
+
txParams.value,
|
|
543
|
+
calldata.is_some,
|
|
544
|
+
selector,
|
|
545
|
+
noWords,
|
|
546
|
+
txParams.accessListEntryCount
|
|
547
|
+
]);
|
|
548
|
+
|
|
549
|
+
// The calldata words below noWords, chained onto the head one word at a time.
|
|
550
|
+
// calldata.value is read whether or not the calldata is present: with noWords
|
|
551
|
+
// at zero every step passes the head through.
|
|
552
|
+
const wordsScan = fold(
|
|
553
|
+
(acc: [Uint<16>, Field], word: Bytes<32>): [Uint<16>, Field] =>
|
|
554
|
+
[(acc[0] + 1) as Uint<16>, acc[0] < noWords ? transientHash<[HashDomain, Field, Bytes<32>]>([HashDomain.evmType2TxWord, acc[1], word]) : acc[1]],
|
|
555
|
+
[0 as Uint<16>, head],
|
|
556
|
+
calldata.value.words
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
// The access-list entries below accessListEntryCount, each chained on as its
|
|
560
|
+
// address, its key count and a digest of its storage keys below that count
|
|
561
|
+
// (the inner fold, started from a zero Field).
|
|
562
|
+
const listScan = fold(
|
|
563
|
+
(acc: [Uint<8>, Field], entry: EvmAccessListEntry<maxStorageKeysPerEntry>): [Uint<8>, Field] =>
|
|
564
|
+
[
|
|
565
|
+
(acc[0] + 1) as Uint<8>,
|
|
566
|
+
acc[0] < txParams.accessListEntryCount
|
|
567
|
+
? transientHash<[HashDomain, Field, Bytes<20>, Uint<8>, Field]>([
|
|
568
|
+
HashDomain.evmType2TxAccessEntry,
|
|
569
|
+
acc[1],
|
|
570
|
+
entry.address,
|
|
571
|
+
entry.storageKeyCount,
|
|
572
|
+
fold(
|
|
573
|
+
(keyAcc: [Uint<8>, Field], key: Bytes<32>): [Uint<8>, Field] =>
|
|
574
|
+
[(keyAcc[0] + 1) as Uint<8>, keyAcc[0] < entry.storageKeyCount ? transientHash<[HashDomain, Field, Bytes<32>]>([HashDomain.evmType2TxStorageKey, keyAcc[1], key]) : keyAcc[1]],
|
|
575
|
+
[0 as Uint<8>, 0 as Field],
|
|
576
|
+
entry.storageKeys
|
|
577
|
+
)[1]
|
|
578
|
+
])
|
|
579
|
+
: acc[1]
|
|
580
|
+
],
|
|
581
|
+
[0 as Uint<8>, wordsScan[1]],
|
|
582
|
+
txParams.accessList
|
|
583
|
+
);
|
|
584
|
+
|
|
585
|
+
// The running hash is a Field. upgradeFromTransient writes it out as Bytes<32>.
|
|
586
|
+
return upgradeFromTransient(listScan[1]);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// The request id of an EVM type-2 request: its preimage with the transaction digested
|
|
590
|
+
// by calculateEvmType2TxParamsDigestV1. Off-chain code uses the TypeScript twin:
|
|
591
|
+
// import { calculateRequestId } from "@sig-net/midnight";
|
|
592
|
+
export pure circuit calculateEvmType2RequestIdV1<#maxCalldataWords, #maxAccessListEntries, #maxStorageKeysPerEntry, #LenOutputDeserialization>(
|
|
593
|
+
request: SignBidirectionalEventV1<EvmType2TxParams<maxCalldataWords, maxAccessListEntries, maxStorageKeysPerEntry>, LenOutputDeserialization, 0>
|
|
594
|
+
): RequestId {
|
|
595
|
+
assert(request.txParamType == TxParamType.evmType2, "txParamType must tag txParams as evmType2");
|
|
596
|
+
const preimage = RequestIdPreimageV1 {
|
|
597
|
+
keyVersion: request.keyVersion,
|
|
598
|
+
sender: request.sender,
|
|
599
|
+
path: request.path,
|
|
600
|
+
algo: request.algo,
|
|
601
|
+
txParamType: request.txParamType,
|
|
602
|
+
txParamsDigest: calculateEvmType2TxParamsDigestV1<maxCalldataWords, maxAccessListEntries, maxStorageKeysPerEntry>(request.txParams),
|
|
603
|
+
executionDest: request.executionDest,
|
|
604
|
+
};
|
|
605
|
+
return calculateRequestIdV1(preimage);
|
|
606
|
+
}
|
|
607
|
+
|
|
421
608
|
// ---- ABI Calldata Word Construction Utilities ------------------------------------------------------
|
|
422
609
|
// Canonical ABI word builders and readers. Words MUST be stored in canonical
|
|
423
610
|
// ABI form (big-endian, per the ABI spec). These utilities help construct
|
|
@@ -489,6 +676,17 @@ module Signet {
|
|
|
489
676
|
return b15 as Uint<128>;
|
|
490
677
|
}
|
|
491
678
|
|
|
679
|
+
// ---- Attested output readers ------------------------------------------------------------------
|
|
680
|
+
|
|
681
|
+
// Narrow an attested uint256 (32 little-endian bytes) to Uint<128>.
|
|
682
|
+
// Aborts if the value is 2^128 or more.
|
|
683
|
+
export pure circuit checkedTruncationU128(x: Bytes<32>): Uint<128> {
|
|
684
|
+
// This check seems pointless but is important. DO NOT REMOVE.
|
|
685
|
+
const v = x as Vector<32, Uint<8>>;
|
|
686
|
+
assert(v[31] == 0, "Value exceeds Uint<128>");
|
|
687
|
+
return x as Uint<128>;
|
|
688
|
+
}
|
|
689
|
+
|
|
492
690
|
// Read a Boolean ABI word back into a Boolean. The first 31 bytes must be
|
|
493
691
|
// zero and the last byte 0 or 1: any non-canonical word is rejected.
|
|
494
692
|
export pure circuit abiWordToBool(word: Bytes<32>): Boolean {
|