@sig-net/midnight 0.0.5 → 0.0.7

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 (58) hide show
  1. package/README.md +8 -0
  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 +70 -95
  19. package/dist/managed/compiler/contract-manifest.json +8 -8
  20. package/dist/managed/contract/index.d.ts +38 -42
  21. package/dist/managed/contract/index.js +225 -478
  22. package/dist/managed/contract/index.js.map +2 -2
  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 +102 -109
  32. package/dist/signet-contract-state-reader.d.ts.map +1 -1
  33. package/dist/signet-contract-state-reader.js +125 -119
  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 +275 -413
  53. package/src/circuits.compact +24 -25
  54. package/dist/schnorr.d.ts +0 -100
  55. package/dist/schnorr.d.ts.map +0 -1
  56. package/dist/schnorr.js +0 -144
  57. package/dist/schnorr.js.map +0 -1
  58. package/src/Schnorr.compact +0 -63
@@ -1,162 +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 Schnorr-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
+ }
111
+
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
+ }
50
118
 
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
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).
72
124
  }
73
125
 
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)
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
+ };
105
159
  }
106
160
 
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(
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;
175
+ }
176
+
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 an attestation as to its authenticity.
158
- // TODO: currently contains a Schnorr attestation --> migrate to ECDSA.
159
- 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 {
160
243
  // ABI-encoded return data of the remote call (canonical
161
244
  // serialized_output), zero-padded. First 32-byte word == 1 signals
162
245
  // success; a 0xdeadbeef prefix is the MPC's error sentinel for a
@@ -166,18 +249,78 @@ module Signet {
166
249
  // Meaningful byte count of serializedOutput.
167
250
  outputLen: Uint<8>;
168
251
 
169
- // The MPC attestation key (Jubjub). Hash-checked against the signet
170
- // contract's sealed mpcPubKeyHash at post time, and again by requester
171
- // contracts at claim time.
172
- pk: JubjubPoint;
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
+ }
173
263
 
174
- // Schnorr nonce commitment R.
175
- announcement: JubjubPoint;
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
+ };
176
270
 
177
- // Schnorr response scalar s.
178
- response: Field;
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);
179
320
  }
180
321
 
322
+ // ---- EVM Type2 Transaction Parameters ---------------------------------------------------
323
+
181
324
  // The EVM transaction to be signed, decomposed into typed fields
182
325
  // (EIP-1559 / type-2: https://eips.ethereum.org/EIPS/eip-1559).
183
326
  // Decomposed rather than pre-serialized so the requesting contract can
@@ -221,285 +364,4 @@ module Signet {
221
364
  noWords: Uint<16>; // No. of used words in the words list
222
365
  words: Vector<maxWords, Bytes<32>>;
223
366
  }
