@typeberry/lib 0.8.4-7a69737 → 0.8.4-7cde74e

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.
Files changed (36) hide show
  1. package/package.json +3 -1
  2. package/packages/jam/jamnp-s/tasks/ticket-distribution.d.ts +18 -10
  3. package/packages/jam/jamnp-s/tasks/ticket-distribution.d.ts.map +1 -1
  4. package/packages/jam/jamnp-s/tasks/ticket-distribution.js +44 -68
  5. package/packages/jam/jamnp-s/tasks/ticket-distribution.test.js +30 -8
  6. package/packages/jam/ticket-pool/index.d.ts +4 -0
  7. package/packages/jam/ticket-pool/index.d.ts.map +1 -0
  8. package/packages/jam/ticket-pool/index.js +3 -0
  9. package/packages/jam/ticket-pool/pending-ticket-pool.d.ts +30 -0
  10. package/packages/jam/ticket-pool/pending-ticket-pool.d.ts.map +1 -0
  11. package/packages/jam/ticket-pool/pending-ticket-pool.js +56 -0
  12. package/packages/jam/ticket-pool/pending-ticket-pool.test.d.ts +2 -0
  13. package/packages/jam/ticket-pool/pending-ticket-pool.test.d.ts.map +1 -0
  14. package/packages/jam/ticket-pool/pending-ticket-pool.test.js +67 -0
  15. package/packages/jam/ticket-pool/ticket-validator.d.ts +46 -0
  16. package/packages/jam/ticket-pool/ticket-validator.d.ts.map +1 -0
  17. package/packages/jam/ticket-pool/ticket-validator.js +29 -0
  18. package/packages/jam/ticket-pool/ticket-validator.test.d.ts +2 -0
  19. package/packages/jam/ticket-pool/ticket-validator.test.d.ts.map +1 -0
  20. package/packages/jam/ticket-pool/ticket-validator.test.js +34 -0
  21. package/packages/jam/ticket-pool/verified-ticket-pool.d.ts +24 -0
  22. package/packages/jam/ticket-pool/verified-ticket-pool.d.ts.map +1 -0
  23. package/packages/jam/ticket-pool/verified-ticket-pool.js +37 -0
  24. package/packages/jam/ticket-pool/verified-ticket-pool.test.d.ts +2 -0
  25. package/packages/jam/ticket-pool/verified-ticket-pool.test.d.ts.map +1 -0
  26. package/packages/jam/ticket-pool/verified-ticket-pool.test.js +54 -0
  27. package/packages/workers/block-authorship/main.d.ts.map +1 -1
  28. package/packages/workers/block-authorship/main.js +22 -74
  29. package/packages/workers/block-authorship/ticket-validator.d.ts +32 -0
  30. package/packages/workers/block-authorship/ticket-validator.d.ts.map +1 -0
  31. package/packages/workers/block-authorship/ticket-validator.js +56 -0
  32. package/packages/workers/comms-authorship-network/protocol.d.ts +10 -0
  33. package/packages/workers/comms-authorship-network/protocol.d.ts.map +1 -1
  34. package/packages/workers/comms-authorship-network/protocol.js +8 -1
  35. package/packages/workers/jam-network/main.d.ts.map +1 -1
  36. package/packages/workers/jam-network/main.js +20 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typeberry/lib",
3
- "version": "0.8.4-7a69737",
3
+ "version": "0.8.4-7cde74e",
4
4
  "description": "Typeberry Library",
5
5
  "main": "./bin/lib/index.js",
6
6
  "types": "./bin/lib/index.d.ts",
@@ -244,6 +244,8 @@
244
244
  "#@typeberry/state-merkleization/*": "./packages/jam/state-merkleization/*",
245
245
  "#@typeberry/state-vectors": "./packages/jam/state-vectors/index.js",
246
246
  "#@typeberry/state-vectors/*": "./packages/jam/state-vectors/*",
247
+ "#@typeberry/ticket-pool": "./packages/jam/ticket-pool/index.js",
248
+ "#@typeberry/ticket-pool/*": "./packages/jam/ticket-pool/*",
247
249
  "#@typeberry/transition": "./packages/jam/transition/index.js",
