@sig-net/midnight 0.0.6 → 0.0.8

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 (57) hide show
  1. package/README.md +23 -20
  2. package/dist/constants.d.ts +4 -16
  3. package/dist/constants.d.ts.map +1 -1
  4. package/dist/constants.js +4 -16
  5. package/dist/constants.js.map +1 -1
  6. package/dist/ecdsa-attestation.d.ts +79 -0
  7. package/dist/ecdsa-attestation.d.ts.map +1 -0
  8. package/dist/ecdsa-attestation.js +140 -0
  9. package/dist/ecdsa-attestation.js.map +1 -0
  10. package/dist/epsilon-derivation.d.ts +60 -0
  11. package/dist/epsilon-derivation.d.ts.map +1 -1
  12. package/dist/epsilon-derivation.js +106 -5
  13. package/dist/epsilon-derivation.js.map +1 -1
  14. package/dist/index.d.ts +9 -7
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +9 -7
  17. package/dist/index.js.map +1 -1
  18. package/dist/managed/compiler/contract-info.json +74 -52
  19. package/dist/managed/compiler/contract-manifest.json +8 -8
  20. package/dist/managed/contract/index.d.ts +38 -31
  21. package/dist/managed/contract/index.js +232 -382
  22. package/dist/managed/contract/index.js.map +1 -1
  23. package/dist/signature-requests-state-reader.d.ts +14 -13
  24. package/dist/signature-requests-state-reader.d.ts.map +1 -1
  25. package/dist/signature-requests-state-reader.js +31 -20
  26. package/dist/signature-requests-state-reader.js.map +1 -1
  27. package/dist/signature-response-verification.d.ts +5 -5
  28. package/dist/signature-response-verification.d.ts.map +1 -1
  29. package/dist/signature-response-verification.js +4 -4
  30. package/dist/signature-response-verification.js.map +1 -1
  31. package/dist/signet-contract-state-reader.d.ts +101 -112
  32. package/dist/signet-contract-state-reader.d.ts.map +1 -1
  33. package/dist/signet-contract-state-reader.js +123 -115
  34. package/dist/signet-contract-state-reader.js.map +1 -1
  35. package/dist/signet-request-feed.d.ts +4 -3
  36. package/dist/signet-request-feed.d.ts.map +1 -1
  37. package/dist/signet-request-feed.js +9 -8
  38. package/dist/signet-request-feed.js.map +1 -1
  39. package/dist/signet-request-resolver.d.ts +6 -4
  40. package/dist/signet-request-resolver.d.ts.map +1 -1
  41. package/dist/signet-request-resolver.js +8 -6
  42. package/dist/signet-request-resolver.js.map +1 -1
  43. package/dist/signet-request-response-reader.d.ts +21 -20
  44. package/dist/signet-request-response-reader.d.ts.map +1 -1
  45. package/dist/signet-request-response-reader.js +35 -23
  46. package/dist/signet-request-response-reader.js.map +1 -1
  47. package/dist/signet-requests.d.ts +93 -59
  48. package/dist/signet-requests.d.ts.map +1 -1
  49. package/dist/signet-requests.js +84 -67
  50. package/dist/signet-requests.js.map +1 -1
  51. package/package.json +2 -2
  52. package/src/Signet.compact +277 -425
  53. package/src/circuits.compact +22 -21
  54. package/dist/ecdsa.d.ts +0 -114
  55. package/dist/ecdsa.d.ts.map +0 -1
  56. package/dist/ecdsa.js +0 -145
  57. package/dist/ecdsa.js.map +0 -1
@@ -1,166 +1,245 @@
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 and registers a notification in the CENTRAL signet
7
- // contract's notification registry. The sig-net MPC network polls that one
8
- // registry: each notification names the requesting contract and the ledger
9
- // field position of its request index, so the MPC reads the authenticated
10
- // request from there, re-assembles and signs the EVM transaction with the
11
- // caller's derived key, and posts its responses back to the central signet
12
- // contract: first the signature (the client broadcasts the signed
13
- // transaction), then, after watching the remote execution, the ECDSA-signed
14
- // respond-bidirectional attestation of its result. Clients poll the central
15
- // contract's response indexes by request id. Requests and both response kinds
16
- // are counterparts and are defined side by side here; the pure circuits that
17
- // operate on them follow below.
1
+ // =======================================================================================================
2
+ // Sig Network (Signet) shared Compact library enabling contracts on the Midnight blockchain to leverage
3
+ // the Signet Distributed MPC network to execute arbitrary transactions on foreign blockchains.
4
+ // =======================================================================================================
18
5
  //
