@typeberry/lib 0.8.4-2e4ce67 → 0.8.4-70b1490

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 (127) hide show
  1. package/package.json +5 -3
  2. package/packages/configs/index.d.ts +30 -1
  3. package/packages/configs/index.d.ts.map +1 -1
  4. package/packages/configs/index.js +4 -2
  5. package/packages/configs/typeberry-dev-full.json +29 -0
  6. package/packages/core/bytes/bytes.d.ts +1 -0
  7. package/packages/core/bytes/bytes.d.ts.map +1 -1
  8. package/packages/core/bytes/bytes.js +8 -0
  9. package/packages/core/utils/debug.d.ts +4 -2
  10. package/packages/core/utils/debug.d.ts.map +1 -1
  11. package/packages/core/utils/debug.js +18 -13
  12. package/packages/core/utils/debug.test.js +12 -6
  13. package/packages/jam/config-node/node-config.d.ts +2 -1
  14. package/packages/jam/config-node/node-config.d.ts.map +1 -1
  15. package/packages/jam/config-node/node-config.js +8 -3
  16. package/packages/jam/config-node/node-config.test.js +3 -3
  17. package/packages/jam/database-fjall/hybrid-states.d.ts +45 -0
  18. package/packages/jam/database-fjall/hybrid-states.d.ts.map +1 -0
  19. package/packages/jam/database-fjall/hybrid-states.js +113 -0
  20. package/packages/jam/database-fjall/hybrid-states.test.d.ts +2 -0
  21. package/packages/jam/database-fjall/hybrid-states.test.d.ts.map +1 -0
  22. package/packages/jam/database-fjall/hybrid-states.test.js +83 -0
  23. package/packages/jam/database-fjall/index.d.ts +3 -0
  24. package/packages/jam/database-fjall/index.d.ts.map +1 -0
  25. package/packages/jam/database-fjall/index.js +2 -0
  26. package/packages/jam/database-fjall/root.d.ts +52 -0
  27. package/packages/jam/database-fjall/root.d.ts.map +1 -0
  28. package/packages/jam/database-fjall/root.js +85 -0
  29. package/packages/jam/jamnp-s/tasks/ticket-distribution.d.ts +18 -10
  30. package/packages/jam/jamnp-s/tasks/ticket-distribution.d.ts.map +1 -1
  31. package/packages/jam/jamnp-s/tasks/ticket-distribution.js +44 -68
  32. package/packages/jam/jamnp-s/tasks/ticket-distribution.test.js +30 -8
  33. package/packages/jam/node/main-fuzz.d.ts.map +1 -1
  34. package/packages/jam/node/main-fuzz.js +16 -1
  35. package/packages/jam/node/main-importer.d.ts +6 -3
  36. package/packages/jam/node/main-importer.d.ts.map +1 -1
  37. package/packages/jam/node/main-importer.js +3 -2
  38. package/packages/jam/safrole/bandersnatch-vrf.d.ts +22 -2
  39. package/packages/jam/safrole/bandersnatch-vrf.d.ts.map +1 -1
  40. package/packages/jam/safrole/bandersnatch-vrf.js +54 -20
  41. package/packages/jam/safrole/bandersnatch-vrf.test.js +3 -2
  42. package/packages/jam/safrole/bandersnatch-wasm.d.ts +10 -0
  43. package/packages/jam/safrole/bandersnatch-wasm.d.ts.map +1 -1
  44. package/packages/jam/safrole/bandersnatch-wasm.js +12 -0
  45. package/packages/jam/ticket-pool/index.d.ts +4 -0
  46. package/packages/jam/ticket-pool/index.d.ts.map +1 -0
  47. package/packages/jam/ticket-pool/index.js +3 -0
  48. package/packages/jam/ticket-pool/pending-ticket-pool.d.ts +30 -0
  49. package/packages/jam/ticket-pool/pending-ticket-pool.d.ts.map +1 -0
  50. package/packages/jam/ticket-pool/pending-ticket-pool.js +56 -0
  51. package/packages/jam/ticket-pool/pending-ticket-pool.test.d.ts +2 -0
  52. package/packages/jam/ticket-pool/pending-ticket-pool.test.d.ts.map +1 -0
  53. package/packages/jam/ticket-pool/pending-ticket-pool.test.js +67 -0
  54. package/packages/jam/ticket-pool/ticket-validator.d.ts +47 -0
  55. package/packages/jam/ticket-pool/ticket-validator.d.ts.map +1 -0
  56. package/packages/jam/ticket-pool/ticket-validator.js +34 -0
  57. package/packages/jam/ticket-pool/ticket-validator.test.d.ts +2 -0
  58. package/packages/jam/ticket-pool/ticket-validator.test.d.ts.map +1 -0
  59. package/packages/jam/ticket-pool/ticket-validator.test.js +35 -0
  60. package/packages/jam/ticket-pool/verified-ticket-pool.d.ts +26 -0
  61. package/packages/jam/ticket-pool/verified-ticket-pool.d.ts.map +1 -0
  62. package/packages/jam/ticket-pool/verified-ticket-pool.js +41 -0
  63. package/packages/jam/ticket-pool/verified-ticket-pool.test.d.ts +2 -0
  64. package/packages/jam/ticket-pool/verified-ticket-pool.test.d.ts.map +1 -0
  65. package/packages/jam/ticket-pool/verified-ticket-pool.test.js +54 -0
  66. package/packages/workers/api-node/config.d.ts +12 -5
  67. package/packages/workers/api-node/config.d.ts.map +1 -1
  68. package/packages/workers/api-node/config.js +20 -17
  69. package/packages/workers/api-node/config.test.js +38 -1
  70. package/packages/workers/block-authorship/{generator.d.ts → block-generator.d.ts} +5 -5
  71. package/packages/workers/block-authorship/block-generator.d.ts.map +1 -0
  72. package/packages/workers/block-authorship/{generator.js → block-generator.js} +3 -3
  73. package/packages/workers/block-authorship/block-generator.test.d.ts +2 -0
  74. package/packages/workers/block-authorship/block-generator.test.d.ts.map +1 -0
  75. package/packages/workers/block-authorship/{generator.test.js → block-generator.test.js} +8 -8
  76. package/packages/workers/block-authorship/epoch-authoring-slots.d.ts +35 -0
  77. package/packages/workers/block-authorship/epoch-authoring-slots.d.ts.map +1 -0
  78. package/packages/workers/block-authorship/epoch-authoring-slots.js +86 -0
  79. package/packages/workers/block-authorship/epoch-tracker.d.ts +29 -0
  80. package/packages/workers/block-authorship/epoch-tracker.d.ts.map +1 -0
  81. package/packages/workers/block-authorship/epoch-tracker.js +80 -0
  82. package/packages/workers/block-authorship/index.d.ts.map +1 -1
  83. package/packages/workers/block-authorship/index.js +1 -1
  84. package/packages/workers/block-authorship/main.d.ts +3 -0
  85. package/packages/workers/block-authorship/main.d.ts.map +1 -1
  86. package/packages/workers/block-authorship/main.js +197 -317
  87. package/packages/workers/block-authorship/ticket-generator/bootstrap-main.d.ts +2 -0
  88. package/packages/workers/block-authorship/ticket-generator/bootstrap-main.d.ts.map +1 -0
  89. package/packages/workers/block-authorship/ticket-generator/bootstrap-main.js +23 -0
  90. package/packages/workers/block-authorship/ticket-generator/index.d.ts +16 -0
  91. package/packages/workers/block-authorship/ticket-generator/index.d.ts.map +1 -0
  92. package/packages/workers/block-authorship/ticket-generator/index.js +62 -0
  93. package/packages/workers/block-authorship/ticket-generator/protocol.d.ts +50 -0
  94. package/packages/workers/block-authorship/ticket-generator/protocol.d.ts.map +1 -0
  95. package/packages/workers/block-authorship/ticket-generator/protocol.js +54 -0
  96. package/packages/workers/block-authorship/{ticket-generator.d.ts → ticket-generator/ticket-generator.d.ts} +4 -0
  97. package/packages/workers/block-authorship/ticket-generator/ticket-generator.d.ts.map +1 -0
  98. package/packages/workers/block-authorship/{ticket-generator.js → ticket-generator/ticket-generator.js} +19 -9
  99. package/packages/workers/block-authorship/ticket-generator/ticket-generator.test.d.ts.map +1 -0
  100. package/packages/workers/block-authorship/{ticket-generator.test.js → ticket-generator/ticket-generator.test.js} +13 -9
  101. package/packages/workers/block-authorship/ticket-generator/worker-pool.d.ts +36 -0
  102. package/packages/workers/block-authorship/ticket-generator/worker-pool.d.ts.map +1 -0
  103. package/packages/workers/block-authorship/ticket-generator/worker-pool.js +111 -0
  104. package/packages/workers/block-authorship/ticket-validator.d.ts +31 -0
  105. package/packages/workers/block-authorship/ticket-validator.d.ts.map +1 -0
  106. package/packages/workers/block-authorship/ticket-validator.js +59 -0
  107. package/packages/workers/comms-authorship-network/protocol.d.ts +14 -4
  108. package/packages/workers/comms-authorship-network/protocol.d.ts.map +1 -1
  109. package/packages/workers/comms-authorship-network/protocol.js +12 -6
  110. package/packages/workers/comms-authorship-network/tickets-message.d.ts +0 -14
  111. package/packages/workers/comms-authorship-network/tickets-message.d.ts.map +1 -1
  112. package/packages/workers/comms-authorship-network/tickets-message.js +0 -17
  113. package/packages/workers/importer/importer.d.ts +2 -2
  114. package/packages/workers/importer/importer.d.ts.map +1 -1
  115. package/packages/workers/importer/importer.js +5 -5
  116. package/packages/workers/importer/stats.d.ts +1 -3
  117. package/packages/workers/importer/stats.d.ts.map +1 -1
  118. package/packages/workers/importer/stats.js +12 -12
  119. package/packages/workers/jam-network/main.d.ts.map +1 -1
  120. package/packages/workers/jam-network/main.js +25 -4
  121. package/packages/workers/block-authorship/generator.d.ts.map +0 -1
  122. package/packages/workers/block-authorship/generator.test.d.ts +0 -2
  123. package/packages/workers/block-authorship/generator.test.d.ts.map +0 -1
  124. package/packages/workers/block-authorship/ticket-generator.d.ts.map +0 -1
  125. package/packages/workers/block-authorship/ticket-generator.test.d.ts.map +0 -1
  126. /package/packages/configs/{typeberry-dev.json → typeberry-dev-tiny.json} +0 -0
  127. /package/packages/workers/block-authorship/{ticket-generator.test.d.ts → ticket-generator/ticket-generator.test.d.ts} +0 -0
