@leofcoin/chain 1.10.10 → 1.10.12
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 +110 -5053
- 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 +7052 -78
- package/exports/browser/workers/{worker-CZqErLI7-BxofVJAn.js → worker-DMCj1e6z-CTYVa2yX.js} +30 -1
- package/exports/chain.js +91 -47
- 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 +7052 -78
- package/exports/workers/{worker-CZqErLI7-BxofVJAn.js → worker-DMCj1e6z-CTYVa2yX.js} +30 -1
- package/package.json +35 -8
- 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,68 @@
|
|
|
1
|
+
import { secp256k1 } from '@noble/curves/secp256k1';
|
|
2
|
+
import { hkdf } from '@noble/hashes/hkdf';
|
|
3
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
4
|
+
|
|
5
|
+
const encoder = new TextEncoder();
|
|
6
|
+
const SCALAR_BYTES = 32;
|
|
7
|
+
const context = (network, epoch, sender, recipient) => {
|
|
8
|
+
if (!network || epoch < 0n || !sender || !recipient || sender === recipient) {
|
|
9
|
+
throw new TypeError("invalid beacon share envelope context");
|
|
10
|
+
}
|
|
11
|
+
return encoder.encode(`leofcoin-beacon-share-v1:${network}:${epoch}:${sender}:${recipient}`);
|
|
12
|
+
};
|
|
13
|
+
const scalarBytes = (share) => {
|
|
14
|
+
if (share <= 0n || share >= secp256k1.CURVE.n) throw new RangeError("invalid beacon secret share");
|
|
15
|
+
const output = new Uint8Array(SCALAR_BYTES);
|
|
16
|
+
let value = share;
|
|
17
|
+
for (let index = output.length - 1; index >= 0; index -= 1) {
|
|
18
|
+
output[index] = Number(value & 0xffn);
|
|
19
|
+
value >>= 8n;
|
|
20
|
+
}
|
|
21
|
+
return output;
|
|
22
|
+
};
|
|
23
|
+
const bytesScalar = (bytes) => {
|
|
24
|
+
if (bytes.length !== SCALAR_BYTES) throw new Error("invalid decrypted beacon share length");
|
|
25
|
+
let value = 0n;
|
|
26
|
+
for (const byte of bytes) value = value << 8n | BigInt(byte);
|
|
27
|
+
if (value <= 0n || value >= secp256k1.CURVE.n) throw new Error("invalid decrypted beacon share");
|
|
28
|
+
return value;
|
|
29
|
+
};
|
|
30
|
+
const encryptionKey = (privateKey, publicKey, aad) => {
|
|
31
|
+
if (!secp256k1.utils.isValidPrivateKey(privateKey)) throw new Error("invalid beacon encryption private key");
|
|
32
|
+
const sharedSecret = secp256k1.getSharedSecret(privateKey, publicKey, true);
|
|
33
|
+
return hkdf(sha256, sharedSecret, sha256(aad), aad, 32);
|
|
34
|
+
};
|
|
35
|
+
const importAesKey = (key, usage) => globalThis.crypto.subtle.importKey("raw", key, { name: "AES-GCM" }, false, [usage]);
|
|
36
|
+
const createBeaconEncryptionKey = (randomPrivateKey = () => secp256k1.utils.randomPrivateKey()) => {
|
|
37
|
+
const privateKey = randomPrivateKey();
|
|
38
|
+
if (!secp256k1.utils.isValidPrivateKey(privateKey)) throw new Error("invalid generated beacon encryption key");
|
|
39
|
+
return { privateKey, publicKey: secp256k1.getPublicKey(privateKey, true) };
|
|
40
|
+
};
|
|
41
|
+
const encryptBeaconShare = async (share, recipientPublicKey, network, epoch, sender, recipient, randomPrivateKey, randomNonce = () => globalThis.crypto.getRandomValues(new Uint8Array(12))) => {
|
|
42
|
+
const aad = context(network, epoch, sender, recipient);
|
|
43
|
+
const ephemeral = createBeaconEncryptionKey(randomPrivateKey);
|
|
44
|
+
const nonce = randomNonce();
|
|
45
|
+
if (nonce.length !== 12) throw new Error("beacon share nonce must contain 12 bytes");
|
|
46
|
+
const key = await importAesKey(encryptionKey(ephemeral.privateKey, recipientPublicKey, aad), "encrypt");
|
|
47
|
+
const ciphertext = await globalThis.crypto.subtle.encrypt(
|
|
48
|
+
{ name: "AES-GCM", iv: nonce, additionalData: aad, tagLength: 128 },
|
|
49
|
+
key,
|
|
50
|
+
scalarBytes(share)
|
|
51
|
+
);
|
|
52
|
+
return { ciphertext: new Uint8Array(ciphertext), ephemeralPublicKey: ephemeral.publicKey, nonce };
|
|
53
|
+
};
|
|
54
|
+
const decryptBeaconShare = async (envelope, recipientPrivateKey, network, epoch, sender, recipient) => {
|
|
55
|
+
if (envelope.ephemeralPublicKey.length !== 33 || envelope.nonce.length !== 12 || envelope.ciphertext.length !== 48) {
|
|
56
|
+
throw new Error("invalid beacon share envelope");
|
|
57
|
+
}
|
|
58
|
+
const aad = context(network, epoch, sender, recipient);
|
|
59
|
+
const key = await importAesKey(encryptionKey(recipientPrivateKey, envelope.ephemeralPublicKey, aad), "decrypt");
|
|
60
|
+
const plaintext = await globalThis.crypto.subtle.decrypt(
|
|
61
|
+
{ name: "AES-GCM", iv: envelope.nonce, additionalData: aad, tagLength: 128 },
|
|
62
|
+
key,
|
|
63
|
+
envelope.ciphertext
|
|
64
|
+
);
|
|
65
|
+
return bytesScalar(new Uint8Array(plaintext));
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export { createBeaconEncryptionKey, decryptBeaconShare, encryptBeaconShare };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
2
|
+
import { toBase58, fromBase58 } from '@vandeurenglenn/typed-array-utils';
|
|
3
|
+
import { deriveBeaconGroupPublicKey } from './beacon.js';
|
|
4
|
+
import { q as quorumThreshold } from './quorum-S_qdgiAY.js';
|
|
5
|
+
import '@noble/curves/bls12-381';
|
|
6
|
+
|
|
7
|
+
const encoder = new TextEncoder();
|
|
8
|
+
const lengthPrefix = (bytes) => {
|
|
9
|
+
if (bytes.length > 65535) throw new Error("beacon config field is too large");
|
|
10
|
+
const output = new Uint8Array(bytes.length + 2);
|
|
11
|
+
new DataView(output.buffer).setUint16(0, bytes.length, false);
|
|
12
|
+
output.set(bytes, 2);
|
|
13
|
+
return output;
|
|
14
|
+
};
|
|
15
|
+
const concatenate = (parts) => {
|
|
16
|
+
const output = new Uint8Array(parts.reduce((size, part) => size + part.length, 0));
|
|
17
|
+
let offset = 0;
|
|
18
|
+
for (const part of parts) {
|
|
19
|
+
output.set(part, offset);
|
|
20
|
+
offset += part.length;
|
|
21
|
+
}
|
|
22
|
+
return output;
|
|
23
|
+
};
|
|
24
|
+
const uint64 = (value) => {
|
|
25
|
+
if (value < 0n || value > 0xffffffffffffffffn) throw new RangeError("beacon epoch is outside uint64");
|
|
26
|
+
const bytes = new Uint8Array(8);
|
|
27
|
+
new DataView(bytes.buffer).setBigUint64(0, value, false);
|
|
28
|
+
return bytes;
|
|
29
|
+
};
|
|
30
|
+
const beaconParticipants = (validators) => {
|
|
31
|
+
const sorted = [...validators].sort();
|
|
32
|
+
if (sorted.length < 2 || new Set(sorted).size !== sorted.length || sorted.some((validator) => !validator)) {
|
|
33
|
+
throw new Error("beacon requires at least two unique validators");
|
|
34
|
+
}
|
|
35
|
+
return new Map(sorted.map((validator, index) => [validator, BigInt(index + 1)]));
|
|
36
|
+
};
|
|
37
|
+
const validateBeaconEpochConfig = (config) => {
|
|
38
|
+
try {
|
|
39
|
+
if (config.epoch < 0n) return false;
|
|
40
|
+
const participants = beaconParticipants(config.validators);
|
|
41
|
+
const validators = [...participants.keys()];
|
|
42
|
+
if (config.threshold !== quorumThreshold(validators.length) || config.dealers.length < config.threshold)
|
|
43
|
+
return false;
|
|
44
|
+
const dealers = [...config.dealers].sort((left, right) => left.validator.localeCompare(right.validator));
|
|
45
|
+
if (new Set(dealers.map(({ validator }) => validator)).size !== dealers.length) return false;
|
|
46
|
+
return dealers.every(({ validator, commitments }) => {
|
|
47
|
+
if (!participants.has(validator) || commitments.length !== config.threshold) return false;
|
|
48
|
+
return commitments.every((commitment) => fromBase58(commitment).length === 48);
|
|
49
|
+
});
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const canonicalBeaconEpochConfig = (config) => {
|
|
55
|
+
if (!validateBeaconEpochConfig(config)) throw new Error("invalid beacon epoch config");
|
|
56
|
+
return {
|
|
57
|
+
epoch: config.epoch,
|
|
58
|
+
threshold: config.threshold,
|
|
59
|
+
validators: [...config.validators].sort(),
|
|
60
|
+
dealers: [...config.dealers].map((dealer) => ({ validator: dealer.validator, commitments: [...dealer.commitments] })).sort((left, right) => left.validator.localeCompare(right.validator))
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
const encodeBeaconEpochConfig = (input) => {
|
|
64
|
+
const config = canonicalBeaconEpochConfig(input);
|
|
65
|
+
const parts = [encoder.encode("leofcoin-beacon-config-v1"), uint64(config.epoch), uint64(BigInt(config.threshold))];
|
|
66
|
+
parts.push(uint64(BigInt(config.validators.length)));
|
|
67
|
+
for (const validator of config.validators) parts.push(lengthPrefix(encoder.encode(validator)));
|
|
68
|
+
parts.push(uint64(BigInt(config.dealers.length)));
|
|
69
|
+
for (const dealer of config.dealers) {
|
|
70
|
+
parts.push(lengthPrefix(encoder.encode(dealer.validator)));
|
|
71
|
+
for (const commitment of dealer.commitments) parts.push(lengthPrefix(fromBase58(commitment)));
|
|
72
|
+
}
|
|
73
|
+
return concatenate(parts);
|
|
74
|
+
};
|
|
75
|
+
const beaconEpochDigest = (config) => toBase58(sha256(encodeBeaconEpochConfig(config)));
|
|
76
|
+
const beaconEpochGroupPublicKey = (config) => {
|
|
77
|
+
const canonical = canonicalBeaconEpochConfig(config);
|
|
78
|
+
return toBase58(
|
|
79
|
+
deriveBeaconGroupPublicKey(
|
|
80
|
+
canonical.dealers.map(({ commitments }) => commitments.map((commitment) => fromBase58(commitment)))
|
|
81
|
+
)
|
|
82
|
+
);
|
|
83
|
+
};
|
|
84
|
+
class BeaconEpochVotes {
|
|
85
|
+
#votes = /* @__PURE__ */ new Map();
|
|
86
|
+
#equivocations = /* @__PURE__ */ new Map();
|
|
87
|
+
constructor(config) {
|
|
88
|
+
this.config = canonicalBeaconEpochConfig(config);
|
|
89
|
+
this.configDigest = beaconEpochDigest(this.config);
|
|
90
|
+
}
|
|
91
|
+
add(vote) {
|
|
92
|
+
if (vote.epoch !== this.config.epoch || !this.config.validators.includes(vote.from) || !vote.signature || vote.configDigest !== this.configDigest) {
|
|
93
|
+
const existing2 = this.#votes.get(vote.from);
|
|
94
|
+
if (existing2 && vote.epoch === existing2.epoch && vote.configDigest !== existing2.configDigest) {
|
|
95
|
+
this.#equivocations.set(vote.from, [existing2, vote]);
|
|
96
|
+
return "equivocation";
|
|
97
|
+
}
|
|
98
|
+
return "invalid";
|
|
99
|
+
}
|
|
100
|
+
const existing = this.#votes.get(vote.from);
|
|
101
|
+
if (existing) return existing.signature === vote.signature ? "duplicate" : "equivocation";
|
|
102
|
+
this.#votes.set(vote.from, Object.freeze({ ...vote }));
|
|
103
|
+
return "accepted";
|
|
104
|
+
}
|
|
105
|
+
get ready() {
|
|
106
|
+
return this.#votes.size >= this.config.threshold;
|
|
107
|
+
}
|
|
108
|
+
certificate() {
|
|
109
|
+
if (!this.ready) throw new Error("beacon epoch does not have a quorum certificate");
|
|
110
|
+
return [...this.#votes.values()].sort((left, right) => left.from.localeCompare(right.from));
|
|
111
|
+
}
|
|
112
|
+
equivocation(validator) {
|
|
113
|
+
return this.#equivocations.get(validator);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export { BeaconEpochVotes, beaconEpochDigest, beaconEpochGroupPublicKey, beaconParticipants, canonicalBeaconEpochConfig, encodeBeaconEpochConfig, validateBeaconEpochConfig };
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { hkdf } from '@noble/hashes/hkdf';
|
|
2
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
3
|
+
import { canonicalBeaconEpochConfig, beaconEpochDigest } from './beacon-epoch.js';
|
|
4
|
+
import '@vandeurenglenn/typed-array-utils';
|
|
5
|
+
import './beacon.js';
|
|
6
|
+
import '@noble/curves/bls12-381';
|
|
7
|
+
import './quorum-S_qdgiAY.js';
|
|
8
|
+
|
|
9
|
+
const encoder = new TextEncoder();
|
|
10
|
+
const decoder = new TextDecoder();
|
|
11
|
+
const PRIVATE_PREFIX = "beacon/private/";
|
|
12
|
+
const PUBLIC_PREFIX = "beacon/public/";
|
|
13
|
+
const parse = (value) => {
|
|
14
|
+
const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
15
|
+
return JSON.parse(decoder.decode(bytes));
|
|
16
|
+
};
|
|
17
|
+
const stringify = (value) => encoder.encode(JSON.stringify(value, (_, item) => typeof item === "bigint" ? `${item}n` : item));
|
|
18
|
+
const revive = (value) => {
|
|
19
|
+
if (Array.isArray(value)) return value.map(revive);
|
|
20
|
+
if (value && typeof value === "object") {
|
|
21
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, revive(item)]));
|
|
22
|
+
}
|
|
23
|
+
if (typeof value === "string" && /^\d+n$/.test(value)) return BigInt(value.slice(0, -1));
|
|
24
|
+
return value;
|
|
25
|
+
};
|
|
26
|
+
const storageKey = (prefix, network, epoch) => `${prefix}${network}/${epoch}`;
|
|
27
|
+
const sealingContext = (network, validator) => encoder.encode(`leofcoin-beacon-storage-v1:${network}:${validator}`);
|
|
28
|
+
const sealingKey = async (network, validator, sign) => {
|
|
29
|
+
const context = sealingContext(network, validator);
|
|
30
|
+
const signature = await sign(sha256(context));
|
|
31
|
+
return hkdf(sha256, signature, sha256(context), context, 32);
|
|
32
|
+
};
|
|
33
|
+
const aesKey = (raw, usage) => globalThis.crypto.subtle.importKey("raw", raw, { name: "AES-GCM" }, false, [usage]);
|
|
34
|
+
const validateCertificate = (config, certificate) => {
|
|
35
|
+
const digest = beaconEpochDigest(config);
|
|
36
|
+
const voters = /* @__PURE__ */ new Set();
|
|
37
|
+
for (const vote of certificate) {
|
|
38
|
+
if (vote.epoch !== config.epoch || vote.configDigest !== digest || !config.validators.includes(vote.from) || !vote.signature || voters.has(vote.from))
|
|
39
|
+
throw new Error("invalid beacon epoch certificate");
|
|
40
|
+
voters.add(vote.from);
|
|
41
|
+
}
|
|
42
|
+
if (voters.size < config.threshold) throw new Error("beacon epoch certificate is below threshold");
|
|
43
|
+
};
|
|
44
|
+
const persistPublicBeaconEpoch = async (store, network, active) => {
|
|
45
|
+
const config = canonicalBeaconEpochConfig(active.config);
|
|
46
|
+
validateCertificate(config, active.certificate);
|
|
47
|
+
await store.put(
|
|
48
|
+
storageKey(PUBLIC_PREFIX, network, config.epoch),
|
|
49
|
+
stringify({ config, certificate: active.certificate })
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
const loadPublicBeaconEpoch = async (store, network, epoch) => {
|
|
53
|
+
const key = storageKey(PUBLIC_PREFIX, network, epoch);
|
|
54
|
+
if (!await store.has(key)) return void 0;
|
|
55
|
+
const active = revive(parse(await store.get(key)));
|
|
56
|
+
const config = canonicalBeaconEpochConfig(active.config);
|
|
57
|
+
validateCertificate(config, active.certificate);
|
|
58
|
+
return { config, certificate: active.certificate };
|
|
59
|
+
};
|
|
60
|
+
const persistPrivateBeaconEpoch = async (store, network, validator, record, sign, randomNonce = () => globalThis.crypto.getRandomValues(new Uint8Array(12))) => {
|
|
61
|
+
const nonce = randomNonce();
|
|
62
|
+
if (nonce.length !== 12) throw new Error("beacon storage nonce must contain 12 bytes");
|
|
63
|
+
const context = sealingContext(network, validator);
|
|
64
|
+
const key = await aesKey(await sealingKey(network, validator, sign), "encrypt");
|
|
65
|
+
const ciphertext = await globalThis.crypto.subtle.encrypt(
|
|
66
|
+
{ name: "AES-GCM", iv: nonce, additionalData: context, tagLength: 128 },
|
|
67
|
+
key,
|
|
68
|
+
stringify(record)
|
|
69
|
+
);
|
|
70
|
+
const sealed = { version: 1, nonce: [...nonce], ciphertext: [...new Uint8Array(ciphertext)] };
|
|
71
|
+
await store.put(storageKey(PRIVATE_PREFIX, network, record.epoch), stringify(sealed));
|
|
72
|
+
};
|
|
73
|
+
const loadPrivateBeaconEpoch = async (store, network, validator, epoch, sign) => {
|
|
74
|
+
const storage = storageKey(PRIVATE_PREFIX, network, epoch);
|
|
75
|
+
if (!await store.has(storage)) return void 0;
|
|
76
|
+
const sealed = parse(await store.get(storage));
|
|
77
|
+
if (sealed.version !== 1 || sealed.nonce.length !== 12 || sealed.ciphertext.length < 17) {
|
|
78
|
+
throw new Error("invalid sealed beacon epoch record");
|
|
79
|
+
}
|
|
80
|
+
const context = sealingContext(network, validator);
|
|
81
|
+
const key = await aesKey(await sealingKey(network, validator, sign), "decrypt");
|
|
82
|
+
const plaintext = await globalThis.crypto.subtle.decrypt(
|
|
83
|
+
{ name: "AES-GCM", iv: new Uint8Array(sealed.nonce), additionalData: context, tagLength: 128 },
|
|
84
|
+
key,
|
|
85
|
+
new Uint8Array(sealed.ciphertext)
|
|
86
|
+
);
|
|
87
|
+
const record = revive(parse(new Uint8Array(plaintext)));
|
|
88
|
+
if (record.epoch !== epoch) throw new Error("sealed beacon epoch does not match its storage key");
|
|
89
|
+
return record;
|
|
90
|
+
};
|
|
91
|
+
const clearBeaconEpochStorage = async (stores, network) => {
|
|
92
|
+
for (const store of stores) {
|
|
93
|
+
for (const key of await store.keys()) {
|
|
94
|
+
if (key.startsWith(`${PRIVATE_PREFIX}${network}/`) || key.startsWith(`${PUBLIC_PREFIX}${network}/`)) {
|
|
95
|
+
await store.delete(key);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
class BeaconLifecycle {
|
|
101
|
+
#active;
|
|
102
|
+
#pending = /* @__PURE__ */ new Map();
|
|
103
|
+
constructor(epochLength) {
|
|
104
|
+
if (!Number.isSafeInteger(epochLength) || epochLength < 2)
|
|
105
|
+
throw new RangeError("beacon epoch length must be at least 2");
|
|
106
|
+
this.epochLength = epochLength;
|
|
107
|
+
}
|
|
108
|
+
epochAtHeight(height) {
|
|
109
|
+
if (!Number.isSafeInteger(height) || height < 0) throw new RangeError("invalid block height");
|
|
110
|
+
return BigInt(Math.floor(height / this.epochLength));
|
|
111
|
+
}
|
|
112
|
+
ceremonyEpoch(height) {
|
|
113
|
+
return this.epochAtHeight(height) + 1n;
|
|
114
|
+
}
|
|
115
|
+
stage(active) {
|
|
116
|
+
const config = canonicalBeaconEpochConfig(active.config);
|
|
117
|
+
validateCertificate(config, active.certificate);
|
|
118
|
+
this.#pending.set(config.epoch, { config, certificate: [...active.certificate] });
|
|
119
|
+
}
|
|
120
|
+
restore(active) {
|
|
121
|
+
const config = canonicalBeaconEpochConfig(active.config);
|
|
122
|
+
validateCertificate(config, active.certificate);
|
|
123
|
+
this.#active = { config, certificate: [...active.certificate] };
|
|
124
|
+
}
|
|
125
|
+
advance(height) {
|
|
126
|
+
const epoch = this.epochAtHeight(height);
|
|
127
|
+
const pending = this.#pending.get(epoch);
|
|
128
|
+
if (pending) {
|
|
129
|
+
this.#active = pending;
|
|
130
|
+
this.#pending.delete(epoch);
|
|
131
|
+
}
|
|
132
|
+
return this.#active;
|
|
133
|
+
}
|
|
134
|
+
get active() {
|
|
135
|
+
return this.#active;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const storedEpochs = async (store, prefix, network) => {
|
|
139
|
+
const scoped = `${prefix}${network}/`;
|
|
140
|
+
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);
|
|
141
|
+
};
|
|
142
|
+
const restoreBeaconLifecycle = async (publicStore, privateStore, network, validator, height, epochLength, sign) => {
|
|
143
|
+
const lifecycle = new BeaconLifecycle(epochLength);
|
|
144
|
+
const currentEpoch = lifecycle.epochAtHeight(height);
|
|
145
|
+
const epochs = await storedEpochs(publicStore, PUBLIC_PREFIX, network);
|
|
146
|
+
const activeEpoch = epochs.find((epoch) => epoch <= currentEpoch);
|
|
147
|
+
if (activeEpoch === void 0) return { lifecycle };
|
|
148
|
+
const active = await loadPublicBeaconEpoch(publicStore, network, activeEpoch);
|
|
149
|
+
if (!active) throw new Error("beacon public epoch disappeared during restore");
|
|
150
|
+
lifecycle.restore(active);
|
|
151
|
+
const privateEpoch = await loadPrivateBeaconEpoch(privateStore, network, validator, activeEpoch, sign);
|
|
152
|
+
return privateEpoch ? { lifecycle, privateEpoch } : { lifecycle };
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export { BeaconLifecycle, clearBeaconEpochStorage, loadPrivateBeaconEpoch, loadPublicBeaconEpoch, persistPrivateBeaconEpoch, persistPublicBeaconEpoch, restoreBeaconLifecycle };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { fromBase58, toBase58 } from '@vandeurenglenn/typed-array-utils';
|
|
2
|
+
import { beaconMessage, deriveBeaconPublicShare, verifyBeaconSignatureShare, reconstructBeaconSignature, verifyBeaconProof, beaconRandomness } from './beacon.js';
|
|
3
|
+
import { canonicalBeaconEpochConfig, beaconParticipants, beaconEpochGroupPublicKey } from './beacon-epoch.js';
|
|
4
|
+
import '@noble/curves/bls12-381';
|
|
5
|
+
import '@noble/hashes/sha256';
|
|
6
|
+
import './quorum-S_qdgiAY.js';
|
|
7
|
+
|
|
8
|
+
class BeaconRound {
|
|
9
|
+
#config;
|
|
10
|
+
#groupPublicKey;
|
|
11
|
+
#message;
|
|
12
|
+
#participants;
|
|
13
|
+
#publicShares = /* @__PURE__ */ new Map();
|
|
14
|
+
#shares = /* @__PURE__ */ new Map();
|
|
15
|
+
#equivocations = /* @__PURE__ */ new Map();
|
|
16
|
+
constructor(network, configInput, round, previousProof) {
|
|
17
|
+
if (round < 0n || previousProof.length !== 96) throw new Error("invalid beacon round input");
|
|
18
|
+
this.#config = canonicalBeaconEpochConfig(configInput);
|
|
19
|
+
this.epoch = this.#config.epoch;
|
|
20
|
+
this.round = round;
|
|
21
|
+
this.#participants = beaconParticipants(this.#config.validators);
|
|
22
|
+
this.#groupPublicKey = fromBase58(beaconEpochGroupPublicKey(this.#config));
|
|
23
|
+
this.#message = beaconMessage(network, this.epoch, round, previousProof);
|
|
24
|
+
const dealerCommitments = this.#config.dealers.map(
|
|
25
|
+
({ commitments }) => commitments.map((commitment) => fromBase58(commitment))
|
|
26
|
+
);
|
|
27
|
+
for (const [validator, participant] of this.#participants) {
|
|
28
|
+
this.#publicShares.set(validator, deriveBeaconPublicShare(participant, dealerCommitments));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
add(share) {
|
|
32
|
+
const expectedParticipant = this.#participants.get(share.from);
|
|
33
|
+
if (expectedParticipant === void 0 || expectedParticipant !== share.participant || !share.signature)
|
|
34
|
+
return "invalid";
|
|
35
|
+
const existing = this.#shares.get(share.from);
|
|
36
|
+
if (existing) {
|
|
37
|
+
if (existing.signature === share.signature) return "duplicate";
|
|
38
|
+
this.#equivocations.set(share.from, [existing, share]);
|
|
39
|
+
return "equivocation";
|
|
40
|
+
}
|
|
41
|
+
let signature;
|
|
42
|
+
try {
|
|
43
|
+
signature = fromBase58(share.signature);
|
|
44
|
+
} catch {
|
|
45
|
+
return "invalid";
|
|
46
|
+
}
|
|
47
|
+
if (!verifyBeaconSignatureShare(signature, this.#message, this.#publicShares.get(share.from))) return "invalid";
|
|
48
|
+
this.#shares.set(share.from, Object.freeze({ ...share }));
|
|
49
|
+
return "accepted";
|
|
50
|
+
}
|
|
51
|
+
get ready() {
|
|
52
|
+
return this.#shares.size >= this.#config.threshold;
|
|
53
|
+
}
|
|
54
|
+
finalize() {
|
|
55
|
+
if (!this.ready) throw new Error("beacon round has not reached threshold");
|
|
56
|
+
const shares = [...this.#shares.values()].map(({ participant, signature }) => ({
|
|
57
|
+
participant,
|
|
58
|
+
signature: fromBase58(signature)
|
|
59
|
+
}));
|
|
60
|
+
const proof = reconstructBeaconSignature(shares, this.#config.threshold);
|
|
61
|
+
if (!verifyBeaconProof(proof, this.#message, this.#groupPublicKey)) {
|
|
62
|
+
throw new Error("reconstructed beacon proof does not match the activated group key");
|
|
63
|
+
}
|
|
64
|
+
return Object.freeze({
|
|
65
|
+
epoch: this.epoch,
|
|
66
|
+
round: this.round,
|
|
67
|
+
proof: toBase58(proof),
|
|
68
|
+
randomness: toBase58(beaconRandomness(proof)),
|
|
69
|
+
signers: [...this.#shares.keys()].sort()
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
equivocation(validator) {
|
|
73
|
+
return this.#equivocations.get(validator);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export { BeaconRound };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { signTransaction, createTransactionHash } from '@leofcoin/lib';
|
|
2
|
+
import MultiWallet from '@leofcoin/multi-wallet';
|
|
3
|
+
import { fromBase58 } from '@vandeurenglenn/typed-array-utils';
|
|
4
|
+
|
|
5
|
+
const validInteger = (value, minimum = 0n) => {
|
|
6
|
+
try {
|
|
7
|
+
return BigInt(value) >= minimum;
|
|
8
|
+
} catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
const validateBeaconCommitmentData = (message) => {
|
|
13
|
+
if (!validInteger(message.epoch) || !validInteger(message.participant, 1n) || !validInteger(message.threshold, 2n) || typeof message.from !== "string" || !message.from || !Array.isArray(message.commitments)) {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
const threshold = Number(message.threshold);
|
|
17
|
+
if (!Number.isSafeInteger(threshold) || message.commitments.length !== threshold) return false;
|
|
18
|
+
return message.commitments.every((commitment) => {
|
|
19
|
+
if (typeof commitment !== "string" || commitment.length > 128) return false;
|
|
20
|
+
try {
|
|
21
|
+
return fromBase58(commitment).length === 48;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
const validateBeaconShareData = (message) => {
|
|
28
|
+
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) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
return fromBase58(message.signatureShare).length === 96;
|
|
33
|
+
} catch {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const validateBeaconActivationData = (message) => {
|
|
38
|
+
if (!validInteger(message.epoch) || typeof message.from !== "string" || !message.from || typeof message.configDigest !== "string" || message.configDigest.length > 64)
|
|
39
|
+
return false;
|
|
40
|
+
try {
|
|
41
|
+
return fromBase58(message.configDigest).length === 32;
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const commitmentSignableData = (validatorsAddress, message) => ({
|
|
47
|
+
from: String(message.from),
|
|
48
|
+
to: validatorsAddress,
|
|
49
|
+
method: "beacon:commitment",
|
|
50
|
+
params: [
|
|
51
|
+
String(message.epoch),
|
|
52
|
+
String(message.threshold),
|
|
53
|
+
String(message.participant),
|
|
54
|
+
...message.commitments.map(String)
|
|
55
|
+
],
|
|
56
|
+
timestamp: 0
|
|
57
|
+
});
|
|
58
|
+
const shareSignableData = (validatorsAddress, message) => ({
|
|
59
|
+
from: String(message.from),
|
|
60
|
+
to: validatorsAddress,
|
|
61
|
+
method: "beacon:share",
|
|
62
|
+
params: [String(message.epoch), String(message.round), String(message.participant), String(message.signatureShare)],
|
|
63
|
+
timestamp: 0
|
|
64
|
+
});
|
|
65
|
+
const activationSignableData = (validatorsAddress, message) => ({
|
|
66
|
+
from: String(message.from),
|
|
67
|
+
to: validatorsAddress,
|
|
68
|
+
method: "beacon:activation",
|
|
69
|
+
params: [String(message.epoch), String(message.configDigest)],
|
|
70
|
+
timestamp: 0
|
|
71
|
+
});
|
|
72
|
+
const signBeaconCommitmentMessage = async (validatorsAddress, message, identity) => {
|
|
73
|
+
if (!validateBeaconCommitmentData(message)) throw new Error("invalid beacon commitment");
|
|
74
|
+
return (await signTransaction(commitmentSignableData(validatorsAddress, message), identity)).signature;
|
|
75
|
+
};
|
|
76
|
+
const signBeaconShareMessage = async (validatorsAddress, message, identity) => {
|
|
77
|
+
if (!validateBeaconShareData(message)) throw new Error("invalid beacon signature share");
|
|
78
|
+
return (await signTransaction(shareSignableData(validatorsAddress, message), identity)).signature;
|
|
79
|
+
};
|
|
80
|
+
const signBeaconActivationMessage = async (validatorsAddress, message, identity) => {
|
|
81
|
+
if (!validateBeaconActivationData(message)) throw new Error("invalid beacon activation");
|
|
82
|
+
return (await signTransaction(activationSignableData(validatorsAddress, message), identity)).signature;
|
|
83
|
+
};
|
|
84
|
+
const verifyIdentitySignature = async (message, signable, network) => {
|
|
85
|
+
if (typeof message.signature !== "string" || !message.signature) return false;
|
|
86
|
+
try {
|
|
87
|
+
const verifier = new MultiWallet(network);
|
|
88
|
+
await verifier.fromAddress(message.from, null, network);
|
|
89
|
+
return verifier.verify(fromBase58(message.signature), await createTransactionHash(signable));
|
|
90
|
+
} catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const verifyBeaconCommitmentMessage = async (validatorsAddress, message, network) => validateBeaconCommitmentData(message) && verifyIdentitySignature(message, commitmentSignableData(validatorsAddress, message), network);
|
|
95
|
+
const verifyBeaconShareMessage = async (validatorsAddress, message, network) => validateBeaconShareData(message) && verifyIdentitySignature(message, shareSignableData(validatorsAddress, message), network);
|
|
96
|
+
const verifyBeaconActivationMessage = async (validatorsAddress, message, network) => validateBeaconActivationData(message) && verifyIdentitySignature(message, activationSignableData(validatorsAddress, message), network);
|
|
97
|
+
|
|
98
|
+
export { signBeaconActivationMessage, signBeaconCommitmentMessage, signBeaconShareMessage, validateBeaconActivationData, validateBeaconCommitmentData, validateBeaconShareData, verifyBeaconActivationMessage, verifyBeaconCommitmentMessage, verifyBeaconShareMessage };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { bls12_381 } from '@noble/curves/bls12-381';
|
|
2
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
3
|
+
|
|
4
|
+
const encoder = new TextEncoder();
|
|
5
|
+
const Fr = bls12_381.fields.Fr;
|
|
6
|
+
const G1 = bls12_381.G1.ProjectivePoint;
|
|
7
|
+
const G2 = bls12_381.G2.ProjectivePoint;
|
|
8
|
+
const assertParticipant = (participant) => {
|
|
9
|
+
if (typeof participant !== "bigint" || participant <= 0n || participant >= Fr.ORDER) {
|
|
10
|
+
throw new TypeError("beacon participant identifiers must be non-zero field elements");
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
const assertThreshold = (threshold, participantCount) => {
|
|
14
|
+
if (!Number.isSafeInteger(threshold) || threshold < 2 || threshold > participantCount) {
|
|
15
|
+
throw new RangeError("beacon threshold must be between 2 and the participant count");
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
const assertUniqueParticipants = (participants) => {
|
|
19
|
+
for (const participant of participants) assertParticipant(participant);
|
|
20
|
+
if (new Set(participants).size !== participants.length) throw new Error("duplicate beacon participant");
|
|
21
|
+
};
|
|
22
|
+
const evaluatePolynomial = (coefficients, x) => {
|
|
23
|
+
let result = 0n;
|
|
24
|
+
for (let index = coefficients.length - 1; index >= 0; index -= 1) {
|
|
25
|
+
result = Fr.add(Fr.mul(result, x), coefficients[index]);
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
};
|
|
29
|
+
const evaluateCommitments = (commitments, x) => {
|
|
30
|
+
let result = G1.ZERO;
|
|
31
|
+
let power = 1n;
|
|
32
|
+
for (const encoded of commitments) {
|
|
33
|
+
const commitment = G1.fromHex(encoded);
|
|
34
|
+
commitment.assertValidity();
|
|
35
|
+
result = result.add(commitment.multiply(power));
|
|
36
|
+
power = Fr.mul(power, x);
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
const sumPoints = (points) => {
|
|
41
|
+
let result = G1.ZERO;
|
|
42
|
+
for (const encoded of points) {
|
|
43
|
+
const point = G1.fromHex(encoded);
|
|
44
|
+
point.assertValidity();
|
|
45
|
+
result = result.add(point);
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
};
|
|
49
|
+
const lagrangeAtZero = (participant, participants) => {
|
|
50
|
+
let numerator = 1n;
|
|
51
|
+
let denominator = 1n;
|
|
52
|
+
for (const other of participants) {
|
|
53
|
+
if (other === participant) continue;
|
|
54
|
+
numerator = Fr.mul(numerator, Fr.neg(other));
|
|
55
|
+
denominator = Fr.mul(denominator, Fr.sub(participant, other));
|
|
56
|
+
}
|
|
57
|
+
return Fr.mul(numerator, Fr.inv(denominator));
|
|
58
|
+
};
|
|
59
|
+
const createBeaconContribution = (participants, threshold, randomScalar) => {
|
|
60
|
+
assertUniqueParticipants(participants);
|
|
61
|
+
assertThreshold(threshold, participants.length);
|
|
62
|
+
const coefficients = Array.from({ length: threshold }, () => {
|
|
63
|
+
const scalar = Fr.create(randomScalar());
|
|
64
|
+
if (scalar === 0n) throw new Error("beacon contribution contains a zero coefficient");
|
|
65
|
+
return scalar;
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
commitments: coefficients.map((coefficient) => G1.BASE.multiply(coefficient).toRawBytes(true)),
|
|
69
|
+
shares: new Map(participants.map((participant) => [participant, evaluatePolynomial(coefficients, participant)]))
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
const verifyBeaconShare = (participant, share, commitments) => {
|
|
73
|
+
try {
|
|
74
|
+
assertParticipant(participant);
|
|
75
|
+
if (commitments.length < 2) return false;
|
|
76
|
+
return G1.BASE.multiply(Fr.create(share)).equals(evaluateCommitments(commitments, participant));
|
|
77
|
+
} catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
const combineBeaconShares = (shares) => {
|
|
82
|
+
if (shares.length < 2) throw new Error("at least two dealer shares are required");
|
|
83
|
+
return shares.reduce((total, share) => Fr.add(total, Fr.create(share)), 0n);
|
|
84
|
+
};
|
|
85
|
+
const deriveBeaconPublicShare = (participant, dealerCommitments) => {
|
|
86
|
+
assertParticipant(participant);
|
|
87
|
+
if (dealerCommitments.length < 2) throw new Error("at least two dealer commitments are required");
|
|
88
|
+
return sumPoints(
|
|
89
|
+
dealerCommitments.map((commitments) => evaluateCommitments(commitments, participant).toRawBytes(true))
|
|
90
|
+
).toRawBytes(true);
|
|
91
|
+
};
|
|
92
|
+
const deriveBeaconGroupPublicKey = (dealerCommitments) => {
|
|
93
|
+
if (dealerCommitments.length < 2 || dealerCommitments.some((commitments) => commitments.length < 2)) {
|
|
94
|
+
throw new Error("invalid beacon dealer commitments");
|
|
95
|
+
}
|
|
96
|
+
return sumPoints(dealerCommitments.map((commitments) => commitments[0])).toRawBytes(true);
|
|
97
|
+
};
|
|
98
|
+
const beaconMessage = (network, epoch, round, previousProof) => {
|
|
99
|
+
if (!network || epoch < 0n || round < 0n) throw new TypeError("invalid beacon round");
|
|
100
|
+
const prefix = encoder.encode(`leofcoin-beacon-v1:${network}:${epoch}:${round}:`);
|
|
101
|
+
const message = new Uint8Array(prefix.length + previousProof.length);
|
|
102
|
+
message.set(prefix);
|
|
103
|
+
message.set(previousProof, prefix.length);
|
|
104
|
+
return message;
|
|
105
|
+
};
|
|
106
|
+
const signBeaconRound = (secretShare, message) => bls12_381.sign(message, Fr.create(secretShare));
|
|
107
|
+
const verifyBeaconSignatureShare = (signature, message, publicShare) => {
|
|
108
|
+
try {
|
|
109
|
+
return bls12_381.verify(signature, message, publicShare);
|
|
110
|
+
} catch {
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const reconstructBeaconSignature = (rawShares, threshold) => {
|
|
115
|
+
if (!Number.isSafeInteger(threshold) || threshold < 2) throw new RangeError("invalid beacon threshold");
|
|
116
|
+
const shares = [...rawShares].sort((left, right) => left.participant < right.participant ? -1 : 1);
|
|
117
|
+
if (shares.length < threshold) throw new Error("not enough beacon signature shares");
|
|
118
|
+
const selected = shares.slice(0, threshold);
|
|
119
|
+
const participants = selected.map(({ participant }) => participant);
|
|
120
|
+
assertUniqueParticipants(participants);
|
|
121
|
+
let signature = G2.ZERO;
|
|
122
|
+
for (const share of selected) {
|
|
123
|
+
const point = bls12_381.Signature.fromHex(share.signature);
|
|
124
|
+
point.assertValidity();
|
|
125
|
+
signature = signature.add(point.multiply(lagrangeAtZero(share.participant, participants)));
|
|
126
|
+
}
|
|
127
|
+
return bls12_381.Signature.toRawBytes(signature);
|
|
128
|
+
};
|
|
129
|
+
const verifyBeaconProof = (proof, message, groupPublicKey) => {
|
|
130
|
+
try {
|
|
131
|
+
return bls12_381.verify(proof, message, groupPublicKey);
|
|
132
|
+
} catch {
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
const beaconRandomness = (proof) => {
|
|
137
|
+
if (proof.length !== 96) throw new Error("invalid BLS beacon proof length");
|
|
138
|
+
return sha256(proof);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export { beaconMessage, beaconRandomness, combineBeaconShares, createBeaconContribution, deriveBeaconGroupPublicKey, deriveBeaconPublicShare, reconstructBeaconSignature, signBeaconRound, verifyBeaconProof, verifyBeaconShare, verifyBeaconSignatureShare };
|