19
- // Naming follows the MPC's canonical vocabulary (sig-net/mpc
20
- // chain-signatures/primitives): the SignBidirectionalRequest record mirrors
21
- // the MPC's canonical bidirectional request primitive (with the EVM
22
- // transaction decomposed — the Canton-adapter precedent); the
23
- // SignatureResponse record mirrors Signature { big_r, s, recovery_id }; the
24
- // execution attestation mirrors the MPC's respond-bidirectional
25
- // serialized_output.
6
+ // Terms:
7
+ // - client contract(s): contracts on the Midnight blockchain that integrate with the Signet Distributed MPC
8
+ // to execute arbitrary transactions on foreign blockchains.
9
+ // - Signet singleton: a single contract on the Midnight blockchain holding the Signet registry for transaction
10
+ // signature requests and responses, as well as the signed output of transactions executed
11
+ // on foreign blockchains.
12
+ //
13
+ // Usage:
14
+ // // Import this module at the start of a contract (resolved through node_modules via COMPACT_PATH):
15
+ // import "@sig-net/midnight/src/Signet";
16
+ //
17
+ // Key types at a Glance:
18
+ // - SignBidirectionalEvent: Structured request for a signature on a transaction. Generically configured by client
19
+ // contracts based on the type of transaction for which a signature is requested
20
+ // (EVM Type 2, Solana etc.).
21
+ // - RequestId: 32 byte chain agnostic nominal id, calculated as hash of SignBidirectionalEvent (see calculateRequestId).
22
+ // - SignBidirectionalEventMap: Named mapping of RequestId to SignBidirectionalEvent. Declared by client contracts
23
+ // in their ledger. Client contract tells MPC field location of this map when making a cross
24
+ // contract call, via SignetSigner, to the Signet singleton. The MPC looks here to find
25
+ // SignBidirectionalEvents.
26
+ // - SignetSigner: Contract interface used by client contract to make cross contract calls to
27
+ // Signet singleton to notify the MPC of a SignBidirectionalEvent.
28
+ // - SignBidirectionalEventNotification: Notification delivered by client contracts via cross contract call to Signet singleton
29
+ // (via SignetSigner interface) to notify the MPC of a SignBidirectionalEvent.
30
+ // - SignatureRespondedEvent: Counterpart to SignBidirectionalEvent containing MPC generated signature requested by a SignBidirectionalEvent.
31
+ // Posted by MPC back to Signet singleton for clients to extract to build a signed transaction.
32
+ // - RespondBidirectionalEvent: Structure containing the output of a transaction executed on a foreign blockchain,
33
+ // ECDSA-signed over the request id and output (see signetAttestationDigest) by the
34
+ // MPC's RESPONSE key for the requesting contract: the key derived from the client
35
+ // contract's address + the fixed path "midnight response key" (not the root key,
36
+ // and not the request-signing key). Posted by MPC back to Signet singleton for
37
+ // clients to extract, verify (see verifyRespondBidirectionalEvent) and deliver back
38
+ // to the client contract.
39
+ //
40
+ // These key types are best understood within the sign bidirectional flow that they enable:
41
+ // 1. Client contract stores a SignBidirectionalEvent in a SignBidirectionalEventMap its own ledger
42
+ // (against the calculated RequestId) and requests a signature from the MPC by registering a
43
+ // SignBidirectionalEventNotification in the Signet singleton contract's registry
44
+ // (via cross contract call against the SignetSigner interface).
45
+ // 2. MPC polls the Signet singleton contract's registry for the SignBidirectionalEventNotification,
46
+ // using it to read the SignBidirectionalEvent signature request from the client's ledger. It generates the
47
+ // signature and and posts a SignatureRespondedEvent (the requested signature) to the Signet singleton contract.
48
+ // 3. Client polls the Signet singleton contract SignatureRespondedEventMap for the signature by RequestId,
49
+ // verifies the posted SignatureRespondedEvent and broadcasts txn to destination chain.
50
+ // 4. MPC observes txn execution on destination chain, posts a RespondBidirectionalEvent
51
+ // attestation to the central Signet contract; the client polls it by RequestId.
26
52
  //
27
53
  // Read more:
28
- // - sig-net docs (chain signatures, key derivation): https://docs.sig.network/
29
- // - canonical request shape this mirrors: sig-net/mpc
30
- // chain-signatures/primitives/src/bidirectional.rs
54
+ // - For full integration examples (such as an ERC20 cross chain vault example) see
55
+ // the Midnight Examples repository at: https://github.com/sig-net/midnight-examples.
56
+ // - Signet docs (chain signatures, key derivation): https://docs.sig.network
31
57
  // - Compact language reference (modules, types):
32
58
  // https://docs.midnight.network/compact/reference/compact-reference
33
- //
34
- // Import from a contract (resolved through node_modules via COMPACT_PATH —
35
- // see the consuming package's compile script):
36
- // import "@sig-net/midnight/src/Signet";
37
59
 
38
60
  module Signet {
39
61
  import CompactStandardLibrary;
40
62
 
41
- // ---- Identifiers ---------------------------------------------------------
63
+ // ---- SignBidirectionalEvent ------------------------------------------------------------------------
64
+
65
+ // Structured request for a signature on a transaction. Generically configured by client contracts
66
+ // based on the type of transaction for which a signature is requested (EVM Type 2, Solana etc.).
67
+ export struct SignBidirectionalEvent<TxParams, #LenOutputDeserialization, #LenRespondSerialization> {
68
+ // Address of the client contract that stores and notifies of this SignBidirectionalEvent.
69
+ // Set with kernel.self().
70
+ sender: ContractAddress;
71
+
72
+ // Optional nonce field settable by client contracts to ensure SignBidirectionalEvent uniqueness if desired.
73
+ requestNonce: Uint<64>;
74
+
75
+ // MPC Key Derivation Parameters.
76
+ // Every key the MPC signs with is scoped by the requesting contract:
77
+ // derivedSigningKey = f(mpcRootKey[keyVersion], sender, path)
78
+ // The path is 32 opaque bytes of the client contract's choosing (e.g a fixed literal
79
+ // for a contract-owned account or a hash of a caller's secret for per-user
80
+ // accounts): no format is enforced. The sender is always part of the
81
+ // derivation, so no contract can reach another contract's derived keys.
82
+ keyVersion: Uint<8>; // MPC root-key version to derive from. Set to 1.
83
+ path: Bytes<32>; // Key derivation path: 32 opaque bytes, client contract's choice.
84
+
85
+ // MPC Parameters.
86
+ algo: MPCSignatureAlgorithm; // Signature algorithm scheme, e.g. ecdsa
87
+ dest: MPCDestination; // Destination field reserved for future use. Set to unused.
88
+ params: Bytes<64>; // Extra MPC parameters reserved for future use. Leave blank.
89
+
90
+ // Specific destination chain Transaction parameters.
91
+ txParamType: TxParamType; // Off-chain tag indicating what type txParams is set to.
92
+ txParams: TxParams; // The generic transaction-param payload
93
+ caip2Id: Bytes<32>; // Target chain in CAIP-2 form, e.g. "eip155:11155111"
94
+ // (https://chainagnostic.org/CAIPs/caip-2)
95
+
96
+ // Serialisation Schemas.
97
+ // outputDeserializationSchema tells the MPC how to deserialise the destination chain's
98
+ // execution output (destination chain -> MPC). respondSerializationSchema tells the MPC
99
+ // how to serialise that output into the RespondBidirectionalEvent it posts back
100
+ // (MPC -> Midnight). Both are ABI-style JSON schema strings, sized per client contract.
101
+ outputDeserializationSchema: Bytes<LenOutputDeserialization>;
102
+ respondSerializationSchema: Bytes<LenRespondSerialization>;
103
+ }
42
104
 