224
-
225
- // The field value to embed in a calldata word for an EVM address: the
226
- // BIG-ENDIAN numeric reading of its 20 display-order bytes. Embedding the
227
- // address directly (`addr as Field`) would be wrong: that cast is
228
- // little-endian, so the MPC's LE-decode -> BE-encode step would emit the
229
- // address byte-reversed in the calldata. Usage: a calldata word
230
- // `evmAddressAbiValue(addr) as Bytes<32>`.
231
- // (Uint amounts need no helper: `amount as Field` already embeds the
232
- // correct numeric value.)
233
- export pure circuit evmAddressAbiValue(addr: Bytes<20>): Field {
234
- const a0 = slice<1>(addr, 0) as Field;
235
- const a1 = a0 * 256 + (slice<1>(addr, 1) as Field);
236
- const a2 = a1 * 256 + (slice<1>(addr, 2) as Field);
237
- const a3 = a2 * 256 + (slice<1>(addr, 3) as Field);
238
- const a4 = a3 * 256 + (slice<1>(addr, 4) as Field);
239
- const a5 = a4 * 256 + (slice<1>(addr, 5) as Field);
240
- const a6 = a5 * 256 + (slice<1>(addr, 6) as Field);
241
- const a7 = a6 * 256 + (slice<1>(addr, 7) as Field);
242
- const a8 = a7 * 256 + (slice<1>(addr, 8) as Field);
243
- const a9 = a8 * 256 + (slice<1>(addr, 9) as Field);
244
- const a10 = a9 * 256 + (slice<1>(addr, 10) as Field);
245
- const a11 = a10 * 256 + (slice<1>(addr, 11) as Field);
246
- const a12 = a11 * 256 + (slice<1>(addr, 12) as Field);
247
- const a13 = a12 * 256 + (slice<1>(addr, 13) as Field);
248
- const a14 = a13 * 256 + (slice<1>(addr, 14) as Field);
249
- const a15 = a14 * 256 + (slice<1>(addr, 15) as Field);
250
- const a16 = a15 * 256 + (slice<1>(addr, 16) as Field);
251
- const a17 = a16 * 256 + (slice<1>(addr, 17) as Field);
252
- const a18 = a17 * 256 + (slice<1>(addr, 18) as Field);
253
- return a18 * 256 + (slice<1>(addr, 19) as Field);
254
- }
255
-
256
- // ---- Requester Ledger Layout -----------------------------------------------
257
-
258
- // The on-ledger request index (instantiated at the contract's own calldata
259
- // slot capacity). It may sit at ANY ledger field: each notification carries
260
- // the field position holding it (the requestsIndexField byte of the V1
261
- // payload), which is how the MPC finds the index to read the authenticated
262
- // request back from.
263
- export new type SignBidirectionalRequestIndex<TxParams> = Map<RequestId, SignBidirectionalRequest<TxParams>>;
264
-
265
- // The contract-local request counter: a convenient source of each request's
266
- // requestNonce (identical requests must hash to distinct ids). Purely
267
- // contract-local; nothing off-chain reads it.
268
- export new type SignetNonce = Counter;
269
-
270
- // ---- Signet Contract Ledger Layout ------------------------------------------
271
- // The CENTRAL signet contract on Midnight stores both MPC response kinds and
272
- // the request notification registry.
273
- // Its layout convention (find everything knowing only the contract address):
274
- // field 0: SignatureResponseCounterIndex
275
- // field 1: SignatureResponseIndex
276
- // field 2: RespondBidirectionalIndex
277
- // field 3: mpcPubKeyHash (sealed Bytes<32>)
278
- // field 4: SignBidirectionalNotificationIndex
279
- //
280
- // Each store gets the defense its contents allow:
281
- // - Signature responses are secp256k1 ECDSA — unverifiable in-circuit —
282
- // so their log is UNAUTHENTICATED and append-only: each post lands
283
- // under a fresh (requestId, count) key; a rogue poster can add noise
284
- // but never overwrite or block the genuine response. Clients poll a
285
- // request's entries by request id and verify each signature off-chain.
286
- // - Respond-bidirectional attestations are Schnorr on Jubjub — the
287
- // embedded curve — so the contract verifies them IN-CIRCUIT at post
288
- // time against its sealed MPC key hash. Garbage cannot land; one
289
- // authenticated slot per request suffices, first valid write wins.
290
- // Clients poll the slot by request id.
291
- // - Notifications are UNAUTHENTICATED pointers ("contract X stored
292
- // request Y at index field Z"): anyone may register one, so the MPC
293
- // never trusts a notification's content — it authenticates by reading
294
- // the request back from the NAMED caller's own ledger and refusing
295
- // records whose recomputed id does not match.
296
-
297
- // Per-request post counter for signature responses (field 0). The counter
298
- // reads the number of posts made for that request; entries exist for
299
- // counts 0..counter-1.
300
- export new type SignatureResponseCounterIndex = Map<RequestId, Counter>;
301
-
302
- // Composite key of one posted signature response: the request id it
303
- // answers plus this post's 0-based count, sourced from
304
- // SignatureResponseCounterIndex. (Respond-bidirectional posts need no key
305
- // struct — authenticated posts are keyed by request id alone.)
306
- export struct SignetResponseKey {
307
- count: Uint<64>;
308
- requestId: RequestId;
309
- };
310
-
311
- // The on-ledger signature response log (field 1), keyed by
312
- // (requestId, count).
313
- export new type SignatureResponseIndex = Map<SignetResponseKey, SignatureResponse>;
314
-
315
- // The on-ledger respond-bidirectional index (field 2): one
316
- // contract-verified attestation per request.
317
- export new type RespondBidirectionalIndex = Map<RequestId, RespondBidirectional>;
318
-
319
- // The on-ledger notification registry (field 4): one notification per
320
- // request id, append-only (a re-notify is an idempotent overwrite; there is
321
- // deliberately no removal circuit). The MPC discovers requests by
322
- // enumerating this map and diffing against the request ids it has already
323
- // served.
324
- export new type SignBidirectionalNotificationIndex = Map<RequestId, SignBidirectionalNotification>;
325
-
326
- // ---- Request id -----------------------------------------------------------
327
-
328
- // Canonical id of an EVM signature request: the domain-separated persistent
329
- // hash of the entire request record. The id therefore commits to every
330
- // field — the MPC (or anyone) recomputes it off-chain and refuses records
331
- // whose stored key does not match.
332
- //
333
- // The "signet:midnight:request" domain tag partitions the RequestId space:
334
- // ids minted by this circuit can never collide or be confused with hashes
335
- // minted under any other tag.
336
- //
337
- // Off-chain twin: being type-parameterized, this circuit cannot be exported
338
- // through circuits.compact, so signet-requests.ts carries a TS twin
339
- // (calculateRequestId) that MUST stay in lockstep — see the
340
- // deviation note there before changing anything here.
341
- export pure circuit calculateRequestId<TxParams>(
342
- request: SignBidirectionalRequest<TxParams>
343
- ): RequestId {
344
- return persistentHash<Vector<2, Bytes<32>>>([
345
- pad(32, "signet:midnight:request"),
346
- persistentHash<SignBidirectionalRequest<TxParams>>(request)
347
- ]) as RequestId;
348
- }
349
-
350
- // ---- Respond-bidirectional attestation message ------------------------------
351
-
352
- // The message the MPC Schnorr-signs to attest a remote execution:
353
- // (requestId, hash(serializedOutput, outputLen)) as four 16-byte
354
- // little-endian field limbs. The hash binds the output TOGETHER WITH its
355
- // length, so neither can be swapped under a reused signature; the 16-byte
356
- // split is needed because a 32-byte value overflows the BLS scalar field.
357
- // ONE implementation for everyone: the signet contract verifies posts
358
- // against it, requester contracts (e.g. the vault's claim) re-verify against it, and
359
- // off-chain signers call the compiled copy (via circuits.compact) — never
360
- // a TS re-implementation.
361
- export pure circuit signetAttestationMessage(
362
- requestId: RequestId,
363
- serializedOutput: Bytes<128>,
364
- outputLen: Uint<8>
365
- ): Vector<4, Field> {
366
- const rid = requestId as Bytes<32>;
367
- const outHash = persistentHash<Vector<2, Bytes<32>>>([
368
- persistentHash<Bytes<128>>(serializedOutput),
369
- outputLen as Field as Bytes<32>
370
- ]);
371
- return [
372
- slice<16>(rid, 0) as Field,
373
- slice<16>(rid, 16) as Field,
374
- slice<16>(outHash, 0) as Field,
375
- slice<16>(outHash, 16) as Field
376
- ];
377
- }
378
-
379
- // ---- Path <-> identity binding ---------------------------------------------
380
- // Compact cannot ENCODE bytes->hex in-circuit (nibble extraction needs
381
- // integer div/mod/shift, which the prime field has no native op for). But it
382
- // CAN DECODE hex->bytes with multiply/add/compare. So the derivation path (a
383
- // public value) carries the hex string, and we verify in-circuit that it
384
- // canonically decodes to the commitment. The prover cannot supply a wrong or
385
- // non-canonical string, so this is as strong as computing the hex on-chain —
386
- // and it lets the MPC treat the path as a plain opaque string (no hex
387
- // special-case in the MPC).
388
-
389
- // Decode one canonical hex character ('0'-'9', lowercase 'a'-'f') to 0..15.
390
- pure circuit charToNibble(c: Uint<8>): Uint<8> {
391
- const isDigit = (c >= 48) && (c <= 57); // '0'..'9'
392
- const isLower = (c >= 97) && (c <= 102); // 'a'..'f' (lowercase only => canonical)
393
- assert(isDigit || isLower, "non-canonical hex char");
394
- const base = c - 48; // safe: c >= 48 in both accepted ranges
395
- const adjust = isLower ? 39 : 0; // 'a'(97) - 48 - 39 = 10
396
- return base - adjust;
397
- }
398
-
399
- // Assert that hexStr (64 lowercase hex chars) decodes exactly to commitment.
400
- export pure circuit assertHexOf(commitment: Bytes<32>, hexStr: Bytes<64>): [] {
401
- for (const i of 0 .. 32) {
402
- const hi = charToNibble(slice<1>(hexStr, 2 * i) as Uint<8>);
403
- const lo = charToNibble(slice<1>(hexStr, 2 * i + 1) as Uint<8>);
404
- assert((slice<1>(commitment, i) as Field) == (hi as Field) * 16 + (lo as Field),
405
- "path hex does not match commitment");
406
- }
407
- }
408
-
409
- // // Assert the full path <-> identity binding: the path is EXACTLY the
410
- // canonical lowercase hex of `commitment`, zero-padded to 256 bytes. This
411
- // makes identity -> path -> MPC-derived account a forced 1:1 mapping: no one
412
- // can request signatures under another identity's derived key, and no
413
- // identity can spread across multiple derived keys via case/suffix
414
- // malleability. See https://docs.sig.network/ for path-based derivation.
415
- export pure circuit assertPathCommitment(
416
- commitment: Bytes<32>,
417
- path: Bytes<256>
418
- ): [] {
419
- assertHexOf(commitment, slice<64>(path, 0));
420
- assert(slice<192>(path, 64) == pad(192, ""), "Path must be zero-padded after hex");
421
- }
422
-
423
- // ---- Request construction ---------------------------------------------------
424
-
425
- // Assemble the canonical request record for a CALLER-IDENTITY request,
426
- // enforcing the path <-> identity binding on the way — the MPC signs with
427
- // the calling user's derived key and no one else's.
428
- // The requesting contract supplies:
429
- // - pathCommitment: the DISCLOSED identity commitment of the caller
430
- // (derived from a secret-key witness — see the vault's userCommitment)
431
- // - requestNonce: its contract-local nonce (typically a Counter read)
432
- // - txParams: the transaction decomposition; the contract MUST
433
- // build (or overwrite) any part it enforces — calldata above all —
434
- // rather than pass caller input through
435
- // - the remaining MPC routing fields, caller-supplied
436
- // Store the result under calculateRequestId(request) in the
437
- // request index.
438
- export pure circuit constructSignBidirectionalRequest<TxParams>(
439
- pathCommitment: Bytes<32>,
440
- requestNonce: Uint<64>,
441
- txParamType: TxParamType,
442
- txParams: TxParams,
443
- caip2Id: Bytes<32>,
444
- keyVersion: Uint<32>,
445
- path: Bytes<256>,
446
- algo: Bytes<32>,
447
- dest: Bytes<32>,
448
- params: Bytes<64>,
449
- outputDeserializationSchema: Bytes<128>,
450
- respondSerializationSchema: Bytes<128>
451
- ): SignBidirectionalRequest<TxParams> {
452
- assertPathCommitment(pathCommitment, path);
453
- return constructContractPathSignBidirectionalRequest<TxParams>(
454
- requestNonce,
455
- txParamType,
456
- txParams,
457
- caip2Id,
458
- keyVersion,
459
- path,
460
- algo,
461
- dest,
462
- params,
463
- outputDeserializationSchema,
464
- respondSerializationSchema
465
- );
466
- }
467
-
468
- // Assemble the canonical request record for a CONTRACT-OWNED derivation
469
- // path: a literal the requesting contract fixes in-circuit (e.g. a vault
470
- // treasury's "vault"), so the MPC signs with the CONTRACT's derived account
471
- // rather than a caller's. No identity binding applies — the contract itself
472
- // vouches for the path by hardcoding it; never pass a caller-supplied path
473
- // here (that would let anyone request signatures under any derived key —
474
- // use constructSignBidirectionalRequest for caller-identity requests).
475
- // Enforces the canonical keyVersion floor for both constructors.
476
- export pure circuit constructContractPathSignBidirectionalRequest<TxParams>(
477
- requestNonce: Uint<64>,
478
- txParamType: TxParamType,
479
- txParams: TxParams,
480
- caip2Id: Bytes<32>,
481
- keyVersion: Uint<32>,
482
- path: Bytes<256>,
483
- algo: Bytes<32>,
484
- dest: Bytes<32>,
485
- params: Bytes<64>,
486
- outputDeserializationSchema: Bytes<128>,
487
- respondSerializationSchema: Bytes<128>
488
- ): SignBidirectionalRequest<TxParams> {
489
- // The MPC rejects requests for the unsupported legacy key version 0.
490
- assert(keyVersion >= 1, "keyVersion must be >= 1");
491
- return SignBidirectionalRequest<TxParams> {
492
- requestNonce,
493
- txParamType,
494
- txParams,
495
- caip2Id,
496
- keyVersion,
497
- path,
498
- algo,
499
- dest,
500
- params,
501
- outputDeserializationSchema,
502
- respondSerializationSchema
503
- };
504
- }
505
367
  }