@typeberry/lib 0.6.0-079e56c → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typeberry/lib",
3
- "version": "0.6.0-079e56c",
3
+ "version": "0.6.0",
4
4
  "description": "Typeberry Library",
5
5
  "main": "./bin/lib/index.js",
6
6
  "types": "./bin/lib/index.d.ts",
@@ -29,13 +29,6 @@ export declare class TicketDistributionTask {
29
29
  * Deduplicates tickets based on signature.
30
30
  */
31
31
  addTicket(epochIndex: Epoch, ticket: SignedTicket): void;
32
- private onTicketReceivedCallback;
33
- /**
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
- */
38
- setOnTicketReceived(cb: (epochIndex: Epoch, ticket: SignedTicket) => Promise<boolean>): void;
39
32
  private onTicketReceived;
40
33
  }
41
34
  //# 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;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;IAgCjD,OAAO,CAAC,gBAAgB;CAKzB"}
@@ -82,13 +82,8 @@ export class TicketDistributionTask {
82
82
  * Deduplicates tickets based on signature.
83
83
  */
84
84
  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) {
85
+ // Check if epoch changed - if so, clear old tickets
86
+ if (this.currentEpoch !== null && this.currentEpoch !== epochIndex) {
92
87
  logger.log `[addTicket] Epoch changed from ${this.currentEpoch} to ${epochIndex}, clearing ${this.pendingTickets.length} old tickets`;
93
88
  this.pendingTickets = [];
94
89
  // Note: We don't need to clear aux data for all peers here.
@@ -112,37 +107,9 @@ export class TicketDistributionTask {
112
107
  logger.info `[addTicket] Added ticket for epoch ${epochIndex}, total: ${this.pendingTickets.length}`;
113
108
  }
114
109
  }
115
- onTicketReceivedCallback = null;
116
- /**
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).
120
- */
121
- setOnTicketReceived(cb) {
122
- this.onTicketReceivedCallback = cb;
123
- }
124
110
  onTicketReceived(epochIndex, ticket) {
125
111
  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
- }
112
+ // Add to pending queue for potential re-distribution
113
+ this.addTicket(epochIndex, ticket);
147
114
  }
148
115
  }
@@ -208,7 +208,7 @@ describe("TicketDistributionTask", () => {
208
208
  peer1.ticketTask.addTicket(TEST_EPOCH, ticket);
209
209
  peer1.ticketTask.maintainDistribution();
210
210
  await tick();
211
- // Self receives the ticket (via onTicketReceived -> addTicket, no callback set)
211
+ // Self receives the ticket (via onTicketReceived -> addTicket)
212
212
  assert.strictEqual(self.receivedTickets.length, 1);
213
213
  // Self should re-distribute to peer2 (and peer1, but peer1 already has it)
214
214
  self.ticketTask.maintainDistribution();
@@ -217,43 +217,4 @@ describe("TicketDistributionTask", () => {
217
217
  assert.strictEqual(peer2.receivedTickets.length, 1);
218
218
  assert.deepStrictEqual(peer2.receivedTickets[0].ticket, ticket);
219
219
  });
220
- it("should NOT redistribute ticket if validation callback returns false", async () => {
221
- const self = await init("self");
222
- const peer1 = await init("peer1");
223
- const peer2 = await init("peer2");
224
- self.openConnection(peer1);
225
- self.openConnection(peer2);
226
- await tick();
227
- // Validation always rejects
228
- self.ticketTask.setOnTicketReceived(async () => false);
229
- const ticket = createTestTicket(0);
230
- peer1.ticketTask.addTicket(TEST_EPOCH, ticket);
231
- peer1.ticketTask.maintainDistribution();
232
- await tick();
233
- // self.addTicket was NOT called (callback returned false), so nothing to redistribute
234
- assert.strictEqual(self.receivedTickets.length, 0);
235
- self.ticketTask.maintainDistribution();
236
- await tick();
237
- assert.strictEqual(peer2.receivedTickets.length, 0);
238
- });
239
- it("should redistribute ticket if validation callback returns true", async () => {
240
- const self = await init("self");
241
- const peer1 = await init("peer1");
242
- const peer2 = await init("peer2");
243
- self.openConnection(peer1);
244
- self.openConnection(peer2);
245
- await tick();
246
- // Validation always accepts
247
- self.ticketTask.setOnTicketReceived(async () => true);
248
- const ticket = createTestTicket(0);
249
- peer1.ticketTask.addTicket(TEST_EPOCH, ticket);
250
- peer1.ticketTask.maintainDistribution();
251
- await tick();
252
- // self.addTicket WAS called (callback returned true)
253
- assert.strictEqual(self.receivedTickets.length, 1);
254
- self.ticketTask.maintainDistribution();
255
- await tick();
256
- assert.strictEqual(peer2.receivedTickets.length, 1);
257
- assert.deepStrictEqual(peer2.receivedTickets[0].ticket, ticket);
258
- });
259
220
  });
@@ -1,6 +1,5 @@
1
- import { Block, type EntropyHash, type TimeSlot, type ValidatorIndex } from "#@typeberry/block";
1
+ import { Block, type TimeSlot, type ValidatorIndex } from "#@typeberry/block";
2
2
  import { type BlockView } from "#@typeberry/block/block.js";
3
- import type { SignedTicket } from "#@typeberry/block/tickets.js";
4
3
  import { BytesBlob } from "#@typeberry/bytes";
5
4
  import type { ChainSpec } from "#@typeberry/config";
6
5
  import { type BandersnatchSecretSeed } from "#@typeberry/crypto";
@@ -39,10 +38,7 @@ export declare class Generator {
39
38
  static new(args: GeneratorArgs): Generator;
40
39
  private constructor();
41
40
  private getLastHeaderAndState;
42
- nextBlockView(validatorIndex: ValidatorIndex, bandersnatchSecret: BandersnatchSecretSeed, sealPayload: BlockSealInput, timeSlot: TimeSlot, pendingTickets?: {
43
- ticket: SignedTicket;
44
- id: EntropyHash;
45
- }[]): Promise<BlockView>;
41
+ nextBlockView(validatorIndex: ValidatorIndex, bandersnatchSecret: BandersnatchSecretSeed, sealPayload: BlockSealInput, timeSlot: TimeSlot): Promise<BlockView>;
46
42
  /**
47
43
  * Returns y(H_S) part of the VRF signature.
48
44
  *
@@ -54,21 +50,6 @@ export declare class Generator {
54
50
  * data (i.e. the `aux_data`) so we are able to compute it beforehand.
55
51
  */
56
52
  private getEntropyHash;
57
- /**
58
- * Selects tickets to include in the extrinsic from the pending pool.
59
- *
60
- * Tickets were already verified at receipt time (IDs pre-computed). This method:
61
- * 1. Filters out tickets whose IDs are already in `state.ticketsAccumulator` (already processed).
62
- * 2. Sorts remaining tickets by ID ascending (required by Safrole).
63
- * 3. Deduplicates by ID (pool dedup is best-effort; reorgs can produce duplicates).
64
- * 4. Returns at most `chainSpec.maxTicketsPerExtrinsic` tickets.
65
- *
66
- * Called only during the contest period (slotInEpoch < contestLength).
67
- */
68
- private prepareTicketsExtrinsic;
69
- nextBlock(validatorIndex: ValidatorIndex, bandersnatchSecret: BandersnatchSecretSeed, sealPayload: BlockSealInput, timeSlot: TimeSlot, pendingTickets?: {
70
- ticket: SignedTicket;
71
- id: EntropyHash;
72
- }[]): Promise<Block>;
53
+ nextBlock(validatorIndex: ValidatorIndex, bandersnatchSecret: BandersnatchSecretSeed, sealPayload: BlockSealInput, timeSlot: TimeSlot): Promise<Block>;
73
54
  }
