@sig-net/midnight 0.0.1 → 0.0.2
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/dist/index.d.ts +2 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -4
- package/dist/index.js.map +1 -1
- package/dist/managed/compiler/contract-info.json +57 -0
- package/dist/managed/compiler/contract-manifest.json +8 -8
- package/dist/managed/contract/index.d.ts +13 -0
- package/dist/managed/contract/index.js +138 -73
- package/dist/managed/contract/index.js.map +1 -1
- package/dist/signature-requests-state-reader.d.ts +5 -5
- package/dist/signature-requests-state-reader.js +5 -5
- package/dist/signet-contract-state-reader.d.ts +88 -2
- package/dist/signet-contract-state-reader.d.ts.map +1 -1
- package/dist/signet-contract-state-reader.js +106 -3
- package/dist/signet-contract-state-reader.js.map +1 -1
- package/dist/signet-request-feed.d.ts +40 -21
- package/dist/signet-request-feed.d.ts.map +1 -1
- package/dist/signet-request-feed.js +79 -34
- package/dist/signet-request-feed.js.map +1 -1
- package/dist/signet-request-resolver.d.ts +11 -11
- package/dist/signet-request-resolver.d.ts.map +1 -1
- package/dist/signet-request-resolver.js +22 -22
- package/dist/signet-request-resolver.js.map +1 -1
- package/dist/signet-requests.d.ts +18 -4
- package/dist/signet-requests.d.ts.map +1 -1
- package/dist/signet-requests.js +29 -6
- package/dist/signet-requests.js.map +1 -1
- package/package.json +1 -1
- package/src/Signet.compact +75 -99
- package/src/circuits.compact +5 -2
- package/src/index.ts +2 -4
- package/src/managed/compiler/contract-info.json +57 -0
- package/src/managed/compiler/contract-manifest.json +8 -8
- package/src/managed/contract/index.d.ts +13 -0
- package/src/managed/contract/index.js +138 -73
- package/src/managed/contract/index.js.map +1 -1
- package/src/signature-requests-state-reader.ts +5 -5
- package/src/signet-contract-state-reader.ts +200 -4
- package/src/signet-request-feed.ts +98 -48
- package/src/signet-request-resolver.ts +28 -28
- package/src/signet-requests.ts +31 -6
- package/dist/signet-event-observer.d.ts +0 -102
- package/dist/signet-event-observer.d.ts.map +0 -1
- package/dist/signet-event-observer.js +0 -127
- package/dist/signet-event-observer.js.map +0 -1
- package/dist/signet-events.d.ts +0 -175
- package/dist/signet-events.d.ts.map +0 -1
- package/dist/signet-events.js +0 -210
- package/dist/signet-events.js.map +0 -1
- package/dist/signet-response-feed.d.ts +0 -161
- package/dist/signet-response-feed.d.ts.map +0 -1
- package/dist/signet-response-feed.js +0 -234
- package/dist/signet-response-feed.js.map +0 -1
- package/src/signet-event-observer.ts +0 -186
- package/src/signet-events.ts +0 -337
- package/src/signet-response-feed.ts +0 -325
package/src/signet-events.ts
DELETED
|
@@ -1,337 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,325 +0,0 @@
|
|
|
1
|
-
// The requesting client's entry point for discovering MPC signature responses
|
|
2
|
-
// by event observation: watch the central signet contract's
|
|
3
|
-
// SignatureRespondedEvent notifications, and when one announces a post for a
|
|
4
|
-
// request the client cares about, read the response log back and verify every
|
|
5
|
-
// post against the request — the response-direction mirror of
|
|
6
|
-
// signet-request-feed.ts. The event is only the TRIGGER for a state read; the
|
|
7
|
-
// verdicts come from the ledger via the same SignetRequestResponseReader all
|
|
8
|
-
// other consumers use, so an event/state indexing skew in either direction is
|
|
9
|
-
// tolerated (a post visible in state before its event is still yielded; an
|
|
10
|
-
// event whose write has not indexed yet is retried next cycle).
|
|
11
|
-
//
|
|
12
|
-
// Trust model (see "Signet Contract Ledger Layout" in Signet.compact): the
|
|
13
|
-
// response log is UNAUTHENTICATED — neither the event nor the stored record
|
|
14
|
-
// confers authenticity. Only the off-chain signature verification does, which
|
|
15
|
-
// is why the feed yields per-post VERDICTS rather than raw responses.
|
|
16
|
-
//
|
|
17
|
-
// The MPC's OTHER response kind — the respond-bidirectional attestation of a
|
|
18
|
-
// request's remote execution — gets the same event-gated treatment via
|
|
19
|
-
// SignetRespondBidirectionalFeed (RespondBidirectionalEvent → index read),
|
|
20
|
-
// but with the opposite trust model: the signet contract verified the
|
|
21
|
-
// attestation IN-CIRCUIT at post time, so the feed yields the stored record
|
|
22
|
-
// itself, no verdicts.
|
|
23
|
-
|
|
24
|
-
import {
|
|
25
|
-
SignetEventObserver,
|
|
26
|
-
sleepUnlessAborted,
|
|
27
|
-
type SignetEventSource,
|
|
28
|
-
} from "./signet-event-observer.ts";
|
|
29
|
-
import {
|
|
30
|
-
respondBidirectionalEventCodec,
|
|
31
|
-
signatureRespondedEventCodec,
|
|
32
|
-
type RespondBidirectionalEvent,
|
|
33
|
-
type SignatureRespondedEvent,
|
|
34
|
-
} from "./signet-events.ts";
|
|
35
|
-
import {
|
|
36
|
-
readSignetContractLedgerFromState,
|
|
37
|
-
type RespondBidirectional,
|
|
38
|
-
} from "./signet-contract-state-reader.ts";
|
|
39
|
-
import {
|
|
40
|
-
SignetRequestResponseReader,
|
|
41
|
-
type SignatureResponseVerdict,
|
|
42
|
-
type SignetPublicStateSource,
|
|
43
|
-
} from "./signet-request-response-reader.ts";
|
|
44
|
-
import type {
|
|
45
|
-
RequestIdHex,
|
|
46
|
-
SignBidirectionalRequest,
|
|
47
|
-
} from "./signet-requests.ts";
|
|
48
|
-
|
|
49
|
-
/** Default gap between poll cycles of {@link SignetResponseFeed.verdicts}. */
|
|
50
|
-
export const DEFAULT_RESPONSE_FEED_POLL_INTERVAL_MS = 3000;
|
|
51
|
-
|
|
52
|
-
/** Everything a {@link SignetResponseFeed} needs. */
|
|
53
|
-
export interface SignetResponseFeedConfig {
|
|
54
|
-
/** Address of the central signet contract whose events and response log to watch. */
|
|
55
|
-
readonly signetContractAddress: string;
|
|
56
|
-
/** Address of the signet-compliant requester contract holding the request records (e.g. the vault). */
|
|
57
|
-
readonly requesterContractAddress: string;
|
|
58
|
-
/**
|
|
59
|
-
* Source of BOTH events and raw contract state — a full
|
|
60
|
-
* `indexerPublicDataProvider` satisfies both halves.
|
|
61
|
-
*/
|
|
62
|
-
readonly source: SignetEventSource & SignetPublicStateSource;
|
|
63
|
-
/** Durable resume floor for the underlying observer (see {@link SignetEventObserverConfig.fromEventId}). */
|
|
64
|
-
readonly fromEventId?: number;
|
|
65
|
-
/** Poll cadence for {@link SignetResponseFeed.verdicts}; default {@link DEFAULT_RESPONSE_FEED_POLL_INTERVAL_MS}. */
|
|
66
|
-
readonly pollIntervalMs?: number;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* The event-driven signature response feed. Composes a
|
|
71
|
-
* {@link SignetEventObserver} of {@link SignatureRespondedEvent} notifications
|
|
72
|
-
* (discovery) with a {@link SignetRequestResponseReader} (verification),
|
|
73
|
-
* yielding one {@link SignatureResponseVerdict} per posted response, in count
|
|
74
|
-
* order, each exactly once per feed lifetime. Consumers act on the first
|
|
75
|
-
* valid verdict and surface the rejected ones as they see fit — the feed,
|
|
76
|
-
* like the reader, never logs.
|
|
77
|
-
*/
|
|
78
|
-
export class SignetResponseFeed {
|
|
79
|
-
private readonly observer: SignetEventObserver<SignatureRespondedEvent>;
|
|
80
|
-
private readonly reader: SignetRequestResponseReader;
|
|
81
|
-
private readonly pollIntervalMs: number;
|
|
82
|
-
|
|
83
|
-
// Post counts already yielded, per request. NOT a trust boundary (the
|
|
84
|
-
// verdicts are) — just an exactly-once gate so a post is not re-judged.
|
|
85
|
-
private readonly yielded = new Map<RequestIdHex, Set<bigint>>();
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* @param config - The contract pair, combined event/state source, and
|
|
89
|
-
* resume floor.
|
|
90
|
-
*/
|
|
91
|
-
constructor(config: SignetResponseFeedConfig) {
|
|
92
|
-
this.observer = new SignetEventObserver({
|
|
93
|
-
signetContractAddress: config.signetContractAddress,
|
|
94
|
-
source: config.source,
|
|
95
|
-
codec: signatureRespondedEventCodec,
|
|
96
|
-
fromEventId: config.fromEventId,
|
|
97
|
-
pollIntervalMs: config.pollIntervalMs,
|
|
98
|
-
});
|
|
99
|
-
this.reader = new SignetRequestResponseReader({
|
|
100
|
-
requesterContractAddress: config.requesterContractAddress,
|
|
101
|
-
signetContractAddress: config.signetContractAddress,
|
|
102
|
-
publicDataProvider: config.source,
|
|
103
|
-
});
|
|
104
|
-
this.pollIntervalMs =
|
|
105
|
-
config.pollIntervalMs ?? DEFAULT_RESPONSE_FEED_POLL_INTERVAL_MS;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/** The yielded-count set for `requestId`, created on first use. */
|
|
109
|
-
private yieldedCounts(requestId: RequestIdHex): Set<bigint> {
|
|
110
|
-
let counts = this.yielded.get(requestId);
|
|
111
|
-
if (counts === undefined) {
|
|
112
|
-
counts = new Set();
|
|
113
|
-
this.yielded.set(requestId, counts);
|
|
114
|
-
}
|
|
115
|
-
return counts;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* One-shot: the verdicts on every response post for `requestId` not yielded
|
|
120
|
-
* before, in count order. The response log is only read when a currently-
|
|
121
|
-
* visible event announces a post this feed has not yielded yet — no new
|
|
122
|
-
* event, no state query. Ledger posts the read then surfaces beyond the
|
|
123
|
-
* announced ones are yielded too (events are the trigger, the ledger is the
|
|
124
|
-
* source of truth), and a post whose event is visible but whose write has
|
|
125
|
-
* not indexed yet is simply retried next cycle.
|
|
126
|
-
*
|
|
127
|
-
* @param requestId - The request whose response posts to judge.
|
|
128
|
-
* @param expectedSigner - The EVM address (0x hex, any case) the genuine
|
|
129
|
-
* response must be signed by — the requester's MPC-derived address.
|
|
130
|
-
* @returns The newly-judged verdicts this cycle (possibly empty).
|
|
131
|
-
* @throws Error when either contract has no state, the request is not on
|
|
132
|
-
* the requester's ledger, or the responses ledger is inconsistent.
|
|
133
|
-
*/
|
|
134
|
-
async poll(
|
|
135
|
-
requestId: RequestIdHex,
|
|
136
|
-
expectedSigner: string,
|
|
137
|
-
): Promise<SignatureResponseVerdict[]> {
|
|
138
|
-
const events = await this.observer.currentEvents();
|
|
139
|
-
const counts = this.yieldedCounts(requestId);
|
|
140
|
-
const announcesNew = events.some(
|
|
141
|
-
(event) => event.requestId === requestId && !counts.has(event.count),
|
|
142
|
-
);
|
|
143
|
-
if (!announcesNew) {
|
|
144
|
-
return [];
|
|
145
|
-
}
|
|
146
|
-
const { verdicts } = await this.reader.getVerifiedSignatureResponse(
|
|
147
|
-
requestId,
|
|
148
|
-
expectedSigner,
|
|
149
|
-
);
|
|
150
|
-
const fresh = verdicts.filter((verdict) => !counts.has(verdict.count));
|
|
151
|
-
for (const verdict of fresh) {
|
|
152
|
-
counts.add(verdict.count);
|
|
153
|
-
}
|
|
154
|
-
return fresh;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Live stream: poll + sleep, yielding the verdict on each posted response
|
|
159
|
-
* exactly once, in count order, until `opts.signal` aborts. The natural
|
|
160
|
-
* consumption is to warn on each rejected verdict and act on the first
|
|
161
|
-
* valid one (see the cli's poll-signature-response command).
|
|
162
|
-
*
|
|
163
|
-
* @param requestId - The request whose response posts to judge.
|
|
164
|
-
* @param expectedSigner - The EVM address (0x hex, any case) the genuine
|
|
165
|
-
* response must be signed by — the requester's MPC-derived address.
|
|
166
|
-
* @param opts.signal - Abort to stop the stream.
|
|
167
|
-
* @yields Each post's verdict, in discovery order.
|
|
168
|
-
* @throws Error when either contract has no state, the request is not on
|
|
169
|
-
* the requester's ledger, or the responses ledger is inconsistent.
|
|
170
|
-
*/
|
|
171
|
-
async *verdicts(
|
|
172
|
-
requestId: RequestIdHex,
|
|
173
|
-
expectedSigner: string,
|
|
174
|
-
opts?: { signal?: AbortSignal },
|
|
175
|
-
): AsyncIterableIterator<SignatureResponseVerdict> {
|
|
176
|
-
while (!opts?.signal?.aborted) {
|
|
177
|
-
const batch = await this.poll(requestId, expectedSigner);
|
|
178
|
-
for (const verdict of batch) yield verdict;
|
|
179
|
-
if (opts?.signal?.aborted) break;
|
|
180
|
-
await sleepUnlessAborted(this.pollIntervalMs, opts?.signal);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* The request record a verdict's response answers, read through the feed's
|
|
186
|
-
* own reader (cached after the first fetch — free after any {@link poll}
|
|
187
|
-
* that reached verification). What a consumer needs to assemble the signed
|
|
188
|
-
* transaction from a valid verdict via
|
|
189
|
-
* `signBidirectionalRequestToSignedEVMTransaction`.
|
|
190
|
-
*
|
|
191
|
-
* @param requestId - The request id to look up.
|
|
192
|
-
* @returns The stored request record.
|
|
193
|
-
* @throws Error when the requester contract has no state or holds no
|
|
194
|
-
* request under `requestId`.
|
|
195
|
-
*/
|
|
196
|
-
async getSignatureRequest(
|
|
197
|
-
requestId: RequestIdHex,
|
|
198
|
-
): Promise<SignBidirectionalRequest> {
|
|
199
|
-
return this.reader.getSignatureRequest(requestId);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/** Everything a {@link SignetRespondBidirectionalFeed} needs. */
|
|
204
|
-
export interface SignetRespondBidirectionalFeedConfig {
|
|
205
|
-
/**
|
|
206
|
-
* Address of the central signet contract whose events and
|
|
207
|
-
* respond-bidirectional index to watch. Attestations live entirely on the
|
|
208
|
-
* signet contract, so — unlike {@link SignetResponseFeedConfig} — no
|
|
209
|
-
* requester contract address is needed.
|
|
210
|
-
*/
|
|
211
|
-
readonly signetContractAddress: string;
|
|
212
|
-
/**
|
|
213
|
-
* Source of BOTH events and raw contract state — a full
|
|
214
|
-
* `indexerPublicDataProvider` satisfies both halves.
|
|
215
|
-
*/
|
|
216
|
-
readonly source: SignetEventSource & SignetPublicStateSource;
|
|
217
|
-
/** Durable resume floor for the underlying observer (see {@link SignetEventObserverConfig.fromEventId}). */
|
|
218
|
-
readonly fromEventId?: number;
|
|
219
|
-
/** Poll cadence for {@link SignetRespondBidirectionalFeed.attestation}; default {@link DEFAULT_RESPONSE_FEED_POLL_INTERVAL_MS}. */
|
|
220
|
-
readonly pollIntervalMs?: number;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* The event-driven respond-bidirectional feed: composes a
|
|
225
|
-
* {@link SignetEventObserver} of {@link RespondBidirectionalEvent}
|
|
226
|
-
* notifications (discovery) with a read of the signet contract's
|
|
227
|
-
* respond-bidirectional index (the source of truth), yielding the MPC's
|
|
228
|
-
* attestation of a request's remote execution. No verdicts and no off-chain
|
|
229
|
-
* verification — the signet contract verified the attestation IN-CIRCUIT at
|
|
230
|
-
* post time (Schnorr over `(requestId, hash(serializedOutput, outputLen))`
|
|
231
|
-
* against its sealed MPC key), so the stored record is authentic by
|
|
232
|
-
* construction and there is exactly one per request (first valid write wins).
|
|
233
|
-
*/
|
|
234
|
-
export class SignetRespondBidirectionalFeed {
|
|
235
|
-
private readonly observer: SignetEventObserver<RespondBidirectionalEvent>;
|
|
236
|
-
private readonly signetContractAddress: string;
|
|
237
|
-
private readonly source: SignetPublicStateSource;
|
|
238
|
-
private readonly pollIntervalMs: number;
|
|
239
|
-
|
|
240
|
-
// Attestation records never change once stored (one authenticated slot per
|
|
241
|
-
// request, first valid write wins), so cache them across polls.
|
|
242
|
-
private readonly attestations = new Map<RequestIdHex, RespondBidirectional>();
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* @param config - The signet contract, combined event/state source, and
|
|
246
|
-
* resume floor.
|
|
247
|
-
*/
|
|
248
|
-
constructor(config: SignetRespondBidirectionalFeedConfig) {
|
|
249
|
-
this.observer = new SignetEventObserver({
|
|
250
|
-
signetContractAddress: config.signetContractAddress,
|
|
251
|
-
source: config.source,
|
|
252
|
-
codec: respondBidirectionalEventCodec,
|
|
253
|
-
fromEventId: config.fromEventId,
|
|
254
|
-
pollIntervalMs: config.pollIntervalMs,
|
|
255
|
-
});
|
|
256
|
-
this.signetContractAddress = config.signetContractAddress;
|
|
257
|
-
this.source = config.source;
|
|
258
|
-
this.pollIntervalMs =
|
|
259
|
-
config.pollIntervalMs ?? DEFAULT_RESPONSE_FEED_POLL_INTERVAL_MS;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* One-shot: the attestation for `requestId`, or `undefined` while none is
|
|
264
|
-
* discoverable yet. The index is only read when a currently-visible event
|
|
265
|
-
* announces `requestId`'s attestation — no event, no state query. An event
|
|
266
|
-
* whose ledger write has not indexed yet is simply retried next cycle
|
|
267
|
-
* (events are the trigger, the ledger is the source of truth).
|
|
268
|
-
*
|
|
269
|
-
* @param requestId - The request whose attestation to look for.
|
|
270
|
-
* @returns The stored attestation record, or `undefined` when its event is
|
|
271
|
-
* not yet visible or its write not yet indexed.
|
|
272
|
-
* @throws Error when the signet contract has no state on-chain.
|
|
273
|
-
*/
|
|
274
|
-
async poll(requestId: RequestIdHex): Promise<RespondBidirectional | undefined> {
|
|
275
|
-
const cached = this.attestations.get(requestId);
|
|
276
|
-
if (cached !== undefined) {
|
|
277
|
-
return cached;
|
|
278
|
-
}
|
|
279
|
-
const events = await this.observer.currentEvents();
|
|
280
|
-
if (!events.some((event) => event.requestId === requestId)) {
|
|
281
|
-
return undefined;
|
|
282
|
-
}
|
|
283
|
-
const state = await this.source.queryContractState(
|
|
284
|
-
this.signetContractAddress,
|
|
285
|
-
);
|
|
286
|
-
if (!state?.data) {
|
|
287
|
-
throw new Error(
|
|
288
|
-
`no state data found for signet contract '${this.signetContractAddress}' - is it deployed?`,
|
|
289
|
-
);
|
|
290
|
-
}
|
|
291
|
-
const record = readSignetContractLedgerFromState(
|
|
292
|
-
state.data,
|
|
293
|
-
).respondBidirectionalIndex.get(requestId);
|
|
294
|
-
if (record === undefined) {
|
|
295
|
-
return undefined;
|
|
296
|
-
}
|
|
297
|
-
this.attestations.set(requestId, record);
|
|
298
|
-
return record;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Live wait: poll + sleep until `requestId`'s attestation is discoverable,
|
|
303
|
-
* and return it. Runs until `opts.signal` aborts — the caller owns the
|
|
304
|
-
* give-up policy (see the cli's poll-respond-bidirectional command).
|
|
305
|
-
*
|
|
306
|
-
* @param requestId - The request whose attestation to wait for.
|
|
307
|
-
* @param opts.signal - Abort to stop waiting.
|
|
308
|
-
* @returns The stored attestation record, or `undefined` when aborted
|
|
309
|
-
* before one was discovered.
|
|
310
|
-
* @throws Error when the signet contract has no state on-chain.
|
|
311
|
-
*/
|
|
312
|
-
async attestation(
|
|
313
|
-
requestId: RequestIdHex,
|
|
314
|
-
opts?: { signal?: AbortSignal },
|
|
315
|
-
): Promise<RespondBidirectional | undefined> {
|
|
316
|
-
while (!opts?.signal?.aborted) {
|
|
317
|
-
const record = await this.poll(requestId);
|
|
318
|
-
if (record !== undefined) {
|
|
319
|
-
return record;
|
|
320
|
-
}
|
|
321
|
-
await sleepUnlessAborted(this.pollIntervalMs, opts?.signal);
|
|
322
|
-
}
|
|
323
|
-
return undefined;
|
|
324
|
-
}
|
|
325
|
-
}
|