@monolythium/core-sdk 0.3.16 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -0
- package/dist/crypto/index.cjs +334 -0
- package/dist/crypto/index.cjs.map +1 -1
- package/dist/crypto/index.d.cts +252 -3
- package/dist/crypto/index.d.ts +252 -3
- package/dist/crypto/index.js +316 -1
- package/dist/crypto/index.js.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/{submission-D6N5r9Rb.d.cts → submission-CKXlYstD.d.cts} +1 -1
- package/dist/{submission-D6N5r9Rb.d.ts → submission-CKXlYstD.d.ts} +1 -1
- package/package.json +1 -1
package/dist/crypto/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { I as MlDsa65Backend } from '../submission-
|
|
2
|
-
export {
|
|
1
|
+
import { I as MlDsa65Backend, H as RpcClient, iw as NonceAad } from '../submission-CKXlYstD.js';
|
|
2
|
+
export { ix as ADDRESS_DERIVATION_DOMAIN, iy as DKG_AEAD_TAG_LEN, iz as DKG_NONCE_LEN, iA as DecryptHint, iB as ENCRYPTED_SUBMISSION_UNAVAILABLE_MESSAGE, iC as ENUM_VARIANT_INDEX_ML_DSA_65, iD as EncryptedEnvelope, iE as EncryptedSubmission, E as EncryptionKey, iF as ML_DSA_65_PUBLIC_KEY_LEN, iG as ML_DSA_65_SEED_LEN, iH as ML_DSA_65_SIGNATURE_LEN, iI as ML_DSA_65_SIGNING_KEY_LEN, iJ as ML_KEM_768_CIPHERTEXT_LEN, iK as ML_KEM_768_ENCAPSULATION_KEY_LEN, iL as ML_KEM_768_SHARED_SECRET_LEN, F as MempoolClass, D as NativeEvmTxFields, iM as NativeTxExtension, iN as NativeTxExtensionDescriptor, iO as NativeTxExtensionLike, iP as PlaintextSubmission, iQ as STANDARD_ALGO_NUMBER_ML_DSA_65, iR as bincodeDecryptHint, iS as bincodeEncryptedEnvelope, iT as bincodeNonceAad, iU as bincodeSignedTransaction, iV as buildEncryptedEnvelope, iW as buildEncryptedSubmission, iX as buildPlaintextSubmission, iY as encodeMlDsa65Opaque, iZ as encodeTransactionForHash, i_ as encryptInnerTx, i$ as fetchEncryptionKey, j0 as mlDsa65AddressBytes, j1 as mlDsa65AddressFromPublicKey, j2 as outerSigDigest, j3 as submitEncryptedEnvelope, j4 as submitPlaintextTransaction, j5 as submitTransactionWithPrivacy } from '../submission-CKXlYstD.js';
|
|
3
3
|
|
|
4
4
|
declare class BincodeWriter {
|
|
5
5
|
#private;
|
|
@@ -51,4 +51,253 @@ declare function pqm1MnemonicToMlDsa65Backend(mnemonic: string): MlDsa65Backend;
|
|
|
51
51
|
declare function pqm1MnemonicToAddress(mnemonic: string): string;
|
|
52
52
|
declare function generatePqm1Mnemonic(rng?: Pqm1Rng): string;
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
/**
|
|
55
|
+
* LythiumSeal scheme-3 client-side seal primitive.
|
|
56
|
+
*
|
|
57
|
+
* Post-quantum cluster-threshold encrypted-mempool sealing:
|
|
58
|
+
* cluster-ML-KEM-768 (FIPS-203) + information-theoretic GF(256) Shamir
|
|
59
|
+
* `t`-of-`n` + committing ChaCha20-Poly1305 (with an explicit SHAKE256
|
|
60
|
+
* key-commitment). A signed transaction body is sealed to a committee of
|
|
61
|
+
* `n` operators such that any `t` of them, each holding only its own
|
|
62
|
+
* ML-KEM decapsulation key, must cooperate to recover the plaintext. No
|
|
63
|
+
* single operator (and no minority of `< t`) can read the body.
|
|
64
|
+
*
|
|
65
|
+
* This is a byte-exact port of the standalone `lythiumseal` Rust crate
|
|
66
|
+
* (github.com/monolythium/lythiumseal) plus the chain-side
|
|
67
|
+
* `LythiumSealEnvelope` wire shape from `mono-core`'s mempool
|
|
68
|
+
* (`seal_to_cluster`). Byte-compatibility is proven by a cross-language
|
|
69
|
+
* KAT (`tests/lythiumseal-kat.test.ts`) against vectors generated from the
|
|
70
|
+
* Rust reference: the same fixed roster + deterministic draw order
|
|
71
|
+
* reproduces the exact envelope bincode bytes the chain accepts, and a
|
|
72
|
+
* Rust-sealed envelope round-trips through the TS decoder.
|
|
73
|
+
*
|
|
74
|
+
* The cryptography is standardized: ML-KEM-768 from `@noble/post-quantum`,
|
|
75
|
+
* ChaCha20-Poly1305 from `@noble/ciphers`, and SHAKE256 from
|
|
76
|
+
* `@noble/hashes`. The GF(256) Shamir layer is the AES field (reduction
|
|
77
|
+
* polynomial 0x11b) implemented in-module to match the crate exactly.
|
|
78
|
+
*/
|
|
79
|
+
/** ML-KEM-768 encapsulation-key byte length. */
|
|
80
|
+
declare const SEAL_EK_LEN = 1184;
|
|
81
|
+
/** ML-KEM-768 decapsulation-key byte length. */
|
|
82
|
+
declare const SEAL_DK_LEN = 2400;
|
|
83
|
+
/** ML-KEM-768 ciphertext byte length. */
|
|
84
|
+
declare const SEAL_KEM_CT_LEN = 1088;
|
|
85
|
+
/** ML-KEM-768 keygen seed length (`d || z`, FIPS-203). */
|
|
86
|
+
declare const SEAL_KEM_SEED_LEN = 64;
|
|
87
|
+
/** AEAD key length (ChaCha20-Poly1305 / body key). */
|
|
88
|
+
declare const SEAL_KEY_LEN = 32;
|
|
89
|
+
/** AEAD nonce length (96-bit). */
|
|
90
|
+
declare const SEAL_NONCE_LEN = 12;
|
|
91
|
+
/** Poly1305 tag length. */
|
|
92
|
+
declare const SEAL_TAG_LEN = 16;
|
|
93
|
+
/** Explicit SHAKE256 key-commitment length. */
|
|
94
|
+
declare const SEAL_COMMIT_LEN = 32;
|
|
95
|
+
/** Shamir share wire length (`index || value`). */
|
|
96
|
+
declare const SEAL_SHARE_LEN: number;
|
|
97
|
+
/** Scheme selector for the cluster-ML-KEM + Shamir threshold body. */
|
|
98
|
+
declare const CLUSTER_MLKEM_SHAMIR = 3;
|
|
99
|
+
/**
|
|
100
|
+
* Random source for a seal: fills `dest` with random bytes. Production
|
|
101
|
+
* callers pass a CSPRNG-backed source ({@link cryptoRandomSource}); the
|
|
102
|
+
* KAT passes a deterministic source so the seal bytes are reproducible.
|
|
103
|
+
*
|
|
104
|
+
* Each call must consume the source the same way the Rust reference does:
|
|
105
|
+
* the deterministic source models `rand_core`'s `fill_bytes`, which fills
|
|
106
|
+
* in 8-byte chunks (one `u64` per chunk) and discards the unused tail of
|
|
107
|
+
* the final chunk of each call.
|
|
108
|
+
*/
|
|
109
|
+
interface SealRandomSource {
|
|
110
|
+
fillBytes(dest: Uint8Array): void;
|
|
111
|
+
}
|
|
112
|
+
/** CSPRNG-backed source (WebCrypto). The default for production seals. */
|
|
113
|
+
declare function cryptoRandomSource(): SealRandomSource;
|
|
114
|
+
interface CommittingBody {
|
|
115
|
+
nonce: Uint8Array;
|
|
116
|
+
ct: Uint8Array;
|
|
117
|
+
commitment: Uint8Array;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* `keccak256(domain || cluster_id_le || t || n || concat(idx || ek)...)`.
|
|
121
|
+
* Commits to the exact recipient ek set + order. Operators and wallets
|
|
122
|
+
* MUST compute it identically; this is the single canonical site.
|
|
123
|
+
*
|
|
124
|
+
* keccak256 is taken from the ml-dsa module's hash import to avoid a second
|
|
125
|
+
* keccak dependency; passed in by the caller to keep this module
|
|
126
|
+
* cipher-only.
|
|
127
|
+
*/
|
|
128
|
+
declare function sealRosterHash(keccak256: (input: Uint8Array) => Uint8Array, clusterId: number, t: number, n: number, roster: ReadonlyArray<{
|
|
129
|
+
operatorIndex: number;
|
|
130
|
+
ek: Uint8Array;
|
|
131
|
+
}>): Uint8Array;
|
|
132
|
+
/** One recipient slot in the scheme-3 envelope. */
|
|
133
|
+
interface SealRecipient {
|
|
134
|
+
operatorIndex: number;
|
|
135
|
+
kemCt: Uint8Array;
|
|
136
|
+
wrapped: CommittingBody;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Scheme-3 LythiumSeal envelope - the encrypted-tx body for the
|
|
140
|
+
* cluster-ML-KEM + Shamir threshold path. Bincode-encodes into the bytes
|
|
141
|
+
* that ride inside `EncryptedEnvelope.ciphertext`.
|
|
142
|
+
*/
|
|
143
|
+
interface LythiumSealEnvelope {
|
|
144
|
+
clusterId: number;
|
|
145
|
+
epoch: bigint;
|
|
146
|
+
rosterHash: Uint8Array;
|
|
147
|
+
t: number;
|
|
148
|
+
n: number;
|
|
149
|
+
aeadBody: CommittingBody;
|
|
150
|
+
recipients: SealRecipient[];
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Bincode-encode (bincode 1.3 defaults: LE fixint, `u64` length prefixes,
|
|
154
|
+
* raw fixed-size arrays) the envelope into the `EncryptedEnvelope.ciphertext`
|
|
155
|
+
* body bytes. Byte-identical to `LythiumSealEnvelope::encode` in mono-core.
|
|
156
|
+
*/
|
|
157
|
+
declare function encodeSealEnvelope(env: LythiumSealEnvelope): Uint8Array;
|
|
158
|
+
/**
|
|
159
|
+
* Seal `plaintext` to the cluster's ordered `recipientEks` (`n` operators)
|
|
160
|
+
* at reconstruction threshold `t`, bound to `(clusterId, epoch,
|
|
161
|
+
* rosterHash)`. Draws a fresh body key for every call (nonce safety rests
|
|
162
|
+
* on body-key freshness, not nonce uniqueness - see the crate invariants),
|
|
163
|
+
* GF(256) Shamir `t`-of-`n` splits it, and ML-KEM-encapsulates one share
|
|
164
|
+
* to each operator's encapsulation key under a KDF-bound member KEK.
|
|
165
|
+
*
|
|
166
|
+
* The result is the `LythiumSealEnvelope` (scheme 3) that nests inside the
|
|
167
|
+
* outer `EncryptedEnvelope.ciphertext`. Recovering the plaintext requires
|
|
168
|
+
* `t` operators to each decapsulate their own slot; no single operator can.
|
|
169
|
+
*
|
|
170
|
+
* @param rng deterministic source for the KAT; defaults to a CSPRNG.
|
|
171
|
+
*/
|
|
172
|
+
declare function sealToCluster(args: {
|
|
173
|
+
plaintext: Uint8Array;
|
|
174
|
+
recipientEks: ReadonlyArray<Uint8Array>;
|
|
175
|
+
t: number;
|
|
176
|
+
clusterId: number;
|
|
177
|
+
epoch: bigint;
|
|
178
|
+
rosterHash: Uint8Array;
|
|
179
|
+
rng?: SealRandomSource;
|
|
180
|
+
}): LythiumSealEnvelope;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Client-side scheme-3 LythiumSeal seal path for the wallet/SDK.
|
|
184
|
+
*
|
|
185
|
+
* `getClusterSealKeys` reads the cluster seal roster (per-operator ML-KEM-768
|
|
186
|
+
* encapsulation keys + `(t, n)` + roster hash + epoch). `sealTransaction`
|
|
187
|
+
* turns a signed inner transaction into the scheme-3 `LythiumSealEnvelope`,
|
|
188
|
+
* wraps it in an `EncryptedEnvelope` with the outer ML-DSA-65 signature, and
|
|
189
|
+
* yields the wire bytes mono-core's `lyth_submitEncrypted` accepts.
|
|
190
|
+
*
|
|
191
|
+
* Byte-compatibility with the chain is proven by the cross-language KAT in
|
|
192
|
+
* `tests/lythiumseal-kat.test.ts`.
|
|
193
|
+
*/
|
|
194
|
+
|
|
195
|
+
/** Algorithm tag the node serves for the scheme-3 seal path. */
|
|
196
|
+
declare const CLUSTER_MLKEM_SHAMIR_ALGO = "cluster-mlkem768-shamir";
|
|
197
|
+
/**
|
|
198
|
+
* The cluster seal roster the SDK seals a transaction body to.
|
|
199
|
+
*
|
|
200
|
+
* Built from the `lyth_getClusterSealKeys(clusterId)` RPC response (or read
|
|
201
|
+
* from genesis when that RPC is disabled on the public profile): the ordered
|
|
202
|
+
* per-operator ML-KEM-768 encapsulation keys + the `(t, n)` threshold + the
|
|
203
|
+
* roster hash + the epoch.
|
|
204
|
+
*/
|
|
205
|
+
interface ClusterSealKeys {
|
|
206
|
+
algo: string;
|
|
207
|
+
clusterId: number;
|
|
208
|
+
epoch: bigint;
|
|
209
|
+
/** 32-byte roster hash the seal context binds. */
|
|
210
|
+
rosterHash: Uint8Array;
|
|
211
|
+
/** Reconstruction threshold `t`. */
|
|
212
|
+
t: number;
|
|
213
|
+
/** Total operators `n`. */
|
|
214
|
+
n: number;
|
|
215
|
+
/** Per-operator 1184-byte ML-KEM-768 encapsulation keys, ordered `1..=n`. */
|
|
216
|
+
recipientEks: Uint8Array[];
|
|
217
|
+
}
|
|
218
|
+
/** One operator's entry in a roster source (RPC JSON or genesis). */
|
|
219
|
+
interface ClusterSealKeyEntryInput {
|
|
220
|
+
operatorIndex: number;
|
|
221
|
+
/** `0x`-hex of the operator's 1184-byte ML-KEM-768 encapsulation key. */
|
|
222
|
+
mlKemEk: string;
|
|
223
|
+
}
|
|
224
|
+
/** A cluster seal roster as served by the RPC or read from genesis. */
|
|
225
|
+
interface ClusterSealKeysSource {
|
|
226
|
+
algo?: string;
|
|
227
|
+
clusterId: number;
|
|
228
|
+
epoch: number | string | bigint;
|
|
229
|
+
/** `0x`-hex of the 32-byte roster hash (optional; recomputed + verified). */
|
|
230
|
+
rosterHash?: string;
|
|
231
|
+
t: number;
|
|
232
|
+
n: number;
|
|
233
|
+
roster: ClusterSealKeyEntryInput[];
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Normalize a roster source into the typed {@link ClusterSealKeys} the SDK
|
|
237
|
+
* seals against. The roster hash is RECOMPUTED from the ordered ek set via
|
|
238
|
+
* the canonical `seal_roster_hash` and, when the source carries one, the
|
|
239
|
+
* recomputed value must match - so a wallet can never seal under a roster
|
|
240
|
+
* hash that does not commit to the exact recipient set it is sealing to.
|
|
241
|
+
*
|
|
242
|
+
* @throws if the roster is empty, an ek has the wrong length, the operator
|
|
243
|
+
* indices are not the contiguous `1..=n` order, the threshold is out of
|
|
244
|
+
* `2 <= t <= n`, or a supplied roster hash does not match the recomputed one.
|
|
245
|
+
*/
|
|
246
|
+
declare function parseClusterSealKeys(source: ClusterSealKeysSource): ClusterSealKeys;
|
|
247
|
+
/**
|
|
248
|
+
* Fetch the cluster seal roster from a running node via
|
|
249
|
+
* `lyth_getClusterSealKeys(clusterId)`.
|
|
250
|
+
*
|
|
251
|
+
* NOTE: this RPC is DISABLED on the public node profile. When it returns
|
|
252
|
+
* "method not found" / "unavailable", read the roster from genesis instead
|
|
253
|
+
* and pass it through {@link parseClusterSealKeys} - the roster lives in the
|
|
254
|
+
* genesis `[[clusters.members]]` `seal_ek` fields, which is exactly what the
|
|
255
|
+
* RPC would otherwise serve.
|
|
256
|
+
*
|
|
257
|
+
* @throws if the RPC is unavailable (carry the roster as input instead) or
|
|
258
|
+
* the served roster does not validate.
|
|
259
|
+
*/
|
|
260
|
+
declare function getClusterSealKeys(client: RpcClient, clusterId?: number): Promise<ClusterSealKeys>;
|
|
261
|
+
/** A built scheme-3 encrypted submission, ready for `lyth_submitEncrypted`. */
|
|
262
|
+
interface SealedSubmission {
|
|
263
|
+
/** Bincode `EncryptedEnvelope` wire bytes, `0x`-prefixed hex. */
|
|
264
|
+
envelopeWireHex: string;
|
|
265
|
+
/** Bincode `EncryptedEnvelope` wire bytes. */
|
|
266
|
+
envelopeWireBytes: Uint8Array;
|
|
267
|
+
/** Length of the inner scheme-3 ciphertext body in bytes. */
|
|
268
|
+
ciphertextBytes: number;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Seal a signed inner transaction to the cluster and wrap it in an
|
|
272
|
+
* `EncryptedEnvelope` with the outer ML-DSA-65 signature.
|
|
273
|
+
*
|
|
274
|
+
* `signedTxBincode` is the bincode `SignedTransaction` wire bytes (the body
|
|
275
|
+
* `mesh_submitTx` would otherwise carry in the clear). `aad` is the
|
|
276
|
+
* authenticated envelope header; per Law §3.6 / R3-H08 its fee fields MUST
|
|
277
|
+
* mirror the inner tx's fee fields exactly, so the chain's `verify_inner_match`
|
|
278
|
+
* passes on reveal - the caller is responsible for building it from the same
|
|
279
|
+
* fields it signed.
|
|
280
|
+
*
|
|
281
|
+
* The outer signature is taken over the canonical preimage
|
|
282
|
+
* `keccak256(bincode(aad) || ciphertext || bincode(hint) || sender_pubkey)`,
|
|
283
|
+
* identical to mono-core's `EncryptedEnvelope::signed_digest`.
|
|
284
|
+
*
|
|
285
|
+
* @param rng deterministic source for the KAT; defaults to a CSPRNG.
|
|
286
|
+
*/
|
|
287
|
+
declare function sealTransaction(args: {
|
|
288
|
+
signedTxBincode: Uint8Array;
|
|
289
|
+
clusterSealKeys: ClusterSealKeys;
|
|
290
|
+
aad: NonceAad;
|
|
291
|
+
senderAddress: Uint8Array;
|
|
292
|
+
senderPubkey: Uint8Array;
|
|
293
|
+
signOuterDigest: (digest: Uint8Array) => Promise<Uint8Array> | Uint8Array;
|
|
294
|
+
rng?: SealRandomSource;
|
|
295
|
+
}): Promise<SealedSubmission>;
|
|
296
|
+
/**
|
|
297
|
+
* Submit a built scheme-3 encrypted envelope through `lyth_submitEncrypted`.
|
|
298
|
+
*
|
|
299
|
+
* @returns the mempool tx hash the node assigns on admission.
|
|
300
|
+
*/
|
|
301
|
+
declare function submitSealedTransaction(client: RpcClient, submission: SealedSubmission): Promise<string>;
|
|
302
|
+
|
|
303
|
+
export { BincodeWriter, CLUSTER_MLKEM_SHAMIR, CLUSTER_MLKEM_SHAMIR_ALGO, type ClusterSealKeyEntryInput, type ClusterSealKeys, type ClusterSealKeysSource, type LythiumSealEnvelope, MlDsa65Backend, NonceAad, PQM1_ALGO_TAG_FALCON512_RESERVED, PQM1_ALGO_TAG_MLDSA65, PQM1_ALGO_TAG_MLDSA87_RESERVED, PQM1_ALGO_TAG_SLHDSA128S_RESERVED, PQM1_ENTROPY_LEN, PQM1_PAYLOAD_LEN, PQM1_V1_MLDSA65_DOMAIN_TAG, PQM1_V1_MNEMONIC_WORDS, PQM1_VERSION_V1, Pqm1Error, type Pqm1ErrorKind, type Pqm1Payload, type Pqm1Rng, SEAL_COMMIT_LEN, SEAL_DK_LEN, SEAL_EK_LEN, SEAL_KEM_CT_LEN, SEAL_KEM_SEED_LEN, SEAL_KEY_LEN, SEAL_NONCE_LEN, SEAL_SHARE_LEN, SEAL_TAG_LEN, type SealRandomSource, type SealRecipient, type SealedSubmission, assemblePqm1Payload, bytesToHex, concatBytes, cryptoRandomSource, derivePqm1MlDsa65SeedFromPayload, encodeSealEnvelope, expectBytes, generatePqm1Mnemonic, getClusterSealKeys, hexToBytes, parseClusterSealKeys, parsePqm1Payload, pqm1MnemonicToAddress, pqm1MnemonicToMlDsa65Backend, pqm1MnemonicToMlDsa65Seed, pqm1MnemonicToPayload, pqm1PayloadToMnemonic, sealRosterHash, sealToCluster, sealTransaction, submitSealedTransaction };
|
package/dist/crypto/index.js
CHANGED
|
@@ -575,7 +575,322 @@ function bytesEqual(a, b) {
|
|
|
575
575
|
}
|
|
576
576
|
return true;
|
|
577
577
|
}
|
|
578
|
+
var SEAL_EK_LEN = 1184;
|
|
579
|
+
var SEAL_DK_LEN = 2400;
|
|
580
|
+
var SEAL_KEM_CT_LEN = 1088;
|
|
581
|
+
var SEAL_KEM_SEED_LEN = 64;
|
|
582
|
+
var SEAL_KEY_LEN = 32;
|
|
583
|
+
var SEAL_NONCE_LEN = 12;
|
|
584
|
+
var SEAL_TAG_LEN = 16;
|
|
585
|
+
var SEAL_COMMIT_LEN = 32;
|
|
586
|
+
var SEAL_SECRET_LEN = 32;
|
|
587
|
+
var SEAL_SHARE_LEN = 1 + SEAL_SECRET_LEN;
|
|
588
|
+
var CLUSTER_MLKEM_SHAMIR = 3;
|
|
589
|
+
var COMMIT_DOMAIN = new TextEncoder().encode("lythiumseal/commit/v1");
|
|
590
|
+
var KEK_DOMAIN = new TextEncoder().encode("lythiumseal/kek/v1");
|
|
591
|
+
var NONCE_DOMAIN = new TextEncoder().encode("lythiumseal/nonce/v1");
|
|
592
|
+
var BODY_AAD_DOMAIN = new TextEncoder().encode("lythiumseal/body/v1");
|
|
593
|
+
var SHARE_AAD_DOMAIN = new TextEncoder().encode("lythiumseal/share/v1");
|
|
594
|
+
var ROSTER_DOMAIN = new TextEncoder().encode("lythiumseal/roster/v1");
|
|
595
|
+
function cryptoRandomSource() {
|
|
596
|
+
return {
|
|
597
|
+
fillBytes(dest) {
|
|
598
|
+
crypto.getRandomValues(dest);
|
|
599
|
+
}
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
function u32le(n) {
|
|
603
|
+
const out = new Uint8Array(4);
|
|
604
|
+
out[0] = n & 255;
|
|
605
|
+
out[1] = n >>> 8 & 255;
|
|
606
|
+
out[2] = n >>> 16 & 255;
|
|
607
|
+
out[3] = n >>> 24 & 255;
|
|
608
|
+
return out;
|
|
609
|
+
}
|
|
610
|
+
function u64le(n) {
|
|
611
|
+
const out = new Uint8Array(8);
|
|
612
|
+
let v = n;
|
|
613
|
+
for (let i = 0; i < 8; i++) {
|
|
614
|
+
out[i] = Number(v & 0xffn);
|
|
615
|
+
v >>= 8n;
|
|
616
|
+
}
|
|
617
|
+
return out;
|
|
618
|
+
}
|
|
619
|
+
function framed(field) {
|
|
620
|
+
return concatBytes(u32le(field.length), field);
|
|
621
|
+
}
|
|
622
|
+
function keyCommitment(key) {
|
|
623
|
+
return shake256(concatBytes(framed(COMMIT_DOMAIN), key), { dkLen: SEAL_COMMIT_LEN });
|
|
624
|
+
}
|
|
625
|
+
function deriveKek(sharedSecret, domain, clusterId, epoch, opIndex) {
|
|
626
|
+
const input = concatBytes(
|
|
627
|
+
framed(KEK_DOMAIN),
|
|
628
|
+
framed(sharedSecret),
|
|
629
|
+
framed(domain),
|
|
630
|
+
u32le(clusterId),
|
|
631
|
+
u64le(epoch),
|
|
632
|
+
Uint8Array.of(opIndex)
|
|
633
|
+
);
|
|
634
|
+
return shake256(input, { dkLen: SEAL_KEY_LEN });
|
|
635
|
+
}
|
|
636
|
+
function deriveNonce(domain, context) {
|
|
637
|
+
const input = concatBytes(framed(NONCE_DOMAIN), framed(domain), framed(context));
|
|
638
|
+
return shake256(input, { dkLen: SEAL_NONCE_LEN });
|
|
639
|
+
}
|
|
640
|
+
function bodyAad(ctx, k, n) {
|
|
641
|
+
return concatBytes(
|
|
642
|
+
BODY_AAD_DOMAIN,
|
|
643
|
+
u32le(ctx.clusterId),
|
|
644
|
+
u64le(ctx.epoch),
|
|
645
|
+
Uint8Array.of(k),
|
|
646
|
+
Uint8Array.of(n),
|
|
647
|
+
ctx.rosterHash
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
function shareAad(ctx, opIndex) {
|
|
651
|
+
return concatBytes(
|
|
652
|
+
SHARE_AAD_DOMAIN,
|
|
653
|
+
u32le(ctx.clusterId),
|
|
654
|
+
u64le(ctx.epoch),
|
|
655
|
+
Uint8Array.of(opIndex),
|
|
656
|
+
ctx.rosterHash
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
function aeadSeal(key, nonce, plaintext, aad) {
|
|
660
|
+
const cipher = chacha20poly1305(key, nonce, aad);
|
|
661
|
+
const ct = cipher.encrypt(plaintext);
|
|
662
|
+
return { nonce, ct, commitment: keyCommitment(key) };
|
|
663
|
+
}
|
|
664
|
+
function gfMul(a, b) {
|
|
665
|
+
let product = 0;
|
|
666
|
+
let x = a & 255;
|
|
667
|
+
let y = b & 255;
|
|
668
|
+
for (let i = 0; i < 8; i++) {
|
|
669
|
+
const mask = -(y & 1) & 255;
|
|
670
|
+
product ^= x & mask;
|
|
671
|
+
const high = -(x >> 7 & 1) & 255;
|
|
672
|
+
x = x << 1 & 255;
|
|
673
|
+
x ^= 27 & high;
|
|
674
|
+
y >>= 1;
|
|
675
|
+
}
|
|
676
|
+
return product & 255;
|
|
677
|
+
}
|
|
678
|
+
function polyEval(coeffs, x) {
|
|
679
|
+
let acc = 0;
|
|
680
|
+
for (let i = coeffs.length - 1; i >= 0; i--) {
|
|
681
|
+
acc = gfMul(acc, x) ^ coeffs[i];
|
|
682
|
+
}
|
|
683
|
+
return acc & 255;
|
|
684
|
+
}
|
|
685
|
+
function shamirSplit(secret, t, n, rng) {
|
|
686
|
+
const byteCoeffs = [];
|
|
687
|
+
for (let j = 0; j < SEAL_SECRET_LEN; j++) {
|
|
688
|
+
const c = new Uint8Array(t);
|
|
689
|
+
c[0] = secret[j];
|
|
690
|
+
if (t > 1) {
|
|
691
|
+
const tail = new Uint8Array(t - 1);
|
|
692
|
+
rng.fillBytes(tail);
|
|
693
|
+
c.set(tail, 1);
|
|
694
|
+
}
|
|
695
|
+
byteCoeffs.push(c);
|
|
696
|
+
}
|
|
697
|
+
const shares = [];
|
|
698
|
+
for (let k = 0; k < n; k++) {
|
|
699
|
+
const x = k + 1 & 255;
|
|
700
|
+
const value = new Uint8Array(SEAL_SECRET_LEN);
|
|
701
|
+
for (let j = 0; j < SEAL_SECRET_LEN; j++) {
|
|
702
|
+
value[j] = polyEval(byteCoeffs[j], x);
|
|
703
|
+
}
|
|
704
|
+
shares.push({ index: x, value });
|
|
705
|
+
}
|
|
706
|
+
return shares;
|
|
707
|
+
}
|
|
708
|
+
function shareToBytes(s) {
|
|
709
|
+
const out = new Uint8Array(SEAL_SHARE_LEN);
|
|
710
|
+
out[0] = s.index;
|
|
711
|
+
out.set(s.value, 1);
|
|
712
|
+
return out;
|
|
713
|
+
}
|
|
714
|
+
function sealRosterHash(keccak2562, clusterId, t, n, roster) {
|
|
715
|
+
const chunks = [ROSTER_DOMAIN, u32le(clusterId), Uint8Array.of(t), Uint8Array.of(n)];
|
|
716
|
+
for (const { operatorIndex, ek } of roster) {
|
|
717
|
+
chunks.push(Uint8Array.of(operatorIndex), ek);
|
|
718
|
+
}
|
|
719
|
+
return keccak2562(concatBytes(...chunks));
|
|
720
|
+
}
|
|
721
|
+
function encodeSealEnvelope(env) {
|
|
722
|
+
const chunks = [];
|
|
723
|
+
chunks.push(u32le(env.clusterId));
|
|
724
|
+
chunks.push(u64le(env.epoch));
|
|
725
|
+
chunks.push(expectBytes(env.rosterHash, 32, "rosterHash"));
|
|
726
|
+
chunks.push(Uint8Array.of(env.t));
|
|
727
|
+
chunks.push(Uint8Array.of(env.n));
|
|
728
|
+
pushAeadBody(chunks, env.aeadBody);
|
|
729
|
+
chunks.push(u64le(BigInt(env.recipients.length)));
|
|
730
|
+
for (const r of env.recipients) {
|
|
731
|
+
chunks.push(Uint8Array.of(r.operatorIndex));
|
|
732
|
+
chunks.push(u64le(BigInt(r.kemCt.length)));
|
|
733
|
+
chunks.push(r.kemCt);
|
|
734
|
+
pushAeadBody(chunks, r.wrapped);
|
|
735
|
+
}
|
|
736
|
+
return concatBytes(...chunks);
|
|
737
|
+
}
|
|
738
|
+
function pushAeadBody(chunks, body) {
|
|
739
|
+
chunks.push(expectBytes(body.nonce, SEAL_NONCE_LEN, "aead nonce"));
|
|
740
|
+
chunks.push(u64le(BigInt(body.ct.length)));
|
|
741
|
+
chunks.push(body.ct);
|
|
742
|
+
chunks.push(expectBytes(body.commitment, SEAL_COMMIT_LEN, "aead commitment"));
|
|
743
|
+
}
|
|
744
|
+
function sealToCluster(args) {
|
|
745
|
+
const { plaintext, recipientEks, t, clusterId } = args;
|
|
746
|
+
const epoch = args.epoch;
|
|
747
|
+
const rosterHash = expectBytes(args.rosterHash, 32, "rosterHash");
|
|
748
|
+
const rng = args.rng ?? cryptoRandomSource();
|
|
749
|
+
const n = recipientEks.length;
|
|
750
|
+
if (!Number.isInteger(t) || t < 1 || t > n || n < 1 || n > 255) {
|
|
751
|
+
throw new Error(`invalid threshold/recipient count: t=${t} n=${n}`);
|
|
752
|
+
}
|
|
753
|
+
for (let i = 0; i < n; i++) {
|
|
754
|
+
expectBytes(recipientEks[i], SEAL_EK_LEN, `recipientEks[${i}]`);
|
|
755
|
+
}
|
|
756
|
+
const ctx = { clusterId, epoch, rosterHash };
|
|
757
|
+
const bodyKey = new Uint8Array(SEAL_KEY_LEN);
|
|
758
|
+
rng.fillBytes(bodyKey);
|
|
759
|
+
const aad = bodyAad(ctx, t, n);
|
|
760
|
+
const bodyNonce = deriveNonce(new TextEncoder().encode("body"), aad);
|
|
761
|
+
const aeadBody = aeadSeal(bodyKey, bodyNonce, plaintext, aad);
|
|
762
|
+
const shares = shamirSplit(bodyKey, t, n, rng);
|
|
763
|
+
const recipients = [];
|
|
764
|
+
for (let i = 0; i < n; i++) {
|
|
765
|
+
const opIndex = i + 1 & 255;
|
|
766
|
+
const m = new Uint8Array(32);
|
|
767
|
+
rng.fillBytes(m);
|
|
768
|
+
const { cipherText: kemCt, sharedSecret } = ml_kem768.encapsulate(recipientEks[i], m);
|
|
769
|
+
const kek = deriveKek(sharedSecret, rosterHash, clusterId, epoch, opIndex);
|
|
770
|
+
const sAad = shareAad(ctx, opIndex);
|
|
771
|
+
const wrapNonce = deriveNonce(new TextEncoder().encode("share"), sAad);
|
|
772
|
+
const wrapped = aeadSeal(kek, wrapNonce, shareToBytes(shares[i]), sAad);
|
|
773
|
+
recipients.push({ operatorIndex: opIndex, kemCt, wrapped });
|
|
774
|
+
sharedSecret.fill(0);
|
|
775
|
+
kek.fill(0);
|
|
776
|
+
}
|
|
777
|
+
bodyKey.fill(0);
|
|
778
|
+
return {
|
|
779
|
+
clusterId,
|
|
780
|
+
epoch,
|
|
781
|
+
rosterHash,
|
|
782
|
+
t,
|
|
783
|
+
n,
|
|
784
|
+
aeadBody,
|
|
785
|
+
recipients
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
var CLUSTER_MLKEM_SHAMIR_ALGO = "cluster-mlkem768-shamir";
|
|
789
|
+
function parseClusterSealKeys(source) {
|
|
790
|
+
const n = source.roster.length;
|
|
791
|
+
if (n === 0) {
|
|
792
|
+
throw new Error("cluster seal roster is empty");
|
|
793
|
+
}
|
|
794
|
+
if (source.n !== n) {
|
|
795
|
+
throw new Error(`cluster seal roster n=${source.n} disagrees with ${n} entries`);
|
|
796
|
+
}
|
|
797
|
+
if (!Number.isInteger(source.t) || source.t < 2 || source.t > n) {
|
|
798
|
+
throw new Error(`cluster seal threshold t=${source.t} out of range 2..=${n}`);
|
|
799
|
+
}
|
|
800
|
+
const sorted = [...source.roster].sort((a, b) => a.operatorIndex - b.operatorIndex);
|
|
801
|
+
const recipientEks = [];
|
|
802
|
+
const hashInput = [];
|
|
803
|
+
for (let i = 0; i < n; i++) {
|
|
804
|
+
const entry = sorted[i];
|
|
805
|
+
if (entry.operatorIndex !== i + 1) {
|
|
806
|
+
throw new Error(
|
|
807
|
+
`cluster seal roster operator indices must be 1..=${n}; got ${entry.operatorIndex} at slot ${i + 1}`
|
|
808
|
+
);
|
|
809
|
+
}
|
|
810
|
+
const ek = expectBytes(hexToBytes(entry.mlKemEk, `operator ${entry.operatorIndex} mlKemEk`), SEAL_EK_LEN, `operator ${entry.operatorIndex} ek`);
|
|
811
|
+
recipientEks.push(ek);
|
|
812
|
+
hashInput.push({ operatorIndex: entry.operatorIndex, ek });
|
|
813
|
+
}
|
|
814
|
+
const recomputed = sealRosterHash(keccak256, source.clusterId, source.t, n, hashInput);
|
|
815
|
+
if (source.rosterHash !== void 0) {
|
|
816
|
+
const supplied = expectBytes(hexToBytes(source.rosterHash, "rosterHash"), 32, "rosterHash");
|
|
817
|
+
if (!bytesEqual2(supplied, recomputed)) {
|
|
818
|
+
throw new Error(
|
|
819
|
+
`cluster seal roster hash mismatch: source ${bytesToHex(supplied)} != recomputed ${bytesToHex(recomputed)} (the roster hash does not commit to this ek set)`
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return {
|
|
824
|
+
algo: source.algo ?? CLUSTER_MLKEM_SHAMIR_ALGO,
|
|
825
|
+
clusterId: source.clusterId,
|
|
826
|
+
epoch: toBigInt(source.epoch),
|
|
827
|
+
rosterHash: recomputed,
|
|
828
|
+
t: source.t,
|
|
829
|
+
n,
|
|
830
|
+
recipientEks
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
async function getClusterSealKeys(client, clusterId = 0) {
|
|
834
|
+
const result = await client.call(
|
|
835
|
+
"lyth_getClusterSealKeys",
|
|
836
|
+
[clusterId]
|
|
837
|
+
);
|
|
838
|
+
return parseClusterSealKeys({ ...result, clusterId: result.clusterId ?? clusterId });
|
|
839
|
+
}
|
|
840
|
+
async function sealTransaction(args) {
|
|
841
|
+
const keys = args.clusterSealKeys;
|
|
842
|
+
const senderPubkey = expectBytes(args.senderPubkey, ML_DSA_65_PUBLIC_KEY_LEN, "senderPubkey");
|
|
843
|
+
const senderAddress = expectBytes(args.senderAddress, 20, "senderAddress");
|
|
844
|
+
const env = sealToCluster({
|
|
845
|
+
plaintext: args.signedTxBincode,
|
|
846
|
+
recipientEks: keys.recipientEks,
|
|
847
|
+
t: keys.t,
|
|
848
|
+
clusterId: keys.clusterId,
|
|
849
|
+
epoch: keys.epoch,
|
|
850
|
+
rosterHash: keys.rosterHash,
|
|
851
|
+
rng: args.rng
|
|
852
|
+
});
|
|
853
|
+
const ciphertext = encodeSealEnvelope(env);
|
|
854
|
+
const decryptionHint = { epoch: keys.epoch, scheme: CLUSTER_MLKEM_SHAMIR };
|
|
855
|
+
const digest = outerSigDigest(args.aad, ciphertext, decryptionHint, senderPubkey);
|
|
856
|
+
const outerSignature = expectBytes(
|
|
857
|
+
await args.signOuterDigest(digest),
|
|
858
|
+
ML_DSA_65_SIGNATURE_LEN,
|
|
859
|
+
"outerSignature"
|
|
860
|
+
);
|
|
861
|
+
const envelope = {
|
|
862
|
+
nonceAad: args.aad,
|
|
863
|
+
ciphertext,
|
|
864
|
+
decryptionHint,
|
|
865
|
+
senderPubkey,
|
|
866
|
+
outerSignature,
|
|
867
|
+
sender: senderAddress
|
|
868
|
+
};
|
|
869
|
+
const envelopeWireBytes = bincodeEncryptedEnvelope(envelope);
|
|
870
|
+
return {
|
|
871
|
+
envelopeWireHex: `0x${bytesToHex(envelopeWireBytes).slice(2)}`,
|
|
872
|
+
envelopeWireBytes,
|
|
873
|
+
ciphertextBytes: ciphertext.length
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
async function submitSealedTransaction(client, submission) {
|
|
877
|
+
return client.call("lyth_submitEncrypted", [submission.envelopeWireHex]);
|
|
878
|
+
}
|
|
879
|
+
function keccak256(input) {
|
|
880
|
+
return keccak_256(input);
|
|
881
|
+
}
|
|
882
|
+
function toBigInt(value) {
|
|
883
|
+
if (typeof value === "bigint") return value;
|
|
884
|
+
return BigInt(value);
|
|
885
|
+
}
|
|
886
|
+
function bytesEqual2(a, b) {
|
|
887
|
+
if (a.length !== b.length) return false;
|
|
888
|
+
for (let i = 0; i < a.length; i++) {
|
|
889
|
+
if (a[i] !== b[i]) return false;
|
|
890
|
+
}
|
|
891
|
+
return true;
|
|
892
|
+
}
|
|
578
893
|
|
|
579
|
-
export { ADDRESS_DERIVATION_DOMAIN, BincodeWriter, DKG_AEAD_TAG_LEN, DKG_NONCE_LEN, ENCRYPTED_SUBMISSION_UNAVAILABLE_MESSAGE, ENUM_VARIANT_INDEX_ML_DSA_65, ML_DSA_65_PUBLIC_KEY_LEN, ML_DSA_65_SEED_LEN, ML_DSA_65_SIGNATURE_LEN, ML_DSA_65_SIGNING_KEY_LEN, ML_KEM_768_CIPHERTEXT_LEN, ML_KEM_768_ENCAPSULATION_KEY_LEN, ML_KEM_768_SHARED_SECRET_LEN, MempoolClass, MlDsa65Backend, PQM1_ALGO_TAG_FALCON512_RESERVED, PQM1_ALGO_TAG_MLDSA65, PQM1_ALGO_TAG_MLDSA87_RESERVED, PQM1_ALGO_TAG_SLHDSA128S_RESERVED, PQM1_ENTROPY_LEN, PQM1_PAYLOAD_LEN, PQM1_V1_MLDSA65_DOMAIN_TAG, PQM1_V1_MNEMONIC_WORDS, PQM1_VERSION_V1, Pqm1Error, STANDARD_ALGO_NUMBER_ML_DSA_65, assemblePqm1Payload, bincodeDecryptHint, bincodeEncryptedEnvelope, bincodeNonceAad, bincodeSignedTransaction, buildEncryptedEnvelope, buildEncryptedSubmission, buildPlaintextSubmission, bytesToHex, concatBytes, derivePqm1MlDsa65SeedFromPayload, encodeMlDsa65Opaque, encodeTransactionForHash, encryptInnerTx, expectBytes, fetchEncryptionKey, generatePqm1Mnemonic, hexToBytes, mlDsa65AddressBytes, mlDsa65AddressFromPublicKey, outerSigDigest, parsePqm1Payload, pqm1MnemonicToAddress, pqm1MnemonicToMlDsa65Backend, pqm1MnemonicToMlDsa65Seed, pqm1MnemonicToPayload, pqm1PayloadToMnemonic, submitEncryptedEnvelope, submitPlaintextTransaction, submitTransactionWithPrivacy };
|
|
894
|
+
export { ADDRESS_DERIVATION_DOMAIN, BincodeWriter, CLUSTER_MLKEM_SHAMIR, CLUSTER_MLKEM_SHAMIR_ALGO, DKG_AEAD_TAG_LEN, DKG_NONCE_LEN, ENCRYPTED_SUBMISSION_UNAVAILABLE_MESSAGE, ENUM_VARIANT_INDEX_ML_DSA_65, ML_DSA_65_PUBLIC_KEY_LEN, ML_DSA_65_SEED_LEN, ML_DSA_65_SIGNATURE_LEN, ML_DSA_65_SIGNING_KEY_LEN, ML_KEM_768_CIPHERTEXT_LEN, ML_KEM_768_ENCAPSULATION_KEY_LEN, ML_KEM_768_SHARED_SECRET_LEN, MempoolClass, MlDsa65Backend, PQM1_ALGO_TAG_FALCON512_RESERVED, PQM1_ALGO_TAG_MLDSA65, PQM1_ALGO_TAG_MLDSA87_RESERVED, PQM1_ALGO_TAG_SLHDSA128S_RESERVED, PQM1_ENTROPY_LEN, PQM1_PAYLOAD_LEN, PQM1_V1_MLDSA65_DOMAIN_TAG, PQM1_V1_MNEMONIC_WORDS, PQM1_VERSION_V1, Pqm1Error, SEAL_COMMIT_LEN, SEAL_DK_LEN, SEAL_EK_LEN, SEAL_KEM_CT_LEN, SEAL_KEM_SEED_LEN, SEAL_KEY_LEN, SEAL_NONCE_LEN, SEAL_SHARE_LEN, SEAL_TAG_LEN, STANDARD_ALGO_NUMBER_ML_DSA_65, assemblePqm1Payload, bincodeDecryptHint, bincodeEncryptedEnvelope, bincodeNonceAad, bincodeSignedTransaction, buildEncryptedEnvelope, buildEncryptedSubmission, buildPlaintextSubmission, bytesToHex, concatBytes, cryptoRandomSource, derivePqm1MlDsa65SeedFromPayload, encodeMlDsa65Opaque, encodeSealEnvelope, encodeTransactionForHash, encryptInnerTx, expectBytes, fetchEncryptionKey, generatePqm1Mnemonic, getClusterSealKeys, hexToBytes, mlDsa65AddressBytes, mlDsa65AddressFromPublicKey, outerSigDigest, parseClusterSealKeys, parsePqm1Payload, pqm1MnemonicToAddress, pqm1MnemonicToMlDsa65Backend, pqm1MnemonicToMlDsa65Seed, pqm1MnemonicToPayload, pqm1PayloadToMnemonic, sealRosterHash, sealToCluster, sealTransaction, submitEncryptedEnvelope, submitPlaintextTransaction, submitSealedTransaction, submitTransactionWithPrivacy };
|
|
580
895
|
//# sourceMappingURL=index.js.map
|
|
581
896
|
//# sourceMappingURL=index.js.map
|