@@ -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
  });
@@ -1 +1 @@
1
- {"version":3,"file":"main-fuzz.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/node/main-fuzz.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,WAAW,EAAmB,MAAM,oBAAoB,CAAC;AACvE,OAAO,EAAE,EAAE,IAAI,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAOrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAIjD,MAAM,MAAM,UAAU,GAAG;IACvB,OAAO,EAAE,WAAW,CAAC;IACrB,aAAa,EAAE,SAAS,CAAC;IACzB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,uBAAuB,EAAE,OAAO,CAAC;CAClC,CAAC;AAOF;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CASpF;AAED,iFAAiF;AACjF,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE5D;AAED,wBAAgB,cAAc;;;;EAM7B;AAED,wBAAsB,QAAQ,CAAC,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,uBA2GxF"}
1
+ {"version":3,"file":"main-fuzz.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/node/main-fuzz.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,WAAW,EAAmB,MAAM,oBAAoB,CAAC;AACvE,OAAO,EAAE,EAAE,IAAI,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAOrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAIjD,MAAM,MAAM,UAAU,GAAG;IACvB,OAAO,EAAE,WAAW,CAAC;IACrB,aAAa,EAAE,SAAS,CAAC;IACzB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,uBAAuB,EAAE,OAAO,CAAC;CAClC,CAAC;AAWF;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CASpF;AAED,iFAAiF;AACjF,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE5D;AAED,wBAAgB,cAAc;;;;EAM7B;AAED,wBAAsB,QAAQ,CAAC,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,uBAqHxF"}
@@ -14,6 +14,9 @@ import { mainImporter } from "./main-importer.js";
14
14
  const logger = Logger.new(import.meta.filename, "fuzztarget");
