@sig-net/midnight 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +20 -0
  2. package/dist/constants.d.ts +62 -0
  3. package/dist/constants.d.ts.map +1 -0
  4. package/dist/constants.js +83 -0
  5. package/dist/constants.js.map +1 -0
  6. package/dist/epsilon-derivation.d.ts +32 -0
  7. package/dist/epsilon-derivation.d.ts.map +1 -0
  8. package/dist/epsilon-derivation.js +54 -0
  9. package/dist/epsilon-derivation.js.map +1 -0
  10. package/dist/index.d.ts +26 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +30 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/managed/compiler/contract-info.json +165 -0
  15. package/dist/managed/compiler/contract-manifest.json +32 -0
  16. package/dist/managed/contract/index.d.ts +65 -0
  17. package/dist/managed/contract/index.js +611 -0
  18. package/dist/managed/contract/index.js.map +8 -0
  19. package/dist/schnorr.d.ts +100 -0
  20. package/dist/schnorr.d.ts.map +1 -0
  21. package/dist/schnorr.js +144 -0
  22. package/dist/schnorr.js.map +1 -0
  23. package/dist/signature-requests-state-reader.d.ts +74 -0
  24. package/dist/signature-requests-state-reader.d.ts.map +1 -0
  25. package/dist/signature-requests-state-reader.js +167 -0
  26. package/dist/signature-requests-state-reader.js.map +1 -0
  27. package/dist/signature-response-verification.d.ts +27 -0
  28. package/dist/signature-response-verification.d.ts.map +1 -0
  29. package/dist/signature-response-verification.js +45 -0
  30. package/dist/signature-response-verification.js.map +1 -0
  31. package/dist/signature-state-reading.d.ts +36 -0
  32. package/dist/signature-state-reading.d.ts.map +1 -0
  33. package/dist/signature-state-reading.js +62 -0
  34. package/dist/signature-state-reading.js.map +1 -0
  35. package/dist/signet-contract-state-reader.d.ts +146 -0
  36. package/dist/signet-contract-state-reader.d.ts.map +1 -0
  37. package/dist/signet-contract-state-reader.js +226 -0
  38. package/dist/signet-contract-state-reader.js.map +1 -0
  39. package/dist/signet-event-observer.d.ts +102 -0
  40. package/dist/signet-event-observer.d.ts.map +1 -0
  41. package/dist/signet-event-observer.js +127 -0
  42. package/dist/signet-event-observer.js.map +1 -0
  43. package/dist/signet-events.d.ts +175 -0
  44. package/dist/signet-events.d.ts.map +1 -0
  45. package/dist/signet-events.js +210 -0
  46. package/dist/signet-events.js.map +1 -0
  47. package/dist/signet-request-feed.d.ts +78 -0
  48. package/dist/signet-request-feed.d.ts.map +1 -0
  49. package/dist/signet-request-feed.js +116 -0
  50. package/dist/signet-request-feed.js.map +1 -0
  51. package/dist/signet-request-resolver.d.ts +58 -0
  52. package/dist/signet-request-resolver.d.ts.map +1 -0
  53. package/dist/signet-request-resolver.js +76 -0
  54. package/dist/signet-request-resolver.js.map +1 -0
  55. package/dist/signet-request-response-reader.d.ts +160 -0
  56. package/dist/signet-request-response-reader.d.ts.map +1 -0
  57. package/dist/signet-request-response-reader.js +198 -0
  58. package/dist/signet-request-response-reader.js.map +1 -0
  59. package/dist/signet-requests.d.ts +363 -0
  60. package/dist/signet-requests.d.ts.map +1 -0
  61. package/dist/signet-requests.js +497 -0
  62. package/dist/signet-requests.js.map +1 -0
  63. package/dist/signet-response-feed.d.ts +161 -0
  64. package/dist/signet-response-feed.d.ts.map +1 -0
  65. package/dist/signet-response-feed.js +234 -0
  66. package/dist/signet-response-feed.js.map +1 -0
  67. package/package.json +47 -0
  68. package/src/Schnorr.compact +63 -0
  69. package/src/Signet.compact +529 -0
  70. package/src/circuits.compact +36 -0
  71. package/src/constants.ts +97 -0
  72. package/src/epsilon-derivation.ts +62 -0
  73. package/src/index.ts +31 -0
  74. package/src/managed/compiler/contract-info.json +165 -0
  75. package/src/managed/compiler/contract-manifest.json +32 -0
  76. package/src/managed/contract/index.d.ts +65 -0
  77. package/src/managed/contract/index.js +611 -0
  78. package/src/managed/contract/index.js.map +8 -0
  79. package/src/schnorr.ts +210 -0
  80. package/src/signature-requests-state-reader.ts +223 -0
  81. package/src/signature-response-verification.ts +64 -0
  82. package/src/signature-state-reading.ts +87 -0
  83. package/src/signet-contract-state-reader.ts +391 -0
  84. package/src/signet-event-observer.ts +186 -0
  85. package/src/signet-events.ts +337 -0
  86. package/src/signet-request-feed.ts +158 -0
  87. package/src/signet-request-resolver.ts +116 -0
  88. package/src/signet-request-response-reader.ts +324 -0
  89. package/src/signet-requests.ts +737 -0
  90. package/src/signet-response-feed.ts +325 -0
