@sig-net/midnight 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/dist/constants.d.ts +62 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +83 -0
- package/dist/constants.js.map +1 -0
- package/dist/epsilon-derivation.d.ts +32 -0
- package/dist/epsilon-derivation.d.ts.map +1 -0
- package/dist/epsilon-derivation.js +54 -0
- package/dist/epsilon-derivation.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/managed/compiler/contract-info.json +165 -0
- package/dist/managed/compiler/contract-manifest.json +32 -0
- package/dist/managed/contract/index.d.ts +65 -0
- package/dist/managed/contract/index.js +611 -0
- package/dist/managed/contract/index.js.map +8 -0
- package/dist/schnorr.d.ts +100 -0
- package/dist/schnorr.d.ts.map +1 -0
- package/dist/schnorr.js +144 -0
- package/dist/schnorr.js.map +1 -0
- package/dist/signature-requests-state-reader.d.ts +74 -0
- package/dist/signature-requests-state-reader.d.ts.map +1 -0
- package/dist/signature-requests-state-reader.js +167 -0
- package/dist/signature-requests-state-reader.js.map +1 -0
- package/dist/signature-response-verification.d.ts +27 -0
- package/dist/signature-response-verification.d.ts.map +1 -0
- package/dist/signature-response-verification.js +45 -0
- package/dist/signature-response-verification.js.map +1 -0
- package/dist/signature-state-reading.d.ts +36 -0
- package/dist/signature-state-reading.d.ts.map +1 -0
- package/dist/signature-state-reading.js +62 -0
- package/dist/signature-state-reading.js.map +1 -0
- package/dist/signet-contract-state-reader.d.ts +146 -0
- package/dist/signet-contract-state-reader.d.ts.map +1 -0
- package/dist/signet-contract-state-reader.js +226 -0
- package/dist/signet-contract-state-reader.js.map +1 -0
- package/dist/signet-event-observer.d.ts +102 -0
- package/dist/signet-event-observer.d.ts.map +1 -0
- package/dist/signet-event-observer.js +127 -0
- package/dist/signet-event-observer.js.map +1 -0
- package/dist/signet-events.d.ts +175 -0
- package/dist/signet-events.d.ts.map +1 -0
- package/dist/signet-events.js +210 -0
- package/dist/signet-events.js.map +1 -0
- package/dist/signet-request-feed.d.ts +78 -0
- package/dist/signet-request-feed.d.ts.map +1 -0
- package/dist/signet-request-feed.js +116 -0
- package/dist/signet-request-feed.js.map +1 -0
- package/dist/signet-request-resolver.d.ts +58 -0
- package/dist/signet-request-resolver.d.ts.map +1 -0
- package/dist/signet-request-resolver.js +76 -0
- package/dist/signet-request-resolver.js.map +1 -0
- package/dist/signet-request-response-reader.d.ts +160 -0
- package/dist/signet-request-response-reader.d.ts.map +1 -0
- package/dist/signet-request-response-reader.js +198 -0
- package/dist/signet-request-response-reader.js.map +1 -0
- package/dist/signet-requests.d.ts +363 -0
- package/dist/signet-requests.d.ts.map +1 -0
- package/dist/signet-requests.js +497 -0
- package/dist/signet-requests.js.map +1 -0
- package/dist/signet-response-feed.d.ts +161 -0
- package/dist/signet-response-feed.d.ts.map +1 -0
- package/dist/signet-response-feed.js +234 -0
- package/dist/signet-response-feed.js.map +1 -0
- package/package.json +47 -0
- package/src/Schnorr.compact +63 -0
- package/src/Signet.compact +529 -0
- package/src/circuits.compact +36 -0
- package/src/constants.ts +97 -0
- package/src/epsilon-derivation.ts +62 -0
- package/src/index.ts +31 -0
- package/src/managed/compiler/contract-info.json +165 -0
- package/src/managed/compiler/contract-manifest.json +32 -0
- package/src/managed/contract/index.d.ts +65 -0
- package/src/managed/contract/index.js +611 -0
- package/src/managed/contract/index.js.map +8 -0
- package/src/schnorr.ts +210 -0
- package/src/signature-requests-state-reader.ts +223 -0
- package/src/signature-response-verification.ts +64 -0
- package/src/signature-state-reading.ts +87 -0
- package/src/signet-contract-state-reader.ts +391 -0
- package/src/signet-event-observer.ts +186 -0
- package/src/signet-events.ts +337 -0
- package/src/signet-request-feed.ts +158 -0
- package/src/signet-request-resolver.ts +116 -0
- package/src/signet-request-response-reader.ts +324 -0
- package/src/signet-requests.ts +737 -0
- package/src/signet-response-feed.ts +325 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
// Off-chain codecs for the event notifications the central signet contract
|
|
2
|
+
// emits (Signet.compact structs wrapped in stdlib `Misc { name, payload }`
|
|
3
|
+
// events — see packages/xcontract-events/knowledge-base/events.md), one per
|
|
4
|
+
// direction of the MPC round trip:
|
|
5
|
+
// - SignBidirectionalEvent: the MPC watches these to discover WHEN a
|
|
6
|
+
// request was stored and WHERE to read it. Attribution is re-established
|
|
7
|
+
// by reading the request from the named caller's own authenticated ledger
|
|
8
|
+
// (see signet-request-resolver.ts and
|
|
9
|
+
// knowledge-base/caller-attribution.md); a field inside the event is not
|
|
10
|
+
// trustworthy (a Midnight contract cannot see its caller — gotcha #21).
|
|
11
|
+
// - SignatureRespondedEvent: the requesting client watches these to learn a
|
|
12
|
+
// signature response was posted, then reads the post back from the
|
|
13
|
+
// response log and verifies the signature off-chain (the log is
|
|
14
|
+
// unauthenticated — see signet-response-feed.ts).
|
|
15
|
+
// - RespondBidirectionalEvent: the requesting client watches these to learn
|
|
16
|
+
// the MPC's attestation of a request's remote execution landed, then reads
|
|
17
|
+
// the record back from the respond-bidirectional index — no off-chain
|
|
18
|
+
// verification, the contract authenticated it in-circuit at post time.
|
|
19
|
+
// In every case the event is a notification only — never the source of truth.
|
|
20
|
+
//
|
|
21
|
+
// These are the event-side twins of the Compact structs — each MUST stay in
|
|
22
|
+
// lockstep with its struct in Signet.compact (field order and widths). The
|
|
23
|
+
// byte layouts are pinned by the golden e2e tests against a live indexer.
|
|
24
|
+
|
|
25
|
+
import { bytesToHex, requestIdHex, type RequestIdHex } from "./signet-requests.ts";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The `Misc` event `name` tag the signet contract emits alongside the
|
|
29
|
+
* serialized {@link SignBidirectionalEvent} payload (Compact:
|
|
30
|
+
* `pad(32, "SignBidirectionalEvent")`). Observers filter on this ascii tag.
|
|
31
|
+
*/
|
|
32
|
+
export const SIGN_BIDIRECTIONAL_EVENT_TAG = "SignBidirectionalEvent";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Byte width of the leading `version` tag (Compact `Uint<8>`). The event is a
|
|
36
|
+
* FROZEN `{ version, payload }` envelope (see `SignBidirectionalEvent` in
|
|
37
|
+
* Signet.compact): `version` is at offset 0 and every V1 field is shifted one
|
|
38
|
+
* byte to make room for it.
|
|
39
|
+
*/
|
|
40
|
+
const VERSION_BYTES = 1;
|
|
41
|
+
|
|
42
|
+
/** The only payload interpretation this decoder understands today. */
|
|
43
|
+
const SUPPORTED_VERSION = 1;
|
|
44
|
+
|
|
45
|
+
/** Byte width of the `callerAddress` field (Compact `ContractAddress` = 32 raw bytes). */
|
|
46
|
+
const CALLER_ADDRESS_BYTES = 32;
|
|
47
|
+
|
|
48
|
+
/** Byte width of the `requestId` field (Compact `Bytes<32>`). */
|
|
49
|
+
const REQUEST_ID_BYTES = 32;
|
|
50
|
+
|
|
51
|
+
/** Offset of the V1 `callerAddress` in the outer payload (after the version byte). */
|
|
52
|
+
const CALLER_ADDRESS_OFFSET = VERSION_BYTES;
|
|
53
|
+
|
|
54
|
+
/** Offset of the V1 `requestId` (after version + callerAddress). */
|
|
55
|
+
const REQUEST_ID_OFFSET = CALLER_ADDRESS_OFFSET + CALLER_ADDRESS_BYTES;
|
|
56
|
+
|
|
57
|
+
/** Offset of the V1 `signBidirectionalRequestsIndexField` (after version + caller + requestId). */
|
|
58
|
+
const REQUESTS_INDEX_FIELD_OFFSET = REQUEST_ID_OFFSET + REQUEST_ID_BYTES;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Minimum serialized length of a V1 event: `version` (1) + `callerAddress` (32)
|
|
62
|
+
* + `requestId` (32) + `signBidirectionalRequestsIndexField` (1) = 66.
|
|
63
|
+
* `serialize<T,256>` right-pads to 256, so a real payload is longer; this is the
|
|
64
|
+
* floor the decoder needs.
|
|
65
|
+
*/
|
|
66
|
+
const MIN_PAYLOAD_BYTES = REQUESTS_INDEX_FIELD_OFFSET + 1;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* TS twin of the Compact `SignBidirectionalEvent` *event* struct (not the
|
|
70
|
+
* request record). The notification the signet contract emits so the MPC knows
|
|
71
|
+
* a {@link SignBidirectionalRequest} was stored — and, crucially, WHERE to read
|
|
72
|
+
* the authenticated copy of it.
|
|
73
|
+
*/
|
|
74
|
+
export interface SignBidirectionalEvent {
|
|
75
|
+
/**
|
|
76
|
+
* The frozen envelope's `version` tag (Compact `Uint<8>`). Selects how the
|
|
77
|
+
* opaque payload was interpreted; this decoder only understands version 1.
|
|
78
|
+
*/
|
|
79
|
+
version: number;
|
|
80
|
+
/**
|
|
81
|
+
* Address of the contract whose request index holds the request (Compact
|
|
82
|
+
* `ContractAddress`, rendered as lowercase hex, no `0x` prefix — directly
|
|
83
|
+
* usable as a `queryContractState` argument). The MPC reads the request from
|
|
84
|
+
* THIS contract's authenticated state; the event field itself confers no
|
|
85
|
+
* authority.
|
|
86
|
+
*/
|
|
87
|
+
callerAddress: string;
|
|
88
|
+
/** Which request in {@link callerAddress}'s index this notification is about. */
|
|
89
|
+
requestId: RequestIdHex;
|
|
90
|
+
/**
|
|
91
|
+
* Ledger field position of the `Map<RequestId, SignBidirectionalRequest>`
|
|
92
|
+
* request index in {@link callerAddress} (the signet layout convention puts
|
|
93
|
+
* it at field 0, but the event carries it so the reader never assumes).
|
|
94
|
+
*/
|
|
95
|
+
requestsIndexField: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Strip an optional `0x`/`0X` prefix from a hex string.
|
|
100
|
+
*
|
|
101
|
+
* @param hex - A hex string, with or without a `0x` prefix.
|
|
102
|
+
* @returns The bare hex digits.
|
|
103
|
+
*/
|
|
104
|
+
export function stripHexPrefix(hex: string): string {
|
|
105
|
+
return hex.startsWith("0x") || hex.startsWith("0X") ? hex.slice(2) : hex;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Decode a hex string (as the indexer returns `Misc.name` / `Misc.payload`)
|
|
110
|
+
* into bytes.
|
|
111
|
+
*
|
|
112
|
+
* @param hex - Hex digits, with or without a `0x` prefix.
|
|
113
|
+
* @returns The decoded bytes.
|
|
114
|
+
*/
|
|
115
|
+
export function hexToBytes(hex: string): Uint8Array {
|
|
116
|
+
const digits = stripHexPrefix(hex);
|
|
117
|
+
const out = new Uint8Array(digits.length >> 1);
|
|
118
|
+
for (let i = 0; i < out.length; i++) {
|
|
119
|
+
out[i] = Number.parseInt(digits.slice(2 * i, 2 * i + 2), 16);
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The ascii tag of a zero-padded `Bytes<N>` name (the Compact `pad(N, "text")`
|
|
126
|
+
* convention): decode the hex, then trim the trailing NUL padding.
|
|
127
|
+
*
|
|
128
|
+
* @param nameHex - The indexer's hex-encoded `Misc.name`.
|
|
129
|
+
* @returns The trimmed ascii tag, e.g. `"SignBidirectionalEvent"`.
|
|
130
|
+
*/
|
|
131
|
+
export function eventNameTag(nameHex: string): string {
|
|
132
|
+
return new TextDecoder().decode(hexToBytes(nameHex)).replace(/\0+$/, "");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Decode a `Misc` payload (`serialize<SignBidirectionalEvent, 256>`) into the
|
|
137
|
+
* struct. The event is a frozen `{ version: Uint<8>, payload: Bytes<224> }`
|
|
138
|
+
* envelope, and V1 packs the concrete fields into that inner payload with
|
|
139
|
+
* `serialize<SignBidirectionalEventV1Payload, 224>`. `serialize<T,N>` lays
|
|
140
|
+
* fields out in declaration order, packed at the front and right-zero-padded
|
|
141
|
+
* (see knowledge-base/events.md), so in the outer 256-byte payload:
|
|
142
|
+
* `version = payload[0]`, then everything shifts one byte —
|
|
143
|
+
* `callerAddress = payload[1..33]`, `requestId = payload[33..65]`,
|
|
144
|
+
* `requestsIndexField = payload[65]`.
|
|
145
|
+
*
|
|
146
|
+
* The byte layout is pinned against a live indexer by the golden e2e test —
|
|
147
|
+
* `callerAddress` is the V1 payload's FIRST field, a different position than the
|
|
148
|
+
* DepositEvent precedent's `caller`, so do not assume it without that test.
|
|
149
|
+
*
|
|
150
|
+
* Fails closed on an unrecognised `version`: a future payload layout adds a
|
|
151
|
+
* branch here (and, off-chain, a V{N} decoder) rather than silently
|
|
152
|
+
* misinterpreting bytes under the V1 offsets.
|
|
153
|
+
*
|
|
154
|
+
* @param payload - The decoded (raw bytes) `Misc.payload`.
|
|
155
|
+
* @returns The decoded event notification.
|
|
156
|
+
* @throws Error if the payload is shorter than the fixed fields require, or its
|
|
157
|
+
* `version` is not one this decoder understands.
|
|
158
|
+
*/
|
|
159
|
+
export function decodeSignBidirectionalEvent(
|
|
160
|
+
payload: Uint8Array,
|
|
161
|
+
): SignBidirectionalEvent {
|
|
162
|
+
if (payload.length < MIN_PAYLOAD_BYTES) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`SignBidirectionalEvent payload is ${payload.length} bytes — fewer than ` +
|
|
165
|
+
`the ${MIN_PAYLOAD_BYTES} its fixed fields need`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
const version = payload[0];
|
|
169
|
+
if (version !== SUPPORTED_VERSION) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
`SignBidirectionalEvent version ${version} is not supported ` +
|
|
172
|
+
`(this decoder understands version ${SUPPORTED_VERSION})`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
const callerAddress = bytesToHex(
|
|
176
|
+
payload.slice(CALLER_ADDRESS_OFFSET, CALLER_ADDRESS_OFFSET + CALLER_ADDRESS_BYTES),
|
|
177
|
+
);
|
|
178
|
+
const requestId = requestIdHex(
|
|
179
|
+
payload.slice(REQUEST_ID_OFFSET, REQUEST_ID_OFFSET + REQUEST_ID_BYTES),
|
|
180
|
+
);
|
|
181
|
+
const requestsIndexField = payload[REQUESTS_INDEX_FIELD_OFFSET];
|
|
182
|
+
return { version, callerAddress, requestId, requestsIndexField };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The `Misc` event `name` tag the signet contract emits alongside the
|
|
187
|
+
* serialized {@link SignatureRespondedEvent} payload (Compact:
|
|
188
|
+
* `pad(32, "SignatureRespondedEvent")`). Observers filter on this ascii tag.
|
|
189
|
+
*/
|
|
190
|
+
export const SIGNATURE_RESPONDED_EVENT_TAG = "SignatureRespondedEvent";
|
|
191
|
+
|
|
192
|
+
/** Byte width of the `count` field (Compact `Uint<64>` = 8 bytes, little-endian). */
|
|
193
|
+
const COUNT_BYTES = 8;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Minimum serialized length of a {@link SignatureRespondedEvent}: `requestId`
|
|
197
|
+
* (32) + `count` (8). `serialize<T,256>` right-pads to 256, so a real payload
|
|
198
|
+
* is longer; this is the floor the decoder needs.
|
|
199
|
+
*/
|
|
200
|
+
const MIN_RESPONDED_PAYLOAD_BYTES = REQUEST_ID_BYTES + COUNT_BYTES;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* TS twin of the Compact `SignatureRespondedEvent` *event* struct (not the
|
|
204
|
+
* {@link SignatureResponse} record it announces). The notification the signet
|
|
205
|
+
* contract emits from `postSignatureResponse` so the requesting client knows
|
|
206
|
+
* a response post landed — and WHERE in the response log to read it back.
|
|
207
|
+
* A ping only: the log is unauthenticated, so the client must verify the
|
|
208
|
+
* post's signature off-chain (see signet-response-feed.ts).
|
|
209
|
+
*/
|
|
210
|
+
export interface SignatureRespondedEvent {
|
|
211
|
+
/** Which request the posted response answers. */
|
|
212
|
+
requestId: RequestIdHex;
|
|
213
|
+
/**
|
|
214
|
+
* 0-based position of the post in {@link requestId}'s response log — the
|
|
215
|
+
* `count` half of the `SignetResponseKey` it is stored under.
|
|
216
|
+
*/
|
|
217
|
+
count: bigint;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Decode a `Misc` payload (`serialize<SignatureRespondedEvent, 256>`) into
|
|
222
|
+
* the struct. Layout per knowledge-base/events.md: fields in declaration
|
|
223
|
+
* order packed at the front — `requestId = payload[0..32]` (raw bytes),
|
|
224
|
+
* `count = payload[32..40]` (Uint<64>, LITTLE-endian) — right-zero-padded.
|
|
225
|
+
*
|
|
226
|
+
* @param payload - The decoded (raw bytes) `Misc.payload`.
|
|
227
|
+
* @returns The decoded event notification.
|
|
228
|
+
* @throws Error if the payload is shorter than the fixed fields require.
|
|
229
|
+
*/
|
|
230
|
+
export function decodeSignatureRespondedEvent(
|
|
231
|
+
payload: Uint8Array,
|
|
232
|
+
): SignatureRespondedEvent {
|
|
233
|
+
if (payload.length < MIN_RESPONDED_PAYLOAD_BYTES) {
|
|
234
|
+
throw new Error(
|
|
235
|
+
`SignatureRespondedEvent payload is ${payload.length} bytes — fewer than ` +
|
|
236
|
+
`the ${MIN_RESPONDED_PAYLOAD_BYTES} its fixed fields need`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
const requestId = requestIdHex(payload.slice(0, REQUEST_ID_BYTES));
|
|
240
|
+
let count = 0n;
|
|
241
|
+
for (let i = REQUEST_ID_BYTES + COUNT_BYTES - 1; i >= REQUEST_ID_BYTES; i--) {
|
|
242
|
+
count = (count << 8n) | BigInt(payload[i]);
|
|
243
|
+
}
|
|
244
|
+
return { requestId, count };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* The `Misc` event `name` tag the signet contract emits alongside the
|
|
249
|
+
* serialized {@link RespondBidirectionalEvent} payload (Compact:
|
|
250
|
+
* `pad(32, "RespondBidirectionalEvent")`). Observers filter on this ascii tag.
|
|
251
|
+
*/
|
|
252
|
+
export const RESPOND_BIDIRECTIONAL_EVENT_TAG = "RespondBidirectionalEvent";
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Minimum serialized length of a {@link RespondBidirectionalEvent}:
|
|
256
|
+
* `requestId` (32). `serialize<T,256>` right-pads to 256, so a real payload
|
|
257
|
+
* is longer; this is the floor the decoder needs.
|
|
258
|
+
*/
|
|
259
|
+
const MIN_RESPOND_BIDIRECTIONAL_PAYLOAD_BYTES = REQUEST_ID_BYTES;
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* TS twin of the Compact `RespondBidirectionalEvent` *event* struct (not the
|
|
263
|
+
* `RespondBidirectional` record it announces). The notification the signet
|
|
264
|
+
* contract emits from `postRespondBidirectional` when the MPC's attestation
|
|
265
|
+
* of a request's remote execution lands in the respond-bidirectional index —
|
|
266
|
+
* at most once per request (one authenticated slot, first valid write wins).
|
|
267
|
+
* A ping only, but unlike {@link SignatureRespondedEvent} no off-chain
|
|
268
|
+
* verification follows the read-back: the contract verified the attestation
|
|
269
|
+
* in-circuit at post time, so the stored record is authentic by construction.
|
|
270
|
+
*/
|
|
271
|
+
export interface RespondBidirectionalEvent {
|
|
272
|
+
/** Which request the stored attestation answers. */
|
|
273
|
+
requestId: RequestIdHex;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Decode a `Misc` payload (`serialize<RespondBidirectionalEvent, 256>`) into
|
|
278
|
+
* the struct. Layout per knowledge-base/events.md: fields in declaration
|
|
279
|
+
* order packed at the front — `requestId = payload[0..32]` (raw bytes) —
|
|
280
|
+
* right-zero-padded.
|
|
281
|
+
*
|
|
282
|
+
* @param payload - The decoded (raw bytes) `Misc.payload`.
|
|
283
|
+
* @returns The decoded event notification.
|
|
284
|
+
* @throws Error if the payload is shorter than the fixed fields require.
|
|
285
|
+
*/
|
|
286
|
+
export function decodeRespondBidirectionalEvent(
|
|
287
|
+
payload: Uint8Array,
|
|
288
|
+
): RespondBidirectionalEvent {
|
|
289
|
+
if (payload.length < MIN_RESPOND_BIDIRECTIONAL_PAYLOAD_BYTES) {
|
|
290
|
+
throw new Error(
|
|
291
|
+
`RespondBidirectionalEvent payload is ${payload.length} bytes — fewer than ` +
|
|
292
|
+
`the ${MIN_RESPOND_BIDIRECTIONAL_PAYLOAD_BYTES} its fixed fields need`,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
return { requestId: requestIdHex(payload.slice(0, REQUEST_ID_BYTES)) };
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* One signet event kind as an observer consumes it: the `Misc.name` ascii tag
|
|
300
|
+
* to filter on plus the payload decoder. A `SignetEventObserver` is
|
|
301
|
+
* instantiated with one of these — see {@link signBidirectionalEventCodec},
|
|
302
|
+
* {@link signatureRespondedEventCodec}, and
|
|
303
|
+
* {@link respondBidirectionalEventCodec}.
|
|
304
|
+
*/
|
|
305
|
+
export interface SignetMiscEventCodec<T> {
|
|
306
|
+
/** The ascii `Misc.name` tag identifying this event kind. */
|
|
307
|
+
readonly tag: string;
|
|
308
|
+
/**
|
|
309
|
+
* Decode a raw `Misc.payload` into the typed notification.
|
|
310
|
+
*
|
|
311
|
+
* @param payload - The decoded (raw bytes) `Misc.payload`.
|
|
312
|
+
* @returns The decoded event notification.
|
|
313
|
+
* @throws Error if the payload is malformed.
|
|
314
|
+
*/
|
|
315
|
+
decode(payload: Uint8Array): T;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** The {@link SignetMiscEventCodec} of {@link SignBidirectionalEvent} notifications. */
|
|
319
|
+
export const signBidirectionalEventCodec: SignetMiscEventCodec<SignBidirectionalEvent> =
|
|
320
|
+
{
|
|
321
|
+
tag: SIGN_BIDIRECTIONAL_EVENT_TAG,
|
|
322
|
+
decode: decodeSignBidirectionalEvent,
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
/** The {@link SignetMiscEventCodec} of {@link SignatureRespondedEvent} notifications. */
|
|
326
|
+
export const signatureRespondedEventCodec: SignetMiscEventCodec<SignatureRespondedEvent> =
|
|
327
|
+
{
|
|
328
|
+
tag: SIGNATURE_RESPONDED_EVENT_TAG,
|
|
329
|
+
decode: decodeSignatureRespondedEvent,
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
/** The {@link SignetMiscEventCodec} of {@link RespondBidirectionalEvent} notifications. */
|
|
333
|
+
export const respondBidirectionalEventCodec: SignetMiscEventCodec<RespondBidirectionalEvent> =
|
|
334
|
+
{
|
|
335
|
+
tag: RESPOND_BIDIRECTIONAL_EVENT_TAG,
|
|
336
|
+
decode: decodeRespondBidirectionalEvent,
|
|
337
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// The MPC's single entry point for discovering signature requests by event
|
|
2
|
+
// observation: watch the central signet contract's notifications, resolve each
|
|
3
|
+
// to an AUTHENTICATED request read from the named caller's own ledger, and
|
|
4
|
+
// yield only the ones that pass the membership check. Replaces polling a
|
|
5
|
+
// configured list of requester contracts — the feed discovers requesters
|
|
6
|
+
// dynamically, keyed off whose authenticated state it actually read.
|
|
7
|
+
//
|
|
8
|
+
// The requester allow-list is an OPTIONAL policy filter here (drop requests
|
|
9
|
+
// from contracts you choose not to serve), never a security requirement and
|
|
10
|
+
// never the discovery mechanism — attribution comes from the resolver's
|
|
11
|
+
// authenticated read (see signet-request-resolver.ts and
|
|
12
|
+
// knowledge-base/caller-attribution.md).
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
SignetEventObserver,
|
|
16
|
+
sleepUnlessAborted,
|
|
17
|
+
type SignetEventSource,
|
|
18
|
+
} from "./signet-event-observer.ts";
|
|
19
|
+
import {
|
|
20
|
+
signBidirectionalEventCodec,
|
|
21
|
+
stripHexPrefix,
|
|
22
|
+
type SignBidirectionalEvent,
|
|
23
|
+
} from "./signet-events.ts";
|
|
24
|
+
import {
|
|
25
|
+
SignetRequestResolver,
|
|
26
|
+
type ResolvedSignetRequest,
|
|
27
|
+
} from "./signet-request-resolver.ts";
|
|
28
|
+
import type { SignetPublicStateSource } from "./signet-request-response-reader.ts";
|
|
29
|
+
import type { RequestIdHex } from "./signet-requests.ts";
|
|
30
|
+
|
|
31
|
+
/** Default gap between poll cycles of {@link SignetRequestFeed.requests}. */
|
|
32
|
+
export const DEFAULT_FEED_POLL_INTERVAL_MS = 3000;
|
|
33
|
+
|
|
34
|
+
/** Everything a {@link SignetRequestFeed} needs. */
|
|
35
|
+
export interface SignetRequestFeedConfig {
|
|
36
|
+
/** Address of the central signet contract whose events to watch. */
|
|
37
|
+
readonly signetContractAddress: string;
|
|
38
|
+
/**
|
|
39
|
+
* Source of BOTH events and raw contract state — a full
|
|
40
|
+
* `indexerPublicDataProvider` satisfies both halves.
|
|
41
|
+
*/
|
|
42
|
+
readonly source: SignetEventSource & SignetPublicStateSource;
|
|
43
|
+
/**
|
|
44
|
+
* Optional policy allow-list of requester contract addresses to serve
|
|
45
|
+
* (matched case- and `0x`-prefix-insensitively). Omit to serve every
|
|
46
|
+
* requester the resolver can authenticate. NOT a security control.
|
|
47
|
+
*/
|
|
48
|
+
readonly allowContracts?: Iterable<string>;
|
|
49
|
+
/** Durable resume floor for the underlying observer (see {@link SignetEventObserverConfig.fromEventId}). */
|
|
50
|
+
readonly fromEventId?: number;
|
|
51
|
+
/** Poll cadence for {@link SignetRequestFeed.requests}; default {@link DEFAULT_FEED_POLL_INTERVAL_MS}. */
|
|
52
|
+
readonly pollIntervalMs?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Canonical form for comparing contract addresses: no `0x`, lowercase. */
|
|
56
|
+
function normalizeAddress(address: string): string {
|
|
57
|
+
return stripHexPrefix(address).toLowerCase();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The event-driven request feed. Composes a {@link SignetEventObserver}
|
|
62
|
+
* (discovery) with a {@link SignetRequestResolver} (authentication), yielding
|
|
63
|
+
* only notifications that resolve to a real, member request. Dedupes by request
|
|
64
|
+
* id across its lifetime, so a redelivered notification is not re-yielded; call
|
|
65
|
+
* {@link forget} to re-arm a request whose downstream processing failed.
|
|
66
|
+
*/
|
|
67
|
+
export class SignetRequestFeed {
|
|
68
|
+
private readonly observer: SignetEventObserver<SignBidirectionalEvent>;
|
|
69
|
+
private readonly resolver: SignetRequestResolver;
|
|
70
|
+
private readonly allowContracts?: Set<string>;
|
|
71
|
+
private readonly pollIntervalMs: number;
|
|
72
|
+
|
|
73
|
+
// Request ids already yielded. NOT the security boundary (the resolver is) —
|
|
74
|
+
// just an at-most-once gate so one request is not processed twice.
|
|
75
|
+
private readonly yielded = new Set<RequestIdHex>();
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* @param config - The signet contract, combined event/state source, optional
|
|
79
|
+
* policy allow-list, and resume floor.
|
|
80
|
+
*/
|
|
81
|
+
constructor(config: SignetRequestFeedConfig) {
|
|
82
|
+
this.observer = new SignetEventObserver({
|
|
83
|
+
signetContractAddress: config.signetContractAddress,
|
|
84
|
+
source: config.source,
|
|
85
|
+
codec: signBidirectionalEventCodec,
|
|
86
|
+
fromEventId: config.fromEventId,
|
|
87
|
+
pollIntervalMs: config.pollIntervalMs,
|
|
88
|
+
});
|
|
89
|
+
this.resolver = new SignetRequestResolver({ source: config.source });
|
|
90
|
+
this.allowContracts = config.allowContracts
|
|
91
|
+
? new Set(Array.from(config.allowContracts, normalizeAddress))
|
|
92
|
+
: undefined;
|
|
93
|
+
this.pollIntervalMs =
|
|
94
|
+
config.pollIntervalMs ?? DEFAULT_FEED_POLL_INTERVAL_MS;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Whether the policy allow-list admits `callerAddress` (always true when unset). */
|
|
98
|
+
private allowed(callerAddress: string): boolean {
|
|
99
|
+
return (
|
|
100
|
+
this.allowContracts === undefined ||
|
|
101
|
+
this.allowContracts.has(normalizeAddress(callerAddress))
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* One-shot: every currently-visible notification that is admitted by the
|
|
107
|
+
* allow-list, not already yielded, and resolves to an authenticated member
|
|
108
|
+
* request. Forged / not-yet-indexed / wrong-field notifications resolve to
|
|
109
|
+
* nothing and are dropped WITHOUT being marked yielded, so a genuine
|
|
110
|
+
* request whose ledger write has not indexed yet is retried next cycle.
|
|
111
|
+
*
|
|
112
|
+
* @returns The newly-authenticated requests this cycle, in event id order.
|
|
113
|
+
*/
|
|
114
|
+
async poll(): Promise<ResolvedSignetRequest[]> {
|
|
115
|
+
const events = await this.observer.currentEvents();
|
|
116
|
+
const out: ResolvedSignetRequest[] = [];
|
|
117
|
+
for (const event of events) {
|
|
118
|
+
if (this.yielded.has(event.requestId)) continue;
|
|
119
|
+
if (!this.allowed(event.callerAddress)) continue;
|
|
120
|
+
const resolved = await this.resolver.resolve(event);
|
|
121
|
+
if (resolved === undefined) continue;
|
|
122
|
+
this.yielded.add(event.requestId);
|
|
123
|
+
out.push(resolved);
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Re-arm `requestId` for redelivery on the next {@link poll} / {@link requests}
|
|
130
|
+
* cycle — call when downstream processing of a yielded request failed, so it
|
|
131
|
+
* is retried (mirrors the MPC's delete-on-failure of its processed set).
|
|
132
|
+
*
|
|
133
|
+
* @param requestId - The request id to allow through again.
|
|
134
|
+
*/
|
|
135
|
+
forget(requestId: RequestIdHex): void {
|
|
136
|
+
this.yielded.delete(requestId);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Live stream: poll + sleep, yielding each authenticated request exactly once
|
|
141
|
+
* (subject to {@link forget}), until `opts.signal` aborts. The natural
|
|
142
|
+
* sequential `for await` consumption serializes downstream processing — no
|
|
143
|
+
* two requests are handed over concurrently.
|
|
144
|
+
*
|
|
145
|
+
* @param opts.signal - Abort to stop the stream.
|
|
146
|
+
* @yields Each authenticated request, in discovery order.
|
|
147
|
+
*/
|
|
148
|
+
async *requests(opts?: {
|
|
149
|
+
signal?: AbortSignal;
|
|
150
|
+
}): AsyncIterableIterator<ResolvedSignetRequest> {
|
|
151
|
+
while (!opts?.signal?.aborted) {
|
|
152
|
+
const batch = await this.poll();
|
|
153
|
+
for (const resolved of batch) yield resolved;
|
|
154
|
+
if (opts?.signal?.aborted) break;
|
|
155
|
+
await sleepUnlessAborted(this.pollIntervalMs, opts?.signal);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Turn a SignBidirectionalEvent notification into an AUTHENTICATED request
|
|
2
|
+
// record. This is the security core of the event flow (see
|
|
3
|
+
// knowledge-base/caller-attribution.md and the invariants in this task's
|
|
4
|
+
// brief): the event says only WHERE to look; the request that gets signed is
|
|
5
|
+
// read from the named caller's own authenticated ledger, and the event's
|
|
6
|
+
// `requestId` must be a member of that contract's request index. An attacker
|
|
7
|
+
// cannot write into a contract it does not control, so a forged event can at
|
|
8
|
+
// most re-point at a legitimate request that already exists — never inject one.
|
|
9
|
+
//
|
|
10
|
+
// Key derivation stays f(callerAddress, path), keyed off the contract whose
|
|
11
|
+
// authenticated state was actually read — exactly today's guarantee, now
|
|
12
|
+
// reached via an event ping instead of per-contract polling.
|
|
13
|
+
|
|
14
|
+
import { lookupSignetRequestAt } from "./signature-requests-state-reader.ts";
|
|
15
|
+
import type { RawContractState } from "./signature-state-reading.ts";
|
|
16
|
+
import type { SignBidirectionalEvent } from "./signet-events.ts";
|
|
17
|
+
import type { SignetPublicStateSource } from "./signet-request-response-reader.ts";
|
|
18
|
+
import type {
|
|
19
|
+
RequestIdHex,
|
|
20
|
+
SignBidirectionalRequest,
|
|
21
|
+
} from "./signet-requests.ts";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A {@link SignBidirectionalEvent} resolved to the authenticated request it
|
|
25
|
+
* refers to: the request record read from `callerAddress`'s own ledger, with
|
|
26
|
+
* `requestId` confirmed to be a member of that contract's index.
|
|
27
|
+
*/
|
|
28
|
+
export interface ResolvedSignetRequest {
|
|
29
|
+
/**
|
|
30
|
+
* The contract whose authenticated state the request was read from — the
|
|
31
|
+
* epsilon-derivation predecessor. Key derivation keys off THIS, never off a
|
|
32
|
+
* field taken from the event on faith.
|
|
33
|
+
*/
|
|
34
|
+
callerAddress: string;
|
|
35
|
+
/** The request id, confirmed to be a member of `callerAddress`'s index. */
|
|
36
|
+
requestId: RequestIdHex;
|
|
37
|
+
/** The authenticated request record to sign. */
|
|
38
|
+
request: SignBidirectionalRequest;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Everything a {@link SignetRequestResolver} needs. */
|
|
42
|
+
export interface SignetRequestResolverConfig {
|
|
43
|
+
/** Source of raw contract state, e.g. midnight-js's `indexerPublicDataProvider`. */
|
|
44
|
+
readonly source: SignetPublicStateSource;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Resolves {@link SignBidirectionalEvent} notifications to authenticated
|
|
49
|
+
* {@link ResolvedSignetRequest}s by reading the named caller's ledger and
|
|
50
|
+
* enforcing the membership check. Construct once and reuse: resolved records
|
|
51
|
+
* are immutable (their ledger key is their hash), so they are cached by request
|
|
52
|
+
* id and a repeated notification for the same request costs no query.
|
|
53
|
+
*
|
|
54
|
+
* NB the cache is keyed by request id, NOT by contract address: a caller
|
|
55
|
+
* contract's state grows as it stores new requests, so caching its state would
|
|
56
|
+
* make the resolver blind to later requests from the same caller. Caching the
|
|
57
|
+
* immutable resolved record instead is the same pattern
|
|
58
|
+
* {@link SignetRequestResponseReader.getSignatureRequest} uses.
|
|
59
|
+
*/
|
|
60
|
+
export class SignetRequestResolver {
|
|
61
|
+
private readonly source: SignetPublicStateSource;
|
|
62
|
+
|
|
63
|
+
// Resolved records never change; cache them so a redelivered notification for
|
|
64
|
+
// the same request id does not re-query the caller's state.
|
|
65
|
+
private readonly resolvedCache = new Map<RequestIdHex, ResolvedSignetRequest>();
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param config - The state source to read caller ledgers through.
|
|
69
|
+
*/
|
|
70
|
+
constructor(config: SignetRequestResolverConfig) {
|
|
71
|
+
this.source = config.source;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolve a notification to its authenticated request, or `undefined` when it
|
|
76
|
+
* cannot be trusted: the caller contract has no state, or `requestId` is not
|
|
77
|
+
* a member of the index at `requestsIndexField` (forged, stale, wrong field,
|
|
78
|
+
* or not yet indexed — poll again). Never throws on an untrusted event; a bad
|
|
79
|
+
* notification is dropped, not surfaced as an error.
|
|
80
|
+
*
|
|
81
|
+
* @param event - The decoded event notification.
|
|
82
|
+
* @returns The authenticated request, or `undefined` to drop the event.
|
|
83
|
+
*/
|
|
84
|
+
async resolve(
|
|
85
|
+
event: SignBidirectionalEvent,
|
|
86
|
+
): Promise<ResolvedSignetRequest | undefined> {
|
|
87
|
+
const cached = this.resolvedCache.get(event.requestId);
|
|
88
|
+
if (cached !== undefined) {
|
|
89
|
+
return cached;
|
|
90
|
+
}
|
|
91
|
+
let state: { data: RawContractState } | null;
|
|
92
|
+
try {
|
|
93
|
+
state = await this.source.queryContractState(event.callerAddress);
|
|
94
|
+
} catch {
|
|
95
|
+
return undefined; // caller address not a contract / transient read error
|
|
96
|
+
}
|
|
97
|
+
if (!state?.data) {
|
|
98
|
+
return undefined; // no state at the named caller — cannot authenticate
|
|
99
|
+
}
|
|
100
|
+
const request = lookupSignetRequestAt(
|
|
101
|
+
state.data,
|
|
102
|
+
event.requestsIndexField,
|
|
103
|
+
event.requestId,
|
|
104
|
+
);
|
|
105
|
+
if (request === undefined) {
|
|
106
|
+
return undefined; // membership check failed — drop the notification
|
|
107
|
+
}
|
|
108
|
+
const resolved: ResolvedSignetRequest = {
|
|
109
|
+
callerAddress: event.callerAddress,
|
|
110
|
+
requestId: event.requestId,
|
|
111
|
+
request,
|
|
112
|
+
};
|
|
113
|
+
this.resolvedCache.set(event.requestId, resolved);
|
|
114
|
+
return resolved;
|
|
115
|
+
}
|
|
116
|
+
}
|