248
250
  "#@typeberry/transition/*": "./packages/jam/transition/*",
249
251
  "#@typeberry/disputes": "./packages/jam/transition/disputes/index.js",
@@ -1,6 +1,7 @@
1
1
  import type { Epoch } from "#@typeberry/block";
2
2
  import type { SignedTicket } from "#@typeberry/block/tickets.js";
3
3
  import type { ChainSpec } from "#@typeberry/config";
4
+ import { type TicketValidator } from "#@typeberry/ticket-pool";
4
5
  import type { Connections } from "../peers.js";
5
6
  import type { StreamManager } from "../stream-manager.js";
6
7
  /**
@@ -9,33 +10,40 @@ import type { StreamManager } from "../stream-manager.js";
9
10
  * Uses CE-132 (proxy-to-all) for direct broadcast to all peers.
10
11
  * Implements a maintain pattern similar to SyncTask: tickets are collected
11
12
  * and periodically distributed to peers that haven't received them yet.
13
+ *
14
+ * Incoming tickets from peers are first run through a {@link TicketValidator};
15
+ * only validated tickets are added to the redistribution pool. The default
16
+ * validator denies everything, so callers must wire a real one via
17
+ * {@link setTicketValidator} before any networked ticket can be redistributed.
12
18
  */
13
19
  export declare class TicketDistributionTask {
14
20
  private readonly streamManager;
15
21
  private readonly connections;
16
22
  static start(streamManager: StreamManager, connections: Connections, chainSpec: ChainSpec): TicketDistributionTask;
17
- /** Pending tickets waiting to be distributed to peers */
18
- private pendingTickets;
19
- /** Current epoch being tracked (cleared when epoch changes) */
20
- private currentEpoch;
23
+ private readonly pool;
24
+ private validator;
21
25
  private constructor();
22
26
  /**
23
27
  * Should be called periodically to distribute pending tickets to connected peers.
24
28
  */
25
29
  maintainDistribution(): void;
26
30
  /**
27
- * Add a ticket to the pending queue for distribution.
31
+ * Add a ticket to the redistribution pool.
28
32
  * Clears pending tickets when epoch changes.
29
33
  * Deduplicates tickets based on signature.
30
34
  */
31
35
  addTicket(epochIndex: Epoch, ticket: SignedTicket): void;
32
- private onTicketReceivedCallback;
33
36
  /**
34
- * Register a callback that validates a received ticket.
35
- * The ticket is only added to the redistribution pool if the callback returns `true`.
36
- * This prevents redistribution of invalid tickets (e.g. those with a tampered `attempt` field).
37
+ * Replace the redistribution pool for the given epoch with the supplied tickets.
38
+ * Used when the authorship worker dumps the authoritative pool on an epoch boundary.
39
+ */
40
+ replacePool(epochIndex: Epoch, tickets: readonly SignedTicket[]): void;
41
+ /**
42
+ * Register the validator that decides whether tickets received from peers should be
43
+ * accepted (and therefore redistributed). The default is {@link DenyTicketsValidator},
44
+ * so the caller must install a real validator for any peer ticket to make it through.
37
45
  */
38
- setOnTicketReceived(cb: (epochIndex: Epoch, ticket: SignedTicket) => Promise<boolean>): void;
46
+ setTicketValidator(validator: TicketValidator): void;
39
47
  private onTicketReceived;
40
48
  }
