@typeberry/lib 0.8.4-9587022 → 0.8.4-faebc7a

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 (82) hide show
  1. package/package.json +6 -4
  2. package/packages/jam/database/states.d.ts +7 -0
  3. package/packages/jam/database/states.d.ts.map +1 -1
  4. package/packages/jam/database-fjall/hybrid-states.d.ts +45 -0
  5. package/packages/jam/database-fjall/hybrid-states.d.ts.map +1 -0
  6. package/packages/jam/database-fjall/hybrid-states.js +113 -0
  7. package/packages/jam/database-fjall/hybrid-states.test.d.ts +2 -0
  8. package/packages/jam/database-fjall/hybrid-states.test.d.ts.map +1 -0
  9. package/packages/jam/database-fjall/hybrid-states.test.js +83 -0
  10. package/packages/jam/database-fjall/index.d.ts +3 -0
  11. package/packages/jam/database-fjall/index.d.ts.map +1 -0
  12. package/packages/jam/database-fjall/index.js +2 -0
  13. package/packages/jam/database-fjall/root.d.ts +52 -0
  14. package/packages/jam/database-fjall/root.d.ts.map +1 -0
  15. package/packages/jam/database-fjall/root.js +85 -0
  16. package/packages/jam/database-lmdb/hybrid-states.d.ts +3 -1
  17. package/packages/jam/database-lmdb/hybrid-states.d.ts.map +1 -1
  18. package/packages/jam/database-lmdb/hybrid-states.js +5 -2
  19. package/packages/jam/database-lmdb/root.d.ts +14 -1
  20. package/packages/jam/database-lmdb/root.d.ts.map +1 -1
  21. package/packages/jam/database-lmdb/root.js +25 -5
  22. package/packages/jam/database-lmdb/states.d.ts +1 -0
  23. package/packages/jam/database-lmdb/states.d.ts.map +1 -1
  24. package/packages/jam/database-lmdb/states.js +3 -0
  25. package/packages/jam/database-lmdb/states.test.js +4 -4
  26. package/packages/jam/jamnp-s/tasks/ticket-distribution.d.ts +18 -10
  27. package/packages/jam/jamnp-s/tasks/ticket-distribution.d.ts.map +1 -1
  28. package/packages/jam/jamnp-s/tasks/ticket-distribution.js +44 -68
  29. package/packages/jam/jamnp-s/tasks/ticket-distribution.test.js +30 -8
  30. package/packages/jam/node/main-fuzz.d.ts.map +1 -1
  31. package/packages/jam/node/main-fuzz.js +21 -4
  32. package/packages/jam/node/main-importer.d.ts +7 -4
  33. package/packages/jam/node/main-importer.d.ts.map +1 -1
  34. package/packages/jam/node/main-importer.js +10 -4
  35. package/packages/jam/safrole/bandersnatch-vrf.d.ts +2 -2
  36. package/packages/jam/safrole/bandersnatch-vrf.d.ts.map +1 -1
  37. package/packages/jam/safrole/bandersnatch-vrf.js +9 -6
  38. package/packages/jam/safrole/bandersnatch-vrf.test.js +9 -7
  39. package/packages/jam/safrole/safrole.js +5 -5
  40. package/packages/jam/safrole/safrole.test.js +13 -13
  41. package/packages/jam/ticket-pool/index.d.ts +4 -0
  42. package/packages/jam/ticket-pool/index.d.ts.map +1 -0
  43. package/packages/jam/ticket-pool/index.js +3 -0
  44. package/packages/jam/ticket-pool/pending-ticket-pool.d.ts +30 -0
  45. package/packages/jam/ticket-pool/pending-ticket-pool.d.ts.map +1 -0
  46. package/packages/jam/ticket-pool/pending-ticket-pool.js +56 -0
  47. package/packages/jam/ticket-pool/pending-ticket-pool.test.d.ts +2 -0
  48. package/packages/jam/ticket-pool/pending-ticket-pool.test.d.ts.map +1 -0
  49. package/packages/jam/ticket-pool/pending-ticket-pool.test.js +67 -0
  50. package/packages/jam/ticket-pool/ticket-validator.d.ts +46 -0
  51. package/packages/jam/ticket-pool/ticket-validator.d.ts.map +1 -0
  52. package/packages/jam/ticket-pool/ticket-validator.js +29 -0
  53. package/packages/jam/ticket-pool/ticket-validator.test.d.ts +2 -0
  54. package/packages/jam/ticket-pool/ticket-validator.test.d.ts.map +1 -0
  55. package/packages/jam/ticket-pool/ticket-validator.test.js +34 -0
  56. package/packages/jam/ticket-pool/verified-ticket-pool.d.ts +24 -0
  57. package/packages/jam/ticket-pool/verified-ticket-pool.d.ts.map +1 -0
  58. package/packages/jam/ticket-pool/verified-ticket-pool.js +37 -0
  59. package/packages/jam/ticket-pool/verified-ticket-pool.test.d.ts +2 -0
  60. package/packages/jam/ticket-pool/verified-ticket-pool.test.d.ts.map +1 -0
  61. package/packages/jam/ticket-pool/verified-ticket-pool.test.js +54 -0
  62. package/packages/workers/api-node/config.d.ts +14 -5
  63. package/packages/workers/api-node/config.d.ts.map +1 -1
  64. package/packages/workers/api-node/config.js +29 -20
  65. package/packages/workers/api-node/config.test.js +38 -1
  66. package/packages/workers/block-authorship/main.d.ts.map +1 -1
  67. package/packages/workers/block-authorship/main.js +22 -72
  68. package/packages/workers/block-authorship/ticket-validator.d.ts +32 -0
  69. package/packages/workers/block-authorship/ticket-validator.d.ts.map +1 -0
  70. package/packages/workers/block-authorship/ticket-validator.js +56 -0
  71. package/packages/workers/comms-authorship-network/protocol.d.ts +10 -0
  72. package/packages/workers/comms-authorship-network/protocol.d.ts.map +1 -1
  73. package/packages/workers/comms-authorship-network/protocol.js +8 -1
  74. package/packages/workers/importer/importer.d.ts +1 -1
  75. package/packages/workers/importer/importer.d.ts.map +1 -1
  76. package/packages/workers/importer/importer.js +2 -2
  77. package/packages/workers/importer/{events-logger.d.ts → stats.d.ts} +7 -2
  78. package/packages/workers/importer/stats.d.ts.map +1 -0
  79. package/packages/workers/importer/{events-logger.js → stats.js} +21 -2
  80. package/packages/workers/jam-network/main.d.ts.map +1 -1
  81. package/packages/workers/jam-network/main.js +20 -4
  82. package/packages/workers/importer/events-logger.d.ts.map +0 -1
