@sig-net/midnight 0.0.9 → 0.11.0

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 (44) hide show
  1. package/README.md +254 -112
  2. package/dist/constants.d.ts +1 -1
  3. package/dist/constants.js +1 -1
  4. package/dist/ecdsa-attestation.d.ts +53 -1
  5. package/dist/ecdsa-attestation.d.ts.map +1 -1
  6. package/dist/ecdsa-attestation.js +96 -4
  7. package/dist/ecdsa-attestation.js.map +1 -1
  8. package/dist/epsilon-derivation.d.ts +8 -8
  9. package/dist/epsilon-derivation.d.ts.map +1 -1
  10. package/dist/epsilon-derivation.js +15 -17
  11. package/dist/epsilon-derivation.js.map +1 -1
  12. package/dist/index.d.ts +3 -13
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +3 -13
  15. package/dist/index.js.map +1 -1
  16. package/dist/managed/compiler/contract-info.json +76 -24
  17. package/dist/managed/compiler/contract-manifest.json +8 -8
  18. package/dist/managed/contract/index.d.ts +18 -10
  19. package/dist/managed/contract/index.js +218 -103
  20. package/dist/managed/contract/index.js.map +1 -1
  21. package/dist/signature-requests-state-reader.d.ts +2 -2
  22. package/dist/signature-requests-state-reader.d.ts.map +1 -1
  23. package/dist/signature-requests-state-reader.js +32 -15
  24. package/dist/signature-requests-state-reader.js.map +1 -1
  25. package/dist/signature-response-verification.d.ts +1 -1
  26. package/dist/signature-response-verification.js +5 -5
  27. package/dist/signature-response-verification.js.map +1 -1
  28. package/dist/signet-contract-state-reader.d.ts +49 -30
  29. package/dist/signet-contract-state-reader.d.ts.map +1 -1
  30. package/dist/signet-contract-state-reader.js +55 -35
  31. package/dist/signet-contract-state-reader.js.map +1 -1
  32. package/dist/signet-evtype2tx-requests.d.ts +65 -43
  33. package/dist/signet-evtype2tx-requests.d.ts.map +1 -1
  34. package/dist/signet-evtype2tx-requests.js +77 -78
  35. package/dist/signet-evtype2tx-requests.js.map +1 -1
  36. package/dist/signet-request-response-reader.d.ts +3 -3
  37. package/dist/signet-request-response-reader.js +9 -9
  38. package/dist/signet-request-response-reader.js.map +1 -1
  39. package/dist/signet-requests.d.ts +4 -4
  40. package/dist/signet-requests.js +3 -3
  41. package/dist/signet-requests.js.map +1 -1
  42. package/package.json +1 -1
  43. package/src/Signet.compact +122 -72
  44. package/src/circuits.compact +10 -27
package/README.md CHANGED
@@ -2,146 +2,288 @@
2
2
 