74
55
  //# sourceMappingURL=generator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../../../../packages/workers/block-authorship/generator.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,EACL,KAAK,WAAW,EAIhB,KAAK,QAAQ,EACb,KAAK,cAAc,EACpB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,KAAK,SAAS,EAAa,MAAM,2BAA2B,CAAC;AAEtE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EAAS,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAEpD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAoC,KAAK,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAClG,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAIvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yCAAyC,CAAC;AAGhF,OAAO,EAAqB,KAAK,MAAM,EAAU,MAAM,kBAAkB,CAAC;AAM1E;;;;;;;;GAQG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;AAEvD,oDAAoD;AACpD,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,EAAE,SAAS,CAAC;IACrB,YAAY,EAAE,gBAAgB,CAAC;IAC/B,YAAY,EAAE,MAAM,CAAC,YAAY,CAAC;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,QAAQ,CAAC;IACjB,MAAM,EAAE,QAAQ,CAAC;CAClB,CAAC;AAEF,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2C;IAEnE,SAAgB,SAAS,EAAE,SAAS,CAAC;IACrC,SAAgB,YAAY,EAAE,gBAAgB,CAAC;IAC/C,SAAgB,YAAY,EAAE,MAAM,CAAC,YAAY,CAAC;IAClD,SAAgB,OAAO,EAAE,OAAO,CAAC;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAW;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAW;IAElC,wDAAwD;IACxD,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,aAAa;IAI9B,OAAO;IAUP,OAAO,CAAC,qBAAqB;IAYvB,aAAa,CACjB,cAAc,EAAE,cAAc,EAC9B,kBAAkB,EAAE,sBAAsB,EAC1C,WAAW,EAAE,cAAc,EAC3B,QAAQ,EAAE,QAAQ,EAClB,cAAc,GAAE;QAAE,MAAM,EAAE,YAAY,CAAC;QAAC,EAAE,EAAE,WAAW,CAAA;KAAE,EAAO,GAC/D,OAAO,CAAC,SAAS,CAAC;IAKrB;;;;;;;;;OASG;YACW,cAAc;IAiB5B;;;;;;;;;;OAUG;IACH,OAAO,CAAC,uBAAuB;IA4BzB,SAAS,CACb,cAAc,EAAE,cAAc,EAC9B,kBAAkB,EAAE,sBAAsB,EAC1C,WAAW,EAAE,cAAc,EAC3B,QAAQ,EAAE,QAAQ,EAClB,cAAc,GAAE;QAAE,MAAM,EAAE,YAAY,CAAC;QAAC,EAAE,EAAE,WAAW,CAAA;KAAE,EAAO;CAqGnE"}
1
+ {"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../../../../packages/workers/block-authorship/generator.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,EAIL,KAAK,QAAQ,EACb,KAAK,cAAc,EACpB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,KAAK,SAAS,EAAa,MAAM,2BAA2B,CAAC;AAEtE,OAAO,EAAS,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAoC,KAAK,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAClG,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAIvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yCAAyC,CAAC;AAGhF,OAAO,EAAqB,KAAK,MAAM,EAAU,MAAM,kBAAkB,CAAC;AAM1E;;;;;;;;GAQG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;AAEvD,oDAAoD;AACpD,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,EAAE,SAAS,CAAC;IACrB,YAAY,EAAE,gBAAgB,CAAC;IAC/B,YAAY,EAAE,MAAM,CAAC,YAAY,CAAC;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,QAAQ,CAAC;IACjB,MAAM,EAAE,QAAQ,CAAC;CAClB,CAAC;AAEF,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2C;IAEnE,SAAgB,SAAS,EAAE,SAAS,CAAC;IACrC,SAAgB,YAAY,EAAE,gBAAgB,CAAC;IAC/C,SAAgB,YAAY,EAAE,MAAM,CAAC,YAAY,CAAC;IAClD,SAAgB,OAAO,EAAE,OAAO,CAAC;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAW;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAW;IAElC,wDAAwD;IACxD,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,aAAa;IAI9B,OAAO;IAUP,OAAO,CAAC,qBAAqB;IAYvB,aAAa,CACjB,cAAc,EAAE,cAAc,EAC9B,kBAAkB,EAAE,sBAAsB,EAC1C,WAAW,EAAE,cAAc,EAC3B,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC,SAAS,CAAC;IAKrB;;;;;;;;;OASG;YACW,cAAc;IAiBtB,SAAS,CACb,cAAc,EAAE,cAAc,EAC9B,kBAAkB,EAAE,sBAAsB,EAC1C,WAAW,EAAE,cAAc,EAC3B,QAAQ,EAAE,QAAQ;CAgGrB"}
@@ -2,7 +2,6 @@ import { Block, encodeUnsealedHeader, Header, reencodeAsView, } from "#@typeberr
2
2
  import { Extrinsic } from "#@typeberry/block/block.js";
3
3
  import { DisputesExtrinsic } from "#@typeberry/block/disputes.js";
4
4
  import { Bytes, BytesBlob } from "#@typeberry/bytes";
5
- import { HashSet } from "#@typeberry/collections/hash-set.js";
6
5
  import { BANDERSNATCH_VRF_SIGNATURE_BYTES } from "#@typeberry/crypto";
7
6
  import { Logger } from "#@typeberry/logger";
8
7
  import { Safrole } from "#@typeberry/safrole";
@@ -45,8 +44,8 @@ export class Generator {
45
44
  lastState,
46
45
  };
47
46
  }