@@ -131,6 +131,9 @@ export class LmdbStates {
131
131
  markUnused(header) {
132
132
  this.states.removeSync(header.raw);
133
133
  }
134
+ diskSizeInBytes() {
135
+ return this.root.sizeInBytes();
136
+ }
134
137
  async close() {
135
138
  await Promise.all([this.states.close(), this.values.close()]);
136
139
  }
@@ -37,7 +37,7 @@ describe("LMDB States database", () => {
37
37
  const headerHash = Bytes.zero(HASH_SIZE).asOpaque();
38
38
  const spec = tinyChainSpec;
39
39
  it("should import state and read state", async () => {
40
- const root = LmdbRoot.new(tmpDir);
40
+ const root = LmdbRoot.new(tmpDir, {});
41
41
  const states = LmdbStates.new(spec, blake2b, root);
42
42
  try {
43
43
  const emptyState = InMemoryState.empty(spec);
@@ -58,7 +58,7 @@ describe("LMDB States database", () => {
58
58
  }
59
59
  });
60
60
  it("should update the state", async () => {
61
- const root = LmdbRoot.new(tmpDir);
61
+ const root = LmdbRoot.new(tmpDir, {});
62
62
  const states = LmdbStates.new(spec, blake2b, root);
63
63
  try {
64
64
  const state = InMemoryState.empty(spec);
@@ -142,7 +142,7 @@ describe("LMDB States database", () => {
142
142
  deepEqual(Array.from(set), Array.from(SortedSet.fromArray(leafComparator, Array.from(trie.nodes.leaves()))));
143
143
  });
144
144
  it("should import more complex state", async () => {
145
- const root = LmdbRoot.new(tmpDir);
145
+ const root = LmdbRoot.new(tmpDir, {});
146
146
  const states = LmdbStates.new(spec, blake2b, root);
147
147
  try {
148
148
  const initialState = testState();
@@ -167,7 +167,7 @@ describe("LMDB States database", () => {
167
167
  }
168
168
  });
169
169
  it("should update more complex entries", async () => {
170
- const root = LmdbRoot.new(tmpDir);
170
+ const root = LmdbRoot.new(tmpDir, {});
171
171
  const states = LmdbStates.new(spec, blake2b, root);
172
172
  try {
173
173
  const state = testState();
@@ -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,uBAyGxF"}
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, {
@@ -101,10 +113,12 @@ export async function mainFuzz(fuzzConfig, withRelPath) {
101
113
  // like the in-memory backend; only the large values live on disk.
102
114
  dummyFinalityDepth: 20,
103
115
  pruneBlocks: true,
104
- // The fuzz db is wiped on every reset, so durability is pointless:
105
- // skip fsync + compression to cut the per-block value write cost.
106
- ephemeralDb: isPersistent,
107
- stateBackend: isPersistent ? "hybrid" : "lmdb",
116
+ // Long full-spec sessions accumulate a large, never-pruned values db.
117
+ // Syncing lets the OS reclaim dirty mmap pages, and compression (full
118
+ // spec only, where values are big) bounds its on-disk/page-cache size.
119
+ // Tiny stays uncompressed since its db is small and speed matters more.
120
+ ephemeral: isPersistent,
121
+ stateBackend: isPersistent ? hybridStateBackend : "lmdb",
108
122
  });
109
123
  };
110
124
  if (fuzzDbBase !== undefined) {
@@ -134,3 +148,6 @@ export async function mainFuzz(fuzzConfig, withRelPath) {
134
148
  }
135
149
  };
136
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
- ephemeralDb?: boolean;
9
- /** Persistent backend to use when `databaseBasePath` is set. Defaults to full LMDB. */
10
- stateBackend?: "lmdb" | "hybrid";
8
+ /** Open the database without fsync/compression. Only safe for throwaway dbs (e.g. fuzzing). */
9
+ ephemeral?: boolean;
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":"AAUA,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,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,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,CA+FlB"}
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"}
@@ -1,5 +1,6 @@
1
1
  import { Bytes } from "#@typeberry/bytes";
2
2
  import { PvmBackend } from "#@typeberry/config";
3
+ import { KnownChainSpec } from "#@typeberry/config-node";
3
4
  import { bandersnatch, initWasm } from "#@typeberry/crypto";
4
5
  import { Blake2b, HASH_SIZE } from "#@typeberry/hash";
5
6
  import { createImporter, ImporterConfig } from "#@typeberry/importer";
@@ -28,6 +29,9 @@ export async function mainImporter(config, withRelPath, options = {}) {
28
29
  dummyFinalityDepth: tryAsU16(options.dummyFinalityDepth ?? 0),
29
30
  pruneBlocks: options.pruneBlocks ?? false,
30
31
  });
32
+ const ephemeral = options.ephemeral ?? false;
33
+ // enable compression when running full test suite
34
+ const compression = ephemeral && config.node.flavor === KnownChainSpec.Full;
31
35
  const workerConfig = dbBackend === "in-memory"
32
36
  ? InMemWorkerConfig.new({
33
37
  nodeName,
@@ -35,14 +39,16 @@ export async function mainImporter(config, withRelPath, options = {}) {
35
39
  blake2b,
36
40
  workerParams,
37
41
  })
38
- : dbBackend === "hybrid"
39
- ? HybridWorkerConfig.new({
42
+ : dbBackend === "lmdb-hybrid" || dbBackend === "fjall-hybrid"
43
+ ? await HybridWorkerConfig.new({
40
44
  nodeName,
41
45
  chainSpec,
42
46
  blake2b,
43
47
  dbPath,
44
48
  workerParams,
45
- ephemeral: options.ephemeralDb ?? false,
49
+ ephemeral,
50
+ compression,
51
+ backend: dbBackend === "lmdb-hybrid" ? "lmdb" : "fjall",
46
52
  })
47
53
  : LmdbWorkerConfig.new({
48
54
  nodeName,
@@ -50,7 +56,7 @@ export async function mainImporter(config, withRelPath, options = {}) {
50
56
  blake2b,
51
57
  dbPath,
52
58
  workerParams,
53
- ephemeral: options.ephemeralDb ?? false,
59
+ ephemeral,
54
60
  });
55
61
  // Initialize the database with genesis state and block if there isn't one.
56
62
  logger.info `🛢️ Opening database at ${dbPath}`;
@@ -21,8 +21,8 @@ declare function verifySeal(bandersnatch: BandernsatchWasm, authorKey: Bandersna
21
21
  declare function getRingCommitment(bandersnatch: BandernsatchWasm, validators: BandersnatchKey[]): Promise<Result<BandersnatchRingRoot, null>>;
22
22
  declare function verifyTickets(bandersnatch: BandernsatchWasm, numberOfValidators: number, epochRoot: BandersnatchRingRoot, tickets: readonly SignedTicket[], entropy: EntropyHash): Promise<{
23
23
  isValid: boolean;
24
- entropyHash: EntropyHash;
25
- }[]>;
24
+ tickets: EntropyHash[];
25
+ }>;
26
26
  declare function generateSeal(bandersnatch: BandernsatchWasm, authorKey: BandersnatchSecretSeed, input: BytesBlob, auxData: BytesBlob): Promise<Result<BandersnatchVrfSignature, null>>;
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>>;
@@ -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;AAmBD,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,WAAW,EAAE,WAAW,CAAA;CAAE,EAAE,CAAC,CAqB3D;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":"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"}
@@ -88,17 +88,20 @@ async function getRingCommitmentNoCache(bandersnatch, keys) {
88
88
  }
89
89
  return Result.ok(Bytes.fromBlob(commitmentResult.subarray(1), BANDERSNATCH_RING_ROOT_BYTES).asOpaque());
90
90
  }
91
- // One byte for result discriminator (`ResultValues`) and the rest is entropy hash.
92
- const TICKET_RESULT_LENGTH = 1 + HASH_SIZE;
93
91
  async function verifyTickets(bandersnatch, numberOfValidators, epochRoot, tickets, entropy) {
94
92
  const contextLength = entropy.length + JAM_TICKET_SEAL.length + 1;
95
93
  const ticketsData = BytesBlob.blobFromParts(tickets.map((ticket) => BytesBlob.blobFromParts([ticket.signature.raw, JAM_TICKET_SEAL, entropy.raw, Uint8Array.of(ticket.attempt)])
96
94
  .raw)).raw;
97
95
  const verificationResult = await bandersnatch.batchVerifyTicket(numberOfValidators, epochRoot.raw, ticketsData, contextLength);
98
- return Array.from(BytesBlob.blobFrom(verificationResult).chunks(TICKET_RESULT_LENGTH)).map((result) => ({
99
- isValid: result.raw[RESULT_INDEX] === ResultValues.Ok,
100
- entropyHash: Bytes.fromBlob(result.raw.subarray(1, TICKET_RESULT_LENGTH), HASH_SIZE).asOpaque(),
101
- }));
96
+ const isValid = verificationResult[RESULT_INDEX] === ResultValues.Ok;
97
+ // NOTE: in case of failure, the hashes will be all zeros, but we can safely
98
+ // keep the same code path.
99
+ const chunks = BytesBlob.blobFrom(verificationResult.subarray(1)).chunks(HASH_SIZE);
100
+ const results = [];
101
+ for (const entropyHash of chunks) {
102
+ results.push(Bytes.fromBlob(entropyHash.raw, HASH_SIZE).asOpaque());
103
+ }
104
+ return { isValid, tickets: results };
102
105
  }
103
106
  const SEAL_FAILED_ERROR = () => "Seal generation failed";
104
107
  async function generateSeal(bandersnatch, authorKey, input, auxData) {
@@ -71,8 +71,8 @@ describe("Bandersnatch verification", () => {
71
71
  "0x3a5d10abc80dda33fe3f40b3bb2e3eefd3e97dda3d617a860c9d94eb70b832ad",
72
72
  ].map((x) => Bytes.parseBytes(x, HASH_SIZE));
73
73
  const result = await bandersnatchVrf.verifyTickets(await bandersnatchWasm, bandersnatchKeys.length, commitment, tickets, entropy);
74
- assert.strictEqual(result.every((x) => x.isValid), true);
75
- assert.deepStrictEqual(result.map((x) => x.entropyHash.toString()), expectedIds.map((x) => x.toString()));
74
+ assert.strictEqual(result.isValid, true);
75
+ assert.deepStrictEqual(result.tickets.map((x) => x.toString()), expectedIds.map((x) => x.toString()));
76
76
  });
77
77
  it("should detect that one signature is incorrect", async () => {
78
78
  const tickets = [
@@ -90,14 +90,16 @@ describe("Bandersnatch verification", () => {
90
90
  },
91
91
  ];
92
92
  const entropy = Bytes.parseBytes("0xbb30a42c1e62f0afda5f0a4e8a562f7a13a24cea00ee81917b86b89e801314aa", HASH_SIZE).asOpaque();
93
+ // Batch verification fails as a whole when any signature is invalid,
94
+ // and in that case all returned entropy hashes are zeroed out.
93
95
  const expectedIds = [
94
96
  "0x0000000000000000000000000000000000000000000000000000000000000000",
95
- "0x13fecb426e0a73b84b58b9a0832b11582dc971e79c5399e69f0baf1a244c7787",
96
- "0x3a5d10abc80dda33fe3f40b3bb2e3eefd3e97dda3d617a860c9d94eb70b832ad",
97
+ "0x0000000000000000000000000000000000000000000000000000000000000000",
98
+ "0x0000000000000000000000000000000000000000000000000000000000000000",
97
99
  ].map((x) => Bytes.parseBytes(x, HASH_SIZE));
98
100
  const result = await bandersnatchVrf.verifyTickets(await bandersnatchWasm, bandersnatchKeys.length, commitment, tickets, entropy);
99
- assert.deepStrictEqual(result.map((x) => x.isValid), [false, true, true]);
100
- assert.deepStrictEqual(result.map((x) => x.entropyHash.toString()), expectedIds.map((x) => x.toString()));
101
+ assert.strictEqual(result.isValid, false);
102
+ assert.deepStrictEqual(result.tickets.map((x) => x.toString()), expectedIds.map((x) => x.toString()));
101
103
  });
102
104
  });
103
105
  describe("verifySeal", () => {
@@ -168,7 +170,7 @@ describe("Bandersnatch verification", () => {
168
170
  const commitment = await bandersnatchVrf.getRingCommitment(await bandersnatchWasm, ringKeys);
169
171
  assert.ok(commitment.isOk);
170
172
  const verifyResult = await bandersnatchVrf.verifyTickets(await bandersnatchWasm, ringKeys.length, commitment.ok, genResult.ok, entropy);
171
- assert.ok(verifyResult.every((r) => r.isValid), "Generated tickets should pass verification");
173
+ assert.ok(verifyResult.isValid, "Generated tickets should pass verification");
172
174
  });
173
175
  });
174
176
  });
@@ -260,15 +260,15 @@ export class Safrole {
260
260
  */
261
261
  // TODO [ToDr] Verify that ticket attempt is in correct range.
262
262
  const verificationResult = extrinsic.length === 0
263
- ? []
263
+ ? { isValid: true, tickets: [] }
264
264
  : await bandersnatchVrf.verifyTickets(await this.bandersnatch, validators.length, epochRoot, extrinsic, entropy);
265
+ if (!verificationResult.isValid) {
266
+ return Result.error(SafroleErrorCode.BadTicketProof, () => "Safrole: invalid ticket proof in extrinsic");
267
+ }
265
268
  const tickets = extrinsic.map((ticket, i) => ({
266
- id: verificationResult[i].entropyHash,
269
+ id: verificationResult.tickets[i],
267
270
  attempt: ticket.attempt,
268
271
  }));
269
- if (!verificationResult.every((x) => x.isValid)) {
270
- return Result.error(SafroleErrorCode.BadTicketProof, () => "Safrole: invalid ticket proof in extrinsic");
271
- }
272
272
  /**
273
273
  * Verify if tickets are sorted and unique
274
274
  *