43
- // Nominal id of a signet request. Deliberately CHAIN-AGNOSTIC: downstream
44
- // consumers (the signet contract, response pollers, the MPC)
45
- // treat it as an opaque 32-byte key, whatever kind of request it identifies.
46
- // Each request kind mints ids via its own domain-separated hash circuit
47
- // (see calculateRequestId). `new type` makes it NOT interchangeable
48
- // with any other Bytes<32> — cast explicitly: `someHash as RequestId`.
49
- export new type RequestId = Bytes<32>;
105
+ // MPCSignatureAlgorithm indicates which signature algorithm
106
+ // will be used by the MPC to generate requested signature.
107
+ export enum MPCSignatureAlgorithm {
108
+ ecdsa, // ECDSA signature algorithm
109
+ reserved, // Reserved for future use. Exists to keep enum >= 2 (proof server bug).
110
+ }
50
111
 
51
- // ---- The canonical request/response pair ----------------------------------
52
- // 1. Client contract stores a SignBidirectionalRequest in its own ledger and registers a
53
- // SignBidirectionalNotification in the central signet contract's registry, requesting a
54
- // signature from the MPC
55
- // 2. MPC polls the registry, reads the request from the client's ledger, and posts a
56
- // SignatureResponse (the requested signature) to the central signet contract
57
- // 3. Client polls the signature response log by request id, verifies the posted
58
- // SignatureResponse, broadcasts txn to destination chain
59
- // 4. MPC observes txn execution on destination chain, posts a RespondBidirectional
60
- // attestation to the central signet contract; the client polls it by request id
61
-
62
- // TxParamType indiciates which transaction-param decomposition a request
112
+ // MPCDestination used to specify destination field for SignBidirectionalEvent.
113
+ // Currently unused.
114
+ export enum MPCDestination {
115
+ unused,
116
+ reserved, // Reserved for future use. Exists to keep enum >= 2 (proof server bug).
117
+ }
118
+
119
+ // TxParamType indicates which transaction-param decomposition a request
63
120
  // contains so an off-chain consumer (e.g. the MPC) knows how to re-assemble it.
64
- // In-circuit this is redundant as the type is fixed at compile time (i.e. monomorphized).
65
121
  export enum TxParamType {
66
122
  evmType2, // EVMType2TxParams: an EIP-1559 EVM transaction (envelope + calldata)
67
- reserved, // never used — exists only to keep the enum at >= 2 variants. A
68
- // 1-variant enum compiles to a zero-byte value the proof server's
69
- // field-repr parser reads as ZERO field elements while the compiler
70
- // allocates ONE, so persistentHash over any struct containing it
71
- // (i.e. every request id) fails to prove.
123
+ reserved, // Reserved for future use. Exists to keep enum >= 2 (proof server bug).
124
+ }
125
+
126
+ // Construction helper for the canonical Signet SignBidirectionalEvent
127
+ export pure circuit constructSignBidirectionalEvent<TxParams, #LenOutputDeserialization, #LenRespondSerialization>(
128
+ sender: ContractAddress,
129
+ requestNonce: Uint<64>,
130
+ keyVersion: Uint<8>,
131
+ path: Bytes<32>,
132
+ algo: MPCSignatureAlgorithm,
133
+ dest: MPCDestination,
134
+ params: Bytes<64>,
135
+ txParamType: TxParamType,
136
+ txParams: TxParams,
137
+ caip2Id: Bytes<32>,
138
+ outputDeserializationSchema: Bytes<LenOutputDeserialization>,
139
+ respondSerializationSchema: Bytes<LenRespondSerialization>,
140
+ ): SignBidirectionalEvent<TxParams, LenOutputDeserialization, LenRespondSerialization> {
141
+ // validate supported values
142
+ assert(keyVersion >= 1, "keyVersion must be >= 1");
143
+
144
+ // construct and return request
145
+ return SignBidirectionalEvent<TxParams, LenOutputDeserialization, LenRespondSerialization> {
146
+ sender,
147
+ requestNonce,
148
+ keyVersion,
149
+ path,
150
+ algo,
151
+ dest,
152
+ params,
153
+ txParamType,
154
+ txParams,
155
+ caip2Id,
156
+ outputDeserializationSchema,
157
+ respondSerializationSchema,
158
+ };
72
159
  }
73
160
 
