@typeberry/lib 0.8.4 → 0.9.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/package.json +6 -4
- package/packages/configs/index.d.ts +30 -1
- package/packages/configs/index.d.ts.map +1 -1
- package/packages/configs/index.js +4 -2
- package/packages/configs/typeberry-dev-full.json +29 -0
- package/packages/core/bytes/bytes.d.ts +1 -0
- package/packages/core/bytes/bytes.d.ts.map +1 -1
- package/packages/core/bytes/bytes.js +8 -0
- package/packages/core/utils/debug.d.ts +4 -2
- package/packages/core/utils/debug.d.ts.map +1 -1
- package/packages/core/utils/debug.js +18 -13
- package/packages/core/utils/debug.test.js +12 -6
- package/packages/jam/config-node/node-config.d.ts +2 -1
- package/packages/jam/config-node/node-config.d.ts.map +1 -1
- package/packages/jam/config-node/node-config.js +8 -3
- package/packages/jam/config-node/node-config.test.js +3 -3
- package/packages/jam/database-fjall/hybrid-states.d.ts +45 -0
- package/packages/jam/database-fjall/hybrid-states.d.ts.map +1 -0
- package/packages/jam/database-fjall/hybrid-states.js +113 -0
- package/packages/jam/database-fjall/hybrid-states.test.d.ts +2 -0
- package/packages/jam/database-fjall/hybrid-states.test.d.ts.map +1 -0
- package/packages/jam/database-fjall/hybrid-states.test.js +83 -0
- package/packages/jam/database-fjall/index.d.ts +3 -0
- package/packages/jam/database-fjall/index.d.ts.map +1 -0
- package/packages/jam/database-fjall/index.js +2 -0
- package/packages/jam/database-fjall/root.d.ts +52 -0
- package/packages/jam/database-fjall/root.d.ts.map +1 -0
- package/packages/jam/database-fjall/root.js +85 -0
- package/packages/jam/jamnp-s/tasks/ticket-distribution.d.ts +18 -10
- package/packages/jam/jamnp-s/tasks/ticket-distribution.d.ts.map +1 -1
- package/packages/jam/jamnp-s/tasks/ticket-distribution.js +44 -68
- package/packages/jam/jamnp-s/tasks/ticket-distribution.test.js +30 -8
- package/packages/jam/node/main-fuzz.d.ts.map +1 -1
- package/packages/jam/node/main-fuzz.js +16 -1
- package/packages/jam/node/main-importer.d.ts +6 -3
- package/packages/jam/node/main-importer.d.ts.map +1 -1
- package/packages/jam/node/main-importer.js +3 -2
- package/packages/jam/safrole/bandersnatch-vrf.d.ts +24 -4
- package/packages/jam/safrole/bandersnatch-vrf.d.ts.map +1 -1
- package/packages/jam/safrole/bandersnatch-vrf.js +63 -26
- package/packages/jam/safrole/bandersnatch-vrf.test.js +12 -9
- package/packages/jam/safrole/bandersnatch-wasm.d.ts +10 -0
- package/packages/jam/safrole/bandersnatch-wasm.d.ts.map +1 -1
- package/packages/jam/safrole/bandersnatch-wasm.js +12 -0
- package/packages/jam/safrole/safrole.js +5 -5
- package/packages/jam/safrole/safrole.test.js +13 -13
- package/packages/jam/ticket-pool/index.d.ts +4 -0
- package/packages/jam/ticket-pool/index.d.ts.map +1 -0
- package/packages/jam/ticket-pool/index.js +3 -0
- package/packages/jam/ticket-pool/pending-ticket-pool.d.ts +30 -0
- package/packages/jam/ticket-pool/pending-ticket-pool.d.ts.map +1 -0
- package/packages/jam/ticket-pool/pending-ticket-pool.js +56 -0
- package/packages/jam/ticket-pool/pending-ticket-pool.test.d.ts +2 -0
- package/packages/jam/ticket-pool/pending-ticket-pool.test.d.ts.map +1 -0
- package/packages/jam/ticket-pool/pending-ticket-pool.test.js +67 -0
- package/packages/jam/ticket-pool/ticket-validator.d.ts +47 -0
- package/packages/jam/ticket-pool/ticket-validator.d.ts.map +1 -0
- package/packages/jam/ticket-pool/ticket-validator.js +34 -0
- package/packages/jam/ticket-pool/ticket-validator.test.d.ts +2 -0
- package/packages/jam/ticket-pool/ticket-validator.test.d.ts.map +1 -0
- package/packages/jam/ticket-pool/ticket-validator.test.js +35 -0
- package/packages/jam/ticket-pool/verified-ticket-pool.d.ts +26 -0
- package/packages/jam/ticket-pool/verified-ticket-pool.d.ts.map +1 -0
- package/packages/jam/ticket-pool/verified-ticket-pool.js +41 -0
- package/packages/jam/ticket-pool/verified-ticket-pool.test.d.ts +2 -0
- package/packages/jam/ticket-pool/verified-ticket-pool.test.d.ts.map +1 -0
- package/packages/jam/ticket-pool/verified-ticket-pool.test.js +54 -0
- package/packages/workers/api-node/config.d.ts +12 -5
- package/packages/workers/api-node/config.d.ts.map +1 -1
- package/packages/workers/api-node/config.js +20 -17
- package/packages/workers/api-node/config.test.js +38 -1
- package/packages/workers/block-authorship/{generator.d.ts → block-generator.d.ts} +5 -5
- package/packages/workers/block-authorship/block-generator.d.ts.map +1 -0
- package/packages/workers/block-authorship/{generator.js → block-generator.js} +3 -3
- package/packages/workers/block-authorship/block-generator.test.d.ts +2 -0
- package/packages/workers/block-authorship/block-generator.test.d.ts.map +1 -0
- package/packages/workers/block-authorship/{generator.test.js → block-generator.test.js} +8 -8
- package/packages/workers/block-authorship/epoch-authoring-slots.d.ts +35 -0
- package/packages/workers/block-authorship/epoch-authoring-slots.d.ts.map +1 -0
- package/packages/workers/block-authorship/epoch-authoring-slots.js +86 -0
- package/packages/workers/block-authorship/epoch-tracker.d.ts +29 -0
- package/packages/workers/block-authorship/epoch-tracker.d.ts.map +1 -0
- package/packages/workers/block-authorship/epoch-tracker.js +80 -0
- package/packages/workers/block-authorship/index.d.ts.map +1 -1
- package/packages/workers/block-authorship/index.js +1 -1
- package/packages/workers/block-authorship/main.d.ts +3 -0
- package/packages/workers/block-authorship/main.d.ts.map +1 -1
- package/packages/workers/block-authorship/main.js +197 -315
- package/packages/workers/block-authorship/ticket-generator/bootstrap-main.d.ts +2 -0
- package/packages/workers/block-authorship/ticket-generator/bootstrap-main.d.ts.map +1 -0
- package/packages/workers/block-authorship/ticket-generator/bootstrap-main.js +23 -0
- package/packages/workers/block-authorship/ticket-generator/index.d.ts +16 -0
- package/packages/workers/block-authorship/ticket-generator/index.d.ts.map +1 -0
- package/packages/workers/block-authorship/ticket-generator/index.js +62 -0
- package/packages/workers/block-authorship/ticket-generator/protocol.d.ts +50 -0
- package/packages/workers/block-authorship/ticket-generator/protocol.d.ts.map +1 -0
- package/packages/workers/block-authorship/ticket-generator/protocol.js +54 -0
- package/packages/workers/block-authorship/{ticket-generator.d.ts → ticket-generator/ticket-generator.d.ts} +4 -0
- package/packages/workers/block-authorship/ticket-generator/ticket-generator.d.ts.map +1 -0
- package/packages/workers/block-authorship/{ticket-generator.js → ticket-generator/ticket-generator.js} +19 -9
- package/packages/workers/block-authorship/ticket-generator/ticket-generator.test.d.ts.map +1 -0
- package/packages/workers/block-authorship/{ticket-generator.test.js → ticket-generator/ticket-generator.test.js} +13 -9
- package/packages/workers/block-authorship/ticket-generator/worker-pool.d.ts +36 -0
- package/packages/workers/block-authorship/ticket-generator/worker-pool.d.ts.map +1 -0
- package/packages/workers/block-authorship/ticket-generator/worker-pool.js +111 -0
- package/packages/workers/block-authorship/ticket-validator.d.ts +31 -0
- package/packages/workers/block-authorship/ticket-validator.d.ts.map +1 -0
- package/packages/workers/block-authorship/ticket-validator.js +59 -0
- package/packages/workers/comms-authorship-network/protocol.d.ts +14 -4
- package/packages/workers/comms-authorship-network/protocol.d.ts.map +1 -1
- package/packages/workers/comms-authorship-network/protocol.js +12 -6
- package/packages/workers/comms-authorship-network/tickets-message.d.ts +0 -14
- package/packages/workers/comms-authorship-network/tickets-message.d.ts.map +1 -1
- package/packages/workers/comms-authorship-network/tickets-message.js +0 -17
- package/packages/workers/importer/importer.d.ts +2 -2
- package/packages/workers/importer/importer.d.ts.map +1 -1
- package/packages/workers/importer/importer.js +5 -5
- package/packages/workers/importer/stats.d.ts +1 -3
- package/packages/workers/importer/stats.d.ts.map +1 -1
- package/packages/workers/importer/stats.js +12 -12
- package/packages/workers/jam-network/main.d.ts.map +1 -1
- package/packages/workers/jam-network/main.js +25 -4
- package/packages/workers/block-authorship/generator.d.ts.map +0 -1
- package/packages/workers/block-authorship/generator.test.d.ts +0 -2
- package/packages/workers/block-authorship/generator.test.d.ts.map +0 -1
- package/packages/workers/block-authorship/ticket-generator.d.ts.map +0 -1
- package/packages/workers/block-authorship/ticket-generator.test.d.ts.map +0 -1
- /package/packages/configs/{typeberry-dev.json → typeberry-dev-tiny.json} +0 -0
- /package/packages/workers/block-authorship/{ticket-generator.test.d.ts → ticket-generator/ticket-generator.test.d.ts} +0 -0
|
@@ -71,8 +71,8 @@ describe("Bandersnatch verification", () => {
|
|
|
71
71
|
"0x3a5d10abc80dda33fe3f40b3bb2e3eefd3e97dda3d617a860c9d94eb70b832ad",
|
|
72
72
|
].map((x) => Bytes.parseBytes(x, HASH_SIZE));
|
|
73
73
|
const result = await bandersnatchVrf.verifyTickets(await bandersnatchWasm, bandersnatchKeys.length, commitment, tickets, entropy);
|
|
74
|
-
assert.strictEqual(result.
|
|
75
|
-
assert.deepStrictEqual(result.map((x) => x.
|
|
74
|
+
assert.strictEqual(result.isValid, true);
|
|
75
|
+
assert.deepStrictEqual(result.tickets.map((x) => x.toString()), expectedIds.map((x) => x.toString()));
|
|
76
76
|
});
|
|
77
77
|
it("should detect that one signature is incorrect", async () => {
|
|
78
78
|
const tickets = [
|
|
@@ -90,14 +90,16 @@ describe("Bandersnatch verification", () => {
|
|
|
90
90
|
},
|
|
91
91
|
];
|
|
92
92
|
const entropy = Bytes.parseBytes("0xbb30a42c1e62f0afda5f0a4e8a562f7a13a24cea00ee81917b86b89e801314aa", HASH_SIZE).asOpaque();
|
|
93
|
+
// Batch verification fails as a whole when any signature is invalid,
|
|
94
|
+
// and in that case all returned entropy hashes are zeroed out.
|
|
93
95
|
const expectedIds = [
|
|
94
96
|
"0x0000000000000000000000000000000000000000000000000000000000000000",
|
|
95
|
-
"
|
|
96
|
-
"
|
|
97
|
+
"0x0000000000000000000000000000000000000000000000000000000000000000",
|
|
98
|
+
"0x0000000000000000000000000000000000000000000000000000000000000000",
|
|
97
99
|
].map((x) => Bytes.parseBytes(x, HASH_SIZE));
|
|
98
100
|
const result = await bandersnatchVrf.verifyTickets(await bandersnatchWasm, bandersnatchKeys.length, commitment, tickets, entropy);
|
|
99
|
-
assert.
|
|
100
|
-
assert.deepStrictEqual(result.map((x) => x.
|
|
101
|
+
assert.strictEqual(result.isValid, false);
|
|
102
|
+
assert.deepStrictEqual(result.tickets.map((x) => x.toString()), expectedIds.map((x) => x.toString()));
|
|
101
103
|
});
|
|
102
104
|
});
|
|
103
105
|
describe("verifySeal", () => {
|
|
@@ -163,12 +165,13 @@ describe("Bandersnatch verification", () => {
|
|
|
163
165
|
const ringKeys = secrets.map((secret) => deriveBandersnatchPublicKey(secret));
|
|
164
166
|
const proverIndex = 0;
|
|
165
167
|
const entropy = Bytes.fill(HASH_SIZE, 123).asOpaque();
|
|
166
|
-
const genResult = await bandersnatchVrf.generateTickets(await bandersnatchWasm, ringKeys, proverIndex, secrets[proverIndex], entropy, 2);
|
|
168
|
+
const genResult = await bandersnatchVrf.generateTickets(await bandersnatchWasm, ringKeys, [proverIndex], [secrets[proverIndex]], entropy, 2);
|
|
167
169
|
assert.ok(genResult.isOk);
|
|
168
170
|
const commitment = await bandersnatchVrf.getRingCommitment(await bandersnatchWasm, ringKeys);
|
|
169
171
|
assert.ok(commitment.isOk);
|
|
170
|
-
|
|
171
|
-
|
|
172
|
+
assert.strictEqual(genResult.ok.length, 1);
|
|
173
|
+
const verifyResult = await bandersnatchVrf.verifyTickets(await bandersnatchWasm, ringKeys.length, commitment.ok, genResult.ok[0], entropy);
|
|
174
|
+
assert.ok(verifyResult.isValid, "Generated tickets should pass verification");
|
|
172
175
|
});
|
|
173
176
|
});
|
|
174
177
|
});
|
|
@@ -8,5 +8,15 @@ export declare class BandernsatchWasm {
|
|
|
8
8
|
generateSeal(authorKey: Uint8Array, input: Uint8Array, auxData: Uint8Array): Promise<Uint8Array<ArrayBufferLike>>;
|
|
9
9
|
getVrfOutputHash(authorKey: Uint8Array, input: Uint8Array): Promise<Uint8Array<ArrayBufferLike>>;
|
|
10
10
|
batchGenerateRingVrf(ringKeys: Uint8Array, proverKeyIndex: number, secretSeed: Uint8Array, inputsData: Uint8Array, vrfInputDataLen: number): Promise<Uint8Array<ArrayBufferLike>>;
|
|
11
|
+
/**
|
|
12
|
+
* Batch-generate ring VRF tickets for multiple validators in a single call,
|
|
13
|
+
* reusing the ring prover setup across all of them.
|
|
14
|
+
*
|
|
15
|
+
* `secretSeedsData` is the fixed-width concatenation of the validators' secret
|
|
16
|
+
* seeds (each `secretSeedDataLen` bytes); `proverKeyIndices` are their indices
|
|
17
|
+
* within the ring and must have the same count. Output records are ordered
|
|
18
|
+
* validator-major then input-major, each `status byte || signature`.
|
|
19
|
+
*/
|
|
20
|
+
batchGenerateRingVrfForValidators(ringKeys: Uint8Array, proverKeyIndices: Uint32Array, secretSeedsData: Uint8Array, secretSeedDataLen: number, inputsData: Uint8Array, vrfInputDataLen: number): Promise<Uint8Array<ArrayBufferLike>>;
|
|
11
21
|
}
|
|
12
22
|
//# sourceMappingURL=bandersnatch-wasm.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bandersnatch-wasm.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/safrole/bandersnatch-wasm.ts"],"names":[],"mappings":"AAEA,qBAAa,gBAAgB;IAC3B,OAAO;WAEM,GAAG;IAKV,UAAU,CAAC,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU;IAIjG,iBAAiB,CACrB,SAAS,EAAE,UAAU,EACrB,UAAU,EAAE,UAAU,EACtB,iBAAiB,EAAE,UAAU,EAC7B,cAAc,EAAE,UAAU,EAC1B,WAAW,EAAE,UAAU,EACvB,oBAAoB,EAAE,UAAU;IAY5B,iBAAiB,CAAC,IAAI,EAAE,UAAU;IAIlC,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM;IAI1G,YAAY,CAAC,SAAS,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU;IAI1E,gBAAgB,CAAC,SAAS,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU;IAIzD,oBAAoB,CACxB,QAAQ,EAAE,UAAU,EACpB,cAAc,EAAE,MAAM,EACtB,UAAU,EAAE,UAAU,EACtB,UAAU,EAAE,UAAU,EACtB,eAAe,EAAE,MAAM;
|
|
1
|
+
{"version":3,"file":"bandersnatch-wasm.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/safrole/bandersnatch-wasm.ts"],"names":[],"mappings":"AAEA,qBAAa,gBAAgB;IAC3B,OAAO;WAEM,GAAG;IAKV,UAAU,CAAC,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU;IAIjG,iBAAiB,CACrB,SAAS,EAAE,UAAU,EACrB,UAAU,EAAE,UAAU,EACtB,iBAAiB,EAAE,UAAU,EAC7B,cAAc,EAAE,UAAU,EAC1B,WAAW,EAAE,UAAU,EACvB,oBAAoB,EAAE,UAAU;IAY5B,iBAAiB,CAAC,IAAI,EAAE,UAAU;IAIlC,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM;IAI1G,YAAY,CAAC,SAAS,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU;IAI1E,gBAAgB,CAAC,SAAS,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU;IAIzD,oBAAoB,CACxB,QAAQ,EAAE,UAAU,EACpB,cAAc,EAAE,MAAM,EACtB,UAAU,EAAE,UAAU,EACtB,UAAU,EAAE,UAAU,EACtB,eAAe,EAAE,MAAM;IAKzB;;;;;;;;OAQG;IACG,iCAAiC,CACrC,QAAQ,EAAE,UAAU,EACpB,gBAAgB,EAAE,WAAW,EAC7B,eAAe,EAAE,UAAU,EAC3B,iBAAiB,EAAE,MAAM,EACzB,UAAU,EAAE,UAAU,EACtB,eAAe,EAAE,MAAM;CAW1B"}
|
|
@@ -26,4 +26,16 @@ export class BandernsatchWasm {
|
|
|
26
26
|
async batchGenerateRingVrf(ringKeys, proverKeyIndex, secretSeed, inputsData, vrfInputDataLen) {
|
|
27
27
|
return bandersnatchWasm.batchGenerateRingVrf(ringKeys, proverKeyIndex, secretSeed, inputsData, vrfInputDataLen);
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Batch-generate ring VRF tickets for multiple validators in a single call,
|
|
31
|
+
* reusing the ring prover setup across all of them.
|
|
32
|
+
*
|
|
33
|
+
* `secretSeedsData` is the fixed-width concatenation of the validators' secret
|
|
34
|
+
* seeds (each `secretSeedDataLen` bytes); `proverKeyIndices` are their indices
|
|
35
|
+
* within the ring and must have the same count. Output records are ordered
|
|
36
|
+
* validator-major then input-major, each `status byte || signature`.
|
|
37
|
+
*/
|
|
38
|
+
async batchGenerateRingVrfForValidators(ringKeys, proverKeyIndices, secretSeedsData, secretSeedDataLen, inputsData, vrfInputDataLen) {
|
|
39
|
+
return bandersnatchWasm.batchGenerateRingVrfForValidators(ringKeys, proverKeyIndices, secretSeedsData, secretSeedDataLen, inputsData, vrfInputDataLen);
|
|
40
|
+
}
|
|
29
41
|
}
|
|
@@ -260,15 +260,15 @@ export class Safrole {
|
|
|
260
260
|
*/
|
|
261
261
|
// TODO [ToDr] Verify that ticket attempt is in correct range.
|
|
262
262
|
const verificationResult = extrinsic.length === 0
|
|
263
|
-
? []
|
|
263
|
+
? { isValid: true, tickets: [] }
|
|
264
264
|
: await bandersnatchVrf.verifyTickets(await this.bandersnatch, validators.length, epochRoot, extrinsic, entropy);
|
|
265
|
+
if (!verificationResult.isValid) {
|
|
266
|
+
return Result.error(SafroleErrorCode.BadTicketProof, () => "Safrole: invalid ticket proof in extrinsic");
|
|
267
|
+
}
|
|
265
268
|
const tickets = extrinsic.map((ticket, i) => ({
|
|
266
|
-
id: verificationResult[i]
|
|
269
|
+
id: verificationResult.tickets[i],
|
|
267
270
|
attempt: ticket.attempt,
|
|
268
271
|
}));
|
|
269
|
-
if (!verificationResult.every((x) => x.isValid)) {
|
|
270
|
-
return Result.error(SafroleErrorCode.BadTicketProof, () => "Safrole: invalid ticket proof in extrinsic");
|
|
271
|
-
}
|
|
272
272
|
/**
|
|
273
273
|
* Verify if tickets are sorted and unique
|
|
274
274
|
*
|
|
@@ -67,10 +67,10 @@ const fakeSealingKeys = {
|
|
|
67
67
|
describe("Safrole", () => {
|
|
68
68
|
let blake2b;
|
|
69
69
|
beforeEach(async () => {
|
|
70
|
-
mock.method(bandersnatchVrf, "verifyTickets", () => Promise.resolve(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
mock.method(bandersnatchVrf, "verifyTickets", () => Promise.resolve({
|
|
71
|
+
isValid: true,
|
|
72
|
+
tickets: [Bytes.zero(HASH_SIZE), Bytes.fill(HASH_SIZE, 1)],
|
|
73
|
+
}));
|
|
74
74
|
blake2b = await Blake2b.createHasher();
|
|
75
75
|
});
|
|
76
76
|
afterEach(() => {
|
|
@@ -146,7 +146,7 @@ describe("Safrole", () => {
|
|
|
146
146
|
}
|
|
147
147
|
});
|
|
148
148
|
it("should return bad ticket proof error", async () => {
|
|
149
|
-
mock.method(bandersnatchVrf, "verifyTickets", () => Promise.resolve(
|
|
149
|
+
mock.method(bandersnatchVrf, "verifyTickets", () => Promise.resolve({ isValid: false, tickets: [Bytes.zero(HASH_SIZE)] }));
|
|
150
150
|
const punishSet = SortedSet.fromArray(hashComparator);
|
|
151
151
|
const state = {
|
|
152
152
|
timeslot: tryAsTimeSlot(1),
|
|
@@ -188,10 +188,10 @@ describe("Safrole", () => {
|
|
|
188
188
|
}
|
|
189
189
|
});
|
|
190
190
|
it("should return duplicated ticket error", async () => {
|
|
191
|
-
mock.method(bandersnatchVrf, "verifyTickets", () => Promise.resolve(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
191
|
+
mock.method(bandersnatchVrf, "verifyTickets", () => Promise.resolve({
|
|
192
|
+
isValid: true,
|
|
193
|
+
tickets: [Bytes.zero(HASH_SIZE), Bytes.zero(HASH_SIZE)],
|
|
194
|
+
}));
|
|
195
195
|
const punishSet = SortedSet.fromArray(hashComparator);
|
|
196
196
|
const state = {
|
|
197
197
|
timeslot: tryAsTimeSlot(1),
|
|
@@ -237,10 +237,10 @@ describe("Safrole", () => {
|
|
|
237
237
|
}
|
|
238
238
|
});
|
|
239
239
|
it("should return bad ticket order error", async () => {
|
|
240
|
-
mock.method(bandersnatchVrf, "verifyTickets", () => Promise.resolve(
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
240
|
+
mock.method(bandersnatchVrf, "verifyTickets", () => Promise.resolve({
|
|
241
|
+
isValid: true,
|
|
242
|
+
tickets: [Bytes.fill(HASH_SIZE, 1), Bytes.zero(HASH_SIZE)],
|
|
243
|
+
}));
|
|
244
244
|
const punishSet = SortedSet.fromArray(hashComparator);
|
|
245
245
|
const state = {
|
|
246
246
|
timeslot: tryAsTimeSlot(1),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/ticket-pool/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Epoch } from "#@typeberry/block";
|
|
2
|
+
import type { SignedTicket } from "#@typeberry/block/tickets.js";
|
|
3
|
+
/**
|
|
4
|
+
* An ordered, signature-deduplicated pool of tickets waiting to be redistributed to peers.
|
|
5
|
+
*
|
|
6
|
+
* Used on the networking side. Indices are stable within an epoch so callers can track
|
|
7
|
+
* per-peer "sent" sets by index. The pool is cleared whenever a new epoch is observed,
|
|
8
|
+
* and tickets for older epochs are dropped (can happen when an async validation completes
|
|
9
|
+
* after the epoch already advanced).
|
|
10
|
+
*/
|
|
11
|
+
export declare class PendingTicketPool {
|
|
12
|
+
private tickets;
|
|
13
|
+
private currentEpochValue;
|
|
14
|
+
/** Epoch the pool is currently holding tickets for, or `null` if empty. */
|
|
15
|
+
get currentEpoch(): Epoch | null;
|
|
16
|
+
/** Returns the ordered tickets currently in the pool. Caller must not mutate the array. */
|
|
17
|
+
getTickets(): readonly {
|
|
18
|
+
epochIndex: Epoch;
|
|
19
|
+
ticket: SignedTicket;
|
|
20
|
+
}[];
|
|
21
|
+
/** Returns true if the ticket was added, false if it was a duplicate or dropped (old epoch). */
|
|
22
|
+
addTicket(epochIndex: Epoch, ticket: SignedTicket): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Replace the pool contents for the given epoch with the supplied tickets. Used when the
|
|
25
|
+
* authorship worker pushes an authoritative pool dump on an epoch boundary; any tickets
|
|
26
|
+
* that aren't in the dump are dropped, and dedup runs over the new set.
|
|
27
|
+
*/
|
|
28
|
+
replace(epochIndex: Epoch, tickets: readonly SignedTicket[]): void;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=pending-ticket-pool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pending-ticket-pool.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/ticket-pool/pending-ticket-pool.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAKhE;;;;;;;GAOG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,OAAO,CAA0D;IACzE,OAAO,CAAC,iBAAiB,CAAsB;IAE/C,2EAA2E;IAC3E,IAAI,YAAY,IAAI,KAAK,GAAG,IAAI,CAE/B;IAED,2FAA2F;IAC3F,UAAU,IAAI,SAAS;QAAE,UAAU,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,YAAY,CAAA;KAAE,EAAE;IAIpE,gGAAgG;IAChG,SAAS,CAAC,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO;IAyB3D;;;;OAIG;IACH,OAAO,CAAC,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,IAAI;CAWnE"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Logger } from "#@typeberry/logger";
|
|
2
|
+
const logger = Logger.new(import.meta.filename, "pending-pool");
|
|
3
|
+
/**
|
|
4
|
+
* An ordered, signature-deduplicated pool of tickets waiting to be redistributed to peers.
|
|
5
|
+
*
|
|
6
|
+
* Used on the networking side. Indices are stable within an epoch so callers can track
|
|
7
|
+
* per-peer "sent" sets by index. The pool is cleared whenever a new epoch is observed,
|
|
8
|
+
* and tickets for older epochs are dropped (can happen when an async validation completes
|
|
9
|
+
* after the epoch already advanced).
|
|
10
|
+
*/
|
|
11
|
+
export class PendingTicketPool {
|
|
12
|
+
tickets = [];
|
|
13
|
+
currentEpochValue = null;
|
|
14
|
+
/** Epoch the pool is currently holding tickets for, or `null` if empty. */
|
|
15
|
+
get currentEpoch() {
|
|
16
|
+
return this.currentEpochValue;
|
|
17
|
+
}
|
|
18
|
+
/** Returns the ordered tickets currently in the pool. Caller must not mutate the array. */
|
|
19
|
+
getTickets() {
|
|
20
|
+
return this.tickets;
|
|
21
|
+
}
|
|
22
|
+
/** Returns true if the ticket was added, false if it was a duplicate or dropped (old epoch). */
|
|
23
|
+
addTicket(epochIndex, ticket) {
|
|
24
|
+
if (this.currentEpochValue !== null && epochIndex < this.currentEpochValue) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
if (this.currentEpochValue !== null && epochIndex > this.currentEpochValue) {
|
|
28
|
+
logger.log `Epoch changed from ${this.currentEpochValue} to ${epochIndex}, clearing ${this.tickets.length} old tickets`;
|
|
29
|
+
this.tickets = [];
|
|
30
|
+
}
|
|
31
|
+
this.currentEpochValue = epochIndex;
|
|
32
|
+
const isDuplicate = this.tickets.some((pending) => pending.epochIndex === epochIndex && pending.ticket.signature.isEqualTo(ticket.signature));
|
|
33
|
+
if (isDuplicate) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
this.tickets.push({ epochIndex, ticket });
|
|
37
|
+
logger.info `[addTicket] Added ticket for epoch ${epochIndex}, total: ${this.tickets.length}`;
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Replace the pool contents for the given epoch with the supplied tickets. Used when the
|
|
42
|
+
* authorship worker pushes an authoritative pool dump on an epoch boundary; any tickets
|
|
43
|
+
* that aren't in the dump are dropped, and dedup runs over the new set.
|
|
44
|
+
*/
|
|
45
|
+
replace(epochIndex, tickets) {
|
|
46
|
+
this.tickets = [];
|
|
47
|
+
this.currentEpochValue = epochIndex;
|
|
48
|
+
for (const ticket of tickets) {
|
|
49
|
+
const isDuplicate = this.tickets.some((pending) => pending.ticket.signature.isEqualTo(ticket.signature));
|
|
50
|
+
if (!isDuplicate) {
|
|
51
|
+
this.tickets.push({ epochIndex, ticket });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
logger.log `Pool replaced for epoch ${epochIndex} with ${this.tickets.length} tickets`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pending-ticket-pool.test.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/ticket-pool/pending-ticket-pool.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { tryAsEpoch } from "#@typeberry/block";
|
|
4
|
+
import { SignedTicket, tryAsTicketAttempt } from "#@typeberry/block/tickets.js";
|
|
5
|
+
import { Bytes } from "#@typeberry/bytes";
|
|
6
|
+
import { BANDERSNATCH_PROOF_BYTES } from "#@typeberry/crypto";
|
|
7
|
+
import { PendingTicketPool } from "./pending-ticket-pool.js";
|
|
8
|
+
const E1 = tryAsEpoch(1);
|
|
9
|
+
const E2 = tryAsEpoch(2);
|
|
10
|
+
function makeTicket(seed, attempt = 0) {
|
|
11
|
+
const sig = Bytes.zero(BANDERSNATCH_PROOF_BYTES);
|
|
12
|
+
sig.raw[0] = seed;
|
|
13
|
+
return SignedTicket.create({
|
|
14
|
+
attempt: tryAsTicketAttempt(attempt),
|
|
15
|
+
signature: sig.asOpaque(),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
describe("PendingTicketPool", () => {
|
|
19
|
+
it("starts empty with no current epoch", () => {
|
|
20
|
+
const pool = new PendingTicketPool();
|
|
21
|
+
assert.strictEqual(pool.currentEpoch, null);
|
|
22
|
+
assert.deepStrictEqual(pool.getTickets(), []);
|
|
23
|
+
});
|
|
24
|
+
it("adds a ticket and tracks the epoch", () => {
|
|
25
|
+
const pool = new PendingTicketPool();
|
|
26
|
+
const t = makeTicket(1);
|
|
27
|
+
assert.strictEqual(pool.addTicket(E1, t), true);
|
|
28
|
+
assert.strictEqual(pool.currentEpoch, E1);
|
|
29
|
+
assert.strictEqual(pool.getTickets().length, 1);
|
|
30
|
+
});
|
|
31
|
+
it("dedups by signature within an epoch", () => {
|
|
32
|
+
const pool = new PendingTicketPool();
|
|
33
|
+
const t = makeTicket(1);
|
|
34
|
+
pool.addTicket(E1, t);
|
|
35
|
+
assert.strictEqual(pool.addTicket(E1, t), false);
|
|
36
|
+
assert.strictEqual(pool.getTickets().length, 1);
|
|
37
|
+
});
|
|
38
|
+
it("clears tickets when a newer epoch arrives", () => {
|
|
39
|
+
const pool = new PendingTicketPool();
|
|
40
|
+
pool.addTicket(E1, makeTicket(1));
|
|
41
|
+
pool.addTicket(E1, makeTicket(2));
|
|
42
|
+
pool.addTicket(E2, makeTicket(3));
|
|
43
|
+
const tickets = pool.getTickets();
|
|
44
|
+
assert.strictEqual(tickets.length, 1);
|
|
45
|
+
assert.strictEqual(tickets[0].epochIndex, E2);
|
|
46
|
+
assert.strictEqual(pool.currentEpoch, E2);
|
|
47
|
+
});
|
|
48
|
+
it("drops late tickets for older epochs", () => {
|
|
49
|
+
const pool = new PendingTicketPool();
|
|
50
|
+
pool.addTicket(E2, makeTicket(1));
|
|
51
|
+
assert.strictEqual(pool.addTicket(E1, makeTicket(2)), false);
|
|
52
|
+
assert.strictEqual(pool.getTickets().length, 1);
|
|
53
|
+
assert.strictEqual(pool.currentEpoch, E2);
|
|
54
|
+
});
|
|
55
|
+
it("replace clears existing tickets and dedups the new set", () => {
|
|
56
|
+
const pool = new PendingTicketPool();
|
|
57
|
+
pool.addTicket(E1, makeTicket(1));
|
|
58
|
+
pool.addTicket(E1, makeTicket(2));
|
|
59
|
+
const dump = [makeTicket(3), makeTicket(4), makeTicket(3)];
|
|
60
|
+
pool.replace(E2, dump);
|
|
61
|
+
const tickets = pool.getTickets();
|
|
62
|
+
assert.strictEqual(tickets.length, 2);
|
|
63
|
+
assert.strictEqual(pool.currentEpoch, E2);
|
|
64
|
+
assert.strictEqual(tickets[0].ticket.signature.raw[0], 3);
|
|
65
|
+
assert.strictEqual(tickets[1].ticket.signature.raw[0], 4);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { EntropyHash, Epoch } from "#@typeberry/block";
|
|
2
|
+
import type { SignedTicket } from "#@typeberry/block/tickets.js";
|
|
3
|
+
import { Result } from "#@typeberry/utils";
|
|
4
|
+
/**
|
|
5
|
+
* Outcome of a successful validation.
|
|
6
|
+
*
|
|
7
|
+
* `id` is the entropy hash the validator computed for this ticket. It is `null` when the
|
|
8
|
+
* concrete validator doesn't actually verify (e.g. {@link AcceptTicketsValidator}) or when
|
|
9
|
+
* it delegates to another process that doesn't bother to send the id back over the wire.
|
|
10
|
+
*/
|
|
11
|
+
export type ValidatedTicket = {
|
|
12
|
+
ticket: SignedTicket;
|
|
13
|
+
id: EntropyHash;
|
|
14
|
+
};
|
|
15
|
+
/** Reasons a ticket may fail validation. */
|
|
16
|
+
export declare enum ValidationError {
|
|
17
|
+
/** Verifier rejected the signature / proof. */
|
|
18
|
+
InvalidProof = "invalid_proof",
|
|
19
|
+
/** Validator could not run (e.g. state unavailable, transient internal failure). */
|
|
20
|
+
ValidatorUnavailable = "validator_unavailable",
|
|
21
|
+
/** Ticket is for an epoch outside the validator's window of interest. */
|
|
22
|
+
WrongEpoch = "wrong_epoch"
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Strategy for verifying tickets arriving from peers.
|
|
26
|
+
*
|
|
27
|
+
* The concrete implementation may call into the bandersnatch verifier, defer to another
|
|
28
|
+
* worker via IPC, or short-circuit (Accept/Deny defaults for tests).
|
|
29
|
+
*/
|
|
30
|
+
export interface TicketValidator {
|
|
31
|
+
validate(epochIndex: Epoch, tickets: SignedTicket[]): Promise<Result<ValidatedTicket[], ValidationError>>;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Accepts every ticket without inspection. Useful for unit tests where the validator
|
|
35
|
+
* isn't the subject under test. Must never be used in production.
|
|
36
|
+
*/
|
|
37
|
+
export declare class AcceptTicketsValidator implements TicketValidator {
|
|
38
|
+
validate(_epochIndex: Epoch, ticket: SignedTicket[]): Promise<Result<ValidatedTicket[], ValidationError>>;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Rejects every ticket. Used as the default for any task that needs an explicit, real
|
|
42
|
+
* validator wired in before it will accept anything from the network.
|
|
43
|
+
*/
|
|
44
|
+
export declare class DenyTicketsValidator implements TicketValidator {
|
|
45
|
+
validate(_epochIndex: Epoch, _tickets: SignedTicket[]): Promise<Result<ValidatedTicket[], ValidationError>>;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=ticket-validator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ticket-validator.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/ticket-pool/ticket-validator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAGhE,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE1C;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,YAAY,CAAC;IACrB,EAAE,EAAE,WAAW,CAAC;CACjB,CAAC;AAEF,4CAA4C;AAC5C,oBAAY,eAAe;IACzB,+CAA+C;IAC/C,YAAY,kBAAkB;IAC9B,oFAAoF;IACpF,oBAAoB,0BAA0B;IAC9C,yEAAyE;IACzE,UAAU,gBAAgB;CAC3B;AAED;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,EAAE,eAAe,CAAC,CAAC,CAAC;CAC3G;AAED;;;GAGG;AACH,qBAAa,sBAAuB,YAAW,eAAe;IACtD,QAAQ,CAAC,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,EAAE,eAAe,CAAC,CAAC;CAQhH;AAED;;;GAGG;AACH,qBAAa,oBAAqB,YAAW,eAAe;IACpD,QAAQ,CAAC,WAAW,EAAE,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,EAAE,eAAe,CAAC,CAAC;CAGlH"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { Bytes } from "#@typeberry/bytes";
|
|
2
|
+
import { HASH_SIZE } from "#@typeberry/hash";
|
|
3
|
+
import { Result } from "#@typeberry/utils";
|
|
4
|
+
/** Reasons a ticket may fail validation. */
|
|
5
|
+
export var ValidationError;
|
|
6
|
+
(function (ValidationError) {
|
|
7
|
+
/** Verifier rejected the signature / proof. */
|
|
8
|
+
ValidationError["InvalidProof"] = "invalid_proof";
|
|
9
|
+
/** Validator could not run (e.g. state unavailable, transient internal failure). */
|
|
10
|
+
ValidationError["ValidatorUnavailable"] = "validator_unavailable";
|
|
11
|
+
/** Ticket is for an epoch outside the validator's window of interest. */
|
|
12
|
+
ValidationError["WrongEpoch"] = "wrong_epoch";
|
|
13
|
+
})(ValidationError || (ValidationError = {}));
|
|
14
|
+
/**
|
|
15
|
+
* Accepts every ticket without inspection. Useful for unit tests where the validator
|
|
16
|
+
* isn't the subject under test. Must never be used in production.
|
|
17
|
+
*/
|
|
18
|
+
export class AcceptTicketsValidator {
|
|
19
|
+
async validate(_epochIndex, ticket) {
|
|
20
|
+
return Result.ok(ticket.map((ticket) => ({
|
|
21
|
+
ticket,
|
|
22
|
+
id: Bytes.zero(HASH_SIZE).asOpaque(),
|
|
23
|
+
})));
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Rejects every ticket. Used as the default for any task that needs an explicit, real
|
|
28
|
+
* validator wired in before it will accept anything from the network.
|
|
29
|
+
*/
|
|
30
|
+
export class DenyTicketsValidator {
|
|
31
|
+
async validate(_epochIndex, _tickets) {
|
|
32
|
+
return Result.error(ValidationError.ValidatorUnavailable, () => "no ticket validator wired");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ticket-validator.test.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/ticket-pool/ticket-validator.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { tryAsEpoch } from "#@typeberry/block";
|
|
4
|
+
import { SignedTicket, tryAsTicketAttempt } from "#@typeberry/block/tickets.js";
|
|
5
|
+
import { Bytes } from "#@typeberry/bytes";
|
|
6
|
+
import { BANDERSNATCH_PROOF_BYTES } from "#@typeberry/crypto";
|
|
7
|
+
import { HASH_SIZE } from "#@typeberry/hash";
|
|
8
|
+
import { AcceptTicketsValidator, DenyTicketsValidator, ValidationError } from "./ticket-validator.js";
|
|
9
|
+
const E1 = tryAsEpoch(1);
|
|
10
|
+
function makeTicket() {
|
|
11
|
+
return SignedTicket.create({
|
|
12
|
+
attempt: tryAsTicketAttempt(0),
|
|
13
|
+
signature: Bytes.zero(BANDERSNATCH_PROOF_BYTES).asOpaque(),
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
describe("AcceptTicketsValidator", () => {
|
|
17
|
+
it("returns ok with zero id", async () => {
|
|
18
|
+
const v = new AcceptTicketsValidator();
|
|
19
|
+
const res = await v.validate(E1, [makeTicket()]);
|
|
20
|
+
assert.strictEqual(res.isOk, true);
|
|
21
|
+
if (res.isOk) {
|
|
22
|
+
assert.strictEqual(res.ok[0].id.toString(), Bytes.zero(HASH_SIZE).toString());
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
describe("DenyTicketsValidator", () => {
|
|
27
|
+
it("returns ValidatorUnavailable", async () => {
|
|
28
|
+
const v = new DenyTicketsValidator();
|
|
29
|
+
const res = await v.validate(E1, [makeTicket()]);
|
|
30
|
+
assert.strictEqual(res.isError, true);
|
|
31
|
+
if (res.isError) {
|
|
32
|
+
assert.strictEqual(res.error, ValidationError.ValidatorUnavailable);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { EntropyHash, Epoch } from "#@typeberry/block";
|
|
2
|
+
import type { SignedTicket } from "#@typeberry/block/tickets.js";
|
|
3
|
+
/** A ticket the validator already verified, paired with the entropy hash (ticket id). */
|
|
4
|
+
export type VerifiedTicket = {
|
|
5
|
+
ticket: SignedTicket;
|
|
6
|
+
id: EntropyHash;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* In-memory pool of verified tickets for the current epoch, keyed by ticket id.
|
|
10
|
+
*
|
|
11
|
+
* Used on the authorship side. Tickets are stored per epoch and deduplicated by their
|
|
12
|
+
* computed entropy hash (so duplicates arriving via different peers / paths are coalesced
|
|
13
|
+
* cheaply). The pool only ever needs to hold tickets for one epoch at a time; switching
|
|
14
|
+
* to a new epoch clears everything older.
|
|
15
|
+
*/
|
|
16
|
+
export declare class VerifiedTicketPool {
|
|
17
|
+
private readonly perEpoch;
|
|
18
|
+
private readonly idSets;
|
|
19
|
+
static new(): VerifiedTicketPool;
|
|
20
|
+
private constructor();
|
|
21
|
+
/** Add pre-verified tickets to the pool, deduping by id. */
|
|
22
|
+
add(epochIndex: Epoch, verifiedTickets: readonly VerifiedTicket[]): void;
|
|
23
|
+
/** Returns the verified tickets for the given epoch, or an empty array if none. */
|
|
24
|
+
getForEpoch(epochIndex: Epoch): readonly VerifiedTicket[];
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=verified-ticket-pool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verified-ticket-pool.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/ticket-pool/verified-ticket-pool.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAGhE,yFAAyF;AACzF,MAAM,MAAM,cAAc,GAAG;IAC3B,MAAM,EAAE,YAAY,CAAC;IACrB,EAAE,EAAE,WAAW,CAAC;CACjB,CAAC;AAEF;;;;;;;GAOG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsC;IAC/D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0C;IAEjE,MAAM,CAAC,GAAG;IAIV,OAAO;IAEP,4DAA4D;IAC5D,GAAG,CAAC,UAAU,EAAE,KAAK,EAAE,eAAe,EAAE,SAAS,cAAc,EAAE,GAAG,IAAI;IAoBxE,mFAAmF;IACnF,WAAW,CAAC,UAAU,EAAE,KAAK,GAAG,SAAS,cAAc,EAAE;CAG1D"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { HashSet } from "#@typeberry/collections/hash-set.js";
|
|
2
|
+
/**
|
|
3
|
+
* In-memory pool of verified tickets for the current epoch, keyed by ticket id.
|
|
4
|
+
*
|
|
5
|
+
* Used on the authorship side. Tickets are stored per epoch and deduplicated by their
|
|
6
|
+
* computed entropy hash (so duplicates arriving via different peers / paths are coalesced
|
|
7
|
+
* cheaply). The pool only ever needs to hold tickets for one epoch at a time; switching
|
|
8
|
+
* to a new epoch clears everything older.
|
|
9
|
+
*/
|
|
10
|
+
export class VerifiedTicketPool {
|
|
11
|
+
perEpoch = new Map();
|
|
12
|
+
idSets = new Map();
|
|
13
|
+
static new() {
|
|
14
|
+
return new VerifiedTicketPool();
|
|
15
|
+
}
|
|
16
|
+
constructor() { }
|
|
17
|
+
/** Add pre-verified tickets to the pool, deduping by id. */
|
|
18
|
+
add(epochIndex, verifiedTickets) {
|
|
19
|
+
if (this.perEpoch.size > 0 && !this.perEpoch.has(epochIndex)) {
|
|
20
|
+
this.perEpoch.clear();
|
|
21
|
+
this.idSets.clear();
|
|
22
|
+
}
|
|
23
|
+
const existing = this.perEpoch.get(epochIndex) ?? [];
|
|
24
|
+
let idSet = this.idSets.get(epochIndex) ?? null;
|
|
25
|
+
if (idSet === null) {
|
|
26
|
+
idSet = HashSet.new();
|
|
27
|
+
this.idSets.set(epochIndex, idSet);
|
|
28
|
+
}
|
|
29
|
+
for (const entry of verifiedTickets) {
|
|
30
|
+
if (!idSet.has(entry.id)) {
|
|
31
|
+
existing.push(entry);
|
|
32
|
+
idSet.insert(entry.id);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
this.perEpoch.set(epochIndex, existing);
|
|
36
|
+
}
|
|
37
|
+
/** Returns the verified tickets for the given epoch, or an empty array if none. */
|
|
38
|
+
getForEpoch(epochIndex) {
|
|
39
|
+
return this.perEpoch.get(epochIndex) ?? [];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verified-ticket-pool.test.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/ticket-pool/verified-ticket-pool.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { tryAsEpoch } from "#@typeberry/block";
|
|
4
|
+
import { SignedTicket, tryAsTicketAttempt } from "#@typeberry/block/tickets.js";
|
|
5
|
+
import { Bytes } from "#@typeberry/bytes";
|
|
6
|
+
import { BANDERSNATCH_PROOF_BYTES } from "#@typeberry/crypto";
|
|
7
|
+
import { HASH_SIZE } from "#@typeberry/hash";
|
|
8
|
+
import { VerifiedTicketPool } from "./verified-ticket-pool.js";
|
|
9
|
+
const E1 = tryAsEpoch(1);
|
|
10
|
+
const E2 = tryAsEpoch(2);
|
|
11
|
+
function makeTicket(seed) {
|
|
12
|
+
const sig = Bytes.zero(BANDERSNATCH_PROOF_BYTES);
|
|
13
|
+
sig.raw[0] = seed;
|
|
14
|
+
return SignedTicket.create({
|
|
15
|
+
attempt: tryAsTicketAttempt(0),
|
|
16
|
+
signature: sig.asOpaque(),
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function makeId(byte) {
|
|
20
|
+
return Bytes.fill(HASH_SIZE, byte).asOpaque();
|
|
21
|
+
}
|
|
22
|
+
describe("VerifiedTicketPool", () => {
|
|
23
|
+
it("starts empty", () => {
|
|
24
|
+
const pool = VerifiedTicketPool.new();
|
|
25
|
+
assert.deepStrictEqual(pool.getForEpoch(E1), []);
|
|
26
|
+
});
|
|
27
|
+
it("adds and retrieves tickets per epoch", () => {
|
|
28
|
+
const pool = VerifiedTicketPool.new();
|
|
29
|
+
pool.add(E1, [{ ticket: makeTicket(1), id: makeId(0xaa) }]);
|
|
30
|
+
assert.strictEqual(pool.getForEpoch(E1).length, 1);
|
|
31
|
+
assert.deepStrictEqual(pool.getForEpoch(E2), []);
|
|
32
|
+
});
|
|
33
|
+
it("dedups by id", () => {
|
|
34
|
+
const pool = VerifiedTicketPool.new();
|
|
35
|
+
const id = makeId(0x01);
|
|
36
|
+
pool.add(E1, [{ ticket: makeTicket(1), id }]);
|
|
37
|
+
pool.add(E1, [{ ticket: makeTicket(2), id }]);
|
|
38
|
+
assert.strictEqual(pool.getForEpoch(E1).length, 1);
|
|
39
|
+
assert.strictEqual(pool.getForEpoch(E1)[0].ticket.signature.raw[0], 1);
|
|
40
|
+
});
|
|
41
|
+
it("clears previous epochs when a new epoch is added", () => {
|
|
42
|
+
const pool = VerifiedTicketPool.new();
|
|
43
|
+
pool.add(E1, [{ ticket: makeTicket(1), id: makeId(1) }]);
|
|
44
|
+
pool.add(E2, [{ ticket: makeTicket(2), id: makeId(2) }]);
|
|
45
|
+
assert.deepStrictEqual(pool.getForEpoch(E1), []);
|
|
46
|
+
assert.strictEqual(pool.getForEpoch(E2).length, 1);
|
|
47
|
+
});
|
|
48
|
+
it("appends across multiple add() calls for the same epoch", () => {
|
|
49
|
+
const pool = VerifiedTicketPool.new();
|
|
50
|
+
pool.add(E1, [{ ticket: makeTicket(1), id: makeId(1) }]);
|
|
51
|
+
pool.add(E1, [{ ticket: makeTicket(2), id: makeId(2) }]);
|
|
52
|
+
assert.strictEqual(pool.getForEpoch(E1).length, 2);
|
|
53
|
+
});
|
|
54
|
+
});
|