15
15
  /** Dedicated subdirectory under the configured base path that the fuzzer owns and wipes. */
16
16
  const FUZZ_DB_SUBDIR = "typeberry-fuzz-db";
17
+ const FUZZ_DB_FJALL = "fjall-hybrid";
18
+ const FUZZ_DB_LMDB = "lmdb-hybrid";
19
+ const FUZZ_DB_OPTIONS = [FUZZ_DB_FJALL, FUZZ_DB_LMDB];
17
20
  /**
18
21
  * Resolve the directory the fuzzer should use for its on-disk database, or
19
22
  * `undefined` for an in-memory database. The dedicated `FUZZ_DB_SUBDIR` is
@@ -50,6 +53,15 @@ export async function mainFuzz(fuzzConfig, withRelPath) {
50
53
  logHostEnvironment(logger);
51
54
  const { jamNodeConfig: config } = fuzzConfig;
52
55
  const fuzzDbBase = resolveFuzzDbBase(config.node.databaseBasePath);
56
+ const rawFuzzDb = process.env.JAM_FUZZ_DB?.trim() ?? "";
57
+ // Using experimental fjall-hybrid by default, with an option to test lmdb as well.
58
+ const hybridStateBackend = rawFuzzDb === "" ? FUZZ_DB_FJALL : rawFuzzDb;
59
+ if (!isValidStateBackend(hybridStateBackend)) {
60
+ throw new Error(`JAM_FUZZ_DB must be one of: ${FUZZ_DB_OPTIONS} (got: "${rawFuzzDb}").`);
61
+ }
62
+ if (fuzzDbBase !== undefined) {
63
+ logger.info `🗄️ Fuzz persistent backend: ${hybridStateBackend}.`;
64
+ }
53
65
  let runningNode = null;
54
66
  const chainSpec = getChainSpec(config.node.flavor);
55
67
  const closeFuzzTarget = startFuzzTarget(fuzzConfig.version, fuzzConfig.socket, {
@@ -106,7 +118,7 @@ export async function mainFuzz(fuzzConfig, withRelPath) {
106
118
  // spec only, where values are big) bounds its on-disk/page-cache size.
107
119
  // Tiny stays uncompressed since its db is small and speed matters more.
108
120
  ephemeral: isPersistent,
109
- stateBackend: isPersistent ? "hybrid" : "lmdb",
121
+ stateBackend: isPersistent ? hybridStateBackend : "lmdb",
110
122
  });
111
123
  };
112
124
  if (fuzzDbBase !== undefined) {
@@ -136,3 +148,6 @@ export async function mainFuzz(fuzzConfig, withRelPath) {
136
148
  }
137
149
  };
138
150
  }
151
+ function isValidStateBackend(val) {
152
+ return FUZZ_DB_OPTIONS.indexOf(val) !== -1;
153
+ }
@@ -1,13 +1,16 @@
1
1
  import type { JamConfig } from "./jam-config.js";
2
2
  import type { NodeApi } from "./main.js";
3
+ export type StateBackend = "lmdb" | "lmdb-hybrid" | "fjall-hybrid";
3
4
  export type ImporterOptions = {
4
5
  initGenesisFromAncestry?: boolean;
5
6
  dummyFinalityDepth?: number;
6
7
  pruneBlocks?: boolean;
7
- /** Open the LMDB database without fsync/compression. Only safe for throwaway dbs (e.g. fuzzing). */
8
+ /** Open the database without fsync/compression. Only safe for throwaway dbs (e.g. fuzzing). */
8
9
  ephemeral?: boolean;