74
- // SignBidirectionalRequest is the record a Client contract stores to request
75
- // a signature from the MPC (and hashes into its RequestId).
76
- export struct SignBidirectionalRequest<TxParams> {
77
- requestNonce: Uint<64>; // contract-local nonce at creation time
78
-
79
- txParamType: TxParamType; // which decomposition txParams is (off-chain decode tag)
80
- txParams: TxParams; // the generic transaction-param payload
81
-
82
- // TODO: confirm sufficient size for CAIP-2 standard.
83
- caip2Id: Bytes<32>; // target chain in CAIP-2 form, e.g. "eip155:11155111"
84
- // (https://chainagnostic.org/CAIPs/caip-2)
85
- // TODO: can be much smaller
86
- keyVersion: Uint<32>; // MPC root-key version to derive from (>= 1;
87
- // version 0 is the unsupported legacy format)
88
- // TODO: consider possible optimisation. keep unchanged for now.
89
- path: Bytes<256>; // key-derivation path; by signet-Midnight
90
- // convention the canonical lowercase hex of the
91
- // caller's identity commitment, zero-padded
92
- // (enforced by assertPathCommitment)
93
-
94
- // TODO: Possibly ENUMs, these fields are not used so we could make them small now.
95
- algo: Bytes<32>; // signature scheme, e.g. "ecdsa"
96
- dest: Bytes<32>; // response destination, e.g. "ethereum"
97
- params: Bytes<64>; // scheme-specific extras (opaque to contracts)
98
-
99
- // Look at most popular defi contracts for schematisation of schema types
100
- // TODO: dynamically size these 2 schemas to reduce proving type
101
- outputDeserializationSchema: Bytes<128>; // MPC output_deserialization_schema
102
- // (destination chain -> MPC)
103
- respondSerializationSchema: Bytes<128>; // MPC respond_serialization_schema
104
- // (MPC -> source chain)
161
+ // ---- SignBidirectionalEvent RequestId --------------------------------------------------------------
162
+
163
+ // 32 byte chain agnostic nominal id, calculated as persistent hash of SignBidirectionalEvent
164
+ // (see calculateRequestId).
165
+ export new type RequestId = Bytes<32>;
166
+
167
+ // RequestId calculation helper: the persistent hash of the full event record, so the id
168
+ // commits to every field, including the sender address (which scopes ids per contract).
169
+ // There is deliberately no extra domain-separation tag. Off-chain consumers (the MPC,
170
+ // clients) must compute this exact hash to locate and authenticate requests.
171
+ export pure circuit calculateRequestId<TxParams, #LenOutputDeserialization, #LenRespondSerialization>(
172
+ request: SignBidirectionalEvent<TxParams, LenOutputDeserialization, LenRespondSerialization>
173
+ ): RequestId {
174
+ return persistentHash<SignBidirectionalEvent<TxParams, LenOutputDeserialization, LenRespondSerialization>>(request) as RequestId;
105
175
  }
106
176
 