3
3
  The client-agnostic [Sig Network](https://sig.network) protocol library for the [Midnight blockchain](https://midnight.network), and the seed of a future signet.js Midnight adapter. It carries everything a Midnight contract or off-chain client needs to speak the signet protocol (the MPC's [sign bidirectional flow](https://docs.sig.network/architecture/sign-bidirectional)) without binding to any particular contract.
4
4
 
5
+ > ## 🚧 Under Construction 🚧
6
+ >
7
+ > This Sig Network Midnight integration is still under construction.
8
+ > Use at your own risk and expect rapid iteration.
9
+
5
10
  ## What is in it
6
11
 
7
12
  - **Shared Compact modules**: the package ships its `.compact` sources, so contracts import them directly:
8
13
  ```compact
9
14
  import "@sig-net/midnight/src/Signet";
10
15
  ```
11
- - **Compiled pure circuits** (`pureCircuits`): the executable reference implementation of the client-agnostic circuits (path/identity checks, the attestation message, the sign-bidirectional notification packer, the Schnorr challenge). Off-chain code calls these compiled artefacts instead of re-porting the algorithms, so it always agrees with what the contracts prove.
16
+ - **Compiled pure circuits** (`pureCircuits`): the executable reference implementation of the client-agnostic circuits (the attestation digest, response verification, the deploy-time key pin, the notification packer, the ABI word builders and readers). Off-chain code calls these compiled artefacts instead of re-porting the algorithms, so it always agrees with what the contracts prove.
12
17
  - **TypeScript twins of the wire structs** and signet request-id computation.
13
18
  - **State readers, request feed and resolver**: poll the signet contract for pending requests and their signature / remote-execution responses.
14
- - **Crypto helpers**: epsilon derivation and Jubjub Schnorr response verification.
19
+ - **Crypto helpers**: epsilon derivation and ECDSA attestation verification.
20
+
21
+ Where signet.js already covers something (for example EVM transaction preparation or address derivation), import it from there. Only what is Midnight-specific lives here.
22
+
23
+ ## Install
24
+
25
+ ```sh
26
+ npm install @sig-net/midnight
27
+ ```
28
+
29
+ ## Sign bidirectional flow
30
+
31
+ The flow comprises 5 steps:
15
32
 
16
- Where signet.js already covers something (for example EVM transaction preparation or address derivation), import it from there; only what is Midnight-specific lives here.
33
+ 1. Client calls a contract on Midnight which requests a signature for a transaction destined for a foreign chain. The signature is made with a key derived for the requesting contract (see [Derived keys](#derived-keys)).
34
+ 2. The Sig Network MPC honours the request, generating the transaction signature and posting it back to Midnight.
35
+ 3. Client extracts the signature, using it to submit the signed transaction to the foreign chain.
36
+ 4. The Sig Network MPC observes the foreign transaction and posts the output of the execution (signed) back to Midnight.
37
+ 5. Client extracts the signed foreign execution output and submits it back to the Midnight contract, which verifies the MPC's signature over it in-circuit against the contract's own response key (see [Derived keys](#derived-keys)), completing the foreign transaction execution.
17
38
 
18
39
  ## Derived keys
19
40
 
20
- Every key the MPC signs with is scoped by the requesting contract:
41
+ Every key the MPC uses is derived for the requesting contract and a path. There are two kinds: the request signing key, whose path each contract chooses, and the response signing key, whose path is fixed by the protocol. Both key derivations are **scoped by the address** of the requesting contract.
21
42
 
22
- `derivedSigningKey = f(mpcRootKey[keyVersion], contractAddress, path)`
43
+ ### Request signing key
23
44
 
24
- The path is 32 opaque bytes of the contract's choosing (a fixed literal for a contract-owned account, a hash of a caller's secret for per-user accounts). There are no format requirements. The contract address is always part of the derivation, so no contract can reach another contract's derived keys.
45
+ The key the MPC signs requested foreign transactions with:
25
46
 
26
- ## Install
47
+ `requestSigningKey = f(mpcRootKey[keyVersion], contractAddress, path)`
27
48
 
28
- ```sh
29
- npm install @sig-net/midnight
49
+ The path is 32 opaque bytes of the contract's choosing (e.g. a fixed literal for a contract-owned account like "vault", or a hash of a caller's secret for per-user accounts). There are no format requirements. The contract address is always part of the derivation, so no contract can reach another contract's derived keys.
50
+
51
+ ### Response key
52
+
53
+ The key the MPC signs foreign execution outputs with when posting them back to Midnight:
54
+
55
+ `responseKey = f(mpcRootKey[keyVersion], contractAddress, "midnight response key")`
56
+
57
+ The same derivation, but with the path fixed to the literal `"midnight response key"`, giving each contract one well-known response key. A contract pins its own response key in its ledger after deploy and verifies every response against it in-circuit (step 5 of the flow above).
58
+
59
+ ## Integrator guide
60
+
61
+ A signet-compliant client contract does four things: it stores its requests in a public `SignBidirectionalEventMap` in its own ledger, it pins its counterparties (the Signet singleton contract and its own MPC response key), it submits signature requests, and it verifies execution responses in-circuit. Concretely, the integration consists of:
62
+
63
+ - 3 once-off **setup** steps
64
+ - 5 per-request **runtime** steps that drive the full sign bidirectional flow
65
+
66
+ ### Setup
67
+
68
+ Set up your contract for integration with the Sig Network MPC's sign bidirectional flow:
69
+
70
+ 1. Import the Signet module at the top of your contract (resolved through `node_modules` via `COMPACT_PATH`):
71
+
72
+ ```compact
73
+ import "@sig-net/midnight/src/Signet";
74
+ ```
75
+
76
+ Then tell the compact compiler about the npm packages with its `COMPACT_PATH` environment variable at compile time:
77
+
78
+ ```sh
79
+ COMPACT_PATH=node_modules compact compile --feature-zkir-v3 src/my-contract.compact src/managed/my-contract
80
+ ```
81
+
82
+ Compile with the pinned toolchain (currently `compact update 0.33.0-rc.2`) and always pass `--feature-zkir-v3`: compiled output without it is not compatible with the ledger-9 matched stack (node, indexer, proof server).
83
+
84
+ 2. Declare the required Sig Network protocol state in your ledger (plus recommended deployer identity and initialisation state). The event map can sit at ANY ledger field: each notification your contract registers names the field position holding it (runtime step 1), and the MPC reads the authenticated request from there.
85
+
86
+ ```compact
87
+ // Required: Map of SignBidirectionalEvent signature requests, configured by transaction type.
88
+ // Configured and sized here for an EVM Type 2 transaction with
89
+ // <1 calldata word, 0 access-list entries, 0 storage keys> and
90
+ // 34-byte serialisation schemas.
91
+ export ledger signBidirectionalEventMap: SignBidirectionalEventMap<EvmType2TxParams<1, 0, 0>, 34, 34>;
92
+
93
+ // Required: The Signet singleton signer interface, set at deploy.
94
+ // Used to notify the MPC of events you add to your signBidirectionalEventMap.
95
+ sealed ledger signetSigner: SignetSigner;
96
+
97
+ // Required: This contract's MPC response key, set in step 3.
98
+ // Used to verify RespondBidirectionalEvents containing the serialised output of foreign chain execution.
99
+ export ledger mpcResponseKey: Secp256k1Point;
100
+
101
+ // Recommended: contract-local source of request nonces, so identical
102
+ // requests hash to distinct request ids. Nothing off-chain reads it.
103
+ export ledger signetRequestNonce: Counter;
104
+
105
+ // Recommended: used in step 3 to ensure initialisation runs only once.
106
+ export ledger initialised: Counter;
107
+
108
+ // Recommended: set on deploy, used in step 3 to ensure only the deployer may set the mpcResponseKey.
109
+ sealed ledger deployer: Bytes<32>;
110
+
111
+ // Recommended: supplies the deployer's identity secret from private state
112
+ // off-chain; only its commitment (below) ever reaches the ledger.
113
+ witness witnessDeployerSecretKey(): Bytes<32>;
114
+
115
+ // Recommended: the deployer identity commitment scheme. Exported so deploy
116
+ // tooling can compute the constructor argument by calling the compiled circuit.
117
+ export pure circuit calculateDeployerCommitment(sk: Bytes<32>): Bytes<32> {
118
+ return persistentHash<Vector<2, Bytes<32>>>([pad(32, "my-contract:deployer:"), sk]);
119
+ }
120
+
121
+ // Required: set signet contract and (recommended) deployer commitment on deployment.
122
+ constructor(signetContract: SignetSigner, deployerCommitment: Bytes<32>) {
123
+ signetSigner = disclose(signetContract);
124
+ deployer = disclose(deployerCommitment);
125
+ }
126
+ ```
127
+
128
+ 3. Set the contract's MPC response key once, right after deploy. Deriving this key requires the address of the contract, which only exists after deploy (see [Response key](#response-key)):
129
+
130
+ ```compact
131
+ export circuit initialise(responseKey: Secp256k1Point): [] {
132
+ // Recommended: confirm that only the deployer may initialise, and only once:
133
+ assert(deployer == calculateDeployerCommitment(witnessDeployerSecretKey()), "Not the deployer");
134
+ assert(initialised == 0, "Already initialised");
135
+ initialised.increment(1);
136
+
137
+ // Required: set MPC response key for verification of RespondBidirectionalEvents
138
+ mpcResponseKey = disclose(responseKey);
139
+ }
140
+ ```
141
+
142
+ ### Runtime
143
+
144
+ Each interaction with your contract that executes a transaction on a foreign chain runs these 5 steps.
145
+
146
+ Steps 1 and 5 are circuits on your contract, and steps 2 to 4 are off-chain client code built on the utilities in `@sig-net/midnight`.
147
+
148
+ The off-chain steps share one `SignetRequestResponseReader` over your contract / Signet singleton pair, and the expected signer of the requested transaction (the key the MPC derives for your contract and the request's path, see [Derived keys](#derived-keys)):
149
+
150
+ ```ts
151
+ import { indexerPublicDataProvider } from "@midnight-ntwrk/midnight-js-indexer-public-data-provider";
152
+ import { deriveEvmAddress, SignetRequestResponseReader } from "@sig-net/midnight";
153
+
154
+ // SignetRequestResponseReader to poll for Signed Transactions and Signed RespondBidirectionalEvents
155
+ const reader = new SignetRequestResponseReader({
156
+ // Address of YOUR deployed contract
157
+ requesterContractAddress: myContractAddress,
158
+
159
+ // signBidirectionalEventMap's field position (Setup step 2)
160
+ requesterRequestsIndexField: 0,
161
+
162
+ // Address of the Signet singleton contract
163
+ signetContractAddress,
164
+
165
+ // Provider to index Midnight Blockchain
166
+ publicDataProvider: indexerPublicDataProvider({
167
+ queryURL: indexerUrl,
168
+ subscriptionURL: indexerWsUrl
169
+ }),
170
+ });
171
+
172
+ const expectedSigner = deriveEvmAddress(mpcRootPublicKey, myContractAddress, "my-path");
173
+ ```
174
+
175
+ 1. Store a signature request and notify the MPC via cross contract call. Build (or overwrite) every part of the transaction your contract enforces in-circuit, calldata above all (see [EVM Type 2 transactions and ABI calldata words](#evm-type-2-transactions-and-abi-calldata-words)); never pass caller input through unchecked:
176
+
177
+ ```compact
178
+ // Construct SignBidirectionalEvent signature request and calculate its RequestId
179
+ const request = constructSignBidirectionalEvent<EvmType2TxParams<1, 0, 0>, 34, 34>(/* ... */);
180
+ const requestId = disclose(calculateRequestId<EvmType2TxParams<1, 0, 0>, 34, 34>(request));
181
+
182
+ // Store the signature request in your signBidirectionalEventMap for MPC to discover
183
+ signetRequestNonce.increment(1);
184
+ signBidirectionalEventMap.insert(requestId, disclose(request));
185
+
186
+ // Notify the MPC of the SignBidirectionalEvent and the location of your signBidirectionalEventMap.
187
+ // The location is 0 here based on the position of the declaration in Setup step 2.
188
+ signetSigner.signBidirectional(
189
+ requestId,
190
+ constructSignBidirectionalEventNotificationV1(kernel.self(), 0 as Uint<8>),
191
+ );
192
+ ```
193
+
194
+ **NOTE:** `requestId` should be returned from the above circuit call so that it may be used in subsequent steps (or compute it off-chain with the `calculateRequestId` TS twin).
195
+
196
+ 2. Poll the Signet singleton for the MPC's signature response. The response log is unauthenticated (anyone can post), so use the verifying getter: it only returns a post whose signature recovers to `expectedSigner` over the requested transaction's signing hash:
197
+
198
+ ```ts
199
+ const { verified } = await reader.getVerifiedSignatureRespondedEvent(requestId, expectedSigner);
200
+ // verified === undefined: no valid response posted yet, poll again.
201
+ ```
202
+
203
+ 3. Construct the signed transaction and submit it to the foreign chain. The reader rebuilds the transaction from the request record on your ledger and attaches the verified MPC signature:
204
+
205
+ ```ts
206
+ import { JsonRpcProvider } from "ethers";
207
+
208
+ const signedTx = await reader.getSignedEvmTransaction(requestId, expectedSigner);
209
+ await new JsonRpcProvider(foreignChainRpcUrl).broadcastTransaction(signedTx.serialized);
210
+ ```
211
+
212
+ 4. Poll the Signet singleton for the MPC's signed remote execution output (posted once the MPC observes the transaction execute on the foreign chain). Posts are stored unverified, so treat them as candidates: the authoritative check is your contract's verify circuit in step 5:
213
+
214
+ ```ts
215
+ const [respondBidirectionalEvent] = await reader.getRespondBidirectionalEvents(requestId);
216
+ // Empty array: not posted yet, poll again.
217
+ ```
218
+
219
+ 5. Deliver the response to your contract, which verifies it in-circuit against the response key pinned in Setup step 3 and consumes the request:
220
+
221
+ ```compact
222
+ assert(
223
+ verifyRespondBidirectionalEvent(requestId, respondBidirectionalEvent, mpcResponseKey),
224
+ "Invalid attestation signature"
225
+ );
226
+ signBidirectionalEventMap.remove(requestId);
227
+ ```
228
+
229
+ ## EVM Type 2 transactions and ABI calldata words
230
+
231
+ An `EvmType2TxParams` request decomposes the EVM transaction into typed fields your contract can enforce field by field in-circuit. Its optional `calldata` is an `EvmCalldata<maxWords>`: the 4-byte function selector plus a list of 32-byte ABI words, per the [Solidity ABI spec](https://docs.soliditylang.org/en/latest/abi-spec.html). Slots past `noWords` are unused capacity and never reach the transaction.
232
+
233
+ Every word must be stored in canonical ABI form (big-endian). The MPC signs a transaction whose calldata is exactly `selector || words[0..noWords]`, byte for byte, so a word stored in any other form becomes a signed transaction calling the foreign contract with garbage arguments. Compact's integer casts are little-endian, so do not hand-roll the byte order: build every word with the module's helper circuits, and read words back with the matching readers.
234
+
235
+ | Solidity type | Build with | Read back with |
236
+ |---|---|---|
237
+ | `address` | `evmAddressAbiWord(addr: Bytes<20>)` | |
238
+ | unsigned integers up to `uint128` (amounts, ids) | `numericAbiWord(value: Uint<128>)` | `abiWordToUint128(word)` |
239
+ | `bool` | `boolAbiWord(value: Boolean)` | `abiWordToBool(word)` |
240
+
241
+ ### Example: an ERC20 transfer
242
+
243
+ `transfer(address,uint256)`, selector `0xa9059cbb`, takes an address word and a numeric word:
244
+
245
+ ```compact
246
+ const calldata = EvmCalldata<2> {
247
+ selector: Bytes[0xa9, 0x05, 0x9c, 0xbb],
248
+ noWords: 2 as Uint<16>,
249
+ words: [
250
+ evmAddressAbiWord(recipient), // address argument (Bytes<20>)
251
+ numericAbiWord(amount) // uint256 argument (from a Uint<128>)
252
+ ]
253
+ };
30
254
  ```
31
255
 
32
- ## How a client contract integrates
256
+ ### Example: a bool argument, and decoding a bool result
33
257
 
34
- A signet-compliant client contract does four things:
258
+ `setApprovalForAll(address,bool)`, selector `0xa22cb465`:
35
259
 
36
- 1. **Stores requests in a public index**: a `SignBidirectionalRequestIndex` map in its ledger. The index can sit at ANY ledger field: each notification the contract registers names the field position holding it, and the MPC reads the authenticated request from there. A contract-local `SignetNonce` counter is a convenient source of request nonces (identical requests must hash to distinct ids); nothing off-chain reads it.
37
- 2. **Pins its counterparties at deploy time**: the central signet contract it notifies, and the MPC attestation key its verify circuit accepts.
38
- 3. **Submits signature requests**: builds the transaction decomposition in-circuit, constructs the canonical request record, stores it in its own index under the request id, and registers a notification in the central registry (which the MPC polls; the MPC then reads the authenticated request back from the ledger field the notification names).
39
- 4. **Verifies the execution response**: checks the attestation key, verifies the MPC's Schnorr attestation in-circuit over (request id, output), and consumes the request.
260
+ ```compact
261
+ const calldata = EvmCalldata<2> {
262
+ selector: Bytes[0xa2, 0x2c, 0xb4, 0x65],
263
+ noWords: 2 as Uint<16>,
264
+ words: [
265
+ evmAddressAbiWord(operator),
266
+ boolAbiWord(true)
267
+ ]
268
+ };
269
+ ```
270
+
271
+ The readers run the same rules in the other direction, rejecting any non-canonical word instead of silently truncating or coercing it. A `RespondBidirectionalEvent`'s `serializedOutput` is the ABI-encoded return data of the remote call, so a settle circuit can decode an ERC20 `transfer`'s `bool` return from the first output word:
40
272
 
41
273
  ```compact
42
- pragma language_version >= 0.25;
43
-
44
- import CompactStandardLibrary;
45
- import "@sig-net/midnight/src/Signet";
46
-
47
- // (1) The request index: the public map the MPC reads requests back from.
48
- // It can sit at any ledger field; the notification in submitSignatureRequest
49
- // names its position (0 in this contract). The counter is contract-local
50
- // (it sources each request's nonce so identical requests get distinct ids);
51
- // the MPC never reads it. The generic size parameters cap the EVM calldata
52
- // words, access list entries and storage keys your requests may carry.
53
- export ledger signetRequestsIndex: SignBidirectionalRequestIndex<EVMType2TxParams<1, 0, 0>>;
54
- export ledger signetNonce: SignetNonce;
55
-
56
- // (2) Pinned at deploy time.
57
- export sealed ledger mpcPubKeyHash: Bytes<32>;
58
- sealed ledger signetNotifier: SignetNotifier;
59
-
60
- constructor(mpcPk: JubjubPoint, signetContract: SignetNotifier) {
61
- mpcPubKeyHash = disclose(persistentHash<JubjubPoint>(mpcPk));
62
- signetNotifier = disclose(signetContract);
63
- }
64
-
65
- // (3) Construct, store and announce a signature request.
66
- export circuit submitSignatureRequest(evmNonce: Uint<64>, keyVersion: Uint<32>): [] {
67
- // Decompose the EVM transaction to be signed. Build (or overwrite) every
68
- // part your contract enforces in-circuit, calldata above all; never pass
69
- // caller input through unchecked.
70
- const txParams = EVMType2TxParams<1, 0, 0> {
71
- to: pad(20, "recipient"),
72
- chainId: 31337 as Uint<64>,
73
- nonce: evmNonce,
74
- gasLimit: 100000 as Uint<64>,
75
- maxFeePerGas: 30000000000 as Uint<128>,
76
- maxPriorityFeePerGas: 1000000000 as Uint<128>,
77
- value: 0 as Uint<128>,
78
- accessListEntryCount: 0,
79
- accessList: [],
80
- calldata: none<EVMCalldata<1>>()
81
- };
82
-
83
- // The canonical request record. This variant signs with the CONTRACT's own
84
- // derived account (a path literal the contract fixes); use
85
- // constructSignBidirectionalRequest instead for caller-identity requests,
86
- // where the path is bound to the caller's identity commitment.
87
- const schema = pad(128, "[{\"name\":\"success\",\"type\":\"bool\"}]");
88
- const request = constructContractPathSignBidirectionalRequest<EVMType2TxParams<1, 0, 0>>(
89
- signetNonce as Uint<64>, // contract-local request nonce
90
- TxParamType.evmType2,
91
- txParams,
92
- pad(32, "eip155:31337"), // target chain (CAIP-2)
93
- keyVersion, // MPC root-key version (>= 1)
94
- pad(256, "my-treasury"), // contract-owned derivation path
95
- pad(32, "ecdsa"),
96
- pad(32, "ethereum"),
97
- pad(64, ""),
98
- schema,
99
- schema
100
- );
101
-
102
- // Store the request in YOUR ledger under its id, then register the
103
- // notification in the central signet contract's registry. The final
104
- // argument is the ledger field position of signetRequestsIndex, which is
105
- // how the MPC finds the index to read the request back from.
106
- const requestId = disclose(calculateRequestId<EVMType2TxParams<1, 0, 0>>(request));
107
- assert(!signetRequestsIndex.member(requestId), "Request already exists");
108
- signetNonce.increment(1);
109
- signetRequestsIndex.insert(requestId, disclose(request));
110
- signetNotifier.notifyBidirectionalSignatureRequest(requestId,
111
- constructSignBidirectionalNotificationV1(kernel.self(), requestId as Bytes<32>, 0 as Uint<8>));
112
- }
113
-
114
- // (4) Verify the MPC's Schnorr-signed attestation of the remote execution
115
- // and consume the request (double-verify protection).
116
- export circuit verifyResponse(requestId: RequestId, respondBidirectional: RespondBidirectional): [] {
117
- const rid = disclose(requestId);
118
- assert(
119
- persistentHash<JubjubPoint>(respondBidirectional.pk) == mpcPubKeyHash,
120
- "Unauthorized: attestation pk is not the MPC key"
121
- );
122
- assert(
123
- jubjubSchnorrVerify<4>(
124
- signetAttestationMessage(rid, respondBidirectional.serializedOutput, respondBidirectional.outputLen),
125
- JubjubSchnorrSignature {
126
- announcement: respondBidirectional.announcement,
127
- response: respondBidirectional.response,
128
- },
129
- respondBidirectional.pk
130
- ),
131
- "Invalid attestation signature"
132
- );
133
- assert(signetRequestsIndex.member(rid), "Request not found");
134
- signetRequestsIndex.remove(rid);
135
- }
274
+ const success = abiWordToBool(slice<32>(respondBidirectionalEvent.serializedOutput, 0));
275
+ assert(success, "Remote transfer failed");
136
276
  ```
137
277
 
138
- Between the two circuits sit the off-chain steps, which this package also covers: poll the central signet contract with `SignetRequestResponseReader` (configured with both contract addresses and the ledger field position of your request index) to collect and verify the MPC's secp256k1 signature (then broadcast the signed transaction to the destination chain), and later the Schnorr-attested execution output to feed back into `verifyResponse`.
278
+ The same builders and readers exist as TypeScript twins under identical names, for composing expected words off-chain (UIs, expected-record builders, tests). They are kept in lockstep with the compiled circuits by this package's test suite.
279
+
280
+ ## More examples
139
281
 
140
- The [signet-caller contract](https://github.com/sig-net/midnight-integration/blob/main/packages/caller-contract/src/signet-caller.compact) is the complete, runnable version of this skeleton, exercised end to end by the repository's integration suite; a fuller application (an ERC20 cross-chain vault) lives in [sig-net/midnight-examples](https://github.com/sig-net/midnight-examples).
282
+ For full integration examples (such as an ERC20 cross chain vault) see the [`sig-net/midnight-examples`](https://github.com/sig-net/midnight-examples) repository.
141
283
 
142
284
  ## Related packages
143
285
 
144
286
  - [`@sig-net/midnight-contract`](https://www.npmjs.com/package/@sig-net/midnight-contract): the central signet contract this library reads from.
145
287
  - [`@sig-net/midnight-contract-deploy`](https://www.npmjs.com/package/@sig-net/midnight-contract-deploy): deploy tooling for that contract plus generic Midnight deploy/wallet plumbing.
146
288
 
147
- Developed in [sig-net/midnight-integration](https://github.com/sig-net/midnight-integration); example applications live in [sig-net/midnight-examples](https://github.com/sig-net/midnight-examples).
289
+ Developed in [sig-net/midnight-integration](https://github.com/sig-net/midnight-integration).
@@ -2,7 +2,7 @@
2
2
  export declare const CAIP2_ID_BYTES = 32;
3
3
  /** Width of `SignBidirectionalEvent.params` (`Bytes<64>`). */
4
4
  export declare const MPC_PARAMS_BYTES = 64;
5
- /** Width of `EVMCalldata.selector` (`Bytes<4>`) — the literal first 4 calldata bytes. */
5
+ /** Width of `EvmCalldata.selector` (`Bytes<4>`) — the literal first 4 calldata bytes. */
6
6
  export declare const SELECTOR_BYTES = 4;
7
7
  /** Width of `RespondBidirectionalEvent.serializedOutput` (`Bytes<128>`). */
8
8
  export declare const SERIALIZED_OUTPUT_BYTES = 128;
package/dist/constants.js CHANGED
@@ -11,7 +11,7 @@
11
11
  export const CAIP2_ID_BYTES = 32;
12
12
  /** Width of `SignBidirectionalEvent.params` (`Bytes<64>`). */
13
13
  export const MPC_PARAMS_BYTES = 64;
14
- /** Width of `EVMCalldata.selector` (`Bytes<4>`) — the literal first 4 calldata bytes. */
14
+ /** Width of `EvmCalldata.selector` (`Bytes<4>`) — the literal first 4 calldata bytes. */
15
15
  export const SELECTOR_BYTES = 4;
16
16
  /** Width of `RespondBidirectionalEvent.serializedOutput` (`Bytes<128>`). */
17
17
  export const SERIALIZED_OUTPUT_BYTES = 128;
@@ -1,4 +1,5 @@
1
1
  import type { Secp256k1Point } from "@midnight-ntwrk/compact-runtime";
2
+ import type { MpcSignature } from "./signet-contract-state-reader.ts";
2
3
  export type { Secp256k1Point } from "@midnight-ntwrk/compact-runtime";
3
4
  /** secp256k1 curve (group) order n. */
4
5
  export declare const SECP256K1_ORDER: bigint;
@@ -23,11 +24,30 @@ export declare function bytesToBigint(bytes: Uint8Array): bigint;
23
24
  * @returns The 32-byte little-endian encoding.
24
25
  */
25
26
  export declare function bigintToBytes32(n: bigint): Uint8Array;
27
+ /**
28
+ * Convert big-endian bytes to a bigint (SEC1 coordinate order, the byte
29
+ * order the respond events' stored signature uses).
30
+ *
31
+ * @param bytes - Big-endian byte array.
32
+ * @returns The decoded non-negative integer.
33
+ */
34
+ export declare function bytesToBigintBE(bytes: Uint8Array): bigint;
35
+ /**
36
+ * Convert a bigint to exactly 32 BIG-endian bytes (SEC1 scalar/coordinate
37
+ * order, also the ABI word encoding) — the big-endian counterpart of
38
+ * {@link bigintToBytes32} and the inverse of {@link bytesToBigintBE}.
39
+ *
40
+ * @param value - The non-negative integer to encode.
41
+ * @returns The 32-byte big-endian encoding.
42
+ * @throws Error if the value is negative or does not fit 32 bytes.
43
+ */
44
+ export declare function bigintToBytes32BE(value: bigint): Uint8Array;
26
45
  /**
27
46
  * An ECDSA signature over the attestation digest, in the MPC's canonical
28
47
  * `Signature { big_r, s, recovery_id }` spirit with `r` already reduced to
29
48
  * the scalar `big_r.x mod n` — the form Compact's `Secp256k1EcdsaSignature`
30
- * takes.
49
+ * takes. Build the stored form for posting in a respond event with
50
+ * {@link ecdsaSignatureToMpcSignature}.
31
51
  */
32
52
  export interface EcdsaSignature {
33
53
  /** Signature scalar r (= R.x mod n). */
@@ -49,6 +69,38 @@ export interface EcdsaSignature {
49
69
  * @returns The signature with its recovery id.
50
70
  */
51
71
  export declare function signAttestationDigest(digest: Uint8Array, secretKey: Uint8Array): EcdsaSignature;
72
+ /**
73
+ * Build the stored-form signature both respond events carry from a
74
+ * scalar-form one: R is reconstructed by decompressing the curve point with
75
+ * x = `r` and the parity `recoveryId` names — undoing the compression an
76
+ * `r || s || v` signature performs. For MPC-side posters (the fakenet
77
+ * signer, tests). The negligible caveat: an R.x that overflowed the curve
78
+ * order cannot be told apart from its reduced twin, so reconstruction picks
79
+ * the x equal to `r` itself.
80
+ *
81
+ * @param signature - The scalar-form signature (the signer's output shape).
82
+ * @returns The stored-form signature: R as a full point, big-endian bytes.
83
+ * @throws Error if the recovery id is not 0 or 1, or `r` is not the x
84
+ * coordinate of a secp256k1 point.
85
+ */
86
+ export declare function ecdsaSignatureToMpcSignature(signature: EcdsaSignature): MpcSignature;
87
+ /**
88
+ * Read a stored-form signature back into scalar form — the inverse of
89
+ * {@link ecdsaSignatureToMpcSignature}, for off-chain consumers building a
90
+ * transaction from the response. In-circuit verification does not need this:
91
+ * `verifyRespondBidirectionalEvent` takes the stored record as-is. `r` comes
92
+ * out as `bigR.x` reduced mod the curve order. Rejects records that do not
93
+ * even hold the shape, since posts are unauthenticated and a malformed
94
+ * record is garbage, not a signature. `bigR.y` is NOT checked against the
95
+ * curve or the recovery id: signature verification is the authority, not
96
+ * this decoder.
97
+ *
98
+ * @param signature - The stored-form signature as posted.
99
+ * @returns The scalar-form signature.
100
+ * @throws Error if a component has the wrong byte length or the recovery id
101
+ * is not 0 or 1.
102
+ */
103
+ export declare function mpcSignatureToEcdsaSignature(signature: MpcSignature): EcdsaSignature;
52
104
  /**
53
105
  * Parse a secp256k1 public key from SEC1 hex (compressed or uncompressed,
54
106
  * optional `0x` prefix) into the Compact runtime's `Secp256k1Point` shape —
@@ -1 +1 @@
1
- {"version":3,"file":"ecdsa-attestation.d.ts","sourceRoot":"","sources":["../src/ecdsa-attestation.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAItE,YAAY,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAEtE,uCAAuC;AACvC,eAAO,MAAM,eAAe,QAA2B,CAAC;AAExD,uEAAuE;AACvE,eAAO,MAAM,SAAS,iFAAiF,CAAC;AAExG;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAMvD;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,CAQrD;AAWD;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,wCAAwC;IACxC,CAAC,EAAE,MAAM,CAAC;IACV,2DAA2D;IAC3D,CAAC,EAAE,MAAM,CAAC;IACV,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,UAAU,EAClB,SAAS,EAAE,UAAU,GACpB,cAAc,CAiBhB;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,CAcrE;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAGtE;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,UAAU,GAAG,cAAc,CAO1E"}
1
+ {"version":3,"file":"ecdsa-attestation.d.ts","sourceRoot":"","sources":["../src/ecdsa-attestation.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAEtE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AAItE,YAAY,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAEtE,uCAAuC;AACvC,eAAO,MAAM,eAAe,QAA2B,CAAC;AAExD,uEAAuE;AACvE,eAAO,MAAM,SAAS,iFAAiF,CAAC;AAExG;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAMvD;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,GAAG,UAAU,CAQrD;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAMzD;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAW3D;AAED;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,wCAAwC;IACxC,CAAC,EAAE,MAAM,CAAC;IACV,2DAA2D;IAC3D,CAAC,EAAE,MAAM,CAAC;IACV,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,UAAU,EAClB,SAAS,EAAE,UAAU,GACpB,cAAc,CAiBhB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,4BAA4B,CAAC,SAAS,EAAE,cAAc,GAAG,YAAY,CAmBpF;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,4BAA4B,CAAC,SAAS,EAAE,YAAY,GAAG,cAAc,CAapF;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,MAAM,GAAG,cAAc,CAcrE;AAED;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAGtE;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,UAAU,GAAG,cAAc,CAO1E"}
@@ -1,8 +1,8 @@
1
1
  // secp256k1 ECDSA helpers: the TS side of the respond-bidirectional
2
2
  // attestation flow in Signet.compact — SIGNING (which needs the secret
3
3
  // scalar, so it cannot be a circuit), key parsing/formatting, and the
4
- // byte-order conversions between noble's bigints and the ledger-stored
5
- // little-endian scalar bytes. Everything provable stays in Compact: the
4
+ // byte-order conversions between noble's bigints and the little-endian
5
+ // scalar bytes Compact's casts read. Everything provable stays in Compact: the
6
6
  // attestation digest is the COMPILED `pureCircuits.signetAttestationDigest`,
7
7
  // verification is `pureCircuits.verifyRespondBidirectionalEvent` (the same
8
8
  // check client contracts run in-circuit), and the deploy-time key pin is
@@ -48,14 +48,41 @@ export function bigintToBytes32(n) {
48
48
  }
49
49
  return buf;
50
50
  }
51
- /** Convert big-endian bytes to a bigint (SEC1 coordinate order). */
52
- function bytesToBigintBE(bytes) {
51
+ /**
52
+ * Convert big-endian bytes to a bigint (SEC1 coordinate order, the byte
53
+ * order the respond events' stored signature uses).
54
+ *
55
+ * @param bytes - Big-endian byte array.
56
+ * @returns The decoded non-negative integer.
57
+ */
58
+ export function bytesToBigintBE(bytes) {
53
59
  let value = 0n;
54
60
  for (const byte of bytes) {
55
61
  value = (value << 8n) | BigInt(byte);
56
62
  }
57
63
  return value;
58
64
  }
65
+ /**
66
+ * Convert a bigint to exactly 32 BIG-endian bytes (SEC1 scalar/coordinate
67
+ * order, also the ABI word encoding) — the big-endian counterpart of
68
+ * {@link bigintToBytes32} and the inverse of {@link bytesToBigintBE}.
69
+ *
70
+ * @param value - The non-negative integer to encode.
71
+ * @returns The 32-byte big-endian encoding.
72
+ * @throws Error if the value is negative or does not fit 32 bytes.
73
+ */
74
+ export function bigintToBytes32BE(value) {
75
+ if (value < 0n || value >= 1n << 256n) {
76
+ throw new Error(`value does not fit 32 big-endian bytes: ${value}`);
77
+ }
78
+ const out = new Uint8Array(32);
79
+ let v = value;
80
+ for (let i = 31; i >= 0; i--) {
81
+ out[i] = Number(v & 0xffn);
82
+ v >>= 8n;
83
+ }
84
+ return out;
85
+ }
59
86
  /**
60
87
  * ECDSA-sign a 32-byte digest with a secp256k1 secret key, exactly as the
61
88
  * MPC signs the attestation digest: RFC 6979 deterministic, low-s, the digest
@@ -85,6 +112,71 @@ export function signAttestationDigest(digest, secretKey) {
85
112
  /* v8 ignore next: unreachable for a signature this function just produced */
86
113
  throw new Error("signature does not recover to its own public key");
87
114
  }
115
+ /**
116
+ * Build the stored-form signature both respond events carry from a
117
+ * scalar-form one: R is reconstructed by decompressing the curve point with
118
+ * x = `r` and the parity `recoveryId` names — undoing the compression an
119
+ * `r || s || v` signature performs. For MPC-side posters (the fakenet
120
+ * signer, tests). The negligible caveat: an R.x that overflowed the curve
121
+ * order cannot be told apart from its reduced twin, so reconstruction picks
122
+ * the x equal to `r` itself.
123
+ *
124
+ * @param signature - The scalar-form signature (the signer's output shape).
125
+ * @returns The stored-form signature: R as a full point, big-endian bytes.
126
+ * @throws Error if the recovery id is not 0 or 1, or `r` is not the x
127
+ * coordinate of a secp256k1 point.
128
+ */
129
+ export function ecdsaSignatureToMpcSignature(signature) {
130
+ if (signature.recoveryId !== 0 && signature.recoveryId !== 1) {
131
+ throw new Error(`expected a recovery id of 0 or 1, got ${signature.recoveryId}`);
132
+ }
133
+ // SEC1 compressed form: parity prefix (02 even, 03 odd) || x big-endian.
134
+ const parityPrefix = signature.recoveryId === 0 ? "02" : "03";
135
+ const xHex = signature.r.toString(16).padStart(64, "0");
136
+ let point;
137
+ try {
138
+ point = secp256k1.Point.fromHex(`${parityPrefix}${xHex}`);
139
+ }
140
+ catch (error) {
141
+ throw new Error(`signature r is not the x coordinate of a secp256k1 point (${String(error)})`);
142
+ }
143
+ const uncompressed = point.toBytes(false); // 0x04 || x || y
144
+ return {
145
+ bigR: { x: uncompressed.slice(1, 33), y: uncompressed.slice(33, 65) },
146
+ s: bigintToBytes32BE(signature.s),
147
+ recoveryId: BigInt(signature.recoveryId),
148
+ };
149
+ }
150
+ /**
151
+ * Read a stored-form signature back into scalar form — the inverse of
152
+ * {@link ecdsaSignatureToMpcSignature}, for off-chain consumers building a
153
+ * transaction from the response. In-circuit verification does not need this:
154
+ * `verifyRespondBidirectionalEvent` takes the stored record as-is. `r` comes
155
+ * out as `bigR.x` reduced mod the curve order. Rejects records that do not
156
+ * even hold the shape, since posts are unauthenticated and a malformed
157
+ * record is garbage, not a signature. `bigR.y` is NOT checked against the
158
+ * curve or the recovery id: signature verification is the authority, not
159
+ * this decoder.
160
+ *
161
+ * @param signature - The stored-form signature as posted.
162
+ * @returns The scalar-form signature.
163
+ * @throws Error if a component has the wrong byte length or the recovery id
164
+ * is not 0 or 1.
165
+ */
166
+ export function mpcSignatureToEcdsaSignature(signature) {
167
+ const { bigR, s, recoveryId } = signature;
168
+ if (bigR.x.length !== 32 || bigR.y.length !== 32 || s.length !== 32) {
169
+ throw new Error("expected 32-byte bigR.x/bigR.y/s in a stored signature");
170
+ }
171
+ if (recoveryId !== 0n && recoveryId !== 1n) {
172
+ throw new Error(`expected a recovery id of 0 or 1, got ${recoveryId}`);
173
+ }
174
+ return {
175
+ r: bytesToBigintBE(bigR.x) % SECP256K1_ORDER,
176
+ s: bytesToBigintBE(s),
177
+ recoveryId: Number(recoveryId),
178
+ };
179
+ }
88
180
  /**
89
181
  * Parse a secp256k1 public key from SEC1 hex (compressed or uncompressed,
90
182
  * optional `0x` prefix) into the Compact runtime's `Secp256k1Point` shape —