41
49
  //# sourceMappingURL=ticket-distribution.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ticket-distribution.d.ts","sourceRoot":"","sources":["../../../../../../packages/jam/jamnp-s/tasks/ticket-distribution.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAGnD,OAAO,KAAK,EAAW,WAAW,EAAE,MAAM,aAAa,CAAC;AAExD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAe1D;;;;;;GAMG;AACH,qBAAa,sBAAsB;IAuB/B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,WAAW;IAvB9B,MAAM,CAAC,KAAK,CAAC,aAAa,EAAE,aAAa,EAAE,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS;IAgBzF,yDAAyD;IACzD,OAAO,CAAC,cAAc,CAA0D;IAChF,+DAA+D;IAC/D,OAAO,CAAC,YAAY,CAAsB;IAE1C,OAAO;IAKP;;OAEG;IACH,oBAAoB;IAkDpB;;;;OAIG;IACH,SAAS,CAAC,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY;IAsCjD,OAAO,CAAC,wBAAwB,CAAgF;IAEhH;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,EAAE,CAAC,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,KAAK,OAAO,CAAC,OAAO,CAAC;IAIrF,OAAO,CAAC,gBAAgB;CAsBzB"}
1
+ {"version":3,"file":"ticket-distribution.d.ts","sourceRoot":"","sources":["../../../../../../packages/jam/jamnp-s/tasks/ticket-distribution.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAEnD,OAAO,EAA2C,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAEvG,OAAO,KAAK,EAAW,WAAW,EAAE,MAAM,aAAa,CAAC;AAExD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAe1D;;;;;;;;;;;GAWG;AACH,qBAAa,sBAAsB;IAqB/B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,WAAW;IArB9B,MAAM,CAAC,KAAK,CAAC,aAAa,EAAE,aAAa,EAAE,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS;IAgBzF,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA2B;IAChD,OAAO,CAAC,SAAS,CAA+C;IAEhE,OAAO;IAKP;;OAEG;IACH,oBAAoB;IAgDpB;;;;OAIG;IACH,SAAS,CAAC,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY;IAIjD;;;OAGG;IACH,WAAW,CAAC,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,YAAY,EAAE;IAI/D;;;;OAIG;IACH,kBAAkB,CAAC,SAAS,EAAE,eAAe;IAI7C,OAAO,CAAC,gBAAgB;CAkBzB"}
@@ -1,4 +1,5 @@
1
1
  import { Logger } from "#@typeberry/logger";
2
+ import { DenyTicketsValidator, PendingTicketPool } from "#@typeberry/ticket-pool";
2
3
  import { OK } from "#@typeberry/utils";
3
4
  import { ce131 } from "../protocol/index.js";
4
5
  const logger = Logger.new(import.meta.filename, "net:tickets");
@@ -12,6 +13,11 @@ const TICKET_AUX = {
12
13
  * Uses CE-132 (proxy-to-all) for direct broadcast to all peers.
13
14
  * Implements a maintain pattern similar to SyncTask: tickets are collected
14
15
  * and periodically distributed to peers that haven't received them yet.
16
+ *
17
+ * Incoming tickets from peers are first run through a {@link TicketValidator};
18
+ * only validated tickets are added to the redistribution pool. The default
19
+ * validator denies everything, so callers must wire a real one via
20
+ * {@link setTicketValidator} before any networked ticket can be redistributed.
15
21
  */
16
22
  export class TicketDistributionTask {
17
23
  streamManager;
@@ -26,10 +32,8 @@ export class TicketDistributionTask {
26
32
  streamManager.registerOutgoingHandlers(ce131.ClientHandler.new(chainSpec, ce131.STREAM_KIND_PROXY_TO_ALL));
27
33
  return task;
28
34
  }
29
- /** Pending tickets waiting to be distributed to peers */
30
- pendingTickets = [];
31
- /** Current epoch being tracked (cleared when epoch changes) */
32
- currentEpoch = null;
35
+ pool = new PendingTicketPool();
36
+ validator = new DenyTicketsValidator();
33
37
  constructor(streamManager, connections) {
34
38
  this.streamManager = streamManager;
35
39
  this.connections = connections;
@@ -38,14 +42,13 @@ export class TicketDistributionTask {
38
42
  * Should be called periodically to distribute pending tickets to connected peers.
39
43
  */
40
44
  maintainDistribution() {
41
- if (this.currentEpoch === null) {
42
- return; // No tickets to distribute yet
45
+ const currentEpoch = this.pool.currentEpoch;
46
+ if (currentEpoch === null) {
47
+ return;
43
48
  }
44
- /** `this` is mutable and TS can't narrow this.currentEpoch inside the callback closure */
45
- const currentEpoch = this.currentEpoch;
46
- // Iterate through all pending tickets
47
- for (let ticketIdx = 0; ticketIdx < this.pendingTickets.length; ticketIdx++) {
48
- const { epochIndex, ticket } = this.pendingTickets[ticketIdx];
49
+ const tickets = this.pool.getTickets();
50
+ for (let ticketIdx = 0; ticketIdx < tickets.length; ticketIdx++) {
51
+ const { epochIndex, ticket } = tickets[ticketIdx];
49
52
  // Try to send to each connected peer
50
53
  for (const peerInfo of this.connections.getConnectedPeers()) {
51
54
  this.connections.withAuxData(peerInfo.peerId, TICKET_AUX, (maybeAux) => {
@@ -77,72 +80,45 @@ export class TicketDistributionTask {
77
80
  }
78
81
  }
79
82
  /**
80
- * Add a ticket to the pending queue for distribution.
83
+ * Add a ticket to the redistribution pool.
81
84
  * Clears pending tickets when epoch changes.
82
85
  * Deduplicates tickets based on signature.
83
86
  */
84
87
  addTicket(epochIndex, ticket) {
85
- // Drop tickets for older epochs (can happen when a delayed validation callback completes
86
- // after the epoch has already advanced — accepting it would roll back currentEpoch).
87
- if (this.currentEpoch !== null && epochIndex < this.currentEpoch) {
88
- return;
89
- }
90
- // Epoch advanced — clear old tickets
91
- if (this.currentEpoch !== null && epochIndex > this.currentEpoch) {
92
- logger.log `[addTicket] Epoch changed from ${this.currentEpoch} to ${epochIndex}, clearing ${this.pendingTickets.length} old tickets`;
93
- this.pendingTickets = [];
94
- // Note: We don't need to clear aux data for all peers here.
95
- // The aux data contains the epoch, so maintainDistribution will lazily
96
- // reset it when it detects an epoch mismatch. This handles both connected
97
- // and disconnected peers correctly.
98
- }
99
- this.currentEpoch = epochIndex;
100
- /**
101
- * Deduplicate: check if a ticket with the same signature already exists
102
- *
103
- * Here we are risking "poisoning" the local pendingTickets - i.e:
104
- * 1. The adversary sees a signature and swaps the ticket attempt to something different.
105
- * 2. This creates an invalid ticket, but prevents a valid ticket with the same signature from being included and distributed.
106
- *
107
- * TODO [MaSi]: The poisoning risk should be fixed during implementation of ticket validation.
108
- */
109
- const isDuplicate = this.pendingTickets.some((pending) => pending.epochIndex === epochIndex && pending.ticket.signature.isEqualTo(ticket.signature));
110
- if (!isDuplicate) {
111
- this.pendingTickets.push({ epochIndex, ticket });
112
- logger.info `[addTicket] Added ticket for epoch ${epochIndex}, total: ${this.pendingTickets.length}`;
113
- }
88
+ this.pool.addTicket(epochIndex, ticket);
114
89
  }
115
- onTicketReceivedCallback = null;
116
90
  /**
117
- * Register a callback that validates a received ticket.
118
- * The ticket is only added to the redistribution pool if the callback returns `true`.
119
- * This prevents redistribution of invalid tickets (e.g. those with a tampered `attempt` field).
91
+ * Replace the redistribution pool for the given epoch with the supplied tickets.
92
+ * Used when the authorship worker dumps the authoritative pool on an epoch boundary.
120
93
  */
121
- setOnTicketReceived(cb) {
122
- this.onTicketReceivedCallback = cb;
94
+ replacePool(epochIndex, tickets) {
95
+ this.pool.replace(epochIndex, tickets);
96
+ }
97
+ /**
98
+ * Register the validator that decides whether tickets received from peers should be
99
+ * accepted (and therefore redistributed). The default is {@link DenyTicketsValidator},
100
+ * so the caller must install a real validator for any peer ticket to make it through.
101
+ */
102
+ setTicketValidator(validator) {
103
+ this.validator = validator;
123
104
  }
124
105
  onTicketReceived(epochIndex, ticket) {
125
106
  logger.trace `Received ticket for epoch ${epochIndex}, attempt ${ticket.attempt}`;
126
- if (this.onTicketReceivedCallback !== null) {
127
- // Validate first; only redistribute if valid to avoid spreading tampered tickets.
128
- // Wrap with Promise.resolve().then() to catch both sync throws and async rejections.
129
- const cb = this.onTicketReceivedCallback;
130
- Promise.resolve()
131
- .then(() => cb(epochIndex, ticket))
132
- .then((isValid) => {
133
- if (isValid) {
134
- this.addTicket(epochIndex, ticket);
135
- }
136
- else {
137
- logger.warn `Dropping invalid ticket for epoch ${epochIndex} (validation failed)`;
138
- }
139
- })
140
- .catch((error) => {
141
- logger.error `Error validating ticket for epoch ${epochIndex}, attempt ${ticket.attempt}: ${error}`;
142
- });
143
- }
144
- else {
145
- this.addTicket(epochIndex, ticket);
146
- }
107
+ const validator = this.validator;
108
+ // Wrap with Promise.resolve().then() so a synchronous throw inside the validator
109
+ // funnels into the same .catch() as an async rejection.
110
+ Promise.resolve()
111
+ .then(() => validator.validate(epochIndex, ticket))
112
+ .then((result) => {
113
+ if (result.isOk) {
114
+ this.addTicket(epochIndex, ticket);
115
+ }
116
+ else {
117
+ logger.trace `Dropping ticket for epoch ${epochIndex}: ${result.error}`;
118
+ }
119
+ })
120
+ .catch((error) => {
121
+ logger.error `Error validating ticket for epoch ${epochIndex}, attempt ${ticket.attempt}: ${error}`;
122
+ });
147
123
  }
148
124
  }
@@ -8,7 +8,8 @@ import { tinyChainSpec } from "#@typeberry/config";
8
8
  import { BANDERSNATCH_PROOF_BYTES } from "#@typeberry/crypto";
9
9
  import { Logger } from "#@typeberry/logger";
10
10
  import { createTestPeerPair, MockNetwork } from "#@typeberry/networking/testing.js";
11
- import { OK } from "#@typeberry/utils";
11
+ import { AcceptTicketsValidator, ValidationError } from "#@typeberry/ticket-pool";
12
+ import { OK, Result } from "#@typeberry/utils";
12
13
  import { Connections } from "../peers.js";
13
14
  import { StreamManager } from "../stream-manager.js";
14
15
  import { TicketDistributionTask } from "./ticket-distribution.js";
@@ -34,6 +35,9 @@ describe("TicketDistributionTask", () => {
34
35
  const receivedTickets = [];
35
36
  // Use real TicketDistributionTask
36
37
  const ticketTask = TicketDistributionTask.start(streamManager, connections, tinyChainSpec);
38
+ // Default validator accepts every ticket so the test asserts purely on distribution
39
+ // behaviour. Tests that exercise the rejection path overwrite this.
40
+ ticketTask.setTicketValidator(new AcceptTicketsValidator());
37
41
  // Intercept received tickets by wrapping onTicketReceived behavior
38
42
  // The task already adds received tickets to pending queue via addTicket,
39
43
  // so we can track them by checking the pending queue growth or by
@@ -217,7 +221,7 @@ describe("TicketDistributionTask", () => {
217
221
  assert.strictEqual(peer2.receivedTickets.length, 1);
218
222
  assert.deepStrictEqual(peer2.receivedTickets[0].ticket, ticket);
219
223
  });
220
- it("should NOT redistribute ticket if validation callback returns false", async () => {
224
+ it("should NOT redistribute ticket if validator rejects", async () => {
221
225
  const self = await init("self");
222
226
  const peer1 = await init("peer1");
223
227
  const peer2 = await init("peer2");
@@ -225,35 +229,53 @@ describe("TicketDistributionTask", () => {
225
229
  self.openConnection(peer2);
226
230
  await tick();
227
231
  // Validation always rejects
228
- self.ticketTask.setOnTicketReceived(async () => false);
232
+ self.ticketTask.setTicketValidator({
233
+ validate: async () => Result.error(ValidationError.InvalidProof, () => "rejected"),
234
+ });
229
235
  const ticket = createTestTicket(0);
230
236
  peer1.ticketTask.addTicket(TEST_EPOCH, ticket);
231
237
  peer1.ticketTask.maintainDistribution();
232
238
  await tick();
233
- // self.addTicket was NOT called (callback returned false), so nothing to redistribute
239
+ // self.addTicket was NOT called (validator rejected), so nothing to redistribute
234
240
  assert.strictEqual(self.receivedTickets.length, 0);
235
241
  self.ticketTask.maintainDistribution();
236
242
  await tick();
237
243
  assert.strictEqual(peer2.receivedTickets.length, 0);
238
244
  });
239
- it("should redistribute ticket if validation callback returns true", async () => {
245
+ it("should redistribute ticket if validator accepts", async () => {
240
246
  const self = await init("self");
241
247
  const peer1 = await init("peer1");
242
248
  const peer2 = await init("peer2");
243
249
  self.openConnection(peer1);
244
250
  self.openConnection(peer2);
245
251
  await tick();
246
- // Validation always accepts
247
- self.ticketTask.setOnTicketReceived(async () => true);
252
+ // Default init() already wires an AcceptTicketsValidator
248
253
  const ticket = createTestTicket(0);
249
254
  peer1.ticketTask.addTicket(TEST_EPOCH, ticket);
250
255
  peer1.ticketTask.maintainDistribution();
251
256
  await tick();
252
- // self.addTicket WAS called (callback returned true)
257
+ // self.addTicket WAS called
253
258
  assert.strictEqual(self.receivedTickets.length, 1);
254
259
  self.ticketTask.maintainDistribution();
255
260
  await tick();
256
261
  assert.strictEqual(peer2.receivedTickets.length, 1);
257
262
  assert.deepStrictEqual(peer2.receivedTickets[0].ticket, ticket);
258
263
  });
264
+ it("replacePool overwrites the redistribution pool", async () => {
265
+ const self = await init("self");
266
+ const peer1 = await init("peer1");
267
+ self.openConnection(peer1);
268
+ await tick();
269
+ // Locally added tickets first
270
+ self.ticketTask.addTicket(TEST_EPOCH, createTestTicket(0));
271
+ self.ticketTask.addTicket(TEST_EPOCH, createTestTicket(1));
272
+ // Pool dump replaces with a different set
273
+ const dump = [createTestTicket(2), createTestTicket(3)];
274
+ self.ticketTask.replacePool(TEST_EPOCH, dump);
275
+ self.ticketTask.maintainDistribution();
276
+ await tick();
277
+ assert.strictEqual(peer1.receivedTickets.length, 2);
278
+ assert.deepStrictEqual(peer1.receivedTickets[0].ticket, dump[0]);
279
+ assert.deepStrictEqual(peer1.receivedTickets[1].ticket, dump[1]);
280
+ });
259
281
  });
@@ -0,0 +1,4 @@
1
+ export * from "./pending-ticket-pool.js";
2
+ export * from "./ticket-validator.js";
3
+ export * from "./verified-ticket-pool.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -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,3 @@
1
+ export * from "./pending-ticket-pool.js";
2
+ export * from "./ticket-validator.js";
3
+ export * from "./verified-ticket-pool.js";
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=pending-ticket-pool.test.d.ts.map
@@ -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,46 @@
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
+ id: EntropyHash | null;
13
+ };
14
+ /** Reasons a ticket may fail validation. */
15
+ export declare enum ValidationError {
16
+ /** Verifier rejected the signature / proof. */
17
+ InvalidProof = "invalid_proof",
18
+ /** Validator could not run (e.g. state unavailable, transient internal failure). */
19
+ ValidatorUnavailable = "validator_unavailable",
20
+ /** Ticket is for an epoch outside the validator's window of interest. */
21
+ WrongEpoch = "wrong_epoch"
22
+ }
23
+ /**
24
+ * Strategy for verifying tickets arriving from peers.
25
+ *
26
+ * The concrete implementation may call into the bandersnatch verifier, defer to another
27
+ * worker via IPC, or short-circuit (Accept/Deny defaults for tests).
28
+ */
29
+ export interface TicketValidator {
30
+ validate(epochIndex: Epoch, ticket: SignedTicket): Promise<Result<ValidatedTicket, ValidationError>>;
31
+ }
32
+ /**
33
+ * Accepts every ticket without inspection. Useful for unit tests where the validator
34
+ * isn't the subject under test. Must never be used in production.
35
+ */
36
+ export declare class AcceptTicketsValidator implements TicketValidator {
37
+ validate(_epochIndex: Epoch, _ticket: SignedTicket): Promise<Result<ValidatedTicket, ValidationError>>;
38
+ }
39
+ /**
40
+ * Rejects every ticket. Used as the default for any task that needs an explicit, real
41
+ * validator wired in before it will accept anything from the network.
42
+ */
43
+ export declare class DenyTicketsValidator implements TicketValidator {
44
+ validate(_epochIndex: Epoch, _ticket: SignedTicket): Promise<Result<ValidatedTicket, ValidationError>>;
45
+ }
46
+ //# 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;AAChE,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE1C;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,WAAW,GAAG,IAAI,CAAC;CACxB,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,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,eAAe,CAAC,CAAC,CAAC;CACtG;AAED;;;GAGG;AACH,qBAAa,sBAAuB,YAAW,eAAe;IACtD,QAAQ,CAAC,WAAW,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,eAAe,CAAC,CAAC;CAG7G;AAED;;;GAGG;AACH,qBAAa,oBAAqB,YAAW,eAAe;IACpD,QAAQ,CAAC,WAAW,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,eAAe,CAAC,CAAC;CAG7G"}
@@ -0,0 +1,29 @@
1
+ import { Result } from "#@typeberry/utils";
2
+ /** Reasons a ticket may fail validation. */
3
+ export var ValidationError;
4
+ (function (ValidationError) {
5
+ /** Verifier rejected the signature / proof. */
6
+ ValidationError["InvalidProof"] = "invalid_proof";
7
+ /** Validator could not run (e.g. state unavailable, transient internal failure). */
8
+ ValidationError["ValidatorUnavailable"] = "validator_unavailable";
9
+ /** Ticket is for an epoch outside the validator's window of interest. */
10
+ ValidationError["WrongEpoch"] = "wrong_epoch";
11
+ })(ValidationError || (ValidationError = {}));
12
+ /**
13
+ * Accepts every ticket without inspection. Useful for unit tests where the validator
14
+ * isn't the subject under test. Must never be used in production.
15
+ */
16
+ export class AcceptTicketsValidator {
17
+ async validate(_epochIndex, _ticket) {
18
+ return Result.ok({ id: null });
19
+ }
20
+ }
21
+ /**
22
+ * Rejects every ticket. Used as the default for any task that needs an explicit, real
23
+ * validator wired in before it will accept anything from the network.
24
+ */
25
+ export class DenyTicketsValidator {
26
+ async validate(_epochIndex, _ticket) {
27
+ return Result.error(ValidationError.ValidatorUnavailable, () => "no ticket validator wired");
28
+ }
29
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ticket-validator.test.d.ts.map
@@ -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,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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=verified-ticket-pool.test.d.ts.map
@@ -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
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../../../../packages/workers/block-authorship/main.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qCAAqC,CAAC;AAiB3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAE3D,OAAO,KAAK,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAK9E,KAAK,MAAM,GAAG,YAAY,CAAC,qBAAqB,CAAC,CAAC;AAwBlD,wBAAsB,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,eAAe,iBAmapG"}
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
- // Ticket pool: epochIndex -> {ticket, id}[]
185
- // IDs (entropyHash) are computed at receipt time via verifyTickets(), enabling O(1) dedup by ID.
186
- const ticketPool = new Map();
187
- const ticketIdSets = new Map();
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 hash = blocks.getBestHeaderHash();
252
- const state = states.getState(hash);
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 to get IDs, then add to pool
307
- await verifyAndAddToPool(epoch, ticketsResult.ok, state);
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 = ticketPool.get(epoch) ?? [];
342
- const newBlock = await generator.nextBlockView(validatorIndex, key.bandersnatchSecret, sealPayload, timeSlot, currentEpochTickets);
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;;;;;;;;;;;;;;;;;;;;;;EAiBnB,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,GAAG,CAAC,OAAO,QAAQ,CAAC,CAAC;AACnD,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC,OAAO,QAAQ,CAAC,CAAC"}
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
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../../../../packages/workers/jam-network/main.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qCAAqC,CAAC;AAK3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAI1E;;;;;;GAMG;AACH,wBAAsB,IAAI,CACxB,MAAM,EAAE,YAAY,CAAC,gBAAgB,CAAC,EACtC,KAAK,EAAE,kBAAkB,EACzB,eAAe,EAAE,eAAe,iBAyDjC"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../../../../packages/workers/jam-network/main.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qCAAqC,CAAC;AAO3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,KAAK,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAI1E;;;;;;GAMG;AACH,wBAAsB,IAAI,CACxB,MAAM,EAAE,YAAY,CAAC,gBAAgB,CAAC,EACtC,KAAK,EAAE,kBAAkB,EACzB,eAAe,EAAE,eAAe,iBAwEjC"}
@@ -2,6 +2,8 @@ import { parseBootnode } from "#@typeberry/config-node";
2
2
  import { ed25519, initWasm } from "#@typeberry/crypto";
3
3
  import { setup } from "#@typeberry/jamnp-s";
4
4
  import { Logger } from "#@typeberry/logger";
5
+ import { ValidationError } from "#@typeberry/ticket-pool";
6
+ import { Result } from "#@typeberry/utils";
5
7
  const logger = Logger.new(import.meta.filename, "net");
6
8
  /**
7
9
  * JAM networking worker.
@@ -38,11 +40,25 @@ export async function main(config, comms, authorshipComms) {
38
40
  network.ticketTask.addTicket(epochIndex, ticket);
39
41
  }
40
42
  });
41
- // Relay tickets received from peers back to block-authorship (one ticket at a time).
42
- // Returns the validation result so ticket-distribution knows whether to redistribute.
43
- network.ticketTask.setOnTicketReceived(async (epochIndex, ticket) => {
44
- return await authorshipComms.sendReceivedTickets({ epochIndex, ticket });
43
+ // Authorship pushes the authoritative ticket pool on epoch boundaries; networking
44
+ // replaces its redistribution pool wholesale so the two sides cannot drift.
45
+ authorshipComms.setOnReplaceTicketPool(async ({ epochIndex, tickets }) => {
46
+ logger.log `Replacing redistribution pool from block-authorship for epoch ${epochIndex} (${tickets.length} tickets)`;
47
+ network.ticketTask.replacePool(epochIndex, tickets);
45
48
  });
49
+ // Validator that hands a received ticket to block-authorship over IPC and waits
50
+ // for an accept/reject decision. The wire protocol stays a simple bool; the
51
+ // computed id stays inside authorship (it owns the verified pool).
52
+ const ipcValidator = {
53
+ validate: async (epochIndex, ticket) => {
54
+ const ok = await authorshipComms.sendReceivedTickets({ epochIndex, ticket });
55
+ if (!ok) {
56
+ return Result.error(ValidationError.InvalidProof, () => "authorship rejected the ticket");
57
+ }
58
+ return Result.ok({ id: null });
59
+ },
60
+ };
61
+ network.ticketTask.setTicketValidator(ipcValidator);
46
62
  await network.network.start();
47
63
  // stop the network when the worker is finishing.
48
64
  await waitForFinish;