@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,529 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Signet — shared Compact library for sig-net (signet) MPC signature
|
|
3
|
+
// requests and responses on Midnight.
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// A signet compliant contract records "please sign this EVM transaction" requests
|
|
6
|
+
// in its public ledger. The sig-net MPC network needs only the contract address:
|
|
7
|
+
// it reads the request index from ledger field 0 (the signet layout convention),
|
|
8
|
+
// re-assembles and signs the EVM transaction with the caller's derived key,
|
|
9
|
+
// and posts its responses to the CENTRAL signet contract for polling — first
|
|
10
|
+
// the signature (the client broadcasts the signed transaction), then, after
|
|
11
|
+
// watching the remote execution, the Schnorr-signed respond-bidirectional
|
|
12
|
+
// attestation of its result. Requests and both response kinds are counterparts
|
|
13
|
+
// and are defined side by side here; the pure circuits that operate on them
|
|
14
|
+
// follow below.
|
|
15
|
+
//
|
|
16
|
+
// Naming follows the MPC's canonical vocabulary (sig-net/mpc
|
|
17
|
+
// chain-signatures/primitives): the SignBidirectionalRequest record mirrors
|
|
18
|
+
// the MPC's SignBidirectionalEvent (with the EVM transaction decomposed — the
|
|
19
|
+
// Canton-adapter precedent) and a client emits a SignBidirectionalEvent to
|
|
20
|
+
// notify the MPC one was stored; the SignatureResponse record mirrors
|
|
21
|
+
// Signature { big_r, s, recovery_id } and the signet contract emits a
|
|
22
|
+
// SignatureRespondedEvent to notify the client one was posted; the execution
|
|
23
|
+
// attestation mirrors RespondBidirectionalEvent / serialized_output.
|
|
24
|
+
//
|
|
25
|
+
// Read more:
|
|
26
|
+
// - sig-net docs (chain signatures, key derivation): https://docs.sig.network/
|
|
27
|
+
// - canonical request shape this mirrors: sig-net/mpc
|
|
28
|
+
// chain-signatures/primitives/src/bidirectional.rs (SignBidirectionalEvent)
|
|
29
|
+
// - Compact language reference (modules, types):
|
|
30
|
+
// https://docs.midnight.network/compact/reference/compact-reference
|
|
31
|
+
//
|
|
32
|
+
// Import from a contract (resolved through node_modules via COMPACT_PATH —
|
|
33
|
+
// see the consuming package's compile script):
|
|
34
|
+
// import "@sig-net/midnight/src/Signet";
|
|
35
|
+
|
|
36
|
+
module Signet {
|
|
37
|
+
import CompactStandardLibrary;
|
|
38
|
+
|
|
39
|
+
// ---- Identifiers ---------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
// Nominal id of a signet request. Deliberately CHAIN-AGNOSTIC: downstream
|
|
42
|
+
// consumers (the signet contract, response pollers, the MPC)
|
|
43
|
+
// treat it as an opaque 32-byte key, whatever kind of request it identifies.
|
|
44
|
+
// Each request kind mints ids via its own domain-separated hash circuit
|
|
45
|
+
// (see calculateRequestId). `new type` makes it NOT interchangeable
|
|
46
|
+
// with any other Bytes<32> — cast explicitly: `someHash as RequestId`.
|
|
47
|
+
export new type RequestId = Bytes<32>;
|
|
48
|
+
|
|
49
|
+
// ---- The canonical request/response pair ----------------------------------
|
|
50
|
+
// 1. Client contract stores a SignBidirectionalRequest and emits a SignBidirectionalEvent, requesting a signature from the MPC
|
|
51
|
+
// 2. MPC reads the request and posts a SignatureResponse (the requested signature) to the central signet contract, which emits a SignatureRespondedEvent
|
|
52
|
+
// 3. Client observes SignatureRespondedEvent, verifies the posted SignatureResponse, broadcasts txn to destination chain
|
|
53
|
+
// 4. MPC observes txn execution on destination chain, responds with RespondBidirectionalEvent
|
|
54
|
+
|
|
55
|
+
// TxParamType indiciates which transaction-param decomposition a request
|
|
56
|
+
// contains so an off-chain consumer (e.g. the MPC) knows how to re-assemble it.
|
|
57
|
+
// In-circuit this is redundant as the type is fixed at compile time (i.e. monomorphized).
|
|
58
|
+
export enum TxParamType {
|
|
59
|
+
evmType2, // EVMType2TxParams: an EIP-1559 EVM transaction (envelope + calldata)
|
|
60
|
+
reserved, // never emitted — exists only to keep the enum at >= 2 variants. A
|
|
61
|
+
// 1-variant enum compiles to a zero-byte value the proof server's
|
|
62
|
+
// field-repr parser reads as ZERO field elements while the compiler
|
|
63
|
+
// allocates ONE, so persistentHash over any struct containing it
|
|
64
|
+
// (i.e. every request id) fails to prove.
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// SignBidirectionalRequest is the record a Client contract stores to request
|
|
68
|
+
// a signature from the MPC (and hashes into its RequestId).
|
|
69
|
+
export struct SignBidirectionalRequest<TxParams> {
|
|
70
|
+
requestNonce: Uint<64>; // contract-local nonce at creation time
|
|
71
|
+
|
|
72
|
+
txParamType: TxParamType; // which decomposition txParams is (off-chain decode tag)
|
|
73
|
+
txParams: TxParams; // the generic transaction-param payload
|
|
74
|
+
|
|
75
|
+
// TODO: confirm sufficient size for CAIP-2 standard.
|
|
76
|
+
caip2Id: Bytes<32>; // target chain in CAIP-2 form, e.g. "eip155:11155111"
|
|
77
|
+
// (https://chainagnostic.org/CAIPs/caip-2)
|
|
78
|
+
// TODO: can be much smaller
|
|
79
|
+
keyVersion: Uint<32>; // MPC root-key version to derive from (>= 1;
|
|
80
|
+
// version 0 is the unsupported legacy format)
|
|
81
|
+
// TODO: consider possible optimisation. keep unchanged for now.
|
|
82
|
+
path: Bytes<256>; // key-derivation path; by signet-Midnight
|
|
83
|
+
// convention the canonical lowercase hex of the
|
|
84
|
+
// caller's identity commitment, zero-padded
|
|
85
|
+
// (enforced by assertPathCommitment)
|
|
86
|
+
|
|
87
|
+
// TODO: Possibly ENUMs, these fields are not used so we could make them small now.
|
|
88
|
+
algo: Bytes<32>; // signature scheme, e.g. "ecdsa"
|
|
89
|
+
dest: Bytes<32>; // response destination, e.g. "ethereum"
|
|
90
|
+
params: Bytes<64>; // scheme-specific extras (opaque to contracts)
|
|
91
|
+
|
|
92
|
+
// Look at most popular defi contracts for schematisation of schema types
|
|
93
|
+
// TODO: dynamically size these 2 schemas to reduce proving type
|
|
94
|
+
outputDeserializationSchema: Bytes<128>; // MPC output_deserialization_schema
|
|
95
|
+
// (destination chain -> MPC)
|
|
96
|
+
respondSerializationSchema: Bytes<128>; // MPC respond_serialization_schema
|
|
97
|
+
// (MPC -> source chain)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// External contract surface of the central signet contract: a client cross-
|
|
101
|
+
// contract-calls emitSignBidirectionalEvent to emit the notification event.
|
|
102
|
+
export contract SignetEventEmitter {
|
|
103
|
+
circuit emitSignBidirectionalEvent(signBidirectionalEvent: SignBidirectionalEvent): [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// SignBidirectionalEvent notifies the MPC that a SignBidirectionalRequest has
|
|
107
|
+
// been stored in the caller's ledger. Attribution comes from whose
|
|
108
|
+
// authenticated state the MPC reads, never from a field here.
|
|
109
|
+
export struct SignBidirectionalEvent {
|
|
110
|
+
version: Uint<8>;
|
|
111
|
+
payload: Bytes<224>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// V1 interpretation of SignBidirectionalEvent.payload.
|
|
115
|
+
export struct SignBidirectionalEventV1Payload {
|
|
116
|
+
// Address of the calling contract; the MPC reads its request index there.
|
|
117
|
+
callerAddress: ContractAddress;
|
|
118
|
+
|
|
119
|
+
// Representation type (Bytes<32>), NOT the RequestId new type: a `new type`
|
|
120
|
+
// field crashes the serialize<> event encoder (compiler self-diagnosed bug).
|
|
121
|
+
requestId: Bytes<32>;
|
|
122
|
+
|
|
123
|
+
// Ledger field position of the Map<RequestId, SignBidirectionalRequest> in
|
|
124
|
+
// the caller contract.
|
|
125
|
+
signBidirectionalRequestsIndexField: Uint<8>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Build the V1 event: pack the concrete fields into the opaque payload with
|
|
129
|
+
// serialize<> and tag version = 1. Client contracts should call
|
|
130
|
+
// this instead of hand-constructing the envelope.
|
|
131
|
+
export pure circuit constructSignBidirectionalEventV1(
|
|
132
|
+
callerAddress: ContractAddress,
|
|
133
|
+
requestId: Bytes<32>,
|
|
134
|
+
signBidirectionalRequestsIndexField: Uint<8>
|
|
135
|
+
): SignBidirectionalEvent {
|
|
136
|
+
return SignBidirectionalEvent {
|
|
137
|
+
version: 1,
|
|
138
|
+
payload: serialize<SignBidirectionalEventV1Payload, 224>(
|
|
139
|
+
SignBidirectionalEventV1Payload {
|
|
140
|
+
callerAddress,
|
|
141
|
+
requestId,
|
|
142
|
+
signBidirectionalRequestsIndexField
|
|
143
|
+
}
|
|
144
|
+
)
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// The counterpart to SignBidirectionalRequest: the record the MPC posts to
|
|
149
|
+
// the central signet contract's response log after it observes the request
|
|
150
|
+
// (mirrors the MPC's canonical Signature { big_r, s, recovery_id }).
|
|
151
|
+
// Contains the MPC's secp256k1 signature over the requested transaction.
|
|
152
|
+
// The requesting client uses this to rebuild a signed transaction and broadcast.
|
|
153
|
+
export struct SignatureResponse {
|
|
154
|
+
bigRx: Bytes<32>; // signature R.x
|
|
155
|
+
bigRy: Bytes<32>; // signature R.y
|
|
156
|
+
s: Bytes<32>; // signature s
|
|
157
|
+
recoveryId: Uint<8>; // parity of R.y for public-key recovery
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// SignatureRespondedEvent notifies the requesting client that a
|
|
161
|
+
// SignatureResponse has been posted to the central signet contract's
|
|
162
|
+
// response log — the response-direction counterpart of
|
|
163
|
+
// SignBidirectionalEvent. A ping, never the source of truth: the client
|
|
164
|
+
// reads the post back from the log at (requestId, count) and verifies the
|
|
165
|
+
// signature off-chain — the log is unauthenticated, so verification, not
|
|
166
|
+
// the event, confers trust.
|
|
167
|
+
export struct SignatureRespondedEvent {
|
|
168
|
+
// Representation type (Bytes<32>), NOT the RequestId new type: a `new type`
|
|
169
|
+
// field crashes the serialize<> event encoder (compiler self-diagnosed bug).
|
|
170
|
+
requestId: Bytes<32>,
|
|
171
|
+
|
|
172
|
+
// 0-based position of the post in the request's response log — the
|
|
173
|
+
// `count` half of the SignetResponseKey it is stored under.
|
|
174
|
+
count: Uint<64>,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// The counterpart to SignatureResponse, emitted by the MPC after
|
|
178
|
+
// it observes execution of the signed transaction on the destination chain.
|
|
179
|
+
// This response contains execution output and an attestation as to its authenticity.
|
|
180
|
+
// TODO: currently contains a Schnorr attestation --> migrate to ECDSA.
|
|
181
|
+
export struct RespondBidirectional {
|
|
182
|
+
// ABI-encoded return data of the remote call (canonical
|
|
183
|
+
// serialized_output), zero-padded. First 32-byte word == 1 signals
|
|
184
|
+
// success; a 0xdeadbeef prefix is the MPC's error sentinel for a
|
|
185
|
+
// failed/absent execution.
|
|
186
|
+
serializedOutput: Bytes<128>;
|
|
187
|
+
|
|
188
|
+
// Meaningful byte count of serializedOutput.
|
|
189
|
+
outputLen: Uint<8>;
|
|
190
|
+
|
|
191
|
+
// The MPC attestation key (Jubjub). Hash-checked against the signet
|
|
192
|
+
// contract's sealed mpcPubKeyHash at post time, and again by requester
|
|
193
|
+
// contracts at claim time.
|
|
194
|
+
pk: JubjubPoint;
|
|
195
|
+
|
|
196
|
+
// Schnorr nonce commitment R.
|
|
197
|
+
announcement: JubjubPoint;
|
|
198
|
+
|
|
199
|
+
// Schnorr response scalar s.
|
|
200
|
+
response: Field;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// RespondBidirectionalEvent notifies the requesting client that the MPC's
|
|
204
|
+
// RespondBidirectional attestation of a request's remote execution has been
|
|
205
|
+
// stored in the central signet contract's respond-bidirectional index — the
|
|
206
|
+
// attestation-direction counterpart of SignatureRespondedEvent. A ping,
|
|
207
|
+
// never the source of truth: the client reads the record back from the
|
|
208
|
+
// index at requestId. Unlike signature responses, NO off-chain verification
|
|
209
|
+
// follows — the contract verified the attestation in-circuit at post time,
|
|
210
|
+
// so the stored record is authentic by construction. Emitted at most once
|
|
211
|
+
// per request: the index holds one authenticated slot, first valid write
|
|
212
|
+
// wins, and a re-post is a no-op that emits nothing.
|
|
213
|
+
export struct RespondBidirectionalEvent {
|
|
214
|
+
// Representation type (Bytes<32>), NOT the RequestId new type: a `new type`
|
|
215
|
+
// field crashes the serialize<> event encoder (compiler self-diagnosed bug).
|
|
216
|
+
requestId: Bytes<32>;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// The EVM transaction to be signed, decomposed into typed fields
|
|
220
|
+
// (EIP-1559 / type-2: https://eips.ethereum.org/EIPS/eip-1559).
|
|
221
|
+
// Decomposed rather than pre-serialized so the requesting contract can
|
|
222
|
+
// enforce invariants field by field; the MPC re-assembles and RLP-encodes
|
|
223
|
+
// off-chain. (The MPC's chain-agnostic primitive carries an opaque
|
|
224
|
+
// serialized_transaction instead; the decomposition follows its Canton
|
|
225
|
+
// adapter's EvmType2TransactionParams.)
|
|
226
|
+
//
|
|
227
|
+
// Compact has no runtime-dynamic sizes, so every variable-length EVM tx
|
|
228
|
+
// part follows one scheme: a compile-time CAPACITY (a generic size
|
|
229
|
+
// parameter each requesting contract fixes as its throttle) plus a runtime
|
|
230
|
+
// count that says how many leading slots are real. Off-chain consumers use
|
|
231
|
+
// the counts; unused capacity is hashed as part of the record (zero-fill by
|
|
232
|
+
// convention) but never reaches the assembled transaction.
|
|
233
|
+
export struct EVMType2TxParams<#maxCalldataWords, #maxAccessListEntries, #maxStorageKeysPerEntry> {
|
|
234
|
+
to: Bytes<20>; // call target (e.g. the ERC20 contract)
|
|
235
|
+
chainId: Uint<64>; // EVM chain id (also expressed in caip2Id)
|
|
236
|
+
nonce: Uint<64>; // account nonce of the derived sender
|
|
237
|
+
gasLimit: Uint<64>; // gas ceiling for the call
|
|
238
|
+
maxFeePerGas: Uint<128>; // wei
|
|
239
|
+
maxPriorityFeePerGas: Uint<128>; // wei
|
|
240
|
+
value: Uint<128>; // ETH sent with the call, in wei
|
|
241
|
+
accessListEntryCount: Uint<8>; // how many leading accessList entries are real
|
|
242
|
+
accessList: Vector<maxAccessListEntries, EVMAccessListEntry<maxStorageKeysPerEntry>>;
|
|
243
|
+
calldata: Maybe<EVMCalldata<maxCalldataWords>>; // none = plain ETH transfer ("0x" data)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// One EIP-2930 access-list entry (https://eips.ethereum.org/EIPS/eip-2930):
|
|
247
|
+
// an address plus the storage keys the transaction pre-declares. Keys are
|
|
248
|
+
// raw 32-byte values, copied verbatim into the RLP encoding.
|
|
249
|
+
export struct EVMAccessListEntry<#maxStorageKeys> {
|
|
250
|
+
address: Bytes<20>;
|
|
251
|
+
storageKeyCount: Uint<8>; // how many leading storageKeys are real
|
|
252
|
+
storageKeys: Vector<maxStorageKeys, Bytes<32>>;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ABI calldata for the EVM call: 4-byte selector + 32-byte slots
|
|
256
|
+
// (https://docs.soliditylang.org/en/latest/abi-spec.html).
|
|
257
|
+
export struct EVMCalldata<#maxWords> {
|
|
258
|
+
selector: Bytes<4>; // the literal first 4 calldata bytes (big-endian, as broadcast)
|
|
259
|
+
noWords: Uint<16>; // No. of used words in the words list
|
|
260
|
+
words: Vector<maxWords, Bytes<32>>;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// The field value to embed in a calldata word for an EVM address: the
|
|
264
|
+
// BIG-ENDIAN numeric reading of its 20 display-order bytes. Embedding the
|
|
265
|
+
// address directly (`addr as Field`) would be wrong: that cast is
|
|
266
|
+
// little-endian, so the MPC's LE-decode -> BE-encode step would emit the
|
|
267
|
+
// address byte-reversed in the calldata. Usage: a calldata word
|
|
268
|
+
// `evmAddressAbiValue(addr) as Bytes<32>`.
|
|
269
|
+
// (Uint amounts need no helper: `amount as Field` already embeds the
|
|
270
|
+
// correct numeric value.)
|
|
271
|
+
export pure circuit evmAddressAbiValue(addr: Bytes<20>): Field {
|
|
272
|
+
const a0 = slice<1>(addr, 0) as Field;
|
|
273
|
+
const a1 = a0 * 256 + (slice<1>(addr, 1) as Field);
|
|
274
|
+
const a2 = a1 * 256 + (slice<1>(addr, 2) as Field);
|
|
275
|
+
const a3 = a2 * 256 + (slice<1>(addr, 3) as Field);
|
|
276
|
+
const a4 = a3 * 256 + (slice<1>(addr, 4) as Field);
|
|
277
|
+
const a5 = a4 * 256 + (slice<1>(addr, 5) as Field);
|
|
278
|
+
const a6 = a5 * 256 + (slice<1>(addr, 6) as Field);
|
|
279
|
+
const a7 = a6 * 256 + (slice<1>(addr, 7) as Field);
|
|
280
|
+
const a8 = a7 * 256 + (slice<1>(addr, 8) as Field);
|
|
281
|
+
const a9 = a8 * 256 + (slice<1>(addr, 9) as Field);
|
|
282
|
+
const a10 = a9 * 256 + (slice<1>(addr, 10) as Field);
|
|
283
|
+
const a11 = a10 * 256 + (slice<1>(addr, 11) as Field);
|
|
284
|
+
const a12 = a11 * 256 + (slice<1>(addr, 12) as Field);
|
|
285
|
+
const a13 = a12 * 256 + (slice<1>(addr, 13) as Field);
|
|
286
|
+
const a14 = a13 * 256 + (slice<1>(addr, 14) as Field);
|
|
287
|
+
const a15 = a14 * 256 + (slice<1>(addr, 15) as Field);
|
|
288
|
+
const a16 = a15 * 256 + (slice<1>(addr, 16) as Field);
|
|
289
|
+
const a17 = a16 * 256 + (slice<1>(addr, 17) as Field);
|
|
290
|
+
const a18 = a17 * 256 + (slice<1>(addr, 18) as Field);
|
|
291
|
+
return a18 * 256 + (slice<1>(addr, 19) as Field);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ---- Requester Ledger Layout -----------------------------------------------
|
|
295
|
+
|
|
296
|
+
// The on-ledger request index. Signet layout convention: a conforming
|
|
297
|
+
// contract declares this as its FIRST ledger field (instantiated at its own
|
|
298
|
+
// calldata slot capacity), so the MPC can find and deserialize it knowing
|
|
299
|
+
// only the contract address.
|
|
300
|
+
export new type SignBidirectionalRequestIndex<TxParams> = Map<RequestId, SignBidirectionalRequest<TxParams>>;
|
|
301
|
+
|
|
302
|
+
// The contract-local request counter. Signet layout convention: declared as
|
|
303
|
+
// the SECOND ledger field (directly after the index). It sources each
|
|
304
|
+
// request's requestNonce and doubles as the MPC's cheap change-detection
|
|
305
|
+
// signal — an unchanged counter means no new requests since the last poll.
|
|
306
|
+
export new type SignetNonce = Counter;
|
|
307
|
+
|
|
308
|
+
// ---- Signet Contract Ledger Layout ------------------------------------------
|
|
309
|
+
// The CENTRAL signet contract on Midnight stores both MPC response kinds.
|
|
310
|
+
// Its layout convention (find everything knowing only the contract address):
|
|
311
|
+
// field 0: SignatureResponseCounterIndex
|
|
312
|
+
// field 1: SignatureResponseIndex
|
|
313
|
+
// field 2: RespondBidirectionalIndex
|
|
314
|
+
// field 3: mpcPubKeyHash (sealed Bytes<32>)
|
|
315
|
+
//
|
|
316
|
+
// The two kinds get different defenses:
|
|
317
|
+
// - Signature responses are secp256k1 ECDSA — unverifiable in-circuit —
|
|
318
|
+
// so their log is UNAUTHENTICATED and append-only: each post lands
|
|
319
|
+
// under a fresh (requestId, count) key (announced by a
|
|
320
|
+
// SignatureRespondedEvent); a rogue poster can add noise
|
|
321
|
+
// but never overwrite or block the genuine response. Clients enumerate
|
|
322
|
+
// a request's entries and verify each signature off-chain.
|
|
323
|
+
// - Respond-bidirectional attestations are Schnorr on Jubjub — the
|
|
324
|
+
// embedded curve — so the contract verifies them IN-CIRCUIT at post
|
|
325
|
+
// time against its sealed MPC key hash. Garbage cannot land; one
|
|
326
|
+
// authenticated slot per request suffices, first valid write wins.
|
|
327
|
+
|
|
328
|
+
// Per-request post counter for signature responses (field 0). The counter
|
|
329
|
+
// reads the number of posts made for that request; entries exist for
|
|
330
|
+
// counts 0..counter-1.
|
|
331
|
+
export new type SignatureResponseCounterIndex = Map<RequestId, Counter>;
|
|
332
|
+
|
|
333
|
+
// Composite key of one posted signature response: the request id it
|
|
334
|
+
// answers plus this post's 0-based count, sourced from
|
|
335
|
+
// SignatureResponseCounterIndex. (Respond-bidirectional posts need no key
|
|
336
|
+
// struct — authenticated posts are keyed by request id alone.)
|
|
337
|
+
export struct SignetResponseKey {
|
|
338
|
+
count: Uint<64>;
|
|
339
|
+
requestId: RequestId;
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
// The on-ledger signature response log (field 1), keyed by
|
|
343
|
+
// (requestId, count).
|
|
344
|
+
export new type SignatureResponseIndex = Map<SignetResponseKey, SignatureResponse>;
|
|
345
|
+
|
|
346
|
+
// The on-ledger respond-bidirectional index (field 2): one
|
|
347
|
+
// contract-verified attestation per request.
|
|
348
|
+
export new type RespondBidirectionalIndex = Map<RequestId, RespondBidirectional>;
|
|
349
|
+
|
|
350
|
+
// ---- Request id -----------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
// Canonical id of an EVM signature request: the domain-separated persistent
|
|
353
|
+
// hash of the entire request record. The id therefore commits to every
|
|
354
|
+
// field — the MPC (or anyone) recomputes it off-chain and refuses records
|
|
355
|
+
// whose stored key does not match.
|
|
356
|
+
//
|
|
357
|
+
// The "signet:midnight:request" domain tag partitions the RequestId space:
|
|
358
|
+
// ids minted by this circuit can never collide or be confused with hashes
|
|
359
|
+
// minted under any other tag.
|
|
360
|
+
//
|
|
361
|
+
// Off-chain twin: being type-parameterized, this circuit cannot be exported
|
|
362
|
+
// through circuits.compact, so signet-requests.ts carries a TS twin
|
|
363
|
+
// (calculateRequestId) that MUST stay in lockstep — see the
|
|
364
|
+
// deviation note there before changing anything here.
|
|
365
|
+
export pure circuit calculateRequestId<TxParams>(
|
|
366
|
+
request: SignBidirectionalRequest<TxParams>
|
|
367
|
+
): RequestId {
|
|
368
|
+
return persistentHash<Vector<2, Bytes<32>>>([
|
|
369
|
+
pad(32, "signet:midnight:request"),
|
|
370
|
+
persistentHash<SignBidirectionalRequest<TxParams>>(request)
|
|
371
|
+
]) as RequestId;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ---- Respond-bidirectional attestation message ------------------------------
|
|
375
|
+
|
|
376
|
+
// The message the MPC Schnorr-signs to attest a remote execution:
|
|
377
|
+
// (requestId, hash(serializedOutput, outputLen)) as four 16-byte
|
|
378
|
+
// little-endian field limbs. The hash binds the output TOGETHER WITH its
|
|
379
|
+
// length, so neither can be swapped under a reused signature; the 16-byte
|
|
380
|
+
// split is needed because a 32-byte value overflows the BLS scalar field.
|
|
381
|
+
// ONE implementation for everyone: the signet contract verifies posts
|
|
382
|
+
// against it, requester contracts (claimDeposit) re-verify against it, and
|
|
383
|
+
// off-chain signers call the compiled copy (via circuits.compact) — never
|
|
384
|
+
// a TS re-implementation.
|
|
385
|
+
export pure circuit signetAttestationMessage(
|
|
386
|
+
requestId: RequestId,
|
|
387
|
+
serializedOutput: Bytes<128>,
|
|
388
|
+
outputLen: Uint<8>
|
|
389
|
+
): Vector<4, Field> {
|
|
390
|
+
const rid = requestId as Bytes<32>;
|
|
391
|
+
const outHash = persistentHash<Vector<2, Bytes<32>>>([
|
|
392
|
+
persistentHash<Bytes<128>>(serializedOutput),
|
|
393
|
+
outputLen as Field as Bytes<32>
|
|
394
|
+
]);
|
|
395
|
+
return [
|
|
396
|
+
slice<16>(rid, 0) as Field,
|
|
397
|
+
slice<16>(rid, 16) as Field,
|
|
398
|
+
slice<16>(outHash, 0) as Field,
|
|
399
|
+
slice<16>(outHash, 16) as Field
|
|
400
|
+
];
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ---- Path <-> identity binding ---------------------------------------------
|
|
404
|
+
// Compact cannot ENCODE bytes->hex in-circuit (nibble extraction needs
|
|
405
|
+
// integer div/mod/shift, which the prime field has no native op for). But it
|
|
406
|
+
// CAN DECODE hex->bytes with multiply/add/compare. So the derivation path (a
|
|
407
|
+
// public value) carries the hex string, and we verify in-circuit that it
|
|
408
|
+
// canonically decodes to the commitment. The prover cannot supply a wrong or
|
|
409
|
+
// non-canonical string, so this is as strong as computing the hex on-chain —
|
|
410
|
+
// and it lets the MPC treat the path as a plain opaque string (no hex
|
|
411
|
+
// special-case in the MPC).
|
|
412
|
+
|
|
413
|
+
// Decode one canonical hex character ('0'-'9', lowercase 'a'-'f') to 0..15.
|
|
414
|
+
pure circuit charToNibble(c: Uint<8>): Uint<8> {
|
|
415
|
+
const isDigit = (c >= 48) && (c <= 57); // '0'..'9'
|
|
416
|
+
const isLower = (c >= 97) && (c <= 102); // 'a'..'f' (lowercase only => canonical)
|
|
417
|
+
assert(isDigit || isLower, "non-canonical hex char");
|
|
418
|
+
const base = c - 48; // safe: c >= 48 in both accepted ranges
|
|
419
|
+
const adjust = isLower ? 39 : 0; // 'a'(97) - 48 - 39 = 10
|
|
420
|
+
return base - adjust;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Assert that hexStr (64 lowercase hex chars) decodes exactly to commitment.
|
|
424
|
+
export pure circuit assertHexOf(commitment: Bytes<32>, hexStr: Bytes<64>): [] {
|
|
425
|
+
for (const i of 0 .. 32) {
|
|
426
|
+
const hi = charToNibble(slice<1>(hexStr, 2 * i) as Uint<8>);
|
|
427
|
+
const lo = charToNibble(slice<1>(hexStr, 2 * i + 1) as Uint<8>);
|
|
428
|
+
assert((slice<1>(commitment, i) as Field) == (hi as Field) * 16 + (lo as Field),
|
|
429
|
+
"path hex does not match commitment");
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// // Assert the full path <-> identity binding: the path is EXACTLY the
|
|
434
|
+
// canonical lowercase hex of `commitment`, zero-padded to 256 bytes. This
|
|
435
|
+
// makes identity -> path -> MPC-derived account a forced 1:1 mapping: no one
|
|
436
|
+
// can request signatures under another identity's derived key, and no
|
|
437
|
+
// identity can spread across multiple derived keys via case/suffix
|
|
438
|
+
// malleability. See https://docs.sig.network/ for path-based derivation.
|
|
439
|
+
export pure circuit assertPathCommitment(
|
|
440
|
+
commitment: Bytes<32>,
|
|
441
|
+
path: Bytes<256>
|
|
442
|
+
): [] {
|
|
443
|
+
assertHexOf(commitment, slice<64>(path, 0));
|
|
444
|
+
assert(slice<192>(path, 64) == pad(192, ""), "Path must be zero-padded after hex");
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ---- Request construction ---------------------------------------------------
|
|
448
|
+
|
|
449
|
+
// Assemble the canonical request record for a CALLER-IDENTITY request,
|
|
450
|
+
// enforcing the path <-> identity binding on the way — the MPC signs with
|
|
451
|
+
// the calling user's derived key and no one else's.
|
|
452
|
+
// The requesting contract supplies:
|
|
453
|
+
// - pathCommitment: the DISCLOSED identity commitment of the caller
|
|
454
|
+
// (derived from a secret-key witness — see the vault's userCommitment)
|
|
455
|
+
// - requestNonce: its contract-local nonce (typically a Counter read)
|
|
456
|
+
// - txParams: the transaction decomposition; the contract MUST
|
|
457
|
+
// build (or overwrite) any part it enforces — calldata above all —
|
|
458
|
+
// rather than pass caller input through
|
|
459
|
+
// - the remaining MPC routing fields, caller-supplied
|
|
460
|
+
// Store the result under calculateRequestId(request) in the
|
|
461
|
+
// request index.
|
|
462
|
+
export pure circuit constructSignBidirectionalRequest<TxParams>(
|
|
463
|
+
pathCommitment: Bytes<32>,
|
|
464
|
+
requestNonce: Uint<64>,
|
|
465
|
+
txParamType: TxParamType,
|
|
466
|
+
txParams: TxParams,
|
|
467
|
+
caip2Id: Bytes<32>,
|
|
468
|
+
keyVersion: Uint<32>,
|
|
469
|
+
path: Bytes<256>,
|
|
470
|
+
algo: Bytes<32>,
|
|
471
|
+
dest: Bytes<32>,
|
|
472
|
+
params: Bytes<64>,
|
|
473
|
+
outputDeserializationSchema: Bytes<128>,
|
|
474
|
+
respondSerializationSchema: Bytes<128>
|
|
475
|
+
): SignBidirectionalRequest<TxParams> {
|
|
476
|
+
assertPathCommitment(pathCommitment, path);
|
|
477
|
+
return constructContractPathSignBidirectionalRequest<TxParams>(
|
|
478
|
+
requestNonce,
|
|
479
|
+
txParamType,
|
|
480
|
+
txParams,
|
|
481
|
+
caip2Id,
|
|
482
|
+
keyVersion,
|
|
483
|
+
path,
|
|
484
|
+
algo,
|
|
485
|
+
dest,
|
|
486
|
+
params,
|
|
487
|
+
outputDeserializationSchema,
|
|
488
|
+
respondSerializationSchema
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Assemble the canonical request record for a CONTRACT-OWNED derivation
|
|
493
|
+
// path: a literal the requesting contract fixes in-circuit (e.g. a vault
|
|
494
|
+
// treasury's "vault"), so the MPC signs with the CONTRACT's derived account
|
|
495
|
+
// rather than a caller's. No identity binding applies — the contract itself
|
|
496
|
+
// vouches for the path by hardcoding it; never pass a caller-supplied path
|
|
497
|
+
// here (that would let anyone request signatures under any derived key —
|
|
498
|
+
// use constructSignBidirectionalRequest for caller-identity requests).
|
|
499
|
+
// Enforces the canonical keyVersion floor for both constructors.
|
|
500
|
+
export pure circuit constructContractPathSignBidirectionalRequest<TxParams>(
|
|
501
|
+
requestNonce: Uint<64>,
|
|
502
|
+
txParamType: TxParamType,
|
|
503
|
+
txParams: TxParams,
|
|
504
|
+
caip2Id: Bytes<32>,
|
|
505
|
+
keyVersion: Uint<32>,
|
|
506
|
+
path: Bytes<256>,
|
|
507
|
+
algo: Bytes<32>,
|
|
508
|
+
dest: Bytes<32>,
|
|
509
|
+
params: Bytes<64>,
|
|
510
|
+
outputDeserializationSchema: Bytes<128>,
|
|
511
|
+
respondSerializationSchema: Bytes<128>
|
|
512
|
+
): SignBidirectionalRequest<TxParams> {
|
|
513
|
+
// The MPC rejects requests for the unsupported legacy key version 0.
|
|
514
|
+
assert(keyVersion >= 1, "keyVersion must be >= 1");
|
|
515
|
+
return SignBidirectionalRequest<TxParams> {
|
|
516
|
+
requestNonce,
|
|
517
|
+
txParamType,
|
|
518
|
+
txParams,
|
|
519
|
+
caip2Id,
|
|
520
|
+
keyVersion,
|
|
521
|
+
path,
|
|
522
|
+
algo,
|
|
523
|
+
dest,
|
|
524
|
+
params,
|
|
525
|
+
outputDeserializationSchema,
|
|
526
|
+
respondSerializationSchema
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
pragma language_version >= 0.25;
|
|
2
|
+
|
|
3
|
+
// Compiled export surface of the Signet and Schnorr modules.
|
|
4
|
+
//
|
|
5
|
+
// Signet.compact / Schnorr.compact must stay module-only files (that is what
|
|
6
|
+
// lets contracts import them), but a bare module compiles to nothing. This
|
|
7
|
+
// contract-less program re-exports the modules' pure circuits so the compiler
|
|
8
|
+
// emits them as pureCircuits in managed/contract/index.js — giving the
|
|
9
|
+
// package an in-process, provable-logic-identical reference implementation:
|
|
10
|
+
// unit tests exercise the REAL circuits, and off-chain code (path checks,
|
|
11
|
+
// Schnorr challenges, attestation messages) reuses them instead of
|
|
12
|
+
// hand-porting the algorithms.
|
|
13
|
+
//
|
|
14
|
+
// ONLY the non-generic circuits are exported here. The request circuits
|
|
15
|
+
// (calculateRequestId, constructSignBidirectionalRequest) are generic over the
|
|
16
|
+
// tx-params type, the compiler cannot export type-parameterized circuits from
|
|
17
|
+
// the top level, and this package is client-agnostic — a capacity
|
|
18
|
+
// instantiation is a CLIENT contract's choice and never belongs here.
|
|
19
|
+
// constructSignBidirectionalRequest is contract-only, so it needs no off-chain
|
|
20
|
+
// copy; calculateRequestId IS needed off-chain (clients recompute ledger map
|
|
21
|
+
// keys), so it alone has a documented-deviation TS twin in signet-requests.ts,
|
|
22
|
+
// kept honest by vault-contract's deposit round-trip test.
|
|
23
|
+
//
|
|
24
|
+
// Compile with --skip-zk only: pure circuits run in TS without proving keys.
|
|
25
|
+
|
|
26
|
+
import CompactStandardLibrary;
|
|
27
|
+
import "./Signet";
|
|
28
|
+
import "./Schnorr";
|
|
29
|
+
|
|
30
|
+
export {
|
|
31
|
+
signetAttestationMessage,
|
|
32
|
+
assertHexOf,
|
|
33
|
+
assertPathCommitment,
|
|
34
|
+
evmAddressAbiValue,
|
|
35
|
+
schnorrChallenge
|
|
36
|
+
};
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// MPC routing constants + the padded-ASCII codec for the signet request
|
|
2
|
+
// structs. Field widths mirror Signet.compact (the wire format —
|
|
3
|
+
// keep in lockstep); the string constants are the values the MPC network
|
|
4
|
+
// routes on. Ported from the MVP's contract-cli signet/constants.ts, adapted
|
|
5
|
+
// to the refactor's zero-padded convention (Compact `pad(N, "text")`) — the
|
|
6
|
+
// MVP's 2-byte length-prefixed codec is gone with the old layout.
|
|
7
|
+
//
|
|
8
|
+
// The routing constants belong in github.com/sig-net/signet.js — kept here
|
|
9
|
+
// until upstreamed.
|
|
10
|
+
|
|
11
|
+
/** Width of `SignBidirectionalRequest.caip2Id` (`Bytes<32>`). */
|
|
12
|
+
export const CAIP2_ID_BYTES = 32;
|
|
13
|
+
|
|
14
|
+
/** Width of `SignBidirectionalRequest.algo` (`Bytes<32>`). */
|
|
15
|
+
export const ALGO_BYTES = 32;
|
|
16
|
+
|
|
17
|
+
/** Width of `SignBidirectionalRequest.dest` (`Bytes<32>`). */
|
|
18
|
+
export const DEST_BYTES = 32;
|
|
19
|
+
|
|
20
|
+
/** Width of `SignBidirectionalRequest.params` (`Bytes<64>`). */
|
|
21
|
+
export const MPC_PARAMS_BYTES = 64;
|
|
22
|
+
|
|
23
|
+
/** Width of `SignBidirectionalRequest.outputDeserializationSchema` (`Bytes<128>`). */
|
|
24
|
+
export const OUTPUT_DESERIALIZATION_SCHEMA_BYTES = 128;
|
|
25
|
+
|
|
26
|
+
/** Width of `SignBidirectionalRequest.respondSerializationSchema` (`Bytes<128>`). */
|
|
27
|
+
export const RESPOND_SERIALIZATION_SCHEMA_BYTES = 128;
|
|
28
|
+
|
|
29
|
+
/** Width of `EVMCalldata.selector` (`Bytes<4>`) — the literal first 4 calldata bytes. */
|
|
30
|
+
export const SELECTOR_BYTES = 4;
|
|
31
|
+
|
|
32
|
+
/** Width of `RespondBidirectional.serializedOutput` (`Bytes<128>`). */
|
|
33
|
+
export const SERIALIZED_OUTPUT_BYTES = 128;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The MPC's error sentinel: `serializedOutput` beginning with these four
|
|
37
|
+
* bytes marks a failed/absent remote execution (mirrors the sig-net MPC's
|
|
38
|
+
* MAGIC_ERROR_PREFIX).
|
|
39
|
+
*/
|
|
40
|
+
export const MPC_ERROR_SENTINEL = new Uint8Array([0xde, 0xad, 0xbe, 0xef]);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Whether an attestation's serialized output reports a successful remote
|
|
44
|
+
* execution: the first byte is 1 — the little-endian encoding of the
|
|
45
|
+
* ABI-decoded success flag, matching the circuits'
|
|
46
|
+
* `serializedOutput as Field == 1` check.
|
|
47
|
+
*
|
|
48
|
+
* @param serializedOutput - The attestation's serialized output.
|
|
49
|
+
* @returns `true` when the remote call succeeded.
|
|
50
|
+
*/
|
|
51
|
+
export function executionSucceeded(serializedOutput: Uint8Array): boolean {
|
|
52
|
+
return serializedOutput[0] === 1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Whether an attestation's serialized output is the MPC's error sentinel
|
|
57
|
+
* (see {@link MPC_ERROR_SENTINEL}) rather than a call result.
|
|
58
|
+
*
|
|
59
|
+
* @param serializedOutput - The attestation's serialized output.
|
|
60
|
+
* @returns `true` when the MPC reported an execution error.
|
|
61
|
+
*/
|
|
62
|
+
export function isExecutionError(serializedOutput: Uint8Array): boolean {
|
|
63
|
+
return MPC_ERROR_SENTINEL.every((byte, index) => serializedOutput[index] === byte);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Signature algorithm the MPC uses for EVM chains (`algo` field value). */
|
|
67
|
+
export const SIGNET_ALGO_ECDSA = "ecdsa";
|
|
68
|
+
|
|
69
|
+
/** Destination chain family for EVM requests (`dest` field value). */
|
|
70
|
+
export const SIGNET_DEST_ETHEREUM = "ethereum";
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Default MPC key version (`keyVersion` field value). Version 0 is the
|
|
74
|
+
* unsupported legacy format — the canonical MPC (and
|
|
75
|
+
* `constructSignBidirectionalRequest`) requires `keyVersion >= 1`.
|
|
76
|
+
*/
|
|
77
|
+
export const SIGNET_DEFAULT_KEY_VERSION = 1n;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Encode text as zero-padded ASCII bytes — the Compact `pad(N, "text")`
|
|
81
|
+
* convention every string-ish field of the request structs uses (consumers
|
|
82
|
+
* NUL-trim on decode).
|
|
83
|
+
*
|
|
84
|
+
* @param text - The ASCII text to encode.
|
|
85
|
+
* @param length - The fixed field width in bytes.
|
|
86
|
+
* @returns `text`'s bytes followed by zero padding to exactly `length`.
|
|
87
|
+
* @throws If the encoded text does not fit in `length` bytes.
|
|
88
|
+
*/
|
|
89
|
+
export function asciiPadded(text: string, length: number): Uint8Array {
|
|
90
|
+
const encoded = new TextEncoder().encode(text);
|
|
91
|
+
if (encoded.length > length) {
|
|
92
|
+
throw new Error(`"${text}" is ${encoded.length} bytes — does not fit the ${length}-byte field`);
|
|
93
|
+
}
|
|
94
|
+
const out = new Uint8Array(length);
|
|
95
|
+
out.set(encoded);
|
|
96
|
+
return out;
|
|
97
|
+
}
|