9
- /** Persistent backend to use when `databaseBasePath` is set. Defaults to full LMDB. */
10
- stateBackend?: "lmdb" | "hybrid";
10
+ /**
11
+ * Persistent backend to use when `databaseBasePath` is set. Defaults to full LMDB.
12
+ */
13
+ stateBackend?: StateBackend;
11
14
  };
12
15
  export declare function mainImporter(config: JamConfig, withRelPath: (v: string) => string, options?: ImporterOptions): Promise<NodeApi>;
13
16
  //# sourceMappingURL=main-importer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"main-importer.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/node/main-importer.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIzC,MAAM,MAAM,eAAe,GAAG;IAC5B,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,oGAAoG;IACpG,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,uFAAuF;IACvF,YAAY,CAAC,EAAE,MAAM,GAAG,QAAQ,CAAC;CAClC,CAAC;AAEF,wBAAsB,YAAY,CAChC,MAAM,EAAE,SAAS,EACjB,WAAW,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,EAClC,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,OAAO,CAAC,CAoGlB"}
1
+ {"version":3,"file":"main-importer.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/node/main-importer.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIzC,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,aAAa,GAAG,cAAc,CAAC;AAEnE,MAAM,MAAM,eAAe,GAAG;IAC5B,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,+FAA+F;IAC/F,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;OAEG;IACH,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B,CAAC;AAEF,wBAAsB,YAAY,CAChC,MAAM,EAAE,SAAS,EACjB,WAAW,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,EAClC,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,OAAO,CAAC,CAqGlB"}
@@ -39,8 +39,8 @@ export async function mainImporter(config, withRelPath, options = {}) {
39
39
  blake2b,
40
40
  workerParams,
41
41
  })