107
- // External contract surface of the central signet contract: a client cross-
108
- // contract-calls notifyBidirectionalSignatureRequest to register the request
109
- // in the central notification registry, which the MPC polls.
110
- export contract SignetNotifier {
111
- circuit notifyBidirectionalSignatureRequest(
177
+ // ---- Client Contract: SignBidirectionalEvent Map ---------------------------------------------------
178
+
179
+ // Named mapping of RequestId to SignBidirectionalEvent. Declared by client contracts
180
+ // in their ledger. Client contract tells MPC field location of this map when making a cross
181
+ // contract call, via SignetSigner, to the Signet singleton. The MPC looks here to find
182
+ // SignBidirectionalEvents.
183
+ export new type SignBidirectionalEventMap<TxParams, #LenOutputDeserialization, #LenRespondSerialization> = Map<RequestId, SignBidirectionalEvent<TxParams, LenOutputDeserialization, LenRespondSerialization>>;
184
+
185
+ // ---- Cross Contract SignBidirectionalEventNotification ---------------------------------------------
186
+
187
+ // Contract interface used by client contract to make cross contract calls to
188
+ // Signet singleton to notify the MPC of a SignBidirectionalEvent.
189
+ export contract SignetSigner {
190
+ // Called by client contracts to notify the MPC of a SignBidirectionalEvent.
191
+ circuit signBidirectionalEvent(
192
+ // Canonical id of the SignBidirectionalEvent (calculate with calculateRequestId).
112
193
  requestId: RequestId,
113
- notification: SignBidirectionalNotification
114
- ): [];
194
+ // Versioned notification of the SignBidirectionalEvent
195
+ // (construct with constructSignBidirectionalEventNotificationV1).
196
+ notification: SignBidirectionalEventNotification
197
+ ): SignetMapKey;
115
198
  }
116
199
 
117
- // SignBidirectionalNotification is the record a client registers in the
118
- // central signet contract's notification registry to tell the MPC a
119
- // SignBidirectionalRequest was stored in the client's own ledger — and WHERE
120
- // to read the authenticated copy. Attribution comes from whose authenticated
121
- // state the MPC reads, never from a field here.
122
- export struct SignBidirectionalNotification {
123
- version: Uint<8>; // payload layout tag; 1 = the V1 layout below
124
- payload: Bytes<65>; // V1: callerAddress (32) ++ requestId (32) ++ requestsIndexField (1)
200
+ // Notification delivered by client contracts via cross contract call to Signet singleton
201
+ // (via SignetSigner interface) to notify the MPC of a SignBidirectionalEvent.
202
+ export struct SignBidirectionalEventNotification {
203
+ version: Uint<8>; // payload layout tag; 1 = the V1 layout:
204
+ // callerAddress (32) ++ requestsIndexField (1) ++ zero padding (95)
205
+ payload: Bytes<128>;
125
206
  }
126
207
 
127
- // Build the V1 notification: manually pack the caller's raw address bytes,
128
- // the raw request id, and the request-index ledger-field position into the
129
- // 65-byte payload (exact, byte-concatenated, no padding) and tag
130
- // version = 1. Client contracts call this instead of hand-constructing the
131
- // envelope; off-chain readers decode by the same fixed offsets.
132
- export pure circuit constructSignBidirectionalNotificationV1(
208
+ // Construction helper for the V1 SignBidirectionalEventNotification
209
+ export pure circuit constructSignBidirectionalEventNotificationV1(
210
+ // Address of the client contract that stores and notifies of this SignBidirectionalEvent.
211
+ // Set with kernel.self(). MUST match sender in associated SignBidirectionalEvent.
133
212
  callerAddress: ContractAddress,
134
- requestId: Bytes<32>,
135
213
  requestsIndexField: Uint<8>
136
- ): SignBidirectionalNotification {
137
- return SignBidirectionalNotification {
214
+ ): SignBidirectionalEventNotification {
215
+ return SignBidirectionalEventNotification {
138
216
  version: 1,
139
- payload: Bytes[...callerAddress.bytes, ...requestId, requestsIndexField]
217
+ // 128 = 32 + 1 + 95
218
+ payload: Bytes[...callerAddress.bytes, requestsIndexField, ...default<Bytes<95>>]
140
219
  };
141
220
  }
142
221
 
143
- // The counterpart to SignBidirectionalRequest: the record the MPC posts to
144
- // the central signet contract's response log after it observes the request
145
- // (mirrors the MPC's canonical Signature { big_r, s, recovery_id }).
146
- // Contains the MPC's secp256k1 signature over the requested transaction.
147
- // The requesting client uses this to rebuild a signed transaction and broadcast.
148
- export struct SignatureResponse {
222
+ // Counterpart to SignBidirectionalEvent containing MPC generated signature requested by a SignBidirectionalEvent.
223
+ // Posted by MPC back to Signet singleton for clients to extract to build a signed transaction.
224
+ export struct SignatureRespondedEvent {
149
225
  bigRx: Bytes<32>; // signature R.x
150
226
  bigRy: Bytes<32>; // signature R.y
151
227
  s: Bytes<32>; // signature s
152
228
  recoveryId: Uint<8>; // parity of R.y for public-key recovery
153
229
  }
154
230
 
155
- // The counterpart to SignatureResponse, posted by the MPC after
156
- // it observes execution of the signed transaction on the destination chain.
157
- // This response contains execution output and a secp256k1 ECDSA attestation
158
- // as to its authenticity. The attestation is verified IN-CIRCUIT against the
159
- // signet contract's sealed MPC key (secp256k1EcdsaVerify), signed over
160
- // signetAttestationMessage(requestId, serializedOutput, outputLen). The
161
- // signing public key is passed alongside as a circuit argument, not stored
162
- // here — it is pinned by hash against the sealed mpcPubKeyHash.
163
- export struct RespondBidirectional {
231
+ // Structure containing the output of a transaction executed on a foreign blockchain,
232
+ // ECDSA-signed (secp256k1) over the attestation digest of the request id and output
233
+ // (see signetAttestationDigest) by the MPC's RESPONSE key for the requesting contract:
234
+ // mpcResponseKey = f(mpcRootKey[keyVersion], sender, "midnight response key")
235
+ // i.e. the same derivation as request-signing keys but with the FIXED
236
+ // respond-bidirectional path, so it is per-client-contract yet independent of any
237
+ // request's own path (mirrors the MPC's per-chain "<chain> response key" convention).
238
+ // Posted by MPC back to Signet singleton for clients to extract, verify and deliver
239
+ // back to the client contract. The singleton stores posts UNVERIFIED: client contracts
240
+ // must verify the signature themselves against their MPC response key, via
241
+ // verifyRespondBidirectionalEvent (see verifyResponse in the caller-contract example).
242
+ export struct RespondBidirectionalEvent {
164
243
  // ABI-encoded return data of the remote call (canonical
165
244
  // serialized_output), zero-padded. First 32-byte word == 1 signals
166
245
  // success; a 0xdeadbeef prefix is the MPC's error sentinel for a
@@ -170,15 +249,78 @@ module Signet {
170
249
  // Meaningful byte count of serializedOutput.
171
250
  outputLen: Uint<8>;
172
251
 
173
- // ECDSA signature scalars r and s as LITTLE-ENDIAN 32-byte values (the byte
174
- // order the `Secp256k1Scalar as Bytes<32>` cast produces). A Secp256k1Scalar
175
- // cannot itself reside in ledger state (the compiler rejects it), so the
176
- // stored record carries bytes and the verifying circuits cast them back with
177
- // `as Secp256k1Scalar` before calling secp256k1EcdsaVerify.
178
- sigR: Bytes<32>;
179
- sigS: Bytes<32>;
252
+ // ECDSA signature scalars as LITTLE-ENDIAN 32-byte values (the byte order the
253
+ // `Secp256k1Scalar as Bytes<32>` cast produces). Stored as bytes because
254
+ // Secp256k1Scalar values cannot live in ledger state (compiler limitation).
255
+ // r is the MPC signature's big_r.x reduced mod the curve order.
256
+ r: Bytes<32>;
257
+ s: Bytes<32>;
258
+
259
+ // Recovery id (parity of R.y), for off-chain public key recovery only.
260
+ // Unused in-circuit: verification takes the public key as an explicit input.
261
+ recoveryId: Uint<8>;
262
+ }
263
+
264
+ // SignetMapKey used to locate SignBidirectionalEventNotifications, SignatureRespondedEvent and
265
+ // RespondBidirectionalEvents in Signet singleton contract storage.
266
+ export struct SignetMapKey {
267
+ count: Uint<64>;
268
+ requestId: RequestId;
269
+ };
270
+
271
+ // The 32-byte digest the MPC ECDSA-signs to attest a remote execution:
272
+ // the persistent hash of the request id together with hash(serializedOutput, outputLen).
273
+ // The inner hash binds the output together with its length so neither can be swapped
274
+ // under a reused signature. secp256k1EcdsaVerify interprets the digest as a BIG-ENDIAN
275
+ // integer reduced mod the curve order (RFC 6979 convention). ONE implementation for
276
+ // everyone: client contracts verify against it in-circuit, and off-chain signers (the
277
+ // MPC, tests) call the compiled copy via circuits.compact, never a TS re-implementation.
278
+ export pure circuit signetAttestationDigest(
279
+ requestId: RequestId,
280
+ serializedOutput: Bytes<128>,
281
+ outputLen: Uint<8>
282
+ ): Bytes<32> {
283
+ const outHash = persistentHash<Vector<2, Bytes<32>>>([
284
+ persistentHash<Bytes<128>>(serializedOutput),
285
+ outputLen as Field as Bytes<32>
286
+ ]);
287
+ return persistentHash<Vector<2, Bytes<32>>>([requestId as Bytes<32>, outHash]);
288
+ }
289
+
290
+ // Verify a RespondBidirectionalEvent against a secp256k1 public key: the event's
291
+ // ECDSA signature must sign the attestation digest of (requestId, output). This is
292
+ // the check client contracts run before accepting a response (the Signet singleton
293
+ // stores posts unverified); pass the contract's MPC RESPONSE key (derived off-chain
294
+ // from the contract's address + the fixed path "midnight response key", typically
295
+ // hash-pinned in the contract's ledger at deploy time).
296
+ export pure circuit verifyRespondBidirectionalEvent(
297
+ requestId: RequestId,
298
+ respondBidirectionalEvent: RespondBidirectionalEvent,
299
+ mpcResponseKey: Secp256k1Point
300
+ ): Boolean {
301
+ return secp256k1EcdsaVerify(
302
+ signetAttestationDigest(
303
+ requestId,
304
+ respondBidirectionalEvent.serializedOutput,
305
+ respondBidirectionalEvent.outputLen
306
+ ),
307
+ Secp256k1EcdsaSignature {
308
+ r: respondBidirectionalEvent.r as Secp256k1Scalar,
309
+ s: respondBidirectionalEvent.s as Secp256k1Scalar,
310
+ },
311
+ mpcResponseKey
312
+ );
313
+ }
314
+
315
+ // Hash a secp256k1 public key exactly as client contracts pin it at deploy time
316
+ // (sealed `persistentHash<Secp256k1Point>`), so off-chain code can recompute the
317
+ // pin without re-deploying.
318
+ export pure circuit signetKeyHash(pk: Secp256k1Point): Bytes<32> {
319
+ return persistentHash<Secp256k1Point>(pk);
180
320
  }
181
321
 
322
+ // ---- EVM Type2 Transaction Parameters ---------------------------------------------------
323
+
182
324
  // The EVM transaction to be signed, decomposed into typed fields
183
325
  // (EIP-1559 / type-2: https://eips.ethereum.org/EIPS/eip-1559).
184
326
  // Decomposed rather than pre-serialized so the requesting contract can
@@ -222,294 +364,4 @@ module Signet {
222
364
  noWords: Uint<16>; // No. of used words in the words list
223
365
  words: Vector<maxWords, Bytes<32>>;
224
366
  }
225
-
226
- // The field value to embed in a calldata word for an EVM address: the
227
- // BIG-ENDIAN numeric reading of its 20 display-order bytes. Embedding the
228
- // address directly (`addr as Field`) would be wrong: that cast is
229
- // little-endian, so the MPC's LE-decode -> BE-encode step would emit the
230
- // address byte-reversed in the calldata. Usage: a calldata word
231
- // `evmAddressAbiValue(addr) as Bytes<32>`.
232
- // (Uint amounts need no helper: `amount as Field` already embeds the
233
- // correct numeric value.)
234
- export pure circuit evmAddressAbiValue(addr: Bytes<20>): Field {
235
- const a0 = slice<1>(addr, 0) as Field;
236
- const a1 = a0 * 256 + (slice<1>(addr, 1) as Field);
237
- const a2 = a1 * 256 + (slice<1>(addr, 2) as Field);
238
- const a3 = a2 * 256 + (slice<1>(addr, 3) as Field);
239
- const a4 = a3 * 256 + (slice<1>(addr, 4) as Field);
240
- const a5 = a4 * 256 + (slice<1>(addr, 5) as Field);
241
- const a6 = a5 * 256 + (slice<1>(addr, 6) as Field);
242
- const a7 = a6 * 256 + (slice<1>(addr, 7) as Field);
243
- const a8 = a7 * 256 + (slice<1>(addr, 8) as Field);
244
- const a9 = a8 * 256 + (slice<1>(addr, 9) as Field);
245
- const a10 = a9 * 256 + (slice<1>(addr, 10) as Field);
246
- const a11 = a10 * 256 + (slice<1>(addr, 11) as Field);
247
- const a12 = a11 * 256 + (slice<1>(addr, 12) as Field);
248
- const a13 = a12 * 256 + (slice<1>(addr, 13) as Field);
249
- const a14 = a13 * 256 + (slice<1>(addr, 14) as Field);
250
- const a15 = a14 * 256 + (slice<1>(addr, 15) as Field);
251
- const a16 = a15 * 256 + (slice<1>(addr, 16) as Field);
252
- const a17 = a16 * 256 + (slice<1>(addr, 17) as Field);
253
- const a18 = a17 * 256 + (slice<1>(addr, 18) as Field);
254
- return a18 * 256 + (slice<1>(addr, 19) as Field);
255
- }
256
-
257
- // ---- Requester Ledger Layout -----------------------------------------------
258
-
259
- // The on-ledger request index (instantiated at the contract's own calldata
260
- // slot capacity). It may sit at ANY ledger field: each notification carries
261
- // the field position holding it (the requestsIndexField byte of the V1
262
- // payload), which is how the MPC finds the index to read the authenticated
263
- // request back from.
264
- export new type SignBidirectionalRequestIndex<TxParams> = Map<RequestId, SignBidirectionalRequest<TxParams>>;
265
-
266
- // The contract-local request counter: a convenient source of each request's
267
- // requestNonce (identical requests must hash to distinct ids). Purely
268
- // contract-local; nothing off-chain reads it.
269
- export new type SignetNonce = Counter;
270
-
271
- // ---- Signet Contract Ledger Layout ------------------------------------------
272
- // The CENTRAL signet contract on Midnight stores both MPC response kinds and
273
- // the request notification registry.
274
- // Its layout convention (find everything knowing only the contract address):
275
- // field 0: SignatureResponseCounterIndex
276
- // field 1: SignatureResponseIndex
277
- // field 2: RespondBidirectionalIndex
278
- // field 3: mpcPubKeyHash (sealed Bytes<32>)
279
- // field 4: SignBidirectionalNotificationIndex
280
- //
281
- // Each store gets the defense its contents allow:
282
- // - Signature responses over the EVM transaction are NOT verified
283
- // in-circuit — by design, to avoid the proving cost of an ECDSA check on
284
- // every posted signature — so their log is UNAUTHENTICATED and
285
- // append-only: each post lands under a fresh (requestId, count) key; a
286
- // rogue poster can add noise but never overwrite or block the genuine
287
- // response. Clients poll a request's entries by request id and verify
288
- // each signature off-chain.
289
- // - Respond-bidirectional attestations are secp256k1 ECDSA verified
290
- // IN-CIRCUIT (secp256k1EcdsaVerify) at post time against the sealed MPC
291
- // key hash. Garbage cannot land; one authenticated slot per request
292
- // suffices, first VALID write wins. (secp256k1EcdsaVerify does not
293
- // enforce low-s, so the malleated twin (r, n-s) also verifies — re-posts
294
- // can differ byte-for-byte, which is exactly why the slot is keyed by
295
- // request id and the first valid write is kept.) Clients poll the slot by
296
- // request id.
297
- // - Notifications are UNAUTHENTICATED pointers ("contract X stored
298
- // request Y at index field Z"): anyone may register one, so the MPC
299
- // never trusts a notification's content — it authenticates by reading
300
- // the request back from the NAMED caller's own ledger and refusing
301
- // records whose recomputed id does not match.
302
-
303
- // Per-request post counter for signature responses (field 0). The counter
304
- // reads the number of posts made for that request; entries exist for
305
- // counts 0..counter-1.
306
- export new type SignatureResponseCounterIndex = Map<RequestId, Counter>;
307
-
308
- // Composite key of one posted signature response: the request id it
309
- // answers plus this post's 0-based count, sourced from
310
- // SignatureResponseCounterIndex. (Respond-bidirectional posts need no key
311
- // struct — authenticated posts are keyed by request id alone.)
312
- export struct SignetResponseKey {
313
- count: Uint<64>;
314
- requestId: RequestId;
315
- };
316
-
317
- // The on-ledger signature response log (field 1), keyed by
318
- // (requestId, count).
319
- export new type SignatureResponseIndex = Map<SignetResponseKey, SignatureResponse>;
320
-
321
- // The on-ledger respond-bidirectional index (field 2): one
322
- // contract-verified attestation per request.
323
- export new type RespondBidirectionalIndex = Map<RequestId, RespondBidirectional>;
324
-
325
- // The on-ledger notification registry (field 4): one notification per
326
- // request id, append-only (a re-notify is an idempotent overwrite; there is
327
- // deliberately no removal circuit). The MPC discovers requests by
328
- // enumerating this map and diffing against the request ids it has already
329
- // served.
330
- export new type SignBidirectionalNotificationIndex = Map<RequestId, SignBidirectionalNotification>;
331
-
332
- // ---- Request id -----------------------------------------------------------
333
-
334
- // Canonical id of an EVM signature request: the domain-separated persistent
335
- // hash of the entire request record. The id therefore commits to every
336
- // field — the MPC (or anyone) recomputes it off-chain and refuses records
337
- // whose stored key does not match.
338
- //
339
- // The "signet:midnight:request" domain tag partitions the RequestId space:
340
- // ids minted by this circuit can never collide or be confused with hashes
341
- // minted under any other tag.
342
- //
343
- // Off-chain twin: being type-parameterized, this circuit cannot be exported
344
- // through circuits.compact, so signet-requests.ts carries a TS twin
345
- // (calculateRequestId) that MUST stay in lockstep — see the
346
- // deviation note there before changing anything here.
347
- export pure circuit calculateRequestId<TxParams>(
348
- request: SignBidirectionalRequest<TxParams>
349
- ): RequestId {
350
- return persistentHash<Vector<2, Bytes<32>>>([
351
- pad(32, "signet:midnight:request"),
352
- persistentHash<SignBidirectionalRequest<TxParams>>(request)
353
- ]) as RequestId;
354
- }
355
-
356
- // ---- Respond-bidirectional attestation message ------------------------------
357
-
358
- // The 32-byte digest the MPC ECDSA-signs to attest a remote execution:
359
- // the domain-separated persistent hash of (requestId, hash(serializedOutput,
360
- // outputLen)). The inner hash binds the output TOGETHER WITH its length, so
361
- // neither can be swapped under a reused signature. The "signet:midnight:attest"
362
- // domain tag partitions this digest from the EVM transaction hashes the SAME
363
- // secp256k1 MPC key signs, so an attestation signature can never be replayed as
364
- // a transaction signature or vice versa.
365
- //
366
- // The digest is passed straight to secp256k1EcdsaVerify as msgHash (interpreted
367
- // big-endian, RFC 6979 convention — no byte reversal).
368
- //
369
- // ONE implementation for everyone: the signet contract verifies posts
370
- // against it, requester contracts (e.g. the vault's claim) re-verify against it, and
371
- // off-chain signers call the compiled copy (via circuits.compact) — never
372
- // a TS re-implementation.
373
- export pure circuit signetAttestationMessage(
374
- requestId: RequestId,
375
- serializedOutput: Bytes<128>,
376
- outputLen: Uint<8>
377
- ): Bytes<32> {
378
- const outHash = persistentHash<Vector<2, Bytes<32>>>([
379
- persistentHash<Bytes<128>>(serializedOutput),
380
- outputLen as Field as Bytes<32>
381
- ]);
382
- return persistentHash<Vector<3, Bytes<32>>>([
383
- pad(32, "signet:midnight:attest"),
384
- requestId as Bytes<32>,
385
- outHash
386
- ]);
387
- }
388
-
389
- // ---- Path <-> identity binding ---------------------------------------------
390
- // Compact cannot ENCODE bytes->hex in-circuit (nibble extraction needs
391
- // integer div/mod/shift, which the prime field has no native op for). But it
392
- // CAN DECODE hex->bytes with multiply/add/compare. So the derivation path (a
393
- // public value) carries the hex string, and we verify in-circuit that it
394
- // canonically decodes to the commitment. The prover cannot supply a wrong or
395
- // non-canonical string, so this is as strong as computing the hex on-chain —
396
- // and it lets the MPC treat the path as a plain opaque string (no hex
397
- // special-case in the MPC).
398
-
399
- // Decode one canonical hex character ('0'-'9', lowercase 'a'-'f') to 0..15.
400
- pure circuit charToNibble(c: Uint<8>): Uint<8> {
401
- const isDigit = (c >= 48) && (c <= 57); // '0'..'9'
402
- const isLower = (c >= 97) && (c <= 102); // 'a'..'f' (lowercase only => canonical)
403
- assert(isDigit || isLower, "non-canonical hex char");
404
- const base = c - 48; // safe: c >= 48 in both accepted ranges
405
- const adjust = isLower ? 39 : 0; // 'a'(97) - 48 - 39 = 10
406
- return base - adjust;
407
- }
408
-
409
- // Assert that hexStr (64 lowercase hex chars) decodes exactly to commitment.
410
- export pure circuit assertHexOf(commitment: Bytes<32>, hexStr: Bytes<64>): [] {
411
- for (const i of 0 .. 32) {
412
- const hi = charToNibble(slice<1>(hexStr, 2 * i) as Uint<8>);
413
- const lo = charToNibble(slice<1>(hexStr, 2 * i + 1) as Uint<8>);
414
- assert((slice<1>(commitment, i) as Field) == (hi as Field) * 16 + (lo as Field),
415
- "path hex does not match commitment");
416
- }
417
- }
418
-
419
- // // Assert the full path <-> identity binding: the path is EXACTLY the
420
- // canonical lowercase hex of `commitment`, zero-padded to 256 bytes. This
421
- // makes identity -> path -> MPC-derived account a forced 1:1 mapping: no one
422
- // can request signatures under another identity's derived key, and no
423
- // identity can spread across multiple derived keys via case/suffix
424
- // malleability. See https://docs.sig.network/ for path-based derivation.
425
- export pure circuit assertPathCommitment(
426
- commitment: Bytes<32>,
427
- path: Bytes<256>
428
- ): [] {
429
- assertHexOf(commitment, slice<64>(path, 0));
430
- assert(slice<192>(path, 64) == pad(192, ""), "Path must be zero-padded after hex");
431
- }
432
-
433
- // ---- Request construction ---------------------------------------------------
434
-
435
- // Assemble the canonical request record for a CALLER-IDENTITY request,
436
- // enforcing the path <-> identity binding on the way — the MPC signs with
437
- // the calling user's derived key and no one else's.
438
- // The requesting contract supplies:
439
- // - pathCommitment: the DISCLOSED identity commitment of the caller
440
- // (derived from a secret-key witness — see the vault's userCommitment)
441
- // - requestNonce: its contract-local nonce (typically a Counter read)
442
- // - txParams: the transaction decomposition; the contract MUST
443
- // build (or overwrite) any part it enforces — calldata above all —
444
- // rather than pass caller input through
445
- // - the remaining MPC routing fields, caller-supplied
446
- // Store the result under calculateRequestId(request) in the
447
- // request index.
448
- export pure circuit constructSignBidirectionalRequest<TxParams>(
449
- pathCommitment: Bytes<32>,
450
- requestNonce: Uint<64>,
451
- txParamType: TxParamType,
452
- txParams: TxParams,
453
- caip2Id: Bytes<32>,
454
- keyVersion: Uint<32>,
455
- path: Bytes<256>,
456
- algo: Bytes<32>,
457
- dest: Bytes<32>,
458
- params: Bytes<64>,
459
- outputDeserializationSchema: Bytes<128>,
460
- respondSerializationSchema: Bytes<128>
461
- ): SignBidirectionalRequest<TxParams> {
462
- assertPathCommitment(pathCommitment, path);
463
- return constructContractPathSignBidirectionalRequest<TxParams>(
464
- requestNonce,
465
- txParamType,
466
- txParams,
467
- caip2Id,
468
- keyVersion,
469
- path,
470
- algo,
471
- dest,
472
- params,
473
- outputDeserializationSchema,
474
- respondSerializationSchema
475
- );
476
- }
477
-
478
- // Assemble the canonical request record for a CONTRACT-OWNED derivation
479
- // path: a literal the requesting contract fixes in-circuit (e.g. a vault
480
- // treasury's "vault"), so the MPC signs with the CONTRACT's derived account
481
- // rather than a caller's. No identity binding applies — the contract itself
482
- // vouches for the path by hardcoding it; never pass a caller-supplied path
483
- // here (that would let anyone request signatures under any derived key —
484
- // use constructSignBidirectionalRequest for caller-identity requests).
485
- // Enforces the canonical keyVersion floor for both constructors.
486
- export pure circuit constructContractPathSignBidirectionalRequest<TxParams>(
487
- requestNonce: Uint<64>,
488
- txParamType: TxParamType,
489
- txParams: TxParams,
490
- caip2Id: Bytes<32>,
491
- keyVersion: Uint<32>,
492
- path: Bytes<256>,
493
- algo: Bytes<32>,
494
- dest: Bytes<32>,
495
- params: Bytes<64>,
496
- outputDeserializationSchema: Bytes<128>,
497
- respondSerializationSchema: Bytes<128>
498
- ): SignBidirectionalRequest<TxParams> {
499
- // The MPC rejects requests for the unsupported legacy key version 0.
500
- assert(keyVersion >= 1, "keyVersion must be >= 1");
501
- return SignBidirectionalRequest<TxParams> {
502
- requestNonce,
503
- txParamType,
504
- txParams,
505
- caip2Id,
506
- keyVersion,
507
- path,
508
- algo,
509
- dest,
510
- params,
511
- outputDeserializationSchema,
512
- respondSerializationSchema
513
- };
514
- }
515
367
  }