@@ -0,0 +1,529 @@
1
+ // ============================================================================
2
+ // Signet — shared Compact library for sig-net (signet) MPC signature
3
+ // requests and responses on Midnight.
4
+ // ============================================================================
5
+ // A signet compliant contract records "please sign this EVM transaction" requests
6
+ // in its public ledger. The sig-net MPC network needs only the contract address:
7
+ // it reads the request index from ledger field 0 (the signet layout convention),
8
+ // re-assembles and signs the EVM transaction with the caller's derived key,
9
+ // and posts its responses to the CENTRAL signet contract for polling — first
10
+ // the signature (the client broadcasts the signed transaction), then, after
11
+ // watching the remote execution, the Schnorr-signed respond-bidirectional
12
+ // attestation of its result. Requests and both response kinds are counterparts
13
+ // and are defined side by side here; the pure circuits that operate on them
14
+ // follow below.
15
+ //
16
+ // Naming follows the MPC's canonical vocabulary (sig-net/mpc
17
+ // chain-signatures/primitives): the SignBidirectionalRequest record mirrors
18
+ // the MPC's SignBidirectionalEvent (with the EVM transaction decomposed — the
19
+ // Canton-adapter precedent) and a client emits a SignBidirectionalEvent to
20
+ // notify the MPC one was stored; the SignatureResponse record mirrors
21
+ // Signature { big_r, s, recovery_id } and the signet contract emits a
22
+ // SignatureRespondedEvent to notify the client one was posted; the execution
23
+ // attestation mirrors RespondBidirectionalEvent / serialized_output.
24
+ //
25
+ // Read more:
26
+ // - sig-net docs (chain signatures, key derivation): https://docs.sig.network/
27
+ // - canonical request shape this mirrors: sig-net/mpc
28
+ // chain-signatures/primitives/src/bidirectional.rs (SignBidirectionalEvent)
29
+ // - Compact language reference (modules, types):
30
+ // https://docs.midnight.network/compact/reference/compact-reference
31
+ //
32
+ // Import from a contract (resolved through node_modules via COMPACT_PATH —
33
+ // see the consuming package's compile script):
34
+ // import "@sig-net/midnight/src/Signet";
35
+
36
+ module Signet {
37
+ import CompactStandardLibrary;
38
+
39
+ // ---- Identifiers ---------------------------------------------------------
40
+
41
+ // Nominal id of a signet request. Deliberately CHAIN-AGNOSTIC: downstream
42
+ // consumers (the signet contract, response pollers, the MPC)
43
+ // treat it as an opaque 32-byte key, whatever kind of request it identifies.
44
+ // Each request kind mints ids via its own domain-separated hash circuit
45
+ // (see calculateRequestId). `new type` makes it NOT interchangeable
46
+ // with any other Bytes<32> — cast explicitly: `someHash as RequestId`.
47
+ export new type RequestId = Bytes<32>;
48
+
49
+ // ---- The canonical request/response pair ----------------------------------
50
+ // 1. Client contract stores a SignBidirectionalRequest and emits a SignBidirectionalEvent, requesting a signature from the MPC
51
+ // 2. MPC reads the request and posts a SignatureResponse (the requested signature) to the central signet contract, which emits a SignatureRespondedEvent
52
+ // 3. Client observes SignatureRespondedEvent, verifies the posted SignatureResponse, broadcasts txn to destination chain
53
+ // 4. MPC observes txn execution on destination chain, responds with RespondBidirectionalEvent
54
+
55
+ // TxParamType indiciates which transaction-param decomposition a request
56
+ // contains so an off-chain consumer (e.g. the MPC) knows how to re-assemble it.
57
+ // In-circuit this is redundant as the type is fixed at compile time (i.e. monomorphized).
58
+ export enum TxParamType {
59
+ evmType2, // EVMType2TxParams: an EIP-1559 EVM transaction (envelope + calldata)
60
+ reserved, // never emitted — exists only to keep the enum at >= 2 variants. A
61
+ // 1-variant enum compiles to a zero-byte value the proof server's
62
+ // field-repr parser reads as ZERO field elements while the compiler
63
+ // allocates ONE, so persistentHash over any struct containing it
64
+ // (i.e. every request id) fails to prove.
65
+ }
66
+
67
+ // SignBidirectionalRequest is the record a Client contract stores to request
68
+ // a signature from the MPC (and hashes into its RequestId).
69
+ export struct SignBidirectionalRequest<TxParams> {
70
+ requestNonce: Uint<64>; // contract-local nonce at creation time
71
+
72
+ txParamType: TxParamType; // which decomposition txParams is (off-chain decode tag)
73
+ txParams: TxParams; // the generic transaction-param payload
74
+
75
+ // TODO: confirm sufficient size for CAIP-2 standard.
76
+ caip2Id: Bytes<32>; // target chain in CAIP-2 form, e.g. "eip155:11155111"
77
+ // (https://chainagnostic.org/CAIPs/caip-2)
78
+ // TODO: can be much smaller
79
+ keyVersion: Uint<32>; // MPC root-key version to derive from (>= 1;
80
+ // version 0 is the unsupported legacy format)
81
+ // TODO: consider possible optimisation. keep unchanged for now.
82
+ path: Bytes<256>; // key-derivation path; by signet-Midnight
83
+ // convention the canonical lowercase hex of the
84
+ // caller's identity commitment, zero-padded
85
+ // (enforced by assertPathCommitment)
86
+
87
+ // TODO: Possibly ENUMs, these fields are not used so we could make them small now.
88
+ algo: Bytes<32>; // signature scheme, e.g. "ecdsa"
89
+ dest: Bytes<32>; // response destination, e.g. "ethereum"
90
+ params: Bytes<64>; // scheme-specific extras (opaque to contracts)
91
+
92
+ // Look at most popular defi contracts for schematisation of schema types
93
+ // TODO: dynamically size these 2 schemas to reduce proving type
94
+ outputDeserializationSchema: Bytes<128>; // MPC output_deserialization_schema
95
+ // (destination chain -> MPC)
96
+ respondSerializationSchema: Bytes<128>; // MPC respond_serialization_schema
97
+ // (MPC -> source chain)
98
+ }
99
+
100
+ // External contract surface of the central signet contract: a client cross-
101
+ // contract-calls emitSignBidirectionalEvent to emit the notification event.
102
+ export contract SignetEventEmitter {
103
+ circuit emitSignBidirectionalEvent(signBidirectionalEvent: SignBidirectionalEvent): [];
104
+ }
105
+
106
+ // SignBidirectionalEvent notifies the MPC that a SignBidirectionalRequest has
107
+ // been stored in the caller's ledger. Attribution comes from whose
108
+ // authenticated state the MPC reads, never from a field here.
109
+ export struct SignBidirectionalEvent {
110
+ version: Uint<8>;
111
+ payload: Bytes<224>;
112
+ }
113
+
114
+ // V1 interpretation of SignBidirectionalEvent.payload.
115
+ export struct SignBidirectionalEventV1Payload {
116
+ // Address of the calling contract; the MPC reads its request index there.
117
+ callerAddress: ContractAddress;
118
+
119
+ // Representation type (Bytes<32>), NOT the RequestId new type: a `new type`
120
+ // field crashes the serialize<> event encoder (compiler self-diagnosed bug).
121
+ requestId: Bytes<32>;
122
+
123
+ // Ledger field position of the Map<RequestId, SignBidirectionalRequest> in
124
+ // the caller contract.
125
+ signBidirectionalRequestsIndexField: Uint<8>;
126
+ }
127
+
128
+ // Build the V1 event: pack the concrete fields into the opaque payload with
129
+ // serialize<> and tag version = 1. Client contracts should call
130
+ // this instead of hand-constructing the envelope.
131
+ export pure circuit constructSignBidirectionalEventV1(
132
+ callerAddress: ContractAddress,
133
+ requestId: Bytes<32>,
134
+ signBidirectionalRequestsIndexField: Uint<8>
135
+ ): SignBidirectionalEvent {
136
+ return SignBidirectionalEvent {
137
+ version: 1,
138
+ payload: serialize<SignBidirectionalEventV1Payload, 224>(
139
+ SignBidirectionalEventV1Payload {
140
+ callerAddress,
141
+ requestId,
142
+ signBidirectionalRequestsIndexField
143
+ }
144
+ )
145
+ };
146
+ }
147
+
148
+ // The counterpart to SignBidirectionalRequest: the record the MPC posts to
149
+ // the central signet contract's response log after it observes the request
150
+ // (mirrors the MPC's canonical Signature { big_r, s, recovery_id }).
151
+ // Contains the MPC's secp256k1 signature over the requested transaction.
152
+ // The requesting client uses this to rebuild a signed transaction and broadcast.
153
+ export struct SignatureResponse {
154
+ bigRx: Bytes<32>; // signature R.x
155
+ bigRy: Bytes<32>; // signature R.y
156
+ s: Bytes<32>; // signature s
157
+ recoveryId: Uint<8>; // parity of R.y for public-key recovery
158
+ }
159
+
160
+ // SignatureRespondedEvent notifies the requesting client that a
161
+ // SignatureResponse has been posted to the central signet contract's
162
+ // response log — the response-direction counterpart of
163
+ // SignBidirectionalEvent. A ping, never the source of truth: the client
164
+ // reads the post back from the log at (requestId, count) and verifies the
165
+ // signature off-chain — the log is unauthenticated, so verification, not
166
+ // the event, confers trust.
167
+ export struct SignatureRespondedEvent {
168
+ // Representation type (Bytes<32>), NOT the RequestId new type: a `new type`
169
+ // field crashes the serialize<> event encoder (compiler self-diagnosed bug).
170
+ requestId: Bytes<32>,
171
+
172
+ // 0-based position of the post in the request's response log — the
173
+ // `count` half of the SignetResponseKey it is stored under.
174
+ count: Uint<64>,
175
+ }
176
+
177
+ // The counterpart to SignatureResponse, emitted by the MPC after
178
+ // it observes execution of the signed transaction on the destination chain.
179
+ // This response contains execution output and an attestation as to its authenticity.
180
+ // TODO: currently contains a Schnorr attestation --> migrate to ECDSA.
181
+ export struct RespondBidirectional {
182
+ // ABI-encoded return data of the remote call (canonical
183
+ // serialized_output), zero-padded. First 32-byte word == 1 signals
184
+ // success; a 0xdeadbeef prefix is the MPC's error sentinel for a
185
+ // failed/absent execution.
186
+ serializedOutput: Bytes<128>;
187
+
188
+ // Meaningful byte count of serializedOutput.
189
+ outputLen: Uint<8>;
190
+
191
+ // The MPC attestation key (Jubjub). Hash-checked against the signet
192
+ // contract's sealed mpcPubKeyHash at post time, and again by requester
193
+ // contracts at claim time.
194
+ pk: JubjubPoint;
195
+
196
+ // Schnorr nonce commitment R.
197
+ announcement: JubjubPoint;
198
+
199
+ // Schnorr response scalar s.
200
+ response: Field;
201
+ }
202
+
203
+ // RespondBidirectionalEvent notifies the requesting client that the MPC's
204
+ // RespondBidirectional attestation of a request's remote execution has been
205
+ // stored in the central signet contract's respond-bidirectional index — the
206
+ // attestation-direction counterpart of SignatureRespondedEvent. A ping,
207
+ // never the source of truth: the client reads the record back from the
208
+ // index at requestId. Unlike signature responses, NO off-chain verification
209
+ // follows — the contract verified the attestation in-circuit at post time,
210
+ // so the stored record is authentic by construction. Emitted at most once
211
+ // per request: the index holds one authenticated slot, first valid write
212
+ // wins, and a re-post is a no-op that emits nothing.
213
+ export struct RespondBidirectionalEvent {
214
+ // Representation type (Bytes<32>), NOT the RequestId new type: a `new type`
215
+ // field crashes the serialize<> event encoder (compiler self-diagnosed bug).
216
+ requestId: Bytes<32>;
217
+ }
218
+
219
+ // The EVM transaction to be signed, decomposed into typed fields
220
+ // (EIP-1559 / type-2: https://eips.ethereum.org/EIPS/eip-1559).
221
+ // Decomposed rather than pre-serialized so the requesting contract can
222
+ // enforce invariants field by field; the MPC re-assembles and RLP-encodes
223
+ // off-chain. (The MPC's chain-agnostic primitive carries an opaque
224
+ // serialized_transaction instead; the decomposition follows its Canton
225
+ // adapter's EvmType2TransactionParams.)
226
+ //
227
+ // Compact has no runtime-dynamic sizes, so every variable-length EVM tx
228
+ // part follows one scheme: a compile-time CAPACITY (a generic size
229
+ // parameter each requesting contract fixes as its throttle) plus a runtime
230
+ // count that says how many leading slots are real. Off-chain consumers use
231
+ // the counts; unused capacity is hashed as part of the record (zero-fill by
232
+ // convention) but never reaches the assembled transaction.
233
+ export struct EVMType2TxParams<#maxCalldataWords, #maxAccessListEntries, #maxStorageKeysPerEntry> {
234
+ to: Bytes<20>; // call target (e.g. the ERC20 contract)
235
+ chainId: Uint<64>; // EVM chain id (also expressed in caip2Id)
236
+ nonce: Uint<64>; // account nonce of the derived sender
237
+ gasLimit: Uint<64>; // gas ceiling for the call
238
+ maxFeePerGas: Uint<128>; // wei
239
+ maxPriorityFeePerGas: Uint<128>; // wei
240
+ value: Uint<128>; // ETH sent with the call, in wei
241
+ accessListEntryCount: Uint<8>; // how many leading accessList entries are real
242
+ accessList: Vector<maxAccessListEntries, EVMAccessListEntry<maxStorageKeysPerEntry>>;
243
+ calldata: Maybe<EVMCalldata<maxCalldataWords>>; // none = plain ETH transfer ("0x" data)
244
+ }
245
+
246
+ // One EIP-2930 access-list entry (https://eips.ethereum.org/EIPS/eip-2930):
247
+ // an address plus the storage keys the transaction pre-declares. Keys are
248
+ // raw 32-byte values, copied verbatim into the RLP encoding.
249
+ export struct EVMAccessListEntry<#maxStorageKeys> {
250
+ address: Bytes<20>;
251
+ storageKeyCount: Uint<8>; // how many leading storageKeys are real
252
+ storageKeys: Vector<maxStorageKeys, Bytes<32>>;
253
+ }
254
+
255
+ // ABI calldata for the EVM call: 4-byte selector + 32-byte slots
256
+ // (https://docs.soliditylang.org/en/latest/abi-spec.html).
257
+ export struct EVMCalldata<#maxWords> {
258
+ selector: Bytes<4>; // the literal first 4 calldata bytes (big-endian, as broadcast)
259
+ noWords: Uint<16>; // No. of used words in the words list
260
+ words: Vector<maxWords, Bytes<32>>;
261
+ }
262
+
263
+ // The field value to embed in a calldata word for an EVM address: the
264
+ // BIG-ENDIAN numeric reading of its 20 display-order bytes. Embedding the
265
+ // address directly (`addr as Field`) would be wrong: that cast is
266
+ // little-endian, so the MPC's LE-decode -> BE-encode step would emit the
267
+ // address byte-reversed in the calldata. Usage: a calldata word
268
+ // `evmAddressAbiValue(addr) as Bytes<32>`.
269
+ // (Uint amounts need no helper: `amount as Field` already embeds the
270
+ // correct numeric value.)
271
+ export pure circuit evmAddressAbiValue(addr: Bytes<20>): Field {
272
+ const a0 = slice<1>(addr, 0) as Field;
273
+ const a1 = a0 * 256 + (slice<1>(addr, 1) as Field);
274
+ const a2 = a1 * 256 + (slice<1>(addr, 2) as Field);
275
+ const a3 = a2 * 256 + (slice<1>(addr, 3) as Field);
276
+ const a4 = a3 * 256 + (slice<1>(addr, 4) as Field);
277
+ const a5 = a4 * 256 + (slice<1>(addr, 5) as Field);
278
+ const a6 = a5 * 256 + (slice<1>(addr, 6) as Field);
279
+ const a7 = a6 * 256 + (slice<1>(addr, 7) as Field);
280
+ const a8 = a7 * 256 + (slice<1>(addr, 8) as Field);
281
+ const a9 = a8 * 256 + (slice<1>(addr, 9) as Field);
282
+ const a10 = a9 * 256 + (slice<1>(addr, 10) as Field);
283
+ const a11 = a10 * 256 + (slice<1>(addr, 11) as Field);
284
+ const a12 = a11 * 256 + (slice<1>(addr, 12) as Field);
285
+ const a13 = a12 * 256 + (slice<1>(addr, 13) as Field);
286
+ const a14 = a13 * 256 + (slice<1>(addr, 14) as Field);
287
+ const a15 = a14 * 256 + (slice<1>(addr, 15) as Field);
288
+ const a16 = a15 * 256 + (slice<1>(addr, 16) as Field);
289
+ const a17 = a16 * 256 + (slice<1>(addr, 17) as Field);
290
+ const a18 = a17 * 256 + (slice<1>(addr, 18) as Field);
291
+ return a18 * 256 + (slice<1>(addr, 19) as Field);
292
+ }
293
+
294
+ // ---- Requester Ledger Layout -----------------------------------------------
295
+
296
+ // The on-ledger request index. Signet layout convention: a conforming
297
+ // contract declares this as its FIRST ledger field (instantiated at its own
298
+ // calldata slot capacity), so the MPC can find and deserialize it knowing
299
+ // only the contract address.
300
+ export new type SignBidirectionalRequestIndex<TxParams> = Map<RequestId, SignBidirectionalRequest<TxParams>>;
301
+
302
+ // The contract-local request counter. Signet layout convention: declared as
303
+ // the SECOND ledger field (directly after the index). It sources each
304
+ // request's requestNonce and doubles as the MPC's cheap change-detection
305
+ // signal — an unchanged counter means no new requests since the last poll.
306
+ export new type SignetNonce = Counter;
307
+
308
+ // ---- Signet Contract Ledger Layout ------------------------------------------
309
+ // The CENTRAL signet contract on Midnight stores both MPC response kinds.
310
+ // Its layout convention (find everything knowing only the contract address):
311
+ // field 0: SignatureResponseCounterIndex
312
+ // field 1: SignatureResponseIndex
313
+ // field 2: RespondBidirectionalIndex
314
+ // field 3: mpcPubKeyHash (sealed Bytes<32>)
315
+ //
316
+ // The two kinds get different defenses:
317
+ // - Signature responses are secp256k1 ECDSA — unverifiable in-circuit —
318
+ // so their log is UNAUTHENTICATED and append-only: each post lands
319
+ // under a fresh (requestId, count) key (announced by a
320
+ // SignatureRespondedEvent); a rogue poster can add noise
321
+ // but never overwrite or block the genuine response. Clients enumerate
322
+ // a request's entries and verify each signature off-chain.
323
+ // - Respond-bidirectional attestations are Schnorr on Jubjub — the
324
+ // embedded curve — so the contract verifies them IN-CIRCUIT at post
325
+ // time against its sealed MPC key hash. Garbage cannot land; one
326
+ // authenticated slot per request suffices, first valid write wins.
327
+
328
+ // Per-request post counter for signature responses (field 0). The counter
329
+ // reads the number of posts made for that request; entries exist for
330
+ // counts 0..counter-1.
331
+ export new type SignatureResponseCounterIndex = Map<RequestId, Counter>;
332
+
333
+ // Composite key of one posted signature response: the request id it
334
+ // answers plus this post's 0-based count, sourced from
335
+ // SignatureResponseCounterIndex. (Respond-bidirectional posts need no key
336
+ // struct — authenticated posts are keyed by request id alone.)
337
+ export struct SignetResponseKey {
338
+ count: Uint<64>;
339
+ requestId: RequestId;
340
+ };
341
+
342
+ // The on-ledger signature response log (field 1), keyed by
343
+ // (requestId, count).
344
+ export new type SignatureResponseIndex = Map<SignetResponseKey, SignatureResponse>;
345
+
346
+ // The on-ledger respond-bidirectional index (field 2): one
347
+ // contract-verified attestation per request.
348
+ export new type RespondBidirectionalIndex = Map<RequestId, RespondBidirectional>;
349
+
350
+ // ---- Request id -----------------------------------------------------------
351
+
352
+ // Canonical id of an EVM signature request: the domain-separated persistent
353
+ // hash of the entire request record. The id therefore commits to every
354
+ // field — the MPC (or anyone) recomputes it off-chain and refuses records
355
+ // whose stored key does not match.
356
+ //
357
+ // The "signet:midnight:request" domain tag partitions the RequestId space:
358
+ // ids minted by this circuit can never collide or be confused with hashes
359
+ // minted under any other tag.
360
+ //
361
+ // Off-chain twin: being type-parameterized, this circuit cannot be exported
362
+ // through circuits.compact, so signet-requests.ts carries a TS twin
363
+ // (calculateRequestId) that MUST stay in lockstep — see the
364
+ // deviation note there before changing anything here.
365
+ export pure circuit calculateRequestId<TxParams>(
366
+ request: SignBidirectionalRequest<TxParams>
367
+ ): RequestId {
368
+ return persistentHash<Vector<2, Bytes<32>>>([
369
+ pad(32, "signet:midnight:request"),
370
+ persistentHash<SignBidirectionalRequest<TxParams>>(request)
371
+ ]) as RequestId;
372
+ }
373
+
374
+ // ---- Respond-bidirectional attestation message ------------------------------
375
+
376
+ // The message the MPC Schnorr-signs to attest a remote execution:
377
+ // (requestId, hash(serializedOutput, outputLen)) as four 16-byte
378
+ // little-endian field limbs. The hash binds the output TOGETHER WITH its
379
+ // length, so neither can be swapped under a reused signature; the 16-byte
380
+ // split is needed because a 32-byte value overflows the BLS scalar field.
381
+ // ONE implementation for everyone: the signet contract verifies posts
382
+ // against it, requester contracts (claimDeposit) re-verify against it, and
383
+ // off-chain signers call the compiled copy (via circuits.compact) — never
384
+ // a TS re-implementation.
385
+ export pure circuit signetAttestationMessage(
386
+ requestId: RequestId,
387
+ serializedOutput: Bytes<128>,
388
+ outputLen: Uint<8>
389
+ ): Vector<4, Field> {
390
+ const rid = requestId as Bytes<32>;
391
+ const outHash = persistentHash<Vector<2, Bytes<32>>>([
392
+ persistentHash<Bytes<128>>(serializedOutput),
393
+ outputLen as Field as Bytes<32>
394
+ ]);
395
+ return [
396
+ slice<16>(rid, 0) as Field,
397
+ slice<16>(rid, 16) as Field,
398
+ slice<16>(outHash, 0) as Field,
399
+ slice<16>(outHash, 16) as Field
400
+ ];
401
+ }
402
+
403
+ // ---- Path <-> identity binding ---------------------------------------------
404
+ // Compact cannot ENCODE bytes->hex in-circuit (nibble extraction needs
405
+ // integer div/mod/shift, which the prime field has no native op for). But it
406
+ // CAN DECODE hex->bytes with multiply/add/compare. So the derivation path (a
407
+ // public value) carries the hex string, and we verify in-circuit that it
408
+ // canonically decodes to the commitment. The prover cannot supply a wrong or
409
+ // non-canonical string, so this is as strong as computing the hex on-chain —
410
+ // and it lets the MPC treat the path as a plain opaque string (no hex
411
+ // special-case in the MPC).
412
+
413
+ // Decode one canonical hex character ('0'-'9', lowercase 'a'-'f') to 0..15.
414
+ pure circuit charToNibble(c: Uint<8>): Uint<8> {
415
+ const isDigit = (c >= 48) && (c <= 57); // '0'..'9'
416
+ const isLower = (c >= 97) && (c <= 102); // 'a'..'f' (lowercase only => canonical)
417
+ assert(isDigit || isLower, "non-canonical hex char");
418
+ const base = c - 48; // safe: c >= 48 in both accepted ranges
419
+ const adjust = isLower ? 39 : 0; // 'a'(97) - 48 - 39 = 10
420
+ return base - adjust;
421
+ }
422
+
423
+ // Assert that hexStr (64 lowercase hex chars) decodes exactly to commitment.
424
+ export pure circuit assertHexOf(commitment: Bytes<32>, hexStr: Bytes<64>): [] {
425
+ for (const i of 0 .. 32) {
426
+ const hi = charToNibble(slice<1>(hexStr, 2 * i) as Uint<8>);
427
+ const lo = charToNibble(slice<1>(hexStr, 2 * i + 1) as Uint<8>);
428
+ assert((slice<1>(commitment, i) as Field) == (hi as Field) * 16 + (lo as Field),
429
+ "path hex does not match commitment");
430
+ }
431
+ }
432
+
433
+ // // Assert the full path <-> identity binding: the path is EXACTLY the
434
+ // canonical lowercase hex of `commitment`, zero-padded to 256 bytes. This
435
+ // makes identity -> path -> MPC-derived account a forced 1:1 mapping: no one
436
+ // can request signatures under another identity's derived key, and no
437
+ // identity can spread across multiple derived keys via case/suffix
438
+ // malleability. See https://docs.sig.network/ for path-based derivation.
439
+ export pure circuit assertPathCommitment(
440
+ commitment: Bytes<32>,
441
+ path: Bytes<256>
442
+ ): [] {
443
+ assertHexOf(commitment, slice<64>(path, 0));
444
+ assert(slice<192>(path, 64) == pad(192, ""), "Path must be zero-padded after hex");
445
+ }
446
+
447
+ // ---- Request construction ---------------------------------------------------
448
+
449
+ // Assemble the canonical request record for a CALLER-IDENTITY request,
450
+ // enforcing the path <-> identity binding on the way — the MPC signs with
451
+ // the calling user's derived key and no one else's.
452
+ // The requesting contract supplies:
453
+ // - pathCommitment: the DISCLOSED identity commitment of the caller
454
+ // (derived from a secret-key witness — see the vault's userCommitment)
455
+ // - requestNonce: its contract-local nonce (typically a Counter read)
456
+ // - txParams: the transaction decomposition; the contract MUST
457
+ // build (or overwrite) any part it enforces — calldata above all —
458
+ // rather than pass caller input through
459
+ // - the remaining MPC routing fields, caller-supplied
460
+ // Store the result under calculateRequestId(request) in the
461
+ // request index.
462
+ export pure circuit constructSignBidirectionalRequest<TxParams>(
463
+ pathCommitment: Bytes<32>,
464
+ requestNonce: Uint<64>,
465
+ txParamType: TxParamType,
466
+ txParams: TxParams,
467
+ caip2Id: Bytes<32>,
468
+ keyVersion: Uint<32>,
469
+ path: Bytes<256>,
470
+ algo: Bytes<32>,
471
+ dest: Bytes<32>,
472
+ params: Bytes<64>,
473
+ outputDeserializationSchema: Bytes<128>,
474
+ respondSerializationSchema: Bytes<128>
475
+ ): SignBidirectionalRequest<TxParams> {
476
+ assertPathCommitment(pathCommitment, path);
477
+ return constructContractPathSignBidirectionalRequest<TxParams>(
478
+ requestNonce,
479
+ txParamType,
480
+ txParams,
481
+ caip2Id,
482
+ keyVersion,
483
+ path,
484
+ algo,
485
+ dest,
486
+ params,
487
+ outputDeserializationSchema,
488
+ respondSerializationSchema
489
+ );
490
+ }
491
+
492
+ // Assemble the canonical request record for a CONTRACT-OWNED derivation
493
+ // path: a literal the requesting contract fixes in-circuit (e.g. a vault
494
+ // treasury's "vault"), so the MPC signs with the CONTRACT's derived account
495
+ // rather than a caller's. No identity binding applies — the contract itself
496
+ // vouches for the path by hardcoding it; never pass a caller-supplied path
497
+ // here (that would let anyone request signatures under any derived key —
498
+ // use constructSignBidirectionalRequest for caller-identity requests).
499
+ // Enforces the canonical keyVersion floor for both constructors.
500
+ export pure circuit constructContractPathSignBidirectionalRequest<TxParams>(
501
+ requestNonce: Uint<64>,
502
+ txParamType: TxParamType,
503
+ txParams: TxParams,
504
+ caip2Id: Bytes<32>,
505
+ keyVersion: Uint<32>,
506
+ path: Bytes<256>,
507
+ algo: Bytes<32>,
508
+ dest: Bytes<32>,
509
+ params: Bytes<64>,
510
+ outputDeserializationSchema: Bytes<128>,
511
+ respondSerializationSchema: Bytes<128>
512
+ ): SignBidirectionalRequest<TxParams> {
513
+ // The MPC rejects requests for the unsupported legacy key version 0.
514
+ assert(keyVersion >= 1, "keyVersion must be >= 1");
515
+ return SignBidirectionalRequest<TxParams> {
516
+ requestNonce,
517
+ txParamType,
518
+ txParams,
519
+ caip2Id,
520
+ keyVersion,
521
+ path,
522
+ algo,
523
+ dest,
524
+ params,
525
+ outputDeserializationSchema,
526
+ respondSerializationSchema
527
+ };
528
+ }
529
+ }
@@ -0,0 +1,36 @@
1
+ pragma language_version >= 0.25;
2
+
3
+ // Compiled export surface of the Signet and Schnorr modules.
4
+ //
5
+ // Signet.compact / Schnorr.compact must stay module-only files (that is what
6
+ // lets contracts import them), but a bare module compiles to nothing. This
7
+ // contract-less program re-exports the modules' pure circuits so the compiler
8
+ // emits them as pureCircuits in managed/contract/index.js — giving the
9
+ // package an in-process, provable-logic-identical reference implementation:
10
+ // unit tests exercise the REAL circuits, and off-chain code (path checks,
11
+ // Schnorr challenges, attestation messages) reuses them instead of
12
+ // hand-porting the algorithms.
13
+ //
14
+ // ONLY the non-generic circuits are exported here. The request circuits
15
+ // (calculateRequestId, constructSignBidirectionalRequest) are generic over the
16
+ // tx-params type, the compiler cannot export type-parameterized circuits from
17
+ // the top level, and this package is client-agnostic — a capacity
18
+ // instantiation is a CLIENT contract's choice and never belongs here.
19
+ // constructSignBidirectionalRequest is contract-only, so it needs no off-chain
20
+ // copy; calculateRequestId IS needed off-chain (clients recompute ledger map
21
+ // keys), so it alone has a documented-deviation TS twin in signet-requests.ts,
22
+ // kept honest by vault-contract's deposit round-trip test.
23
+ //
24
+ // Compile with --skip-zk only: pure circuits run in TS without proving keys.
25
+
26
+ import CompactStandardLibrary;
27
+ import "./Signet";
28
+ import "./Schnorr";
29
+
30
+ export {
31
+ signetAttestationMessage,
32
+ assertHexOf,
33
+ assertPathCommitment,
34
+ evmAddressAbiValue,
35
+ schnorrChallenge
36
+ };
@@ -0,0 +1,97 @@
1
+ // MPC routing constants + the padded-ASCII codec for the signet request
2
+ // structs. Field widths mirror Signet.compact (the wire format —
3
+ // keep in lockstep); the string constants are the values the MPC network
4
+ // routes on. Ported from the MVP's contract-cli signet/constants.ts, adapted
5
+ // to the refactor's zero-padded convention (Compact `pad(N, "text")`) — the
6
+ // MVP's 2-byte length-prefixed codec is gone with the old layout.
7
+ //
8
+ // The routing constants belong in github.com/sig-net/signet.js — kept here
9
+ // until upstreamed.
10
+
11
+ /** Width of `SignBidirectionalRequest.caip2Id` (`Bytes<32>`). */
12
+ export const CAIP2_ID_BYTES = 32;
13
+
14
+ /** Width of `SignBidirectionalRequest.algo` (`Bytes<32>`). */
15
+ export const ALGO_BYTES = 32;
16
+
17
+ /** Width of `SignBidirectionalRequest.dest` (`Bytes<32>`). */
18
+ export const DEST_BYTES = 32;
19
+
20
+ /** Width of `SignBidirectionalRequest.params` (`Bytes<64>`). */
21
+ export const MPC_PARAMS_BYTES = 64;
22
+
23
+ /** Width of `SignBidirectionalRequest.outputDeserializationSchema` (`Bytes<128>`). */
24
+ export const OUTPUT_DESERIALIZATION_SCHEMA_BYTES = 128;
25
+
26
+ /** Width of `SignBidirectionalRequest.respondSerializationSchema` (`Bytes<128>`). */
27
+ export const RESPOND_SERIALIZATION_SCHEMA_BYTES = 128;
28
+
29
+ /** Width of `EVMCalldata.selector` (`Bytes<4>`) — the literal first 4 calldata bytes. */
30
+ export const SELECTOR_BYTES = 4;
31
+
32
+ /** Width of `RespondBidirectional.serializedOutput` (`Bytes<128>`). */
33
+ export const SERIALIZED_OUTPUT_BYTES = 128;
34
+
35
+ /**
36
+ * The MPC's error sentinel: `serializedOutput` beginning with these four
37
+ * bytes marks a failed/absent remote execution (mirrors the sig-net MPC's
38
+ * MAGIC_ERROR_PREFIX).
39
+ */
40
+ export const MPC_ERROR_SENTINEL = new Uint8Array([0xde, 0xad, 0xbe, 0xef]);
41
+
42
+ /**
43
+ * Whether an attestation's serialized output reports a successful remote
44
+ * execution: the first byte is 1 — the little-endian encoding of the
45
+ * ABI-decoded success flag, matching the circuits'
46
+ * `serializedOutput as Field == 1` check.
47
+ *
48
+ * @param serializedOutput - The attestation's serialized output.
49
+ * @returns `true` when the remote call succeeded.
50
+ */
51
+ export function executionSucceeded(serializedOutput: Uint8Array): boolean {
52
+ return serializedOutput[0] === 1;
53
+ }
54
+
55
+ /**
56
+ * Whether an attestation's serialized output is the MPC's error sentinel
57
+ * (see {@link MPC_ERROR_SENTINEL}) rather than a call result.
58
+ *
59
+ * @param serializedOutput - The attestation's serialized output.
60
+ * @returns `true` when the MPC reported an execution error.
61
+ */
62
+ export function isExecutionError(serializedOutput: Uint8Array): boolean {
63
+ return MPC_ERROR_SENTINEL.every((byte, index) => serializedOutput[index] === byte);
64
+ }
65
+
66
+ /** Signature algorithm the MPC uses for EVM chains (`algo` field value). */
67
+ export const SIGNET_ALGO_ECDSA = "ecdsa";
68
+
69
+ /** Destination chain family for EVM requests (`dest` field value). */
70
+ export const SIGNET_DEST_ETHEREUM = "ethereum";
71
+
72
+ /**
73
+ * Default MPC key version (`keyVersion` field value). Version 0 is the
74
+ * unsupported legacy format — the canonical MPC (and
75
+ * `constructSignBidirectionalRequest`) requires `keyVersion >= 1`.
76
+ */
77
+ export const SIGNET_DEFAULT_KEY_VERSION = 1n;
78
+
79
+ /**
80
+ * Encode text as zero-padded ASCII bytes — the Compact `pad(N, "text")`
81
+ * convention every string-ish field of the request structs uses (consumers
82
+ * NUL-trim on decode).
83
+ *
84
+ * @param text - The ASCII text to encode.
85
+ * @param length - The fixed field width in bytes.
86
+ * @returns `text`'s bytes followed by zero padding to exactly `length`.
87
+ * @throws If the encoded text does not fit in `length` bytes.
88
+ */
89
+ export function asciiPadded(text: string, length: number): Uint8Array {
90
+ const encoded = new TextEncoder().encode(text);
91
+ if (encoded.length > length) {
92
+ throw new Error(`"${text}" is ${encoded.length} bytes — does not fit the ${length}-byte field`);
93
+ }
94
+ const out = new Uint8Array(length);
95
+ out.set(encoded);
96
+ return out;
97
+ }