@typeberry/lib 0.8.4-7a69737 → 0.8.4-7d1719f
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 +5 -1
- 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/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 +46 -0
- package/packages/jam/ticket-pool/ticket-validator.d.ts.map +1 -0
- package/packages/jam/ticket-pool/ticket-validator.js +29 -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 +34 -0
- package/packages/jam/ticket-pool/verified-ticket-pool.d.ts +24 -0
- package/packages/jam/ticket-pool/verified-ticket-pool.d.ts.map +1 -0
- package/packages/jam/ticket-pool/verified-ticket-pool.js +37 -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/main.d.ts.map +1 -1
- package/packages/workers/block-authorship/main.js +22 -74
- package/packages/workers/block-authorship/ticket-validator.d.ts +32 -0
- package/packages/workers/block-authorship/ticket-validator.d.ts.map +1 -0
- package/packages/workers/block-authorship/ticket-validator.js +56 -0
- package/packages/workers/comms-authorship-network/protocol.d.ts +10 -0
- package/packages/workers/comms-authorship-network/protocol.d.ts.map +1 -1
- package/packages/workers/comms-authorship-network/protocol.js +8 -1
- package/packages/workers/jam-network/main.d.ts.map +1 -1
- package/packages/workers/jam-network/main.js +20 -4
|
@@ -0,0 +1,34 @@
|
|
|
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 { AcceptTicketsValidator, DenyTicketsValidator, ValidationError } from "./ticket-validator.js";
|
|
8
|
+
const E1 = tryAsEpoch(1);
|
|
9
|
+
function makeTicket() {
|
|
10
|
+
return SignedTicket.create({
|
|
11
|
+
attempt: tryAsTicketAttempt(0),
|
|
12
|
+
signature: Bytes.zero(BANDERSNATCH_PROOF_BYTES).asOpaque(),
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
describe("AcceptTicketsValidator", () => {
|
|
16
|
+
it("returns ok with null id", async () => {
|
|
17
|
+
const v = new AcceptTicketsValidator();
|
|
18
|
+
const res = await v.validate(E1, makeTicket());
|
|
19
|
+
assert.strictEqual(res.isOk, true);
|
|
20
|
+
if (res.isOk) {
|
|
21
|
+
assert.strictEqual(res.ok.id, null);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
describe("DenyTicketsValidator", () => {
|
|
26
|
+
it("returns ValidatorUnavailable", async () => {
|
|
27
|
+
const v = new DenyTicketsValidator();
|
|
28
|
+
const res = await v.validate(E1, makeTicket());
|
|
29
|
+
assert.strictEqual(res.isError, true);
|
|
30
|
+
if (res.isError) {
|
|
31
|
+
assert.strictEqual(res.error, ValidationError.ValidatorUnavailable);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
/** Add pre-verified tickets to the pool, deduping by id. */
|
|
20
|
+
add(epochIndex: Epoch, verifiedTickets: readonly VerifiedTicket[]): void;
|
|
21
|
+
/** Returns the verified tickets for the given epoch, or an empty array if none. */
|
|
22
|
+
getForEpoch(epochIndex: Epoch): readonly VerifiedTicket[];
|
|
23
|
+
}
|
|
24
|
+
//# 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,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,37 @@
|
|
|
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
|
+
/** Add pre-verified tickets to the pool, deduping by id. */
|
|
14
|
+
add(epochIndex, verifiedTickets) {
|
|
15
|
+
if (this.perEpoch.size > 0 && !this.perEpoch.has(epochIndex)) {
|
|
16
|
+
this.perEpoch.clear();
|
|
17
|
+
this.idSets.clear();
|
|
18
|
+
}
|
|
19
|
+
const existing = this.perEpoch.get(epochIndex) ?? [];
|
|
20
|
+
let idSet = this.idSets.get(epochIndex) ?? null;
|
|
21
|
+
if (idSet === null) {
|
|
22
|
+
idSet = HashSet.new();
|
|
23
|
+
this.idSets.set(epochIndex, idSet);
|
|
24
|
+
}
|
|
25
|
+
for (const entry of verifiedTickets) {
|
|
26
|
+
if (!idSet.has(entry.id)) {
|
|
27
|
+
existing.push(entry);
|
|
28
|
+
idSet.insert(entry.id);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
this.perEpoch.set(epochIndex, existing);
|
|
32
|
+
}
|
|
33
|
+
/** Returns the verified tickets for the given epoch, or an empty array if none. */
|
|
34
|
+
getForEpoch(epochIndex) {
|
|
35
|
+
return this.perEpoch.get(epochIndex) ?? [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -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 = new VerifiedTicketPool();
|
|
25
|
+
assert.deepStrictEqual(pool.getForEpoch(E1), []);
|
|
26
|
+
});
|
|
27
|
+
it("adds and retrieves tickets per epoch", () => {
|
|
28
|
+
const pool = new VerifiedTicketPool();
|
|
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 = new VerifiedTicketPool();
|
|
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 = new VerifiedTicketPool();
|
|
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 = new VerifiedTicketPool();
|
|
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
|
+
});
|
|
@@ -71,13 +71,19 @@ export declare class InMemWorkerConfig<T = undefined> implements WorkerConfig<T,
|
|
|
71
71
|
readonly: boolean;
|
|
72
72
|
}): RootDb<BlocksDb, SerializedStatesDb>;
|
|
73
73
|
}
|
|
74
|
+
/** Persistent values store backing the hybrid config. */
|
|
75
|
+
export type HybridBackend = "lmdb" | "fjall";
|
|
74
76
|
/**
|
|
75
77
|
* Hybrid worker config for the fuzz target: in-memory blocks and leaf sets,
|
|
76
|
-
* but large values persisted to LMDB.
|
|
78
|
+
* but large values persisted to disk (LMDB or fjall, selected by `backend`).
|
|
79
|
+
*
|
|
80
|
+
* The fjall backend is opt-in so its performance can be compared against LMDB
|
|
81
|
+
* before committing to it. fjall opens its keyspace asynchronously, hence the
|
|
82
|
+
* async `new`.
|
|
77
83
|
*
|
|
78
84
|
* Like `InMemWorkerConfig`, the blocks and leaf sets are shared across the
|
|
79
85
|
* open/close/reopen dance that genesis init performs, so `openDatabase`
|
|
80
|
-
* returns the same instances and a no-op close. The
|
|
86
|
+
* returns the same instances and a no-op close. The values store is opened once
|
|
81
87
|
* here and closed by `HybridSerializedStates.close()` at importer teardown.
|
|
82
88
|
*
|
|
83
89
|
* In-process only: it holds shared mutable state (the in-memory leaf
|
|
@@ -92,7 +98,8 @@ export declare class HybridWorkerConfig<T = undefined> implements WorkerConfig<T
|
|
|
92
98
|
readonly dbPath: string;
|
|
93
99
|
readonly ephemeral: boolean;
|
|
94
100
|
readonly compression: boolean;
|
|
95
|
-
|
|
101
|
+
private readonly states;
|
|
102
|
+
static new<T>({ nodeName, chainSpec, workerParams, blake2b, dbPath, ephemeral, compression, backend, }: {
|
|
96
103
|
nodeName: string;
|
|
97
104
|
chainSpec: ChainSpec;
|
|
98
105
|
workerParams: T;
|
|
@@ -100,9 +107,9 @@ export declare class HybridWorkerConfig<T = undefined> implements WorkerConfig<T
|
|
|
100
107
|
dbPath: string;
|
|
101
108
|
ephemeral?: boolean;
|
|
102
109
|
compression?: boolean;
|
|
103
|
-
|
|
110
|
+
backend?: HybridBackend;
|
|
111
|
+
}): Promise<HybridWorkerConfig<T>>;
|
|
104
112
|
private readonly blocks;
|
|
105
|
-
private readonly states;
|
|
106
113
|
private constructor();
|
|
107
114
|
openDatabase(_options?: {
|
|
108
115
|
readonly: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../../../packages/workers/api-node/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAE,KAAK,MAAM,EAAW,KAAK,MAAM,EAAW,MAAM,kBAAkB,CAAC;AAC9E,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EACL,KAAK,QAAQ,EAGb,KAAK,MAAM,EACX,KAAK,kBAAkB,EACxB,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../../../../packages/workers/api-node/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,EAAE,KAAK,MAAM,EAAW,KAAK,MAAM,EAAW,MAAM,kBAAkB,CAAC;AAC9E,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,EACL,KAAK,QAAQ,EAGb,KAAK,MAAM,EACX,KAAK,kBAAkB,EACxB,MAAM,qBAAqB,CAAC;AAQ7B,OAAO,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAC1C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,KAAK,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAE9D,+EAA+E;AAC/E,qBAAa,gBAAgB,CAAC,CAAC,GAAG,IAAI,CAAE,YAAW,YAAY,CAAC,CAAC,EAAE,QAAQ,EAAE,kBAAkB,CAAC;aAyC5E,QAAQ,EAAE,MAAM;aAChB,SAAS,EAAE,SAAS;aACpB,YAAY,EAAE,CAAC;aACf,MAAM,EAAE,MAAM;aACd,OAAO,EAAE,OAAO;aAChB,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC;aAI9B,SAAS,EAAE,OAAO;IAjDpC,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,EACZ,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,MAAM,EACN,OAAO,EACP,KAAiB,EACjB,SAAiB,GAClB,EAAE;QACD,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,SAAS,CAAC;QACrB,YAAY,EAAE,CAAC,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,OAAO,CAAC;QACjB,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;QAChC,SAAS,CAAC,EAAE,OAAO,CAAC;KACrB;IAID,6DAA6D;WAChD,gBAAgB,CAAC,CAAC,EAAE,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,kBAAkB;IAkBpF,OAAO;IAaP,YAAY,CAAC,OAAO,GAAE;QAAE,QAAQ,EAAE,OAAO,CAAA;KAAuB,GAAG,MAAM,CAAC,QAAQ,EAAE,kBAAkB,CAAC;IAavG,6DAA6D;IAC7D,gBAAgB,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,kBAAkB;CAS7D;AAED,6DAA6D;AAC7D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,SAAS,CAAC;IACrB,YAAY,EAAE,UAAU,CAAC;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,CAAC,MAAM,EAAE,gBAAgB,CAAC,EAAE,CAAC;CAC3C,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,kBAAkB,GAAG,WAAW,EAAE,CAE5E;AAED;;;;GAIG;AACH,qBAAa,iBAAiB,CAAC,CAAC,GAAG,SAAS,CAAE,YAAW,YAAY,CAAC,CAAC,EAAE,QAAQ,EAAE,kBAAkB,CAAC;aAmBlF,QAAQ,EAAE,MAAM;aAChB,SAAS,EAAE,SAAS;aACpB,YAAY,EAAE,CAAC;aACf,OAAO,EAAE,OAAO;IArBlC,MAAM,CAAC,GAAG,CAAC,CAAC,EAAE,EACZ,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,OAAO,GACR,EAAE;QACD,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,SAAS,CAAC;QACrB,YAAY,EAAE,CAAC,CAAC;QAChB,OAAO,EAAE,OAAO,CAAC;KAClB;IAID,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IACxC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA2B;IAElD,OAAO;IAUP,YAAY,CAAC,QAAQ,GAAE;QAAE,QAAQ,EAAE,OAAO,CAAA;KAAuB,GAAG,MAAM,CAAC,QAAQ,EAAE,kBAAkB,CAAC;CAQzG;AAED,yDAAyD;AACzD,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,OAAO,CAAC;AAE7C;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,kBAAkB,CAAC,CAAC,GAAG,SAAS,CAAE,YAAW,YAAY,CAAC,CAAC,EAAE,QAAQ,EAAE,kBAAkB,CAAC;aAgCnF,QAAQ,EAAE,MAAM;aAChB,SAAS,EAAE,SAAS;aACpB,YAAY,EAAE,CAAC;aACf,OAAO,EAAE,OAAO;aAChB,MAAM,EAAE,MAAM;aACd,SAAS,EAAE,OAAO;aAClB,WAAW,EAAE,OAAO;IACpC,OAAO,CAAC,QAAQ,CAAC,MAAM;WAtCZ,GAAG,CAAC,CAAC,EAAE,EAClB,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,OAAO,EACP,MAAM,EACN,SAAiB,EACjB,WAAkB,EAClB,OAAgB,GACjB,EAAE;QACD,QAAQ,EAAE,MAAM,CAAC;QACjB,SAAS,EAAE,SAAS,CAAC;QACrB,YAAY,EAAE,CAAC,CAAC;QAChB,OAAO,EAAE,OAAO,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,CAAC,EAAE,OAAO,CAAC;QACpB,WAAW,CAAC,EAAE,OAAO,CAAC;QACtB,OAAO,CAAC,EAAE,aAAa,CAAC;KACzB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC;IAUlC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAiB;IAExC,OAAO;IAaP,YAAY,CAAC,QAAQ,GAAE;QAAE,QAAQ,EAAE,OAAO,CAAA;KAAuB,GAAG,MAAM,CAAC,QAAQ,EAAE,kBAAkB,CAAC;CASzG"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Decoder, Encoder } from "#@typeberry/codec";
|
|
2
2
|
import { ChainSpec } from "#@typeberry/config";
|
|
3
3
|
import { InMemoryBlocks, InMemorySerializedStates, } from "#@typeberry/database";
|
|
4
|
-
import { HybridSerializedStates
|
|
4
|
+
import { HybridSerializedStates as FjallHybridSerializedStates } from "#@typeberry/database-fjall";
|
|
5
|
+
import { LmdbBlocks, HybridSerializedStates as LmdbHybridSerializedStates, LmdbRoot, LmdbStates, } from "#@typeberry/database-lmdb";
|
|
5
6
|
import { Blake2b } from "#@typeberry/hash";
|
|
6
7
|
import { ThreadPort } from "./port.js";
|
|
7
8
|
/** A worker config that's usable in node.js and uses LMDB database backend. */
|
|
@@ -110,11 +111,15 @@ export class InMemWorkerConfig {
|
|
|
110
111
|
}
|
|
111
112
|
/**
|
|
112
113
|
* Hybrid worker config for the fuzz target: in-memory blocks and leaf sets,
|
|
113
|
-
* but large values persisted to LMDB.
|
|
114
|
+
* but large values persisted to disk (LMDB or fjall, selected by `backend`).
|
|
115
|
+
*
|
|
116
|
+
* The fjall backend is opt-in so its performance can be compared against LMDB
|
|
117
|
+
* before committing to it. fjall opens its keyspace asynchronously, hence the
|
|
118
|
+
* async `new`.
|
|
114
119
|
*
|
|
115
120
|
* Like `InMemWorkerConfig`, the blocks and leaf sets are shared across the
|
|
116
121
|
* open/close/reopen dance that genesis init performs, so `openDatabase`
|
|
117
|
-
* returns the same instances and a no-op close. The
|
|
122
|
+
* returns the same instances and a no-op close. The values store is opened once
|
|
118
123
|
* here and closed by `HybridSerializedStates.close()` at importer teardown.
|
|
119
124
|
*
|
|
120
125
|
* In-process only: it holds shared mutable state (the in-memory leaf
|
|
@@ -129,12 +134,17 @@ export class HybridWorkerConfig {
|
|
|
129
134
|
dbPath;
|
|
130
135
|
ephemeral;
|
|
131
136
|
compression;
|
|
132
|
-
|
|
133
|
-
|
|
137
|
+
states;
|
|
138
|
+
static async new({ nodeName, chainSpec, workerParams, blake2b, dbPath, ephemeral = false, compression = true, backend = "lmdb", }) {
|
|
139
|
+
// fjall opens its keyspace asynchronously; LMDB is synchronous. Either way
|
|
140
|
+
// the values store is created once here and shared across reopen.
|
|
141
|
+
const states = backend === "fjall"
|
|
142
|
+
? await FjallHybridSerializedStates.new({ spec: chainSpec, blake2b, dbPath, ephemeral })
|
|
143
|
+
: LmdbHybridSerializedStates.new({ spec: chainSpec, blake2b, dbPath, ephemeral, compression, readOnly: false });
|
|
144
|
+
return new HybridWorkerConfig(nodeName, chainSpec, workerParams, blake2b, dbPath, ephemeral, compression, states);
|
|
134
145
|
}
|
|
135
146
|
blocks;
|
|
136
|
-
states
|
|
137
|
-
constructor(nodeName, chainSpec, workerParams, blake2b, dbPath, ephemeral, compression = true) {
|
|
147
|
+
constructor(nodeName, chainSpec, workerParams, blake2b, dbPath, ephemeral, compression, states) {
|
|
138
148
|
this.nodeName = nodeName;
|
|
139
149
|
this.chainSpec = chainSpec;
|
|
140
150
|
this.workerParams = workerParams;
|
|
@@ -142,22 +152,15 @@ export class HybridWorkerConfig {
|
|
|
142
152
|
this.dbPath = dbPath;
|
|
143
153
|
this.ephemeral = ephemeral;
|
|
144
154
|
this.compression = compression;
|
|
155
|
+
this.states = states;
|
|
145
156
|
this.blocks = InMemoryBlocks.new();
|
|
146
|
-
this.states = HybridSerializedStates.new({
|
|
147
|
-
spec: this.chainSpec,
|
|
148
|
-
blake2b: this.blake2b,
|
|
149
|
-
dbPath: this.dbPath,
|
|
150
|
-
ephemeral: this.ephemeral,
|
|
151
|
-
compression: this.compression,
|
|
152
|
-
readOnly: false,
|
|
153
|
-
});
|
|
154
157
|
}
|
|
155
158
|
openDatabase(_options = { readonly: true }) {
|
|
156
159
|
return {
|
|
157
160
|
getBlocksDb: () => this.blocks,
|
|
158
161
|
getStatesDb: () => this.states,
|
|
159
|
-
// Leaf sets and blocks live in memory; the
|
|
160
|
-
//
|
|
162
|
+
// Leaf sets and blocks live in memory; the values store is closed via
|
|
163
|
+
// states.close() at importer teardown, so this is a no-op.
|
|
161
164
|
close: async () => { },
|
|
162
165
|
};
|
|
163
166
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import assert from "node:assert";
|
|
2
|
+
import * as fs from "node:fs";
|
|
2
3
|
import { describe, it } from "node:test";
|
|
3
4
|
import { MessageChannel } from "node:worker_threads";
|
|
4
5
|
import { codec } from "#@typeberry/codec";
|
|
5
6
|
import { tinyChainSpec } from "#@typeberry/config";
|
|
6
7
|
import { Blake2b } from "#@typeberry/hash";
|
|
7
8
|
import { tryAsU32 } from "#@typeberry/numbers";
|
|
8
|
-
import { configTransferList, LmdbWorkerConfig } from "./config.js";
|
|
9
|
+
import { configTransferList, HybridWorkerConfig, LmdbWorkerConfig } from "./config.js";
|
|
9
10
|
import { ThreadPort } from "./port.js";
|
|
10
11
|
const spec = tinyChainSpec;
|
|
11
12
|
describe("LmdbWorkerConfig transfer list", () => {
|
|
@@ -39,3 +40,39 @@ describe("LmdbWorkerConfig transfer list", () => {
|
|
|
39
40
|
}
|
|
40
41
|
});
|
|
41
42
|
});
|
|
43
|
+
describe("HybridWorkerConfig", () => {
|
|
44
|
+
// Both persistent backends must construct asynchronously and hand out a
|
|
45
|
+
// working db. fjall is the experimental backend we want to benchmark.
|
|
46
|
+
for (const backend of ["lmdb", "fjall"]) {
|
|
47
|
+
it(`constructs and opens a ${backend}-backed hybrid db`, async () => {
|
|
48
|
+
const blake2b = await Blake2b.createHasher();
|
|
49
|
+
const dbPath = fs.mkdtempSync(`typeberry-hybrid-${backend}-`);
|
|
50
|
+
try {
|
|
51
|
+
const config = await HybridWorkerConfig.new({
|
|
52
|
+
nodeName: "node",
|
|
53
|
+
chainSpec: spec,
|
|
54
|
+
workerParams: undefined,
|
|
55
|
+
blake2b,
|
|
56
|
+
dbPath,
|
|
57
|
+
ephemeral: true,
|
|
58
|
+
backend,
|
|
59
|
+
});
|
|
60
|
+
const db = config.openDatabase({ readonly: false });
|
|
61
|
+
const states = db.getStatesDb();
|
|
62
|
+
try {
|
|
63
|
+
assert.notStrictEqual(db.getBlocksDb(), undefined);
|
|
64
|
+
assert.notStrictEqual(states, undefined);
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
// The values store owns the on-disk resources (the no-op db.close()
|
|
68
|
+
// does not), so close it explicitly to release the fjall keyspace.
|
|
69
|
+
await states.close();
|
|
70
|
+
await db.close();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
fs.rmSync(dbPath, { recursive: true, force: true });
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../../../../packages/workers/block-authorship/main.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../../../../packages/workers/block-authorship/main.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qCAAqC,CAAC;AAkB3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAE3D,OAAO,KAAK,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAM9E,KAAK,MAAM,GAAG,YAAY,CAAC,qBAAqB,CAAC,CAAC;AAwBlD,wBAAsB,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,eAAe,iBAuWpG"}
|
|
@@ -13,9 +13,11 @@ import bandersnatchVrf from "#@typeberry/safrole/bandersnatch-vrf.js";
|
|
|
13
13
|
import { BandernsatchWasm } from "#@typeberry/safrole/bandersnatch-wasm.js";
|
|
14
14
|
import { JAM_FALLBACK_SEAL, JAM_TICKET_SEAL } from "#@typeberry/safrole/constants.js";
|
|
15
15
|
import { SafroleSealingKeysKind } from "#@typeberry/state";
|
|
16
|
+
import { VerifiedTicketPool } from "#@typeberry/ticket-pool";
|
|
16
17
|
import { asOpaqueType, Result } from "#@typeberry/utils";
|
|
17
18
|
import { Generator } from "./generator.js";
|
|
18
19
|
import { generateTickets } from "./ticket-generator.js";
|
|
20
|
+
import { BandersnatchTicketValidator } from "./ticket-validator.js";
|
|
19
21
|
const logger = Logger.new(import.meta.filename, "author");
|
|
20
22
|
export async function main(config, comms, networkingComms) {
|
|
21
23
|
await initWasm();
|
|
@@ -181,80 +183,16 @@ export async function main(config, comms, networkingComms) {
|
|
|
181
183
|
}
|
|
182
184
|
return Result.ok(state.sealingKeySeries);
|
|
183
185
|
}
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
const
|
|
187
|
-
const
|
|
188
|
-
/**
|
|
189
|
-
* Adds pre-verified tickets to the in-memory ticket pool for the given epoch.
|
|
190
|
-
*
|
|
191
|
-
* Clears the pool when the epoch changes (we only ever need tickets for one epoch at a time).
|
|
192
|
-
* Deduplicates by ticket ID using a HashSet for O(1) lookup — prevents double-counting
|
|
193
|
-
* tickets received from multiple peers or via both CE-131 and CE-132 paths.
|
|
194
|
-
*/
|
|
195
|
-
function addToPool(epochIndex, verifiedTickets) {
|
|
196
|
-
if (ticketPool.size > 0 && !ticketPool.has(epochIndex)) {
|
|
197
|
-
ticketPool.clear();
|
|
198
|
-
ticketIdSets.clear();
|
|
199
|
-
}
|
|
200
|
-
const existing = ticketPool.get(epochIndex) ?? [];
|
|
201
|
-
let idSet = ticketIdSets.get(epochIndex) ?? null;
|
|
202
|
-
if (idSet === null) {
|
|
203
|
-
idSet = HashSet.new();
|
|
204
|
-
ticketIdSets.set(epochIndex, idSet);
|
|
205
|
-
}
|
|
206
|
-
for (const entry of verifiedTickets) {
|
|
207
|
-
if (!idSet.has(entry.id)) {
|
|
208
|
-
existing.push(entry);
|
|
209
|
-
idSet.insert(entry.id);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
ticketPool.set(epochIndex, existing);
|
|
213
|
-
}
|
|
214
|
-
/**
|
|
215
|
-
* Returns the correct tickets entropy for verification given the current state.
|
|
216
|
-
*
|
|
217
|
-
* When `state` is from epoch E-1 (i.e. we haven't produced epoch E's first block yet),
|
|
218
|
-
* the ticket entropy for epoch E is at index 1 (not yet shifted).
|
|
219
|
-
* After the epoch transition it moves to index 2.
|
|
220
|
-
*/
|
|
221
|
-
function getTicketEntropy(epochIndex, state) {
|
|
222
|
-
const stateEpoch = Math.floor(state.timeslot / chainSpec.epochLength);
|
|
223
|
-
return epochIndex > stateEpoch ? state.entropy[1] : state.entropy[2];
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* Verifies tickets against the ring commitment and current epoch entropy, then adds valid
|
|
227
|
-
* ones to the pool with their computed IDs.
|
|
228
|
-
*
|
|
229
|
-
* Called both for own generated tickets and for tickets relayed from peers.
|
|
230
|
-
* Verification computes the ticket ID (entropyHash) which is then used for
|
|
231
|
-
* deduplication in the pool and later when building the extrinsic.
|
|
232
|
-
*/
|
|
233
|
-
async function verifyAndAddToPool(epochIndex, tickets, state) {
|
|
234
|
-
const results = await bandersnatchVrf.verifyTickets(bandersnatch, state.designatedValidatorData.length, state.epochRoot, tickets, getTicketEntropy(epochIndex, state));
|
|
235
|
-
if (results.tickets.length !== tickets.length) {
|
|
236
|
-
logger.error `verifyTickets returned ${results.tickets.length} results for ${tickets.length} tickets`;
|
|
237
|
-
return false;
|
|
238
|
-
}
|
|
239
|
-
// Batch verification: either the whole batch is valid or none of the tickets are.
|
|
240
|
-
if (!results.isValid) {
|
|
241
|
-
return false;
|
|
242
|
-
}
|
|
243
|
-
const verified = tickets.map((ticket, i) => ({ ticket, id: results.tickets[i] }));
|
|
244
|
-
addToPool(epochIndex, verified);
|
|
245
|
-
return verified.length > 0;
|
|
246
|
-
}
|
|
186
|
+
// Verified tickets for the current epoch, keyed by entropy hash (ticket id).
|
|
187
|
+
// Tickets enter via `validator.validate(...)` which both verifies and inserts.
|
|
188
|
+
const verifiedPool = new VerifiedTicketPool();
|
|
189
|
+
const ticketValidator = new BandersnatchTicketValidator(bandersnatch, chainSpec, verifiedPool, () => states.getState(blocks.getBestHeaderHash()));
|
|
247
190
|
// Receive a single ticket from peers (via jam-network worker).
|
|
248
191
|
// Returns true if the ticket passed validation so jam-network can decide whether to redistribute it.
|
|
249
192
|
networkingComms.setOnReceivedTickets(async ({ epochIndex, ticket }) => {
|
|
250
193
|
logger.log `Received ticket from peer for epoch ${epochIndex}`;
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
if (state === null) {
|
|
254
|
-
logger.warn `Cannot verify received ticket: no state available`;
|
|
255
|
-
return false;
|
|
256
|
-
}
|
|
257
|
-
return await verifyAndAddToPool(epochIndex, [ticket], state);
|
|
194
|
+
const result = await ticketValidator.validate(epochIndex, ticket);
|
|
195
|
+
return result.isOk;
|
|
258
196
|
});
|
|
259
197
|
const isFastForward = config.workerParams.isFastForward;
|
|
260
198
|
let lastGeneratedSlot = startTimeSlot;
|
|
@@ -303,8 +241,10 @@ export async function main(config, comms, networkingComms) {
|
|
|
303
241
|
}
|
|
304
242
|
else {
|
|
305
243
|
logger.log `Generated ${ticketsResult.ok.length} tickets for epoch ${epoch}. Distributing...`;
|
|
306
|
-
// Verify own tickets
|
|
307
|
-
|
|
244
|
+
// Verify own tickets (validator stores them in the pool with computed ids).
|
|
245
|
+
for (const ticket of ticketsResult.ok) {
|
|
246
|
+
await ticketValidator.validate(epoch, ticket);
|
|
247
|
+
}
|
|
308
248
|
// Send directly to network worker (bypasses main thread)
|
|
309
249
|
await networkingComms.sendTickets({ epochIndex: epoch, tickets: ticketsResult.ok });
|
|
310
250
|
}
|
|
@@ -330,6 +270,12 @@ export async function main(config, comms, networkingComms) {
|
|
|
330
270
|
}
|
|
331
271
|
await buildTicketAuthorshipCache(selingKeySeriesResult.ok, entropy);
|
|
332
272
|
}
|
|
273
|
+
// On every epoch boundary, push the authoritative ticket pool to networking so it
|
|
274
|
+
// can replace its redistribution set; this keeps the two sides from drifting.
|
|
275
|
+
if (isNewEpoch) {
|
|
276
|
+
const dumpTickets = verifiedPool.getForEpoch(epoch).map((entry) => entry.ticket);
|
|
277
|
+
await networkingComms.sendReplaceTicketPool({ epochIndex: epoch, tickets: dumpTickets });
|
|
278
|
+
}
|
|
333
279
|
const sealData = getSealData(selingKeySeriesResult.ok, keys, timeSlot, entropy);
|
|
334
280
|
if (sealData !== null && currentValidatorData !== null) {
|
|
335
281
|
const { key, sealPayload } = sealData;
|
|
@@ -338,8 +284,10 @@ export async function main(config, comms, networkingComms) {
|
|
|
338
284
|
continue;
|
|
339
285
|
}
|
|
340
286
|
logger.log `Attempting to create a block using ${sealData.logId} located at validator index ${validatorIndex}.`;
|
|
341
|
-
const currentEpochTickets =
|
|
342
|
-
const newBlock = await generator.nextBlockView(validatorIndex, key.bandersnatchSecret, sealPayload, timeSlot,
|
|
287
|
+
const currentEpochTickets = verifiedPool.getForEpoch(epoch);
|
|
288
|
+
const newBlock = await generator.nextBlockView(validatorIndex, key.bandersnatchSecret, sealPayload, timeSlot,
|
|
289
|
+
// VerifiedTicket has the same `{ ticket, id }` shape the generator expects.
|
|
290
|
+
[...currentEpochTickets]);
|
|
343
291
|
counter += 1;
|
|
344
292
|
lastGeneratedSlot = timeSlot;
|
|
345
293
|
logger.trace `Sending block ${counter}`;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Epoch } from "#@typeberry/block";
|
|
2
|
+
import type { SignedTicket } from "#@typeberry/block/tickets.js";
|
|
3
|
+
import type { ChainSpec } from "#@typeberry/config";
|
|
4
|
+
import type { BandernsatchWasm } from "#@typeberry/safrole/bandersnatch-wasm.js";
|
|
5
|
+
import type { State } from "#@typeberry/state";
|
|
6
|
+
import { type TicketValidator, type ValidatedTicket, ValidationError, type VerifiedTicketPool } from "#@typeberry/ticket-pool";
|
|
7
|
+
import { Result } from "#@typeberry/utils";
|
|
8
|
+
/**
|
|
9
|
+
* Real {@link TicketValidator} implementation that verifies a ticket against the ring
|
|
10
|
+
* commitment and current epoch entropy using bandersnatch, then stores the verified
|
|
11
|
+
* ticket (with its computed id) into the supplied {@link VerifiedTicketPool}.
|
|
12
|
+
*
|
|
13
|
+
* `getState` is a thunk because state advances continuously while validation is in
|
|
14
|
+
* flight; we want the latest available state for each call.
|
|
15
|
+
*/
|
|
16
|
+
export declare class BandersnatchTicketValidator implements TicketValidator {
|
|
17
|
+
private readonly bandersnatch;
|
|
18
|
+
private readonly chainSpec;
|
|
19
|
+
private readonly pool;
|
|
20
|
+
private readonly getState;
|
|
21
|
+
constructor(bandersnatch: BandernsatchWasm, chainSpec: ChainSpec, pool: VerifiedTicketPool, getState: () => State | null);
|
|
22
|
+
validate(epochIndex: Epoch, ticket: SignedTicket): Promise<Result<ValidatedTicket, ValidationError>>;
|
|
23
|
+
/**
|
|
24
|
+
* Returns the correct tickets entropy for verification given the current state.
|
|
25
|
+
*
|
|
26
|
+
* When `state` is from epoch E-1 (i.e. we haven't produced epoch E's first block yet),
|
|
27
|
+
* the ticket entropy for epoch E is at index 1 (not yet shifted). After the epoch
|
|
28
|
+
* transition it moves to index 2.
|
|
29
|
+
*/
|
|
30
|
+
private getTicketEntropy;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=ticket-validator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ticket-validator.d.ts","sourceRoot":"","sources":["../../../../../packages/workers/block-authorship/ticket-validator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAe,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAGnD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yCAAyC,CAAC;AAChF,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EACL,KAAK,eAAe,EACpB,KAAK,eAAe,EACpB,eAAe,EAEf,KAAK,kBAAkB,EACxB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAI1C;;;;;;;GAOG;AACH,qBAAa,2BAA4B,YAAW,eAAe;IAE/D,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAHR,YAAY,EAAE,gBAAgB,EAC9B,SAAS,EAAE,SAAS,EACpB,IAAI,EAAE,kBAAkB,EACxB,QAAQ,EAAE,MAAM,KAAK,GAAG,IAAI;IAGzC,QAAQ,CAAC,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,eAAe,CAAC,CAAC;IA+B1G;;;;;;OAMG;IACH,OAAO,CAAC,gBAAgB;CAIzB"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Logger } from "#@typeberry/logger";
|
|
2
|
+
import bandersnatchVrf from "#@typeberry/safrole/bandersnatch-vrf.js";
|
|
3
|
+
import { ValidationError, } from "#@typeberry/ticket-pool";
|
|
4
|
+
import { Result } from "#@typeberry/utils";
|
|
5
|
+
const logger = Logger.new(import.meta.filename, "ticket-validator");
|
|
6
|
+
/**
|
|
7
|
+
* Real {@link TicketValidator} implementation that verifies a ticket against the ring
|
|
8
|
+
* commitment and current epoch entropy using bandersnatch, then stores the verified
|
|
9
|
+
* ticket (with its computed id) into the supplied {@link VerifiedTicketPool}.
|
|
10
|
+
*
|
|
11
|
+
* `getState` is a thunk because state advances continuously while validation is in
|
|
12
|
+
* flight; we want the latest available state for each call.
|
|
13
|
+
*/
|
|
14
|
+
export class BandersnatchTicketValidator {
|
|
15
|
+
bandersnatch;
|
|
16
|
+
chainSpec;
|
|
17
|
+
pool;
|
|
18
|
+
getState;
|
|
19
|
+
constructor(bandersnatch, chainSpec, pool, getState) {
|
|
20
|
+
this.bandersnatch = bandersnatch;
|
|
21
|
+
this.chainSpec = chainSpec;
|
|
22
|
+
this.pool = pool;
|
|
23
|
+
this.getState = getState;
|
|
24
|
+
}
|
|
25
|
+
async validate(epochIndex, ticket) {
|
|
26
|
+
const state = this.getState();
|
|
27
|
+
if (state === null) {
|
|
28
|
+
return Result.error(ValidationError.ValidatorUnavailable, () => "no state available");
|
|
29
|
+
}
|
|
30
|
+
const entropy = this.getTicketEntropy(epochIndex, state);
|
|
31
|
+
// Batch verifier: a single `isValid` covers the whole batch and `tickets` holds the
|
|
32
|
+
// computed id per input ticket. We only ever pass one ticket here.
|
|
33
|
+
const { isValid, tickets } = await bandersnatchVrf.verifyTickets(this.bandersnatch, state.designatedValidatorData.length, state.epochRoot, [ticket], entropy);
|
|
34
|
+
if (tickets.length !== 1) {
|
|
35
|
+
logger.error `verifyTickets returned ${tickets.length} results for 1 ticket`;
|
|
36
|
+
return Result.error(ValidationError.ValidatorUnavailable, () => "verifier returned unexpected result count");
|
|
37
|
+
}
|
|
38
|
+
if (!isValid) {
|
|
39
|
+
return Result.error(ValidationError.InvalidProof, () => "bandersnatch proof rejected");
|
|
40
|
+
}
|
|
41
|
+
const verified = { ticket, id: tickets[0] };
|
|
42
|
+
this.pool.add(epochIndex, [verified]);
|
|
43
|
+
return Result.ok({ id: tickets[0] });
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Returns the correct tickets entropy for verification given the current state.
|
|
47
|
+
*
|
|
48
|
+
* When `state` is from epoch E-1 (i.e. we haven't produced epoch E's first block yet),
|
|
49
|
+
* the ticket entropy for epoch E is at index 1 (not yet shifted). After the epoch
|
|
50
|
+
* transition it moves to index 2.
|
|
51
|
+
*/
|
|
52
|
+
getTicketEntropy(epochIndex, state) {
|
|
53
|
+
const stateEpoch = Math.floor(state.timeslot / this.chainSpec.epochLength);
|
|
54
|
+
return epochIndex > stateEpoch ? state.entropy[1] : state.entropy[2];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -21,6 +21,16 @@ export declare const protocol: import("@typeberry/workers-api").LousyProtocol<{
|
|
|
21
21
|
}>>;
|
|
22
22
|
response: import("@typeberry/codec").Descriptor<void, void>;
|
|
23
23
|
};
|
|
24
|
+
replaceTicketPool: {
|
|
25
|
+
request: import("@typeberry/codec").Descriptor<TicketsMessage, import("@typeberry/codec").ViewOf<TicketsMessage, {
|
|
26
|
+
epochIndex: import("@typeberry/codec").Descriptor<number & import("@typeberry/numbers").WithBytesRepresentation<4> & import("@typeberry/utils").WithOpaque<"Epoch">, import("@typeberry/bytes").Bytes<4>>;
|
|
27
|
+
tickets: import("@typeberry/codec").Descriptor<import("@typeberry/block").SignedTicket[], import("@typeberry/codec").SequenceView<import("@typeberry/block").SignedTicket, import("@typeberry/codec").ViewOf<import("@typeberry/block").SignedTicket, {
|
|
28
|
+
attempt: import("@typeberry/codec").Descriptor<number & import("@typeberry/numbers").WithBytesRepresentation<1> & import("@typeberry/utils").WithOpaque<"TicketAttempt[u8]">, import("@typeberry/numbers").U32>;
|
|
29
|
+
signature: import("@typeberry/codec").Descriptor<import("@typeberry/bytes").Bytes<784> & import("@typeberry/utils").WithOpaque<"BandersnatchRingSignature">, import("@typeberry/bytes").Bytes<784>>;
|
|
30
|
+
}>>>;
|
|
31
|
+
}>>;
|
|
32
|
+
response: import("@typeberry/codec").Descriptor<void, void>;
|
|
33
|
+
};
|
|
24
34
|
}, {
|
|
25
35
|
receivedTickets: {
|
|
26
36
|
request: import("@typeberry/codec").Descriptor<ReceivedTicketMessage, import("@typeberry/codec").ViewOf<ReceivedTicketMessage, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../../../../packages/workers/comms-authorship-network/protocol.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,GAAG,EAAkB,KAAK,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACjF,OAAO,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE7E;;;GAGG;AACH,eAAO,MAAM,uBAAuB,uBAAuB,CAAC;AAE5D;;;;GAIG;AACH,eAAO,MAAM,QAAQ
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../../../../packages/workers/comms-authorship-network/protocol.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,GAAG,EAAkB,KAAK,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AACjF,OAAO,EAAE,qBAAqB,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAE7E;;;GAGG;AACH,eAAO,MAAM,uBAAuB,uBAAuB,CAAC;AAE5D;;;;GAIG;AACH,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwBnB,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,GAAG,CAAC,OAAO,QAAQ,CAAC,CAAC;AACnD,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC,OAAO,QAAQ,CAAC,CAAC"}
|
|
@@ -12,12 +12,19 @@ export const AUTHORSHIP_NETWORK_PORT = "authorship-network";
|
|
|
12
12
|
* This bypasses the main thread for ticket distribution, reducing latency.
|
|
13
13
|
*/
|
|
14
14
|
export const protocol = createProtocol("authorship-network", {
|
|
15
|
-
// Messages from block-authorship to jam-network
|
|
15
|
+
// Messages from block-authorship to jam-network.
|
|
16
16
|
toWorker: {
|
|
17
|
+
// Newly generated own tickets; networking should add them to its redistribution pool.
|
|
17
18
|
tickets: {
|
|
18
19
|
request: TicketsMessage.Codec,
|
|
19
20
|
response: codec.nothing,
|
|
20
21
|
},
|
|
22
|
+
// Authoritative pool snapshot for the given epoch; networking replaces its local
|
|
23
|
+
// pool with these tickets (one-way, source of truth lives in block-authorship).
|
|
24
|
+
replaceTicketPool: {
|
|
25
|
+
request: TicketsMessage.Codec,
|
|
26
|
+
response: codec.nothing,
|
|
27
|
+
},
|
|
21
28
|
},
|
|
22
29
|
// Messages from jam-network to block-authorship (one ticket per relay).
|
|
23
30
|
// Response indicates whether the ticket passed validation — used by jam-network
|