@leofcoin/chain 1.10.9 → 1.10.11
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/exports/beacon-envelope.js +68 -0
- package/exports/beacon-epoch.js +117 -0
- package/exports/beacon-lifecycle.js +155 -0
- package/exports/beacon-round.js +77 -0
- package/exports/beacon-wire.js +98 -0
- package/exports/beacon.js +141 -0
- package/exports/browser/beacon-envelope.js +163 -0
- package/exports/browser/beacon-epoch.js +116 -0
- package/exports/browser/beacon-lifecycle.js +154 -0
- package/exports/browser/beacon-round.js +76 -0
- package/exports/browser/beacon-wire.js +99 -0
- package/exports/browser/beacon.js +1706 -0
- package/exports/browser/{browser-D-r0O9Qn-BZDYY6cg.js → browser-CWeoyGUw-BabtHowB.js} +4 -2
- package/exports/browser/{browser-_hiyXwPp-DYI1tyUr.js → browser-DvU1xNFS-sdlKNCJc.js} +4 -2
- package/exports/browser/chain.js +174 -5008
- package/exports/browser/{client-BVhUamQG-D-D1e_x8.js → client-B-jyclOB-CsNYfj4Z.js} +6 -4
- package/exports/browser/constants-Cv0p224A.js +130 -0
- package/exports/browser/hkdf-DhdhLAAv.js +147 -0
- package/exports/browser/{index-BD0Anx7_-Dg4_tCeN.js → index-Bxr5Iztg-C6xBk0rK.js} +4 -2
- package/exports/browser/index-CvKt4UDE.js +464 -0
- package/exports/browser/index-D7FUx7Dd.js +4996 -0
- package/exports/browser/{messages-UKnuelZ7-DJT-98q-.js → messages-FMRAS8QX-CvlZBzy5.js} +4 -2
- package/exports/browser/{node-browser-DlzZ5CP_.js → node-browser-xlqSBOaN.js} +12 -5
- package/exports/browser/node-browser.js +4 -2
- package/exports/browser/{constants-gMYZLHKp.js → proposal.proto-BcUgd885.js} +61 -620
- package/exports/browser/quorum-S_qdgiAY.js +8 -0
- package/exports/browser/weierstrass-C-jX_jly.js +2156 -0
- package/exports/browser/workers/block-worker.js +1 -1
- package/exports/browser/workers/machine-worker.js +7142 -72
- package/exports/browser/workers/{worker-CZqErLI7-BxofVJAn.js → worker-DMCj1e6z-CTYVa2yX.js} +30 -1
- package/exports/chain.js +158 -75
- package/exports/{constants-D6gWzJZg.js → constants-CMYKv-Rt.js} +1 -1
- package/exports/node.js +1 -1
- package/exports/quorum-S_qdgiAY.js +8 -0
- package/exports/workers/block-worker.js +1 -1
- package/exports/workers/machine-worker.js +7142 -72
- package/exports/workers/{worker-CZqErLI7-BxofVJAn.js → worker-DMCj1e6z-CTYVa2yX.js} +30 -1
- package/package.json +29 -2
- package/types/beacon-envelope.d.ts +27 -0
- package/types/beacon-epoch.d.ts +28 -0
- package/types/beacon-lifecycle.d.ts +40 -0
- package/types/beacon-round.d.ts +18 -0
- package/types/beacon-wire.d.ts +31 -0
- package/types/beacon.d.ts +24 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { w as weierstrass, r as randomBytes, c as concatBytes, F as Field, m as mod, p as pow2, s as sha256 } from './weierstrass-C-jX_jly.js';
|
|
2
|
+
import { h as hmac, a as hkdf } from './hkdf-DhdhLAAv.js';
|
|
3
|
+
|
|
4
|
+
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
|
5
|
+
// connects noble-curves to noble-hashes
|
|
6
|
+
function getHash(hash) {
|
|
7
|
+
return {
|
|
8
|
+
hash,
|
|
9
|
+
hmac: (key, ...msgs) => hmac(hash, key, concatBytes(...msgs)),
|
|
10
|
+
randomBytes,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function createCurve(curveDef, defHash) {
|
|
14
|
+
const create = (hash) => weierstrass({ ...curveDef, ...getHash(hash) });
|
|
15
|
+
return Object.freeze({ ...create(defHash), create });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/*! noble-curves - MIT License (c) 2022 Paul Miller (paulmillr.com) */
|
|
19
|
+
const secp256k1P = BigInt('0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f');
|
|
20
|
+
const secp256k1N = BigInt('0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141');
|
|
21
|
+
const _1n = BigInt(1);
|
|
22
|
+
const _2n = BigInt(2);
|
|
23
|
+
const divNearest = (a, b) => (a + b / _2n) / b;
|
|
24
|
+
/**
|
|
25
|
+
* √n = n^((p+1)/4) for fields p = 3 mod 4. We unwrap the loop and multiply bit-by-bit.
|
|
26
|
+
* (P+1n/4n).toString(2) would produce bits [223x 1, 0, 22x 1, 4x 0, 11, 00]
|
|
27
|
+
*/
|
|
28
|
+
function sqrtMod(y) {
|
|
29
|
+
const P = secp256k1P;
|
|
30
|
+
// prettier-ignore
|
|
31
|
+
const _3n = BigInt(3), _6n = BigInt(6), _11n = BigInt(11), _22n = BigInt(22);
|
|
32
|
+
// prettier-ignore
|
|
33
|
+
const _23n = BigInt(23), _44n = BigInt(44), _88n = BigInt(88);
|
|
34
|
+
const b2 = (y * y * y) % P; // x^3, 11
|
|
35
|
+
const b3 = (b2 * b2 * y) % P; // x^7
|
|
36
|
+
const b6 = (pow2(b3, _3n, P) * b3) % P;
|
|
37
|
+
const b9 = (pow2(b6, _3n, P) * b3) % P;
|
|
38
|
+
const b11 = (pow2(b9, _2n, P) * b2) % P;
|
|
39
|
+
const b22 = (pow2(b11, _11n, P) * b11) % P;
|
|
40
|
+
const b44 = (pow2(b22, _22n, P) * b22) % P;
|
|
41
|
+
const b88 = (pow2(b44, _44n, P) * b44) % P;
|
|
42
|
+
const b176 = (pow2(b88, _88n, P) * b88) % P;
|
|
43
|
+
const b220 = (pow2(b176, _44n, P) * b44) % P;
|
|
44
|
+
const b223 = (pow2(b220, _3n, P) * b3) % P;
|
|
45
|
+
const t1 = (pow2(b223, _23n, P) * b22) % P;
|
|
46
|
+
const t2 = (pow2(t1, _6n, P) * b2) % P;
|
|
47
|
+
const root = pow2(t2, _2n, P);
|
|
48
|
+
if (!Fp.eql(Fp.sqr(root), y))
|
|
49
|
+
throw new Error('Cannot find square root');
|
|
50
|
+
return root;
|
|
51
|
+
}
|
|
52
|
+
const Fp = Field(secp256k1P, undefined, undefined, { sqrt: sqrtMod });
|
|
53
|
+
const secp256k1 = createCurve({
|
|
54
|
+
a: BigInt(0),
|
|
55
|
+
b: BigInt(7),
|
|
56
|
+
Fp,
|
|
57
|
+
n: secp256k1N,
|
|
58
|
+
// Base point (x, y) aka generator point
|
|
59
|
+
Gx: BigInt('55066263022277343669578718895168534326250603453777594175500187360389116729240'),
|
|
60
|
+
Gy: BigInt('32670510020758816978083085130507043184471273380659243275938904335757337482424'),
|
|
61
|
+
h: BigInt(1),
|
|
62
|
+
lowS: true,
|
|
63
|
+
/**
|
|
64
|
+
* secp256k1 belongs to Koblitz curves: it has efficiently computable endomorphism.
|
|
65
|
+
* Endomorphism uses 2x less RAM, speeds up precomputation by 2x and ECDH / key recovery by 20%.
|
|
66
|
+
* For precomputed wNAF it trades off 1/2 init time & 1/3 ram for 20% perf hit.
|
|
67
|
+
* Explanation: https://gist.github.com/paulmillr/eb670806793e84df628a7c434a873066
|
|
68
|
+
*/
|
|
69
|
+
endo: {
|
|
70
|
+
beta: BigInt('0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee'),
|
|
71
|
+
splitScalar: (k) => {
|
|
72
|
+
const n = secp256k1N;
|
|
73
|
+
const a1 = BigInt('0x3086d221a7d46bcde86c90e49284eb15');
|
|
74
|
+
const b1 = -_1n * BigInt('0xe4437ed6010e88286f547fa90abfe4c3');
|
|
75
|
+
const a2 = BigInt('0x114ca50f7a8e2f3f657c1108d9d44cfd8');
|
|
76
|
+
const b2 = a1;
|
|
77
|
+
const POW_2_128 = BigInt('0x100000000000000000000000000000000'); // (2n**128n).toString(16)
|
|
78
|
+
const c1 = divNearest(b2 * k, n);
|
|
79
|
+
const c2 = divNearest(-b1 * k, n);
|
|
80
|
+
let k1 = mod(k - c1 * a1 - c2 * a2, n);
|
|
81
|
+
let k2 = mod(-c1 * b1 - c2 * b2, n);
|
|
82
|
+
const k1neg = k1 > POW_2_128;
|
|
83
|
+
const k2neg = k2 > POW_2_128;
|
|
84
|
+
if (k1neg)
|
|
85
|
+
k1 = n - k1;
|
|
86
|
+
if (k2neg)
|
|
87
|
+
k2 = n - k2;
|
|
88
|
+
if (k1 > POW_2_128 || k2 > POW_2_128) {
|
|
89
|
+
throw new Error('splitScalar: Endomorphism failed, k=' + k);
|
|
90
|
+
}
|
|
91
|
+
return { k1neg, k1, k2neg, k2 };
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
}, sha256);
|
|
95
|
+
// Schnorr signatures are superior to ECDSA from above. Below is Schnorr-specific BIP0340 code.
|
|
96
|
+
// https://github.com/bitcoin/bips/blob/master/bip-0340.mediawiki
|
|
97
|
+
BigInt(0);
|
|
98
|
+
secp256k1.ProjectivePoint;
|
|
99
|
+
|
|
100
|
+
const encoder = new TextEncoder();
|
|
101
|
+
const SCALAR_BYTES = 32;
|
|
102
|
+
const context = (network, epoch, sender, recipient) => {
|
|
103
|
+
if (!network || epoch < 0n || !sender || !recipient || sender === recipient) {
|
|
104
|
+
throw new TypeError("invalid beacon share envelope context");
|
|
105
|
+
}
|
|
106
|
+
return encoder.encode(`leofcoin-beacon-share-v1:${network}:${epoch}:${sender}:${recipient}`);
|
|
107
|
+
};
|
|
108
|
+
const scalarBytes = (share) => {
|
|
109
|
+
if (share <= 0n || share >= secp256k1.CURVE.n) throw new RangeError("invalid beacon secret share");
|
|
110
|
+
const output = new Uint8Array(SCALAR_BYTES);
|
|
111
|
+
let value = share;
|
|
112
|
+
for (let index = output.length - 1; index >= 0; index -= 1) {
|
|
113
|
+
output[index] = Number(value & 0xffn);
|
|
114
|
+
value >>= 8n;
|
|
115
|
+
}
|
|
116
|
+
return output;
|
|
117
|
+
};
|
|
118
|
+
const bytesScalar = (bytes) => {
|
|
119
|
+
if (bytes.length !== SCALAR_BYTES) throw new Error("invalid decrypted beacon share length");
|
|
120
|
+
let value = 0n;
|
|
121
|
+
for (const byte of bytes) value = value << 8n | BigInt(byte);
|
|
122
|
+
if (value <= 0n || value >= secp256k1.CURVE.n) throw new Error("invalid decrypted beacon share");
|
|
123
|
+
return value;
|
|
124
|
+
};
|
|
125
|
+
const encryptionKey = (privateKey, publicKey, aad) => {
|
|
126
|
+
if (!secp256k1.utils.isValidPrivateKey(privateKey)) throw new Error("invalid beacon encryption private key");
|
|
127
|
+
const sharedSecret = secp256k1.getSharedSecret(privateKey, publicKey, true);
|
|
128
|
+
return hkdf(sha256, sharedSecret, sha256(aad), aad, 32);
|
|
129
|
+
};
|
|
130
|
+
const importAesKey = (key, usage) => globalThis.crypto.subtle.importKey("raw", key, { name: "AES-GCM" }, false, [usage]);
|
|
131
|
+
const createBeaconEncryptionKey = (randomPrivateKey = () => secp256k1.utils.randomPrivateKey()) => {
|
|
132
|
+
const privateKey = randomPrivateKey();
|
|
133
|
+
if (!secp256k1.utils.isValidPrivateKey(privateKey)) throw new Error("invalid generated beacon encryption key");
|
|
134
|
+
return { privateKey, publicKey: secp256k1.getPublicKey(privateKey, true) };
|
|
135
|
+
};
|
|
136
|
+
const encryptBeaconShare = async (share, recipientPublicKey, network, epoch, sender, recipient, randomPrivateKey, randomNonce = () => globalThis.crypto.getRandomValues(new Uint8Array(12))) => {
|
|
137
|
+
const aad = context(network, epoch, sender, recipient);
|
|
138
|
+
const ephemeral = createBeaconEncryptionKey(randomPrivateKey);
|
|
139
|
+
const nonce = randomNonce();
|
|
140
|
+
if (nonce.length !== 12) throw new Error("beacon share nonce must contain 12 bytes");
|
|
141
|
+
const key = await importAesKey(encryptionKey(ephemeral.privateKey, recipientPublicKey, aad), "encrypt");
|
|
142
|
+
const ciphertext = await globalThis.crypto.subtle.encrypt(
|
|
143
|
+
{ name: "AES-GCM", iv: nonce, additionalData: aad, tagLength: 128 },
|
|
144
|
+
key,
|
|
145
|
+
scalarBytes(share)
|
|
146
|
+
);
|
|
147
|
+
return { ciphertext: new Uint8Array(ciphertext), ephemeralPublicKey: ephemeral.publicKey, nonce };
|
|
148
|
+
};
|
|
149
|
+
const decryptBeaconShare = async (envelope, recipientPrivateKey, network, epoch, sender, recipient) => {
|
|
150
|
+
if (envelope.ephemeralPublicKey.length !== 33 || envelope.nonce.length !== 12 || envelope.ciphertext.length !== 48) {
|
|
151
|
+
throw new Error("invalid beacon share envelope");
|
|
152
|
+
}
|
|
153
|
+
const aad = context(network, epoch, sender, recipient);
|
|
154
|
+
const key = await importAesKey(encryptionKey(recipientPrivateKey, envelope.ephemeralPublicKey, aad), "decrypt");
|
|
155
|
+
const plaintext = await globalThis.crypto.subtle.decrypt(
|
|
156
|
+
{ name: "AES-GCM", iv: envelope.nonce, additionalData: aad, tagLength: 128 },
|
|
157
|
+
key,
|
|
158
|
+
envelope.ciphertext
|
|
159
|
+
);
|
|
160
|
+
return bytesScalar(new Uint8Array(plaintext));
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export { createBeaconEncryptionKey, decryptBeaconShare, encryptBeaconShare };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { s as sha256 } from './weierstrass-C-jX_jly.js';
|
|
2
|
+
import { t as toBase58, f as fromBase58 } from './index-CvKt4UDE.js';
|
|
3
|
+
import { deriveBeaconGroupPublicKey } from './beacon.js';
|
|
4
|
+
import { q as quorumThreshold } from './quorum-S_qdgiAY.js';
|
|
5
|
+
|
|
6
|
+
const encoder = new TextEncoder();
|
|
7
|
+
const lengthPrefix = (bytes) => {
|
|
8
|
+
if (bytes.length > 65535) throw new Error("beacon config field is too large");
|
|
9
|
+
const output = new Uint8Array(bytes.length + 2);
|
|
10
|
+
new DataView(output.buffer).setUint16(0, bytes.length, false);
|
|
11
|
+
output.set(bytes, 2);
|
|
12
|
+
return output;
|
|
13
|
+
};
|
|
14
|
+
const concatenate = (parts) => {
|
|
15
|
+
const output = new Uint8Array(parts.reduce((size, part) => size + part.length, 0));
|
|
16
|
+
let offset = 0;
|
|
17
|
+
for (const part of parts) {
|
|
18
|
+
output.set(part, offset);
|
|
19
|
+
offset += part.length;
|
|
20
|
+
}
|
|
21
|
+
return output;
|
|
22
|
+
};
|
|
23
|
+
const uint64 = (value) => {
|
|
24
|
+
if (value < 0n || value > 0xffffffffffffffffn) throw new RangeError("beacon epoch is outside uint64");
|
|
25
|
+
const bytes = new Uint8Array(8);
|
|
26
|
+
new DataView(bytes.buffer).setBigUint64(0, value, false);
|
|
27
|
+
return bytes;
|
|
28
|
+
};
|
|
29
|
+
const beaconParticipants = (validators) => {
|
|
30
|
+
const sorted = [...validators].sort();
|
|
31
|
+
if (sorted.length < 2 || new Set(sorted).size !== sorted.length || sorted.some((validator) => !validator)) {
|
|
32
|
+
throw new Error("beacon requires at least two unique validators");
|
|
33
|
+
}
|
|
34
|
+
return new Map(sorted.map((validator, index) => [validator, BigInt(index + 1)]));
|
|
35
|
+
};
|
|
36
|
+
const validateBeaconEpochConfig = (config) => {
|
|
37
|
+
try {
|
|
38
|
+
if (config.epoch < 0n) return false;
|
|
39
|
+
const participants = beaconParticipants(config.validators);
|
|
40
|
+
const validators = [...participants.keys()];
|
|
41
|
+
if (config.threshold !== quorumThreshold(validators.length) || config.dealers.length < config.threshold)
|
|
42
|
+
return false;
|
|
43
|
+
const dealers = [...config.dealers].sort((left, right) => left.validator.localeCompare(right.validator));
|
|
44
|
+
if (new Set(dealers.map(({ validator }) => validator)).size !== dealers.length) return false;
|
|
45
|
+
return dealers.every(({ validator, commitments }) => {
|
|
46
|
+
if (!participants.has(validator) || commitments.length !== config.threshold) return false;
|
|
47
|
+
return commitments.every((commitment) => fromBase58(commitment).length === 48);
|
|
48
|
+
});
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const canonicalBeaconEpochConfig = (config) => {
|
|
54
|
+
if (!validateBeaconEpochConfig(config)) throw new Error("invalid beacon epoch config");
|
|
55
|
+
return {
|
|
56
|
+
epoch: config.epoch,
|
|
57
|
+
threshold: config.threshold,
|
|
58
|
+
validators: [...config.validators].sort(),
|
|
59
|
+
dealers: [...config.dealers].map((dealer) => ({ validator: dealer.validator, commitments: [...dealer.commitments] })).sort((left, right) => left.validator.localeCompare(right.validator))
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
const encodeBeaconEpochConfig = (input) => {
|
|
63
|
+
const config = canonicalBeaconEpochConfig(input);
|
|
64
|
+
const parts = [encoder.encode("leofcoin-beacon-config-v1"), uint64(config.epoch), uint64(BigInt(config.threshold))];
|
|
65
|
+
parts.push(uint64(BigInt(config.validators.length)));
|
|
66
|
+
for (const validator of config.validators) parts.push(lengthPrefix(encoder.encode(validator)));
|
|
67
|
+
parts.push(uint64(BigInt(config.dealers.length)));
|
|
68
|
+
for (const dealer of config.dealers) {
|
|
69
|
+
parts.push(lengthPrefix(encoder.encode(dealer.validator)));
|
|
70
|
+
for (const commitment of dealer.commitments) parts.push(lengthPrefix(fromBase58(commitment)));
|
|
71
|
+
}
|
|
72
|
+
return concatenate(parts);
|
|
73
|
+
};
|
|
74
|
+
const beaconEpochDigest = (config) => toBase58(sha256(encodeBeaconEpochConfig(config)));
|
|
75
|
+
const beaconEpochGroupPublicKey = (config) => {
|
|
76
|
+
const canonical = canonicalBeaconEpochConfig(config);
|
|
77
|
+
return toBase58(
|
|
78
|
+
deriveBeaconGroupPublicKey(
|
|
79
|
+
canonical.dealers.map(({ commitments }) => commitments.map((commitment) => fromBase58(commitment)))
|
|
80
|
+
)
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
class BeaconEpochVotes {
|
|
84
|
+
#votes = /* @__PURE__ */ new Map();
|
|
85
|
+
#equivocations = /* @__PURE__ */ new Map();
|
|
86
|
+
constructor(config) {
|
|
87
|
+
this.config = canonicalBeaconEpochConfig(config);
|
|
88
|
+
this.configDigest = beaconEpochDigest(this.config);
|
|
89
|
+
}
|
|
90
|
+
add(vote) {
|
|
91
|
+
if (vote.epoch !== this.config.epoch || !this.config.validators.includes(vote.from) || !vote.signature || vote.configDigest !== this.configDigest) {
|
|
92
|
+
const existing2 = this.#votes.get(vote.from);
|
|
93
|
+
if (existing2 && vote.epoch === existing2.epoch && vote.configDigest !== existing2.configDigest) {
|
|
94
|
+
this.#equivocations.set(vote.from, [existing2, vote]);
|
|
95
|
+
return "equivocation";
|
|
96
|
+
}
|
|
97
|
+
return "invalid";
|
|
98
|
+
}
|
|
99
|
+
const existing = this.#votes.get(vote.from);
|
|
100
|
+
if (existing) return existing.signature === vote.signature ? "duplicate" : "equivocation";
|
|
101
|
+
this.#votes.set(vote.from, Object.freeze({ ...vote }));
|
|
102
|
+
return "accepted";
|
|
103
|
+
}
|
|
104
|
+
get ready() {
|
|
105
|
+
return this.#votes.size >= this.config.threshold;
|
|
106
|
+
}
|
|
107
|
+
certificate() {
|
|
108
|
+
if (!this.ready) throw new Error("beacon epoch does not have a quorum certificate");
|
|
109
|
+
return [...this.#votes.values()].sort((left, right) => left.from.localeCompare(right.from));
|
|
110
|
+
}
|
|
111
|
+
equivocation(validator) {
|
|
112
|
+
return this.#equivocations.get(validator);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export { BeaconEpochVotes, beaconEpochDigest, beaconEpochGroupPublicKey, beaconParticipants, canonicalBeaconEpochConfig, encodeBeaconEpochConfig, validateBeaconEpochConfig };
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { a as hkdf } from './hkdf-DhdhLAAv.js';
|
|
2
|
+
import { s as sha256 } from './weierstrass-C-jX_jly.js';
|
|
3
|
+
import { canonicalBeaconEpochConfig, beaconEpochDigest } from './beacon-epoch.js';
|
|
4
|
+
import './index-CvKt4UDE.js';
|
|
5
|
+
import './beacon.js';
|
|
6
|
+
import './quorum-S_qdgiAY.js';
|
|
7
|
+
|
|
8
|
+
const encoder = new TextEncoder();
|
|
9
|
+
const decoder = new TextDecoder();
|
|
10
|
+
const PRIVATE_PREFIX = "beacon/private/";
|
|
11
|
+
const PUBLIC_PREFIX = "beacon/public/";
|
|
12
|
+
const parse = (value) => {
|
|
13
|
+
const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
14
|
+
return JSON.parse(decoder.decode(bytes));
|
|
15
|
+
};
|
|
16
|
+
const stringify = (value) => encoder.encode(JSON.stringify(value, (_, item) => typeof item === "bigint" ? `${item}n` : item));
|
|
17
|
+
const revive = (value) => {
|
|
18
|
+
if (Array.isArray(value)) return value.map(revive);
|
|
19
|
+
if (value && typeof value === "object") {
|
|
20
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, revive(item)]));
|
|
21
|
+
}
|
|
22
|
+
if (typeof value === "string" && /^\d+n$/.test(value)) return BigInt(value.slice(0, -1));
|
|
23
|
+
return value;
|
|
24
|
+
};
|
|
25
|
+
const storageKey = (prefix, network, epoch) => `${prefix}${network}/${epoch}`;
|
|
26
|
+
const sealingContext = (network, validator) => encoder.encode(`leofcoin-beacon-storage-v1:${network}:${validator}`);
|
|
27
|
+
const sealingKey = async (network, validator, sign) => {
|
|
28
|
+
const context = sealingContext(network, validator);
|
|
29
|
+
const signature = await sign(sha256(context));
|
|
30
|
+
return hkdf(sha256, signature, sha256(context), context, 32);
|
|
31
|
+
};
|
|
32
|
+
const aesKey = (raw, usage) => globalThis.crypto.subtle.importKey("raw", raw, { name: "AES-GCM" }, false, [usage]);
|
|
33
|
+
const validateCertificate = (config, certificate) => {
|
|
34
|
+
const digest = beaconEpochDigest(config);
|
|
35
|
+
const voters = /* @__PURE__ */ new Set();
|
|
36
|
+
for (const vote of certificate) {
|
|
37
|
+
if (vote.epoch !== config.epoch || vote.configDigest !== digest || !config.validators.includes(vote.from) || !vote.signature || voters.has(vote.from))
|
|
38
|
+
throw new Error("invalid beacon epoch certificate");
|
|
39
|
+
voters.add(vote.from);
|
|
40
|
+
}
|
|
41
|
+
if (voters.size < config.threshold) throw new Error("beacon epoch certificate is below threshold");
|
|
42
|
+
};
|
|
43
|
+
const persistPublicBeaconEpoch = async (store, network, active) => {
|
|
44
|
+
const config = canonicalBeaconEpochConfig(active.config);
|
|
45
|
+
validateCertificate(config, active.certificate);
|
|
46
|
+
await store.put(
|
|
47
|
+
storageKey(PUBLIC_PREFIX, network, config.epoch),
|
|
48
|
+
stringify({ config, certificate: active.certificate })
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
const loadPublicBeaconEpoch = async (store, network, epoch) => {
|
|
52
|
+
const key = storageKey(PUBLIC_PREFIX, network, epoch);
|
|
53
|
+
if (!await store.has(key)) return void 0;
|
|
54
|
+
const active = revive(parse(await store.get(key)));
|
|
55
|
+
const config = canonicalBeaconEpochConfig(active.config);
|
|
56
|
+
validateCertificate(config, active.certificate);
|
|
57
|
+
return { config, certificate: active.certificate };
|
|
58
|
+
};
|
|
59
|
+
const persistPrivateBeaconEpoch = async (store, network, validator, record, sign, randomNonce = () => globalThis.crypto.getRandomValues(new Uint8Array(12))) => {
|
|
60
|
+
const nonce = randomNonce();
|
|
61
|
+
if (nonce.length !== 12) throw new Error("beacon storage nonce must contain 12 bytes");
|
|
62
|
+
const context = sealingContext(network, validator);
|
|
63
|
+
const key = await aesKey(await sealingKey(network, validator, sign), "encrypt");
|
|
64
|
+
const ciphertext = await globalThis.crypto.subtle.encrypt(
|
|
65
|
+
{ name: "AES-GCM", iv: nonce, additionalData: context, tagLength: 128 },
|
|
66
|
+
key,
|
|
67
|
+
stringify(record)
|
|
68
|
+
);
|
|
69
|
+
const sealed = { version: 1, nonce: [...nonce], ciphertext: [...new Uint8Array(ciphertext)] };
|
|
70
|
+
await store.put(storageKey(PRIVATE_PREFIX, network, record.epoch), stringify(sealed));
|
|
71
|
+
};
|
|
72
|
+
const loadPrivateBeaconEpoch = async (store, network, validator, epoch, sign) => {
|
|
73
|
+
const storage = storageKey(PRIVATE_PREFIX, network, epoch);
|
|
74
|
+
if (!await store.has(storage)) return void 0;
|
|
75
|
+
const sealed = parse(await store.get(storage));
|
|
76
|
+
if (sealed.version !== 1 || sealed.nonce.length !== 12 || sealed.ciphertext.length < 17) {
|
|
77
|
+
throw new Error("invalid sealed beacon epoch record");
|
|
78
|
+
}
|
|
79
|
+
const context = sealingContext(network, validator);
|
|
80
|
+
const key = await aesKey(await sealingKey(network, validator, sign), "decrypt");
|
|
81
|
+
const plaintext = await globalThis.crypto.subtle.decrypt(
|
|
82
|
+
{ name: "AES-GCM", iv: new Uint8Array(sealed.nonce), additionalData: context, tagLength: 128 },
|
|
83
|
+
key,
|
|
84
|
+
new Uint8Array(sealed.ciphertext)
|
|
85
|
+
);
|
|
86
|
+
const record = revive(parse(new Uint8Array(plaintext)));
|
|
87
|
+
if (record.epoch !== epoch) throw new Error("sealed beacon epoch does not match its storage key");
|
|
88
|
+
return record;
|
|
89
|
+
};
|
|
90
|
+
const clearBeaconEpochStorage = async (stores, network) => {
|
|
91
|
+
for (const store of stores) {
|
|
92
|
+
for (const key of await store.keys()) {
|
|
93
|
+
if (key.startsWith(`${PRIVATE_PREFIX}${network}/`) || key.startsWith(`${PUBLIC_PREFIX}${network}/`)) {
|
|
94
|
+
await store.delete(key);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
class BeaconLifecycle {
|
|
100
|
+
#active;
|
|
101
|
+
#pending = /* @__PURE__ */ new Map();
|
|
102
|
+
constructor(epochLength) {
|
|
103
|
+
if (!Number.isSafeInteger(epochLength) || epochLength < 2)
|
|
104
|
+
throw new RangeError("beacon epoch length must be at least 2");
|
|
105
|
+
this.epochLength = epochLength;
|
|
106
|
+
}
|
|
107
|
+
epochAtHeight(height) {
|
|
108
|
+
if (!Number.isSafeInteger(height) || height < 0) throw new RangeError("invalid block height");
|
|
109
|
+
return BigInt(Math.floor(height / this.epochLength));
|
|
110
|
+
}
|
|
111
|
+
ceremonyEpoch(height) {
|
|
112
|
+
return this.epochAtHeight(height) + 1n;
|
|
113
|
+
}
|
|
114
|
+
stage(active) {
|
|
115
|
+
const config = canonicalBeaconEpochConfig(active.config);
|
|
116
|
+
validateCertificate(config, active.certificate);
|
|
117
|
+
this.#pending.set(config.epoch, { config, certificate: [...active.certificate] });
|
|
118
|
+
}
|
|
119
|
+
restore(active) {
|
|
120
|
+
const config = canonicalBeaconEpochConfig(active.config);
|
|
121
|
+
validateCertificate(config, active.certificate);
|
|
122
|
+
this.#active = { config, certificate: [...active.certificate] };
|
|
123
|
+
}
|
|
124
|
+
advance(height) {
|
|
125
|
+
const epoch = this.epochAtHeight(height);
|
|
126
|
+
const pending = this.#pending.get(epoch);
|
|
127
|
+
if (pending) {
|
|
128
|
+
this.#active = pending;
|
|
129
|
+
this.#pending.delete(epoch);
|
|
130
|
+
}
|
|
131
|
+
return this.#active;
|
|
132
|
+
}
|
|
133
|
+
get active() {
|
|
134
|
+
return this.#active;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const storedEpochs = async (store, prefix, network) => {
|
|
138
|
+
const scoped = `${prefix}${network}/`;
|
|
139
|
+
return (await store.keys()).filter((key) => key.startsWith(scoped)).map((key) => key.slice(scoped.length)).filter((epoch) => /^\d+$/.test(epoch)).map(BigInt).sort((left, right) => left > right ? -1 : left < right ? 1 : 0);
|
|
140
|
+
};
|
|
141
|
+
const restoreBeaconLifecycle = async (publicStore, privateStore, network, validator, height, epochLength, sign) => {
|
|
142
|
+
const lifecycle = new BeaconLifecycle(epochLength);
|
|
143
|
+
const currentEpoch = lifecycle.epochAtHeight(height);
|
|
144
|
+
const epochs = await storedEpochs(publicStore, PUBLIC_PREFIX, network);
|
|
145
|
+
const activeEpoch = epochs.find((epoch) => epoch <= currentEpoch);
|
|
146
|
+
if (activeEpoch === void 0) return { lifecycle };
|
|
147
|
+
const active = await loadPublicBeaconEpoch(publicStore, network, activeEpoch);
|
|
148
|
+
if (!active) throw new Error("beacon public epoch disappeared during restore");
|
|
149
|
+
lifecycle.restore(active);
|
|
150
|
+
const privateEpoch = await loadPrivateBeaconEpoch(privateStore, network, validator, activeEpoch, sign);
|
|
151
|
+
return privateEpoch ? { lifecycle, privateEpoch } : { lifecycle };
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
export { BeaconLifecycle, clearBeaconEpochStorage, loadPrivateBeaconEpoch, loadPublicBeaconEpoch, persistPrivateBeaconEpoch, persistPublicBeaconEpoch, restoreBeaconLifecycle };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { f as fromBase58, t as toBase58 } from './index-CvKt4UDE.js';
|
|
2
|
+
import { beaconMessage, deriveBeaconPublicShare, verifyBeaconSignatureShare, reconstructBeaconSignature, verifyBeaconProof, beaconRandomness } from './beacon.js';
|
|
3
|
+
import { canonicalBeaconEpochConfig, beaconParticipants, beaconEpochGroupPublicKey } from './beacon-epoch.js';
|
|
4
|
+
import './weierstrass-C-jX_jly.js';
|
|
5
|
+
import './quorum-S_qdgiAY.js';
|
|
6
|
+
|
|
7
|
+
class BeaconRound {
|
|
8
|
+
#config;
|
|
9
|
+
#groupPublicKey;
|
|
10
|
+
#message;
|
|
11
|
+
#participants;
|
|
12
|
+
#publicShares = /* @__PURE__ */ new Map();
|
|
13
|
+
#shares = /* @__PURE__ */ new Map();
|
|
14
|
+
#equivocations = /* @__PURE__ */ new Map();
|
|
15
|
+
constructor(network, configInput, round, previousProof) {
|
|
16
|
+
if (round < 0n || previousProof.length !== 96) throw new Error("invalid beacon round input");
|
|
17
|
+
this.#config = canonicalBeaconEpochConfig(configInput);
|
|
18
|
+
this.epoch = this.#config.epoch;
|
|
19
|
+
this.round = round;
|
|
20
|
+
this.#participants = beaconParticipants(this.#config.validators);
|
|
21
|
+
this.#groupPublicKey = fromBase58(beaconEpochGroupPublicKey(this.#config));
|
|
22
|
+
this.#message = beaconMessage(network, this.epoch, round, previousProof);
|
|
23
|
+
const dealerCommitments = this.#config.dealers.map(
|
|
24
|
+
({ commitments }) => commitments.map((commitment) => fromBase58(commitment))
|
|
25
|
+
);
|
|
26
|
+
for (const [validator, participant] of this.#participants) {
|
|
27
|
+
this.#publicShares.set(validator, deriveBeaconPublicShare(participant, dealerCommitments));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
add(share) {
|
|
31
|
+
const expectedParticipant = this.#participants.get(share.from);
|
|
32
|
+
if (expectedParticipant === void 0 || expectedParticipant !== share.participant || !share.signature)
|
|
33
|
+
return "invalid";
|
|
34
|
+
const existing = this.#shares.get(share.from);
|
|
35
|
+
if (existing) {
|
|
36
|
+
if (existing.signature === share.signature) return "duplicate";
|
|
37
|
+
this.#equivocations.set(share.from, [existing, share]);
|
|
38
|
+
return "equivocation";
|
|
39
|
+
}
|
|
40
|
+
let signature;
|
|
41
|
+
try {
|
|
42
|
+
signature = fromBase58(share.signature);
|
|
43
|
+
} catch {
|
|
44
|
+
return "invalid";
|
|
45
|
+
}
|
|
46
|
+
if (!verifyBeaconSignatureShare(signature, this.#message, this.#publicShares.get(share.from))) return "invalid";
|
|
47
|
+
this.#shares.set(share.from, Object.freeze({ ...share }));
|
|
48
|
+
return "accepted";
|
|
49
|
+
}
|
|
50
|
+
get ready() {
|
|
51
|
+
return this.#shares.size >= this.#config.threshold;
|
|
52
|
+
}
|
|
53
|
+
finalize() {
|
|
54
|
+
if (!this.ready) throw new Error("beacon round has not reached threshold");
|
|
55
|
+
const shares = [...this.#shares.values()].map(({ participant, signature }) => ({
|
|
56
|
+
participant,
|
|
57
|
+
signature: fromBase58(signature)
|
|
58
|
+
}));
|
|
59
|
+
const proof = reconstructBeaconSignature(shares, this.#config.threshold);
|
|
60
|
+
if (!verifyBeaconProof(proof, this.#message, this.#groupPublicKey)) {
|
|
61
|
+
throw new Error("reconstructed beacon proof does not match the activated group key");
|
|
62
|
+
}
|
|
63
|
+
return Object.freeze({
|
|
64
|
+
epoch: this.epoch,
|
|
65
|
+
round: this.round,
|
|
66
|
+
proof: toBase58(proof),
|
|
67
|
+
randomness: toBase58(beaconRandomness(proof)),
|
|
68
|
+
signers: [...this.#shares.keys()].sort()
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
equivocation(validator) {
|
|
72
|
+
return this.#equivocations.get(validator);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export { BeaconRound };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { s as signTransaction, c as MultiWallet, d as createTransactionHash } from './index-D7FUx7Dd.js';
|
|
2
|
+
import { f as fromBase58 } from './index-CvKt4UDE.js';
|
|
3
|
+
import './proposal.proto-BcUgd885.js';
|
|
4
|
+
import 'crypto';
|
|
5
|
+
|
|
6
|
+
const validInteger = (value, minimum = 0n) => {
|
|
7
|
+
try {
|
|
8
|
+
return BigInt(value) >= minimum;
|
|
9
|
+
} catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
const validateBeaconCommitmentData = (message) => {
|
|
14
|
+
if (!validInteger(message.epoch) || !validInteger(message.participant, 1n) || !validInteger(message.threshold, 2n) || typeof message.from !== "string" || !message.from || !Array.isArray(message.commitments)) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
const threshold = Number(message.threshold);
|
|
18
|
+
if (!Number.isSafeInteger(threshold) || message.commitments.length !== threshold) return false;
|
|
19
|
+
return message.commitments.every((commitment) => {
|
|
20
|
+
if (typeof commitment !== "string" || commitment.length > 128) return false;
|
|
21
|
+
try {
|
|
22
|
+
return fromBase58(commitment).length === 48;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
const validateBeaconShareData = (message) => {
|
|
29
|
+
if (!validInteger(message.epoch) || !validInteger(message.round) || !validInteger(message.participant, 1n) || typeof message.from !== "string" || !message.from || typeof message.signatureShare !== "string" || message.signatureShare.length > 256) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
return fromBase58(message.signatureShare).length === 96;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
const validateBeaconActivationData = (message) => {
|
|
39
|
+
if (!validInteger(message.epoch) || typeof message.from !== "string" || !message.from || typeof message.configDigest !== "string" || message.configDigest.length > 64)
|
|
40
|
+
return false;
|
|
41
|
+
try {
|
|
42
|
+
return fromBase58(message.configDigest).length === 32;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
const commitmentSignableData = (validatorsAddress, message) => ({
|
|
48
|
+
from: String(message.from),
|
|
49
|
+
to: validatorsAddress,
|
|
50
|
+
method: "beacon:commitment",
|
|
51
|
+
params: [
|
|
52
|
+
String(message.epoch),
|
|
53
|
+
String(message.threshold),
|
|
54
|
+
String(message.participant),
|
|
55
|
+
...message.commitments.map(String)
|
|
56
|
+
],
|
|
57
|
+
timestamp: 0
|
|
58
|
+
});
|
|
59
|
+
const shareSignableData = (validatorsAddress, message) => ({
|
|
60
|
+
from: String(message.from),
|
|
61
|
+
to: validatorsAddress,
|
|
62
|
+
method: "beacon:share",
|
|
63
|
+
params: [String(message.epoch), String(message.round), String(message.participant), String(message.signatureShare)],
|
|
64
|
+
timestamp: 0
|
|
65
|
+
});
|
|
66
|
+
const activationSignableData = (validatorsAddress, message) => ({
|
|
67
|
+
from: String(message.from),
|
|
68
|
+
to: validatorsAddress,
|
|
69
|
+
method: "beacon:activation",
|
|
70
|
+
params: [String(message.epoch), String(message.configDigest)],
|
|
71
|
+
timestamp: 0
|
|
72
|
+
});
|
|
73
|
+
const signBeaconCommitmentMessage = async (validatorsAddress, message, identity) => {
|
|
74
|
+
if (!validateBeaconCommitmentData(message)) throw new Error("invalid beacon commitment");
|
|
75
|
+
return (await signTransaction(commitmentSignableData(validatorsAddress, message), identity)).signature;
|
|
76
|
+
};
|
|
77
|
+
const signBeaconShareMessage = async (validatorsAddress, message, identity) => {
|
|
78
|
+
if (!validateBeaconShareData(message)) throw new Error("invalid beacon signature share");
|
|
79
|
+
return (await signTransaction(shareSignableData(validatorsAddress, message), identity)).signature;
|
|
80
|
+
};
|
|
81
|
+
const signBeaconActivationMessage = async (validatorsAddress, message, identity) => {
|
|
82
|
+
if (!validateBeaconActivationData(message)) throw new Error("invalid beacon activation");
|
|
83
|
+
return (await signTransaction(activationSignableData(validatorsAddress, message), identity)).signature;
|
|
84
|
+
};
|
|
85
|
+
const verifyIdentitySignature = async (message, signable, network) => {
|
|
86
|
+
if (typeof message.signature !== "string" || !message.signature) return false;
|
|
87
|
+
try {
|
|
88
|
+
const verifier = new MultiWallet(network);
|
|
89
|
+
await verifier.fromAddress(message.from, null, network);
|
|
90
|
+
return verifier.verify(fromBase58(message.signature), await createTransactionHash(signable));
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
const verifyBeaconCommitmentMessage = async (validatorsAddress, message, network) => validateBeaconCommitmentData(message) && verifyIdentitySignature(message, commitmentSignableData(validatorsAddress, message), network);
|
|
96
|
+
const verifyBeaconShareMessage = async (validatorsAddress, message, network) => validateBeaconShareData(message) && verifyIdentitySignature(message, shareSignableData(validatorsAddress, message), network);
|
|
97
|
+
const verifyBeaconActivationMessage = async (validatorsAddress, message, network) => validateBeaconActivationData(message) && verifyIdentitySignature(message, activationSignableData(validatorsAddress, message), network);
|
|
98
|
+
|
|
99
|
+
export { signBeaconActivationMessage, signBeaconCommitmentMessage, signBeaconShareMessage, validateBeaconActivationData, validateBeaconCommitmentData, validateBeaconShareData, verifyBeaconActivationMessage, verifyBeaconCommitmentMessage, verifyBeaconShareMessage };
|