42
- : dbBackend === "hybrid"
43
- ? HybridWorkerConfig.new({
42
+ : dbBackend === "lmdb-hybrid" || dbBackend === "fjall-hybrid"
43
+ ? await HybridWorkerConfig.new({
44
44
  nodeName,
45
45
  chainSpec,
46
46
  blake2b,
@@ -48,6 +48,7 @@ export async function mainImporter(config, withRelPath, options = {}) {
48
48
  workerParams,
49
49
  ephemeral,
50
50
  compression,
51
+ backend: dbBackend === "lmdb-hybrid" ? "lmdb" : "fjall",
51
52
  })
52
53
  : LmdbWorkerConfig.new({
53
54
  nodeName,
@@ -27,7 +27,27 @@ declare function generateSeal(bandersnatch: BandernsatchWasm, authorKey: Banders
27
27
  export type VrfOutputHash = Opaque<OpaqueHash, "VRF Output Hash">;
28
28
  declare function getVrfOutputHash(bandersnatch: BandernsatchWasm, authorKey: BandersnatchSecretSeed, input: BytesBlob): Promise<Result<VrfOutputHash, null>>;
29
29
  /**
30
- * Generates signed tickets for all attempts at once using batch ring VRF.
30
+ * Batch-generate signed tickets for multiple validators in a single native call,
31
+ * reusing the ring prover setup across all of them. Returns one ticket list per
32
+ * validator, in the same order as `proverKeyIndices`/`secrets`.
31
33
  */
32
- declare function generateTickets(bandersnatch: BandernsatchWasm, ringKeys: BandersnatchKey[], proverKeyIndex: number, key: BandersnatchSecretSeed, entropy: EntropyHash, ticketsPerValidator: number): Promise<Result<SignedTicket[], null>>;
34
+ declare function generateTickets(bandersnatch: BandernsatchWasm, ringKeys: BandersnatchKey[], proverKeyIndices: readonly number[], secrets: readonly BandersnatchSecretSeed[], entropy: EntropyHash, ticketsPerValidator: number): Promise<Result<SignedTicket[][], null>>;
35
+ /**
36
+ * Build the concatenated ring-VRF inputs for ticket generation: one
37
+ * `JAM_TICKET_SEAL || entropy || attempt_byte` input per attempt.
38
+ *
39
+ * Exposed so the worker-pool path can build the same inputs to hand off to a
40
+ * worker thread without re-deriving the layout.
41
+ */
42
+ export declare function buildTicketVrfInputs(entropy: EntropyHash, ticketsPerValidator: number): {
43
+ inputsData: Uint8Array;
44
+ vrfInputDataLen: number;
45
+ };
46
+ /**
47
+ * Parse the raw output of `batchGenerateRingVrfForValidators` into per-validator
48
+ * ticket lists. Records are ordered validator-major, then attempt-major; each
49
+ * record is `status byte || signature`. A malformed batch yields a single error
50
+ * byte. Exposed so the worker-pool path can parse a worker's raw result.
51
+ */
52
+ export declare function parseTicketsBatchOutput(result: Uint8Array, numValidators: number, ticketsPerValidator: number): Result<SignedTicket[][], null>;
33
53
  //# sourceMappingURL=bandersnatch-vrf.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bandersnatch-vrf.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/safrole/bandersnatch-vrf.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAsB,MAAM,6BAA6B,CAAC;AAC/E,OAAO,EAAS,SAAS,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,eAAe,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AACjF,OAAO,EAIL,KAAK,oBAAoB,EACzB,KAAK,wBAAwB,EAC9B,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAa,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AACvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAwC/D,QAAA,MAAM,SAAS;;;;;;;;CAQd,CAAC;AAKF,eAAe,SAAS,CAAC;AAIzB,iBAAe,iBAAiB,CAC9B,YAAY,EAAE,gBAAgB,EAC9B,SAAS,EAAE,eAAe,EAC1B,SAAS,EAAE,wBAAwB,EACnC,OAAO,EAAE,SAAS,EAClB,qBAAqB,EAAE,SAAS,EAChC,gBAAgB,EAAE,wBAAwB,EAC1C,oBAAoB,EAAE,SAAS,GAC9B,OAAO,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,WAAW,CAAC,EAAE,IAAI,CAAC,CAAC,CAkBnD;AAED,iBAAe,UAAU,CACvB,YAAY,EAAE,gBAAgB,EAC9B,SAAS,EAAE,eAAe,EAC1B,SAAS,EAAE,wBAAwB,EACnC,OAAO,EAAE,SAAS,EAClB,qBAAqB,EAAE,SAAS,GAC/B,OAAO,CAAC,MAAM,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC,CAapC;AAED,iBAAS,iBAAiB,CACxB,YAAY,EAAE,gBAAgB,EAC9B,UAAU,EAAE,eAAe,EAAE,GAC5B,OAAO,CAAC,MAAM,CAAC,oBAAoB,EAAE,IAAI,CAAC,CAAC,CAe7C;AAgBD,iBAAe,aAAa,CAC1B,YAAY,EAAE,gBAAgB,EAC9B,kBAAkB,EAAE,MAAM,EAC1B,SAAS,EAAE,oBAAoB,EAC/B,OAAO,EAAE,SAAS,YAAY,EAAE,EAChC,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,WAAW,EAAE,CAAA;CAAE,CAAC,CA0BvD;AAGD,iBAAe,YAAY,CACzB,YAAY,EAAE,gBAAgB,EAC9B,SAAS,EAAE,sBAAsB,EACjC,KAAK,EAAE,SAAS,EAChB,OAAO,EAAE,SAAS,GACjB,OAAO,CAAC,MAAM,CAAC,wBAAwB,EAAE,IAAI,CAAC,CAAC,CAQjD;AAED,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,UAAU,EAAE,iBAAiB,CAAC,CAAC;AAGlE,iBAAe,gBAAgB,CAC7B,YAAY,EAAE,gBAAgB,EAC9B,SAAS,EAAE,sBAAsB,EACjC,KAAK,EAAE,SAAS,GACf,OAAO,CAAC,MAAM,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC,CAQtC;AAKD;;GAEG;AACH,iBAAe,eAAe,CAC5B,YAAY,EAAE,gBAAgB,EAC9B,QAAQ,EAAE,eAAe,EAAE,EAC3B,cAAc,EAAE,MAAM,EACtB,GAAG,EAAE,sBAAsB,EAC3B,OAAO,EAAE,WAAW,EACpB,mBAAmB,EAAE,MAAM,GAC1B,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,EAAE,IAAI,CAAC,CAAC,CA0CvC"}
1
+ {"version":3,"file":"bandersnatch-vrf.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/safrole/bandersnatch-vrf.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import { SignedTicket, tryAsTicketAttempt } from "#@typeberry/block/tickets.js";
2
2
  import { Bytes, BytesBlob } from "#@typeberry/bytes";
3
+ import { SEED_SIZE } from "#@typeberry/crypto";
3
4
  import { BANDERSNATCH_PROOF_BYTES, BANDERSNATCH_RING_ROOT_BYTES, BANDERSNATCH_VRF_SIGNATURE_BYTES, } from "#@typeberry/crypto/bandersnatch.js";
4
5
  import { HASH_SIZE } from "#@typeberry/hash";
5
6
  import { Result } from "#@typeberry/utils";
@@ -33,6 +34,10 @@ const ringCommitmentCache = [
33
34
  keys: BytesBlob.empty(),
34
35
  value: Promise.resolve(Result.error(null, () => "")),
35
36
  },
37
+ {
38
+ keys: BytesBlob.empty(),
39
+ value: Promise.resolve(Result.error(null, () => "")),
40
+ },
36
41
  ];
37
42
  const FUNCTIONS = {
38
43
  verifySeal,
@@ -122,31 +127,60 @@ async function getVrfOutputHash(bandersnatch, authorKey, input) {
122
127
  // One byte for result discriminator and the rest is the ring VRF signature.
123
128
  const GENERATE_RESULT_ENTRY_LENGTH = 1 + BANDERSNATCH_PROOF_BYTES;
124
129
  /**
125
- * Generates signed tickets for all attempts at once using batch ring VRF.
130
+ * Batch-generate signed tickets for multiple validators in a single native call,
131
+ * reusing the ring prover setup across all of them. Returns one ticket list per
132
+ * validator, in the same order as `proverKeyIndices`/`secrets`.
126
133
  */
127
- async function generateTickets(bandersnatch, ringKeys, proverKeyIndex, key, entropy, ticketsPerValidator) {
128
- // Build VRF inputs: JAM_TICKET_SEAL || entropy || attempt_byte for each attempt
134
+ async function generateTickets(bandersnatch, ringKeys, proverKeyIndices, secrets, entropy, ticketsPerValidator) {
135
+ if (proverKeyIndices.length !== secrets.length) {
136
+ return Result.error(null, () => "proverKeyIndices and secrets must have the same length");
137
+ }
138
+ if (proverKeyIndices.length === 0) {
139
+ return Result.ok([]);
140
+ }
141
+ const { inputsData, vrfInputDataLen } = buildTicketVrfInputs(entropy, ticketsPerValidator);
142
+ const ringKeysData = BytesBlob.blobFromParts(ringKeys.map((k) => k.raw)).raw;
143
+ const secretSeedsData = BytesBlob.blobFromParts(secrets.map((s) => s.raw)).raw;
144
+ const result = await bandersnatch.batchGenerateRingVrfForValidators(ringKeysData, Uint32Array.from(proverKeyIndices), secretSeedsData, SEED_SIZE, inputsData, vrfInputDataLen);
145
+ return parseTicketsBatchOutput(result, proverKeyIndices.length, ticketsPerValidator);
146
+ }
147
+ /**
148
+ * Build the concatenated ring-VRF inputs for ticket generation: one
149
+ * `JAM_TICKET_SEAL || entropy || attempt_byte` input per attempt.
150
+ *
151
+ * Exposed so the worker-pool path can build the same inputs to hand off to a
152
+ * worker thread without re-deriving the layout.
153
+ */
154
+ export function buildTicketVrfInputs(entropy, ticketsPerValidator) {
129
155
  const vrfInputParts = [];
130
156
  for (let attempt = 0; attempt < ticketsPerValidator; attempt++) {
131
157
  vrfInputParts.push(BytesBlob.blobFromParts([JAM_TICKET_SEAL, entropy.raw, Uint8Array.of(attempt)]).raw);
132
158
  }
133
- const attemptLength = 1;
134
- const vrfInputDataLen = JAM_TICKET_SEAL.length + entropy.length + attemptLength;
135
- const inputsData = BytesBlob.blobFromParts(vrfInputParts).raw;
136
- const ringKeysData = BytesBlob.blobFromParts(ringKeys.map((k) => k.raw)).raw;
137
- const result = await bandersnatch.batchGenerateRingVrf(ringKeysData, proverKeyIndex, key.raw, inputsData, vrfInputDataLen);
138
- const tickets = [];
139
- for (let attempt = 0; attempt < ticketsPerValidator; attempt++) {
140
- const offset = attempt * GENERATE_RESULT_ENTRY_LENGTH;
141
- const resultByte = result[offset];
142
- if (resultByte === ResultValues.Error) {
143
- return Result.error(null, () => `Ring VRF proof generation failed for attempt ${attempt}`);
159
+ return {
160
+ inputsData: BytesBlob.blobFromParts(vrfInputParts).raw,
161
+ vrfInputDataLen: JAM_TICKET_SEAL.length + entropy.length + 1,
162
+ };
163
+ }
164
+ /**
165
+ * Parse the raw output of `batchGenerateRingVrfForValidators` into per-validator
166
+ * ticket lists. Records are ordered validator-major, then attempt-major; each
167
+ * record is `status byte || signature`. A malformed batch yields a single error
168
+ * byte. Exposed so the worker-pool path can parse a worker's raw result.
169
+ */
170
+ export function parseTicketsBatchOutput(result, numValidators, ticketsPerValidator) {
171
+ const perValidator = [];
172
+ let offset = 0;
173
+ for (let v = 0; v < numValidators; v++) {
174
+ const tickets = [];
175
+ for (let attempt = 0; attempt < ticketsPerValidator; attempt++) {
176
+ if (result[offset] === ResultValues.Error) {
177
+ return Result.error(null, () => `Ring VRF proof generation failed for validator ${v}, attempt ${attempt}`);
178
+ }
179
+ const signature = Bytes.fromBlob(result.subarray(offset + 1, offset + GENERATE_RESULT_ENTRY_LENGTH), BANDERSNATCH_PROOF_BYTES).asOpaque();
180
+ tickets.push(SignedTicket.create({ attempt: tryAsTicketAttempt(attempt), signature }));
181
+ offset += GENERATE_RESULT_ENTRY_LENGTH;
144
182
  }
145
- const signature = Bytes.fromBlob(new Uint8Array(result.subarray(offset + 1, offset + GENERATE_RESULT_ENTRY_LENGTH)), BANDERSNATCH_PROOF_BYTES).asOpaque();
146
- tickets.push(SignedTicket.create({
147
- attempt: tryAsTicketAttempt(attempt),
148
- signature,
149
- }));
183
+ perValidator.push(tickets);
150
184
  }
151
- return Result.ok(tickets);
185
+ return Result.ok(perValidator);
152
186
  }
@@ -165,11 +165,12 @@ describe("Bandersnatch verification", () => {
165
165
  const ringKeys = secrets.map((secret) => deriveBandersnatchPublicKey(secret));
166
166
  const proverIndex = 0;
167
167
  const entropy = Bytes.fill(HASH_SIZE, 123).asOpaque();
168
- const genResult = await bandersnatchVrf.generateTickets(await bandersnatchWasm, ringKeys, proverIndex, secrets[proverIndex], entropy, 2);
168
+ const genResult = await bandersnatchVrf.generateTickets(await bandersnatchWasm, ringKeys, [proverIndex], [secrets[proverIndex]], entropy, 2);
169
169
  assert.ok(genResult.isOk);
170
170
  const commitment = await bandersnatchVrf.getRingCommitment(await bandersnatchWasm, ringKeys);
171
171
  assert.ok(commitment.isOk);
172
- const verifyResult = await bandersnatchVrf.verifyTickets(await bandersnatchWasm, ringKeys.length, commitment.ok, genResult.ok, entropy);
172
+ assert.strictEqual(genResult.ok.length, 1);
173
+ const verifyResult = await bandersnatchVrf.verifyTickets(await bandersnatchWasm, ringKeys.length, commitment.ok, genResult.ok[0], entropy);
173
174
  assert.ok(verifyResult.isValid, "Generated tickets should pass verification");
174
175
  });
175
176
  });
@@ -8,5 +8,15 @@ export declare class BandernsatchWasm {
8
8
  generateSeal(authorKey: Uint8Array, input: Uint8Array, auxData: Uint8Array): Promise<Uint8Array<ArrayBufferLike>>;
9
9
  getVrfOutputHash(authorKey: Uint8Array, input: Uint8Array): Promise<Uint8Array<ArrayBufferLike>>;
10
10
  batchGenerateRingVrf(ringKeys: Uint8Array, proverKeyIndex: number, secretSeed: Uint8Array, inputsData: Uint8Array, vrfInputDataLen: number): Promise<Uint8Array<ArrayBufferLike>>;
11
+ /**
12
+ * Batch-generate ring VRF tickets for multiple validators in a single call,
13
+ * reusing the ring prover setup across all of them.
14
+ *
15
+ * `secretSeedsData` is the fixed-width concatenation of the validators' secret
16
+ * seeds (each `secretSeedDataLen` bytes); `proverKeyIndices` are their indices
17
+ * within the ring and must have the same count. Output records are ordered
18
+ * validator-major then input-major, each `status byte || signature`.
19
+ */
20
+ batchGenerateRingVrfForValidators(ringKeys: Uint8Array, proverKeyIndices: Uint32Array, secretSeedsData: Uint8Array, secretSeedDataLen: number, inputsData: Uint8Array, vrfInputDataLen: number): Promise<Uint8Array<ArrayBufferLike>>;
11
21
  }
12
22
  //# sourceMappingURL=bandersnatch-wasm.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bandersnatch-wasm.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/safrole/bandersnatch-wasm.ts"],"names":[],"mappings":"AAEA,qBAAa,gBAAgB;IAC3B,OAAO;WAEM,GAAG;IAKV,UAAU,CAAC,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU;IAIjG,iBAAiB,CACrB,SAAS,EAAE,UAAU,EACrB,UAAU,EAAE,UAAU,EACtB,iBAAiB,EAAE,UAAU,EAC7B,cAAc,EAAE,UAAU,EAC1B,WAAW,EAAE,UAAU,EACvB,oBAAoB,EAAE,UAAU;IAY5B,iBAAiB,CAAC,IAAI,EAAE,UAAU;IAIlC,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM;IAI1G,YAAY,CAAC,SAAS,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU;IAI1E,gBAAgB,CAAC,SAAS,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU;IAIzD,oBAAoB,CACxB,QAAQ,EAAE,UAAU,EACpB,cAAc,EAAE,MAAM,EACtB,UAAU,EAAE,UAAU,EACtB,UAAU,EAAE,UAAU,EACtB,eAAe,EAAE,MAAM;CAI1B"}
1
+ {"version":3,"file":"bandersnatch-wasm.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/safrole/bandersnatch-wasm.ts"],"names":[],"mappings":"AAEA,qBAAa,gBAAgB;IAC3B,OAAO;WAEM,GAAG;IAKV,UAAU,CAAC,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU;IAIjG,iBAAiB,CACrB,SAAS,EAAE,UAAU,EACrB,UAAU,EAAE,UAAU,EACtB,iBAAiB,EAAE,UAAU,EAC7B,cAAc,EAAE,UAAU,EAC1B,WAAW,EAAE,UAAU,EACvB,oBAAoB,EAAE,UAAU;IAY5B,iBAAiB,CAAC,IAAI,EAAE,UAAU;IAIlC,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM;IAI1G,YAAY,CAAC,SAAS,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU,EAAE,OAAO,EAAE,UAAU;IAI1E,gBAAgB,CAAC,SAAS,EAAE,UAAU,EAAE,KAAK,EAAE,UAAU;IAIzD,oBAAoB,CACxB,QAAQ,EAAE,UAAU,EACpB,cAAc,EAAE,MAAM,EACtB,UAAU,EAAE,UAAU,EACtB,UAAU,EAAE,UAAU,EACtB,eAAe,EAAE,MAAM;IAKzB;;;;;;;;OAQG;IACG,iCAAiC,CACrC,QAAQ,EAAE,UAAU,EACpB,gBAAgB,EAAE,WAAW,EAC7B,eAAe,EAAE,UAAU,EAC3B,iBAAiB,EAAE,MAAM,EACzB,UAAU,EAAE,UAAU,EACtB,eAAe,EAAE,MAAM;CAW1B"}
@@ -26,4 +26,16 @@ export class BandernsatchWasm {
26
26
  async batchGenerateRingVrf(ringKeys, proverKeyIndex, secretSeed, inputsData, vrfInputDataLen) {
27
27
  return bandersnatchWasm.batchGenerateRingVrf(ringKeys, proverKeyIndex, secretSeed, inputsData, vrfInputDataLen);
28
28
  }
29
+ /**
30
+ * Batch-generate ring VRF tickets for multiple validators in a single call,
31
+ * reusing the ring prover setup across all of them.
32
+ *
33
+ * `secretSeedsData` is the fixed-width concatenation of the validators' secret
34
+ * seeds (each `secretSeedDataLen` bytes); `proverKeyIndices` are their indices
35
+ * within the ring and must have the same count. Output records are ordered
36
+ * validator-major then input-major, each `status byte || signature`.
37
+ */
38
+ async batchGenerateRingVrfForValidators(ringKeys, proverKeyIndices, secretSeedsData, secretSeedDataLen, inputsData, vrfInputDataLen) {
39
+ return bandersnatchWasm.batchGenerateRingVrfForValidators(ringKeys, proverKeyIndices, secretSeedsData, secretSeedDataLen, inputsData, vrfInputDataLen);
40
+ }
29
41
  }
@@ -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";