48
- async nextBlockView(validatorIndex, bandersnatchSecret, sealPayload, timeSlot, pendingTickets = []) {
49
- const newBlock = await this.nextBlock(validatorIndex, bandersnatchSecret, sealPayload, timeSlot, pendingTickets);
47
+ async nextBlockView(validatorIndex, bandersnatchSecret, sealPayload, timeSlot) {
48
+ const newBlock = await this.nextBlock(validatorIndex, bandersnatchSecret, sealPayload, timeSlot);
50
49
  return reencodeAsView(Block.Codec, newBlock, this.chainSpec);
51
50
  }
52
51
  /**
@@ -66,37 +65,7 @@ export class Generator {
66
65
  }
67
66
  return entropyHashResult;
68
67
  }
69
- /**
70
- * Selects tickets to include in the extrinsic from the pending pool.
71
- *
72
- * Tickets were already verified at receipt time (IDs pre-computed). This method:
73
- * 1. Filters out tickets whose IDs are already in `state.ticketsAccumulator` (already processed).
74
- * 2. Sorts remaining tickets by ID ascending (required by Safrole).
75
- * 3. Deduplicates by ID (pool dedup is best-effort; reorgs can produce duplicates).
76
- * 4. Returns at most `chainSpec.maxTicketsPerExtrinsic` tickets.
77
- *
78
- * Called only during the contest period (slotInEpoch < contestLength).
79
- */
80
- prepareTicketsExtrinsic(pendingTickets, state) {
81
- if (pendingTickets.length === 0) {
82
- return [];
83
- }
84
- // Tickets are already verified at receipt time — just filter, sort and slice.
85
- // Build a set of ticket IDs already in the state accumulator for fast lookup.
86
- const accumulatedIds = HashSet.from(state.ticketsAccumulator.map((t) => t.id));
87
- const filtered = pendingTickets.filter(({ id }) => !accumulatedIds.has(id));
88
- // Sort by ID ascending
89
- filtered.sort((a, b) => a.id.compare(b.id).value);
90
- // Deduplicate by ID (pool dedup is best-effort; state may produce duplicates across reorgs)
91
- const deduped = [];
92
- for (const item of filtered) {
93
- if (deduped.length === 0 || !deduped[deduped.length - 1].id.isEqualTo(item.id)) {
94
- deduped.push(item);
95
- }
96
- }
97
- return deduped.slice(0, this.chainSpec.maxTicketsPerExtrinsic).map(({ ticket }) => ticket);
98
- }
99
- async nextBlock(validatorIndex, bandersnatchSecret, sealPayload, timeSlot, pendingTickets = []) {
68
+ async nextBlock(validatorIndex, bandersnatchSecret, sealPayload, timeSlot) {
100
69
  this.metrics.recordBlockAuthoringStarted(timeSlot);
101
70
  const startTime = now();
102
71
  // fetch latest data from the db.
@@ -116,12 +85,9 @@ export class Generator {
116
85
  // retrieve data from previous block
117
86
  const hasher = TransitionHasher.new(this.keccakHasher, this.blake2b);
118
87
  const stateRoot = this.states.getStateRoot(lastState);
119
- const slotInEpoch = timeSlot % this.chainSpec.epochLength;
120
- const isContestPeriod = slotInEpoch < this.chainSpec.contestLength;
121
- // Include tickets only during contest period
122
- const ticketsForExtrinsic = isContestPeriod ? await this.prepareTicketsExtrinsic(pendingTickets, lastState) : [];
88
+ // TODO create extrinsic
123
89
  const extrinsic = Extrinsic.create({
124
- tickets: asOpaqueType(ticketsForExtrinsic),
90
+ tickets: asOpaqueType([]),
125
91
  preimages: [],
126
92
  guarantees: asOpaqueType([]),
127
93
  assurances: asOpaqueType([]),
@@ -1,10 +1,9 @@
1
1
  import { afterEach, beforeEach, describe, it, mock } from "node:test";
2
2
  import { Block, DisputesExtrinsic, EpochMarker, Extrinsic, Header, tryAsTimeSlot, tryAsValidatorIndex, ValidatorKeys, } from "#@typeberry/block";
3
- import { SignedTicket, Ticket, tryAsTicketAttempt } from "#@typeberry/block/tickets.js";
4
3
  import { Bytes, BytesBlob } from "#@typeberry/bytes";
5
4
  import { asKnownSize, FixedSizeArray } from "#@typeberry/collections";
6
5
  import { tinyChainSpec } from "#@typeberry/config";
7
- import { BANDERSNATCH_KEY_BYTES, BANDERSNATCH_PROOF_BYTES, BANDERSNATCH_VRF_SIGNATURE_BYTES, BLS_KEY_BYTES, ED25519_KEY_BYTES, initWasm, } from "#@typeberry/crypto";
6
+ import { BANDERSNATCH_KEY_BYTES, BANDERSNATCH_VRF_SIGNATURE_BYTES, BLS_KEY_BYTES, ED25519_KEY_BYTES, initWasm, } from "#@typeberry/crypto";
8
7
  import { BANDERSNATCH_RING_ROOT_BYTES } from "#@typeberry/crypto/bandersnatch.js";
9
8
  import { Blake2b, HASH_SIZE, keccak } from "#@typeberry/hash";
10
9
  import bandersnatchVrf from "#@typeberry/safrole/bandersnatch-vrf.js";
@@ -175,206 +174,6 @@ describe("Generator", () => {
175
174
  });
176
175
  deepEqual(block, expectedBlock);
177
176
  });
178
- it("should include sorted tickets during contest period", async () => {
179
- // tinyChainSpec: contestLength = 10, so slot 1 is in contest period (1 < 10)
180
- const state = createMockState(0);
181
- const blocksDb = createMockBlocksDb(MOCK_PARENT_HASH);
182
- const statesDb = createMockStatesDb(state);
183
- const generator = Generator.new({
184
- chainSpec: tinyChainSpec,
185
- bandersnatch,
186
- keccakHasher,
187
- blake2b,
188
- blocks: blocksDb,
189
- states: statesDb,
190
- });
191
- // Create two tickets with different signatures
192
- const sig1 = Bytes.zero(BANDERSNATCH_PROOF_BYTES);
193
- sig1.raw[0] = 1;
194
- const sig2 = Bytes.zero(BANDERSNATCH_PROOF_BYTES);
195
- sig2.raw[0] = 2;
196
- const ticket1 = SignedTicket.create({
197
- attempt: tryAsTicketAttempt(0),
198
- signature: sig1.asOpaque(),
199
- });
200
- const ticket2 = SignedTicket.create({
201
- attempt: tryAsTicketAttempt(0),
202
- signature: sig2.asOpaque(),
203
- });
204
- // ticket2 gets smaller ID (0x01...) and ticket1 gets larger ID (0x02...)
205
- // so the sorted order should be [ticket2, ticket1]
206
- const id1 = Bytes.fill(HASH_SIZE, 0x02).asOpaque();
207
- const id2 = Bytes.fill(HASH_SIZE, 0x01).asOpaque();
208
- const validatorIndex = tryAsValidatorIndex(0);
209
- // Slot 1 is in contest period (1 < contestLength=10)
210
- const timeSlot = tryAsTimeSlot(1);
211
- // IDs are now pre-computed before passing to nextBlock
212
- const block = await generator.nextBlock(validatorIndex, MOCK_BANDERSNATCH_SECRET, MOCK_SEAL_PAYLOAD, timeSlot, [
213
- { ticket: ticket1, id: id1 },
214
- { ticket: ticket2, id: id2 },
215
- ]);
216
- // Tickets should be sorted by ID ascending: ticket2 (id=0x01) before ticket1 (id=0x02)
217
- const tickets = block.extrinsic.tickets;
218
- deepEqual(tickets.length, 2);
219
- deepEqual(tickets[0].signature, sig2.asOpaque());
220
- deepEqual(tickets[1].signature, sig1.asOpaque());
221
- });
222
- it("should exclude tickets outside contest period", async () => {
223
- // tinyChainSpec: contestLength = 10, epochLength = 12
224
- // Slot 10 is outside contest period (10 >= 10)
225
- const state = createMockState(9);
226
- const blocksDb = createMockBlocksDb(MOCK_PARENT_HASH);
227
- const statesDb = createMockStatesDb(state);
228
- const generator = Generator.new({
229
- chainSpec: tinyChainSpec,
230
- bandersnatch,
231
- keccakHasher,
232
- blake2b,
233
- blocks: blocksDb,
234
- states: statesDb,
235
- });
236
- const sig1 = Bytes.zero(BANDERSNATCH_PROOF_BYTES);
237
- const ticket1 = SignedTicket.create({
238
- attempt: tryAsTicketAttempt(0),
239
- signature: sig1.asOpaque(),
240
- });
241
- const validatorIndex = tryAsValidatorIndex(0);
242
- // Slot 10 is NOT in contest period (10 >= contestLength=10)
243
- const timeSlot = tryAsTimeSlot(10);
244
- const mockId = Bytes.fill(HASH_SIZE, 0x01).asOpaque();
245
- const block = await generator.nextBlock(validatorIndex, MOCK_BANDERSNATCH_SECRET, MOCK_SEAL_PAYLOAD, timeSlot, [
246
- { ticket: ticket1, id: mockId },
247
- ]);
248
- // No tickets should be included outside contest period
249
- const tickets = block.extrinsic.tickets;
250
- deepEqual(tickets.length, 0);
251
- });
252
- it("should filter out tickets already in ticketsAccumulator", async () => {
253
- // Build a state that already has ticket with id=0x01 in its accumulator
254
- const accumulatedId = Bytes.fill(HASH_SIZE, 0x01).asOpaque();
255
- const accumulatedTicket = Ticket.create({
256
- id: accumulatedId,
257
- attempt: tryAsTicketAttempt(0),
258
- });
259
- const state = {
260
- ...createMockState(0),
261
- ticketsAccumulator: asKnownSize([accumulatedTicket]),
262
- };
263
- const blocksDb = createMockBlocksDb(MOCK_PARENT_HASH);
264
- const statesDb = createMockStatesDb(state);
265
- const generator = Generator.new({
266
- chainSpec: tinyChainSpec,
267
- bandersnatch,
268
- keccakHasher,
269
- blake2b,
270
- blocks: blocksDb,
271
- states: statesDb,
272
- });
273
- const sig1 = Bytes.zero(BANDERSNATCH_PROOF_BYTES);
274
- sig1.raw[0] = 1;
275
- const sig2 = Bytes.zero(BANDERSNATCH_PROOF_BYTES);
276
- sig2.raw[0] = 2;
277
- const ticketAlreadyAccumulated = SignedTicket.create({
278
- attempt: tryAsTicketAttempt(0),
279
- signature: sig1.asOpaque(),
280
- });
281
- const ticketNew = SignedTicket.create({
282
- attempt: tryAsTicketAttempt(0),
283
- signature: sig2.asOpaque(),
284
- });
285
- // id=0x01 is already in accumulator, id=0x02 is new
286
- const idAccumulated = Bytes.fill(HASH_SIZE, 0x01).asOpaque();
287
- const idNew = Bytes.fill(HASH_SIZE, 0x02).asOpaque();
288
- const validatorIndex = tryAsValidatorIndex(0);
289
- const timeSlot = tryAsTimeSlot(1); // inside contest period
290
- const block = await generator.nextBlock(validatorIndex, MOCK_BANDERSNATCH_SECRET, MOCK_SEAL_PAYLOAD, timeSlot, [
291
- { ticket: ticketAlreadyAccumulated, id: idAccumulated },
292
- { ticket: ticketNew, id: idNew },
293
- ]);
294
- // Only the new ticket (not in accumulator) should be included
295
- const tickets = block.extrinsic.tickets;
296
- deepEqual(tickets.length, 1);
297
- deepEqual(tickets[0].signature, sig2.asOpaque());
298
- });
299
- it("should deduplicate tickets by ID", async () => {
300
- const state = createMockState(0);
301
- const blocksDb = createMockBlocksDb(MOCK_PARENT_HASH);
302
- const statesDb = createMockStatesDb(state);
303
- const generator = Generator.new({
304
- chainSpec: tinyChainSpec,
305
- bandersnatch,
306
- keccakHasher,
307
- blake2b,
308
- blocks: blocksDb,
309
- states: statesDb,
310
- });
311
- const sig1 = Bytes.zero(BANDERSNATCH_PROOF_BYTES);
312
- sig1.raw[0] = 1;
313
- const sig2 = Bytes.zero(BANDERSNATCH_PROOF_BYTES);
314
- sig2.raw[0] = 2;
315
- // Two different SignedTicket objects but with the same ID (e.g. duplicate from reorg)
316
- const ticketA = SignedTicket.create({
317
- attempt: tryAsTicketAttempt(0),
318
- signature: sig1.asOpaque(),
319
- });
320
- const ticketB = SignedTicket.create({
321
- attempt: tryAsTicketAttempt(0),
322
- signature: sig2.asOpaque(),
323
- });
324
- const duplicateId = Bytes.fill(HASH_SIZE, 0x05).asOpaque();
325
- const validatorIndex = tryAsValidatorIndex(0);
326
- const timeSlot = tryAsTimeSlot(1);
327
- const block = await generator.nextBlock(validatorIndex, MOCK_BANDERSNATCH_SECRET, MOCK_SEAL_PAYLOAD, timeSlot, [
328
- { ticket: ticketA, id: duplicateId },
329
- { ticket: ticketB, id: duplicateId }, // same ID — should be deduplicated
330
- ]);
331
- const tickets = block.extrinsic.tickets;
332
- deepEqual(tickets.length, 1);
333
- // First occurrence is kept after sort (both have same ID, ticketA comes first)
334
- deepEqual(tickets[0].signature, sig1.asOpaque());
335
- });
336
- it("should include at most maxTicketsPerExtrinsic tickets", async () => {
337
- // tinyChainSpec.maxTicketsPerExtrinsic = 3
338
- const state = createMockState(0);
339
- const blocksDb = createMockBlocksDb(MOCK_PARENT_HASH);
340
- const statesDb = createMockStatesDb(state);
341
- const generator = Generator.new({
342
- chainSpec: tinyChainSpec,
343
- bandersnatch,
344
- keccakHasher,
345
- blake2b,
346
- blocks: blocksDb,
347
- states: statesDb,
348
- });
349
- // Create 4 tickets — only 3 should be included (lowest IDs win)
350
- const makeTicket = (sigByte, idByte) => {
351
- const sig = Bytes.zero(BANDERSNATCH_PROOF_BYTES);
352
- sig.raw[0] = sigByte;
353
- return {
354
- ticket: SignedTicket.create({ attempt: tryAsTicketAttempt(0), signature: sig.asOpaque() }),
355
- id: Bytes.fill(HASH_SIZE, idByte).asOpaque(),
356
- sig: sig.asOpaque(),
357
- };
358
- };
359
- const t1 = makeTicket(1, 0x01); // lowest ID
360
- const t2 = makeTicket(2, 0x02);
361
- const t3 = makeTicket(3, 0x03);
362
- const t4 = makeTicket(4, 0x04); // highest ID — should be excluded
363
- const validatorIndex = tryAsValidatorIndex(0);
364
- const timeSlot = tryAsTimeSlot(1);
365
- const block = await generator.nextBlock(validatorIndex, MOCK_BANDERSNATCH_SECRET, MOCK_SEAL_PAYLOAD, timeSlot, [
366
- { ticket: t4.ticket, id: t4.id }, // pass out-of-order to verify sorting
367
- { ticket: t2.ticket, id: t2.id },
368
- { ticket: t3.ticket, id: t3.id },
369
- { ticket: t1.ticket, id: t1.id },
370
- ]);
371
- const tickets = block.extrinsic.tickets;
372
- deepEqual(tickets.length, 3); // maxTicketsPerExtrinsic = 3
373
- // Should include the 3 lowest IDs, sorted ascending
374
- deepEqual(tickets[0].signature, t1.sig);
375
- deepEqual(tickets[1].signature, t2.sig);
376
- deepEqual(tickets[2].signature, t3.sig);
377
- });
378
177
  it("should create block with epoch marker at epoch boundary", async () => {
379
178
  // tinyChainSpec.epochLength = 12, so:
380
179
  // - timeslot 11 is last slot of epoch 0
@@ -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,iBAiapG"}
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;AAgB3E,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;AAkBlD,wBAAsB,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,eAAe,iBAsPpG"}
@@ -1,7 +1,6 @@
1
1
  import { setTimeout } from "node:timers/promises";
2
2
  import { tryAsEpoch, tryAsTimeSlot, tryAsValidatorIndex, } from "#@typeberry/block";
3
3
  import { BytesBlob } from "#@typeberry/bytes";
4
- import { HashDictionary } from "#@typeberry/collections/hash-dictionary.js";
5
4
  import { HashSet } from "#@typeberry/collections/hash-set.js";
6
5
  import { initWasm } from "#@typeberry/crypto";
7
6
  import { deriveBandersnatchPublicKey, deriveEd25519PublicKey, } from "#@typeberry/crypto/key-derivation.js";
@@ -9,11 +8,10 @@ import { Blake2b, keccak } from "#@typeberry/hash";
9
8
  import { Logger } from "#@typeberry/logger";
10
9
  import { tryAsU64 } from "#@typeberry/numbers";
11
10
  import { Safrole } from "#@typeberry/safrole";
12
- import bandersnatchVrf from "#@typeberry/safrole/bandersnatch-vrf.js";
13
11
  import { BandernsatchWasm } from "#@typeberry/safrole/bandersnatch-wasm.js";
14
12
  import { JAM_FALLBACK_SEAL, JAM_TICKET_SEAL } from "#@typeberry/safrole/constants.js";
15
13
  import { SafroleSealingKeysKind } from "#@typeberry/state";
16
- import { asOpaqueType, Result } from "#@typeberry/utils";
14
+ import { asOpaqueType, assertNever, Result } from "#@typeberry/utils";
17
15
  import { Generator } from "./generator.js";
18
16
  import { generateTickets } from "./ticket-generator.js";
19
17
  const logger = Logger.new(import.meta.filename, "author");
@@ -53,22 +51,10 @@ export async function main(config, comms, networkingComms) {
53
51
  const initialHash = blocks.getBestHeaderHash();
54
52
  const initialState = states.getState(initialHash);
55
53
  logger.info `Block authorship validator keys: ${keys.map(({ bandersnatchPublic }, index) => `\n ${index}: ${bandersnatchPublic.toString()}`)}`;
56
- // Per-epoch cache for Tickets mode: index corresponds to position in sealingKeySeries.tickets.
57
- // null entry means none of our keys match that slot.
58
- // Rebuilt once per epoch via buildTicketAuthorshipCache().
59
- // Declared here (before the eager startup build below) so its TDZ doesn't fire
60
- // when `buildTicketAuthorshipCache` runs during initialisation.
61
- let ticketAuthorshipCache = null;
62
54
  if (initialState !== null) {
63
- const isEpochStart = startTimeSlot % chainSpec.epochLength === 0;
64
- const initialKeys = await getSealingKeySeries(isEpochStart, startTimeSlot, initialState);
55
+ const initialKeys = await getSealingKeySeries(startTimeSlot % chainSpec.epochLength === 0, startTimeSlot, initialState);
65
56
  if (initialKeys.isOk) {
66
57
  logEpochBlockCreation(tryAsEpoch(Math.floor(startTimeSlot / chainSpec.epochLength)), initialKeys.ok);
67
- // Build the cache eagerly so the first slot of a session doesn't need an
68
- // on-the-fly VRF scan. After this, `buildTicketAuthorshipCache` is only
69
- // re-run on epoch boundaries.
70
- const initialEntropy = isEpochStart ? initialState.entropy[2] : initialState.entropy[3];
71
- await buildTicketAuthorshipCache(initialKeys.ok, initialEntropy);
72
58
  }
73
59
  }
74
60
  function getTime() {
@@ -77,6 +63,14 @@ export async function main(config, comms, networkingComms) {
77
63
  const slotDurationMs = BigInt(chainSpec.slotDuration * 1000);
78
64
  return tryAsU64(BigInt(startTimeSlot) * slotDurationMs + timeFromStart + slotDurationMs);
79
65
  }
66
+ function getKeyForCurrentSlot(sealingKeySeries, keys, timeSlot) {
67
+ if (sealingKeySeries.kind === SafroleSealingKeysKind.Keys) {
68
+ const indexForCurrentSlot = timeSlot % sealingKeySeries.keys.length;
69
+ const sealingKey = sealingKeySeries.keys[indexForCurrentSlot];
70
+ return keys.find((x) => x.bandersnatchPublic.isEqualTo(sealingKey)) ?? null;
71
+ }
72
+ throw new Error("Tickets mode is not supported yet");
73
+ }
80
74
  function getValidatorIndex(key, currentValidatorData) {
81
75
  const index = currentValidatorData.findIndex((data) => data.bandersnatch.isEqualTo(key.bandersnatchPublic));
82
76
  if (index < 0) {
@@ -84,65 +78,14 @@ export async function main(config, comms, networkingComms) {
84
78
  }
85
79
  return tryAsValidatorIndex(index);
86
80
  }
87
- /**
88
- * Precomputes which slots we are the author of for the current epoch (Tickets mode).
89
- */
90
- async function buildTicketAuthorshipCache(sealingKeySeries, entropy) {
91
- if (sealingKeySeries.kind !== SafroleSealingKeysKind.Tickets) {
92
- ticketAuthorshipCache = null;
93
- return;
94
- }
95
- const ownTickets = new HashDictionary();
96
- for (let attempt = 0; attempt < chainSpec.ticketsPerValidator; attempt++) {
97
- const payload = getTicketSealPayload(entropy, attempt);
98
- for (const key of keys) {
99
- const result = await bandersnatchVrf.getVrfOutputHash(bandersnatch, key.bandersnatchSecret, payload);
100
- if (result.isOk) {
101
- ownTickets.set(result.ok.asOpaque(), { key, sealPayload: asOpaqueType(payload) });
102
- }
103
- }
104
- }
105
- const cache = sealingKeySeries.tickets.map((ticket) => ownTickets.get(ticket.id.asOpaque()) ?? null);
106
- ticketAuthorshipCache = cache;
107
- const ours = cache.filter(Boolean).length;
108
- logger.info `Built ticket authorship cache: ${ours}/${cache.length} slots assigned to us this epoch.`;
109
- }
110
- function getTicketSealPayload(entropy, attempt) {
111
- return BytesBlob.blobFromParts(JAM_TICKET_SEAL, entropy.raw, new Uint8Array([attempt]));
112
- }
113
- function getFallbackSealPayload(entropy) {
114
- return asOpaqueType(BytesBlob.blobFromParts(JAM_FALLBACK_SEAL, entropy.raw));
115
- }
116
- /**
117
- * Returns the validator key and seal payload for the current slot, or null if we are not the author.
118
- *
119
- * Keys mode (fallback): matches our key against the slot's assigned bandersnatch key.
120
- * Tickets mode: O(1) lookup against the per-epoch authorship cache (built eagerly at
121
- * startup and on every epoch transition, so we never fall back to on-the-fly VRF).
122
- */
123
- function getSealData(sealingKeySeries, keys, timeSlot, entropy) {
81
+ function getSealPayload(sealingKeySeries, entropy, attempt) {
124
82
  if (sealingKeySeries.kind === SafroleSealingKeysKind.Keys) {
125
- const indexForCurrentSlot = timeSlot % sealingKeySeries.keys.length;
126
- const sealingKey = sealingKeySeries.keys[indexForCurrentSlot];
127
- const key = keys.find((x) => x.bandersnatchPublic.isEqualTo(sealingKey)) ?? null;
128
- if (key === null) {
129
- return null;
130
- }
131
- return {
132
- key,
133
- sealPayload: getFallbackSealPayload(entropy),
134
- logId: `key ${key.bandersnatchPublic}`,
135
- };
83
+ return asOpaqueType(BytesBlob.blobFromParts(JAM_FALLBACK_SEAL, entropy.raw));
136
84
  }
137
- // Tickets mode: each slot is sealed by the validator who can produce the VRF output
138
- // matching the ticket's ID for that slot.
139
- const index = timeSlot % sealingKeySeries.tickets.length;
140
- const ticket = sealingKeySeries.tickets.at(index) ?? null;
141
- const cached = ticketAuthorshipCache?.at(index) ?? null;
142
- if (ticket === null || cached === null) {
143
- return null;
85
+ if (sealingKeySeries.kind === SafroleSealingKeysKind.Tickets) {
86
+ return asOpaqueType(BytesBlob.blobFromParts(JAM_TICKET_SEAL, entropy.raw, new Uint8Array([attempt ?? 0])));
144
87
  }
145
- return { ...cached, logId: `ticket ${ticket.id} (attempt ${ticket.attempt})` };
88
+ assertNever(sealingKeySeries);
146
89
  }
147
90
  function isEpochChanged(lastTimeslot, currentTimeslot) {
148
91
  const lastEpoch = Math.floor(lastTimeslot / chainSpec.epochLength);
@@ -150,17 +93,11 @@ export async function main(config, comms, networkingComms) {
150
93
  return currentEpoch > lastEpoch;
151
94
  }
152
95
  function logEpochBlockCreation(epoch, sealingKeySeries) {
153
- if (sealingKeySeries.kind === SafroleSealingKeysKind.Tickets) {
154
- logger.info `[EPOCH ${epoch}] Tickets mode active with ${sealingKeySeries.tickets.length} tickets.`;
155
- return;
156
- }
157
96
  let isCreating = false;
158
97
  const epochStart = epoch * chainSpec.epochLength;
159
98
  const epochEnd = epochStart + chainSpec.epochLength;
160
99
  for (let slot = epochStart; slot < epochEnd; slot++) {
161
- const indexForCurrentSlot = slot % sealingKeySeries.keys.length;
162
- const sealingKey = sealingKeySeries.keys[indexForCurrentSlot];
163
- const key = keys.find((x) => x.bandersnatchPublic.isEqualTo(sealingKey)) ?? null;
100
+ const key = getKeyForCurrentSlot(sealingKeySeries, keys, tryAsTimeSlot(slot));
164
101
  if (key !== null) {
165
102
  isCreating = true;
166
103
  logger.info `[EPOCH ${epoch}] Validator ${key.bandersnatchPublic.toString()} will author block at slot ${slot}`;
@@ -181,86 +118,13 @@ export async function main(config, comms, networkingComms) {
181
118
  }
182
119
  return Result.ok(state.sealingKeySeries);
183
120
  }
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.length !== tickets.length) {
236
- logger.error `verifyTickets returned ${results.length} results for ${tickets.length} tickets`;
237
- return false;
238
- }
239
- const verified = tickets
240
- .map((ticket, i) => ({ ticket, id: results[i].entropyHash }))
241
- .filter((_, i) => results[i].isValid);
242
- addToPool(epochIndex, verified);
243
- return verified.length > 0;
244
- }
245
- // Receive a single ticket from peers (via jam-network worker).
246
- // Returns true if the ticket passed validation so jam-network can decide whether to redistribute it.
247
- networkingComms.setOnReceivedTickets(async ({ epochIndex, ticket }) => {
248
- logger.log `Received ticket from peer for epoch ${epochIndex}`;
249
- const hash = blocks.getBestHeaderHash();
250
- const state = states.getState(hash);
251
- if (state === null) {
252
- logger.warn `Cannot verify received ticket: no state available`;
253
- return false;
254
- }
255
- return await verifyAndAddToPool(epochIndex, [ticket], state);
256
- });
257
121
  const isFastForward = config.workerParams.isFastForward;
258
122
  let lastGeneratedSlot = startTimeSlot;
259
123
  let ticketsGeneratedForEpoch = -1;
260
124
  while (!isFinished) {
261
125
  const hash = blocks.getBestHeaderHash();
262
126
  const state = states.getState(hash);
263
- const currentValidatorData = state?.currentValidatorData ?? null;
127
+ const currentValidatorData = state?.currentValidatorData;
264
128
  if (state === null) {
265
129
  continue;
266
130
  }
@@ -301,8 +165,6 @@ export async function main(config, comms, networkingComms) {
301
165
  }
302
166
  else {
303
167
  logger.log `Generated ${ticketsResult.ok.length} tickets for epoch ${epoch}. Distributing...`;
304
- // Verify own tickets to get IDs, then add to pool
305
- await verifyAndAddToPool(epoch, ticketsResult.ok, state);
306
168
  // Send directly to network worker (bypasses main thread)
307
169
  await networkingComms.sendTickets({ epochIndex: epoch, tickets: ticketsResult.ok });
308
170
  }
@@ -313,31 +175,19 @@ export async function main(config, comms, networkingComms) {
313
175
  if (selingKeySeriesResult.isError) {
314
176
  continue;
315
177
  }
316
- // On a new epoch, `state.entropy[2]` is the epoch-E entropy (pre-transition);
317
- // mid-epoch, it has already shifted to `entropy[3]`.
318
- const entropy = isNewEpoch ? state.entropy[2] : state.entropy[3];
319
- // Rebuild the authorship cache on each epoch boundary, and also catch the case
320
- // where the startup prebuild was skipped (e.g. initialState was null or the
321
- // initial sealing-key transition errored) so we don't silently miss Tickets-mode
322
- // slots until the next epoch boundary.
323
- const needsCacheRebuild = isNewEpoch ||
324
- (selingKeySeriesResult.ok.kind === SafroleSealingKeysKind.Tickets && ticketAuthorshipCache === null);
325
- if (needsCacheRebuild) {
326
- if (isNewEpoch) {
327
- logEpochBlockCreation(epoch, selingKeySeriesResult.ok);
328
- }
329
- await buildTicketAuthorshipCache(selingKeySeriesResult.ok, entropy);
178
+ if (isNewEpoch) {
179
+ logEpochBlockCreation(epoch, selingKeySeriesResult.ok);
330
180
  }
331
- const sealData = getSealData(selingKeySeriesResult.ok, keys, timeSlot, entropy);
332
- if (sealData !== null && currentValidatorData !== null) {
333
- const { key, sealPayload } = sealData;
181
+ const key = getKeyForCurrentSlot(selingKeySeriesResult.ok, keys, timeSlot);
182
+ if (key !== null && currentValidatorData !== undefined) {
334
183
  const validatorIndex = getValidatorIndex(key, currentValidatorData);
335
184
  if (validatorIndex === null) {
336
185
  continue;
337
186
  }
338
- logger.log `Attempting to create a block using ${sealData.logId} located at validator index ${validatorIndex}.`;
339
- const currentEpochTickets = ticketPool.get(epoch) ?? [];
340
- const newBlock = await generator.nextBlockView(validatorIndex, key.bandersnatchSecret, sealPayload, timeSlot, currentEpochTickets);
187
+ logger.log `Attempting to create a block using key ${key.bandersnatchPublic} located at validator index ${validatorIndex}.`;
188
+ const entropy = isNewEpoch ? state.entropy[2] : state.entropy[3];
189
+ const sealPayload = getSealPayload(selingKeySeriesResult.ok, entropy);
190
+ const newBlock = await generator.nextBlockView(validatorIndex, key.bandersnatchSecret, sealPayload, timeSlot);
341
191
  counter += 1;
342
192
  lastGeneratedSlot = timeSlot;
343
193
  logger.trace `Sending block ${counter}`;
@@ -1,5 +1,5 @@
1
1
  import { type Api, type Internal } from "#@typeberry/workers-api";
2
- import { ReceivedTicketMessage, TicketsMessage } from "./tickets-message.js";
2
+ import { TicketsMessage } from "./tickets-message.js";
3
3
  /**
4
4
  * Port name for authorship-network direct communication.
5
5
  * Used when spawning jam-network worker to pass the port for receiving tickets.
@@ -21,18 +21,7 @@ export declare const protocol: import("@typeberry/workers-api").LousyProtocol<{
21
21
  }>>;
22
22
  response: import("@typeberry/codec").Descriptor<void, void>;
23
23
  };
24
- }, {
25
- receivedTickets: {
26
- request: import("@typeberry/codec").Descriptor<ReceivedTicketMessage, import("@typeberry/codec").ViewOf<ReceivedTicketMessage, {
27
- epochIndex: import("@typeberry/codec").Descriptor<number & import("@typeberry/numbers").WithBytesRepresentation<4> & import("@typeberry/utils").WithOpaque<"Epoch">, import("@typeberry/bytes").Bytes<4>>;
28
- ticket: import("@typeberry/codec").Descriptor<import("@typeberry/block").SignedTicket, import("@typeberry/codec").ViewOf<import("@typeberry/block").SignedTicket, {
29
- attempt: import("@typeberry/codec").Descriptor<number & import("@typeberry/numbers").WithBytesRepresentation<1> & import("@typeberry/utils").WithOpaque<"TicketAttempt[u8]">, import("@typeberry/numbers").U32>;
30
- signature: import("@typeberry/codec").Descriptor<import("@typeberry/bytes").Bytes<784> & import("@typeberry/utils").WithOpaque<"BandersnatchRingSignature">, import("@typeberry/bytes").Bytes<784>>;
31
- }>>;
32
- }>>;
33
- response: import("@typeberry/codec").Descriptor<boolean, boolean>;
34
- };
35
- }>;
24
+ }, {}>;
36
25
  export type NetworkingComms = Api<typeof protocol>;
37
26
  export type AuthorshipComms = Internal<typeof protocol>;
38
27
  //# sourceMappingURL=protocol.d.ts.map
@@ -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,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD;;;GAGG;AACH,eAAO,MAAM,uBAAuB,uBAAuB,CAAC;AAE5D;;;;GAIG;AACH,eAAO,MAAM,QAAQ;;;;;;;;;;;MAUnB,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,GAAG,CAAC,OAAO,QAAQ,CAAC,CAAC;AACnD,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC,OAAO,QAAQ,CAAC,CAAC"}
@@ -1,6 +1,6 @@
1
1
  import { codec } from "#@typeberry/codec";
2
2
  import { createProtocol } from "#@typeberry/workers-api";
3
- import { ReceivedTicketMessage, TicketsMessage } from "./tickets-message.js";
3
+ import { TicketsMessage } from "./tickets-message.js";
4
4
  /**
5
5
  * Port name for authorship-network direct communication.
6
6
  * Used when spawning jam-network worker to pass the port for receiving tickets.
@@ -19,13 +19,6 @@ export const protocol = createProtocol("authorship-network", {
19
19
  response: codec.nothing,
20
20
  },
21
21
  },
22
- // Messages from jam-network to block-authorship (one ticket per relay).
23
- // Response indicates whether the ticket passed validation — used by jam-network
24
- // to decide whether to redistribute the ticket to other peers.
25
- fromWorker: {
26
- receivedTickets: {
27
- request: ReceivedTicketMessage.Codec,
28
- response: codec.bool,
29
- },
30
- },
22
+ // Messages from jam-network to block-authorship (none for now)
23
+ fromWorker: {},
31
24
  });
@@ -15,18 +15,4 @@ export declare class TicketsMessage extends WithDebug {
15
15
  static create({ epochIndex, tickets }: CodecRecord<TicketsMessage>): TicketsMessage;
16
16
  private constructor();
17
17
  }
18
- /** Single-ticket message sent from jam-network to block-authorship (one ticket per peer relay). */
19
- export declare class ReceivedTicketMessage extends WithDebug {
20
- readonly epochIndex: Epoch;
21
- readonly ticket: SignedTicket;
22
- static Codec: import("@typeberry/codec").Descriptor<ReceivedTicketMessage, import("@typeberry/codec").ViewOf<ReceivedTicketMessage, {
23
- epochIndex: import("@typeberry/codec").Descriptor<number & import("@typeberry/numbers").WithBytesRepresentation<4> & import("@typeberry/utils").WithOpaque<"Epoch">, import("@typeberry/bytes").Bytes<4>>;
24
- ticket: import("@typeberry/codec").Descriptor<SignedTicket, import("@typeberry/codec").ViewOf<SignedTicket, {
25
- attempt: import("@typeberry/codec").Descriptor<number & import("@typeberry/numbers").WithBytesRepresentation<1> & import("@typeberry/utils").WithOpaque<"TicketAttempt[u8]">, import("@typeberry/numbers").U32>;
26
- signature: import("@typeberry/codec").Descriptor<import("@typeberry/bytes").Bytes<784> & import("@typeberry/utils").WithOpaque<"BandersnatchRingSignature">, import("@typeberry/bytes").Bytes<784>>;
27
- }>>;
28
- }>>;
29
- static create({ epochIndex, ticket }: CodecRecord<ReceivedTicketMessage>): ReceivedTicketMessage;
30
- private constructor();
31
- }
32
18
  //# sourceMappingURL=tickets-message.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tickets-message.d.ts","sourceRoot":"","sources":["../../../../../packages/workers/comms-authorship-network/tickets-message.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,EAAE,KAAK,WAAW,EAAS,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE7C,qBAAa,cAAe,SAAQ,SAAS;aAWzB,UAAU,EAAE,KAAK;aACjB,OAAO,EAAE,YAAY,EAAE;IAXzC,MAAM,CAAC,KAAK;;;;;;QAGT;IAEH,MAAM,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,WAAW,CAAC,cAAc,CAAC;IAIlE,OAAO;CAMR;AAED,mGAAmG;AACnG,qBAAa,qBAAsB,SAAQ,SAAS;aAWhC,UAAU,EAAE,KAAK;aACjB,MAAM,EAAE,YAAY;IAXtC,MAAM,CAAC,KAAK;;;;;;QAGT;IAEH,MAAM,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,WAAW,CAAC,qBAAqB,CAAC;IAIxE,OAAO;CAMR"}
1
+ {"version":3,"file":"tickets-message.d.ts","sourceRoot":"","sources":["../../../../../packages/workers/comms-authorship-network/tickets-message.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAC3D,OAAO,EAAE,KAAK,WAAW,EAAS,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE7C,qBAAa,cAAe,SAAQ,SAAS;aAWzB,UAAU,EAAE,KAAK;aACjB,OAAO,EAAE,YAAY,EAAE;IAXzC,MAAM,CAAC,KAAK;;;;;;QAGT;IAEH,MAAM,CAAC,MAAM,CAAC,EAAE,UAAU,EAAE,OAAO,EAAE,EAAE,WAAW,CAAC,cAAc,CAAC;IAIlE,OAAO;CAMR"}
@@ -17,20 +17,3 @@ export class TicketsMessage extends WithDebug {
17
17
  this.tickets = tickets;
18
18
  }
19
19
  }
20
- /** Single-ticket message sent from jam-network to block-authorship (one ticket per peer relay). */
21
- export class ReceivedTicketMessage extends WithDebug {
22
- epochIndex;
23
- ticket;
24
- static Codec = codec.Class(ReceivedTicketMessage, {
25
- epochIndex: codec.u32.asOpaque(),
26
- ticket: SignedTicket.Codec,
27
- });
28
- static create({ epochIndex, ticket }) {
29
- return new ReceivedTicketMessage(epochIndex, ticket);
30
- }
31
- constructor(epochIndex, ticket) {
32
- super();
33
- this.epochIndex = epochIndex;
34
- this.ticket = ticket;
35
- }
36
- }
@@ -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":"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,iBAmDjC"}
@@ -38,11 +38,6 @@ export async function main(config, comms, authorshipComms) {
38
38
  network.ticketTask.addTicket(epochIndex, ticket);
39
39
  }
40
40
  });
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 });
45
- });
46
41
  await network.network.start();
47
42
  // stop the network when the worker is finishing.
48
43
  await waitForFinish;