@typeberry/lib 0.8.4-9502983 → 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 (63) hide show
  1. package/package.json +6 -4
  2. package/packages/jam/database-fjall/hybrid-states.d.ts +45 -0
  3. package/packages/jam/database-fjall/hybrid-states.d.ts.map +1 -0
  4. package/packages/jam/database-fjall/hybrid-states.js +113 -0
  5. package/packages/jam/database-fjall/hybrid-states.test.d.ts +2 -0
  6. package/packages/jam/database-fjall/hybrid-states.test.d.ts.map +1 -0
  7. package/packages/jam/database-fjall/hybrid-states.test.js +83 -0
  8. package/packages/jam/database-fjall/index.d.ts +3 -0
  9. package/packages/jam/database-fjall/index.d.ts.map +1 -0
  10. package/packages/jam/database-fjall/index.js +2 -0
  11. package/packages/jam/database-fjall/root.d.ts +52 -0
  12. package/packages/jam/database-fjall/root.d.ts.map +1 -0
  13. package/packages/jam/database-fjall/root.js +85 -0
  14. package/packages/jam/jamnp-s/tasks/ticket-distribution.d.ts +18 -10
  15. package/packages/jam/jamnp-s/tasks/ticket-distribution.d.ts.map +1 -1
  16. package/packages/jam/jamnp-s/tasks/ticket-distribution.js +44 -68
  17. package/packages/jam/jamnp-s/tasks/ticket-distribution.test.js +30 -8
  18. package/packages/jam/node/main-fuzz.d.ts.map +1 -1
  19. package/packages/jam/node/main-fuzz.js +16 -1
  20. package/packages/jam/node/main-importer.d.ts +6 -3
  21. package/packages/jam/node/main-importer.d.ts.map +1 -1
  22. package/packages/jam/node/main-importer.js +3 -2
  23. package/packages/jam/safrole/bandersnatch-vrf.d.ts +2 -2
  24. package/packages/jam/safrole/bandersnatch-vrf.d.ts.map +1 -1
  25. package/packages/jam/safrole/bandersnatch-vrf.js +9 -6
  26. package/packages/jam/safrole/bandersnatch-vrf.test.js +9 -7
  27. package/packages/jam/safrole/safrole.js +5 -5
  28. package/packages/jam/safrole/safrole.test.js +13 -13
  29. package/packages/jam/ticket-pool/index.d.ts +4 -0
  30. package/packages/jam/ticket-pool/index.d.ts.map +1 -0
  31. package/packages/jam/ticket-pool/index.js +3 -0
  32. package/packages/jam/ticket-pool/pending-ticket-pool.d.ts +30 -0
  33. package/packages/jam/ticket-pool/pending-ticket-pool.d.ts.map +1 -0
  34. package/packages/jam/ticket-pool/pending-ticket-pool.js +56 -0
  35. package/packages/jam/ticket-pool/pending-ticket-pool.test.d.ts +2 -0
  36. package/packages/jam/ticket-pool/pending-ticket-pool.test.d.ts.map +1 -0
  37. package/packages/jam/ticket-pool/pending-ticket-pool.test.js +67 -0
  38. package/packages/jam/ticket-pool/ticket-validator.d.ts +46 -0
  39. package/packages/jam/ticket-pool/ticket-validator.d.ts.map +1 -0
  40. package/packages/jam/ticket-pool/ticket-validator.js +29 -0
  41. package/packages/jam/ticket-pool/ticket-validator.test.d.ts +2 -0
  42. package/packages/jam/ticket-pool/ticket-validator.test.d.ts.map +1 -0
  43. package/packages/jam/ticket-pool/ticket-validator.test.js +34 -0
  44. package/packages/jam/ticket-pool/verified-ticket-pool.d.ts +24 -0
  45. package/packages/jam/ticket-pool/verified-ticket-pool.d.ts.map +1 -0
  46. package/packages/jam/ticket-pool/verified-ticket-pool.js +37 -0
  47. package/packages/jam/ticket-pool/verified-ticket-pool.test.d.ts +2 -0
  48. package/packages/jam/ticket-pool/verified-ticket-pool.test.d.ts.map +1 -0
  49. package/packages/jam/ticket-pool/verified-ticket-pool.test.js +54 -0
  50. package/packages/workers/api-node/config.d.ts +12 -5
  51. package/packages/workers/api-node/config.d.ts.map +1 -1
  52. package/packages/workers/api-node/config.js +20 -17
  53. package/packages/workers/api-node/config.test.js +38 -1
  54. package/packages/workers/block-authorship/main.d.ts.map +1 -1
  55. package/packages/workers/block-authorship/main.js +22 -72
  56. package/packages/workers/block-authorship/ticket-validator.d.ts +32 -0
  57. package/packages/workers/block-authorship/ticket-validator.d.ts.map +1 -0
  58. package/packages/workers/block-authorship/ticket-validator.js +56 -0
  59. package/packages/workers/comms-authorship-network/protocol.d.ts +10 -0
  60. package/packages/workers/comms-authorship-network/protocol.d.ts.map +1 -1
  61. package/packages/workers/comms-authorship-network/protocol.js +8 -1
  62. package/packages/workers/jam-network/main.d.ts.map +1 -1
  63. package/packages/workers/jam-network/main.js +20 -4
@@ -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,
@@ -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
  *
@@ -67,10 +67,10 @@ const fakeSealingKeys = {
67
67
  describe("Safrole", () => {
68
68
  let blake2b;
69
69
  beforeEach(async () => {
70
- mock.method(bandersnatchVrf, "verifyTickets", () => Promise.resolve([
71
- { isValid: true, entropyHash: Bytes.zero(HASH_SIZE) },
72
- { isValid: true, entropyHash: Bytes.fill(HASH_SIZE, 1) },
73
- ]));
70
+ mock.method(bandersnatchVrf, "verifyTickets", () => Promise.resolve({
71
+ isValid: true,
72
+ tickets: [Bytes.zero(HASH_SIZE), Bytes.fill(HASH_SIZE, 1)],
73
+ }));
74
74
  blake2b = await Blake2b.createHasher();
75
75
  });
76
76
  afterEach(() => {
@@ -146,7 +146,7 @@ describe("Safrole", () => {
146
146
  }
147
147
  });
148
148
  it("should return bad ticket proof error", async () => {
149
- mock.method(bandersnatchVrf, "verifyTickets", () => Promise.resolve([{ isValid: false, entropyHash: Bytes.zero(HASH_SIZE) }]));
149
+ mock.method(bandersnatchVrf, "verifyTickets", () => Promise.resolve({ isValid: false, tickets: [Bytes.zero(HASH_SIZE)] }));
150
150
  const punishSet = SortedSet.fromArray(hashComparator);
151
151
  const state = {
152
152
  timeslot: tryAsTimeSlot(1),
@@ -188,10 +188,10 @@ describe("Safrole", () => {
188
188
  }
189
189
  });
190
190
  it("should return duplicated ticket error", async () => {
191
- mock.method(bandersnatchVrf, "verifyTickets", () => Promise.resolve([
192
- { isValid: true, entropyHash: Bytes.zero(HASH_SIZE) },
193
- { isValid: true, entropyHash: Bytes.zero(HASH_SIZE) },
194
- ]));
191
+ mock.method(bandersnatchVrf, "verifyTickets", () => Promise.resolve({
192
+ isValid: true,
193
+ tickets: [Bytes.zero(HASH_SIZE), Bytes.zero(HASH_SIZE)],
194
+ }));
195
195
  const punishSet = SortedSet.fromArray(hashComparator);
196
196
  const state = {
197
197
  timeslot: tryAsTimeSlot(1),
@@ -237,10 +237,10 @@ describe("Safrole", () => {
237
237
  }
238
238
  });
239
239
  it("should return bad ticket order error", async () => {
240
- mock.method(bandersnatchVrf, "verifyTickets", () => Promise.resolve([
241
- { isValid: true, entropyHash: Bytes.fill(HASH_SIZE, 1) },
242
- { isValid: true, entropyHash: Bytes.zero(HASH_SIZE) },
243
- ]));
240
+ mock.method(bandersnatchVrf, "verifyTickets", () => Promise.resolve({
241
+ isValid: true,
242
+ tickets: [Bytes.fill(HASH_SIZE, 1), Bytes.zero(HASH_SIZE)],
243
+ }));
244
244
  const punishSet = SortedSet.fromArray(hashComparator);
245
245
  const state = {
246
246
  timeslot: tryAsTimeSlot(1),
@@ -0,0 +1,4 @@
1
+ export * from "./pending-ticket-pool.js";
2
+ export * from "./ticket-validator.js";
3
+ export * from "./verified-ticket-pool.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/ticket-pool/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC"}
@@ -0,0 +1,3 @@
1
+ export * from "./pending-ticket-pool.js";
2
+ export * from "./ticket-validator.js";
3
+ export * from "./verified-ticket-pool.js";
@@ -0,0 +1,30 @@
1
+ import type { Epoch } from "#@typeberry/block";
2
+ import type { SignedTicket } from "#@typeberry/block/tickets.js";
3
+ /**
4
+ * An ordered, signature-deduplicated pool of tickets waiting to be redistributed to peers.
5
+ *
6
+ * Used on the networking side. Indices are stable within an epoch so callers can track
7
+ * per-peer "sent" sets by index. The pool is cleared whenever a new epoch is observed,
8
+ * and tickets for older epochs are dropped (can happen when an async validation completes
9
+ * after the epoch already advanced).
10
+ */
11
+ export declare class PendingTicketPool {
12
+ private tickets;
13
+ private currentEpochValue;
14
+ /** Epoch the pool is currently holding tickets for, or `null` if empty. */
15
+ get currentEpoch(): Epoch | null;
16
+ /** Returns the ordered tickets currently in the pool. Caller must not mutate the array. */
17
+ getTickets(): readonly {
18
+ epochIndex: Epoch;
19
+ ticket: SignedTicket;
20
+ }[];
21
+ /** Returns true if the ticket was added, false if it was a duplicate or dropped (old epoch). */
22
+ addTicket(epochIndex: Epoch, ticket: SignedTicket): boolean;
23
+ /**
24
+ * Replace the pool contents for the given epoch with the supplied tickets. Used when the
25
+ * authorship worker pushes an authoritative pool dump on an epoch boundary; any tickets
26
+ * that aren't in the dump are dropped, and dedup runs over the new set.
27
+ */
28
+ replace(epochIndex: Epoch, tickets: readonly SignedTicket[]): void;
29
+ }
30
+ //# sourceMappingURL=pending-ticket-pool.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pending-ticket-pool.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/ticket-pool/pending-ticket-pool.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAKhE;;;;;;;GAOG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,OAAO,CAA0D;IACzE,OAAO,CAAC,iBAAiB,CAAsB;IAE/C,2EAA2E;IAC3E,IAAI,YAAY,IAAI,KAAK,GAAG,IAAI,CAE/B;IAED,2FAA2F;IAC3F,UAAU,IAAI,SAAS;QAAE,UAAU,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,YAAY,CAAA;KAAE,EAAE;IAIpE,gGAAgG;IAChG,SAAS,CAAC,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO;IAyB3D;;;;OAIG;IACH,OAAO,CAAC,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,IAAI;CAWnE"}
@@ -0,0 +1,56 @@
1
+ import { Logger } from "#@typeberry/logger";
2
+ const logger = Logger.new(import.meta.filename, "pending-pool");
3
+ /**
4
+ * An ordered, signature-deduplicated pool of tickets waiting to be redistributed to peers.
5
+ *
6
+ * Used on the networking side. Indices are stable within an epoch so callers can track
7
+ * per-peer "sent" sets by index. The pool is cleared whenever a new epoch is observed,
8
+ * and tickets for older epochs are dropped (can happen when an async validation completes
9
+ * after the epoch already advanced).
10
+ */
11
+ export class PendingTicketPool {
12
+ tickets = [];
13
+ currentEpochValue = null;
14
+ /** Epoch the pool is currently holding tickets for, or `null` if empty. */
15
+ get currentEpoch() {
16
+ return this.currentEpochValue;
17
+ }
18
+ /** Returns the ordered tickets currently in the pool. Caller must not mutate the array. */
19
+ getTickets() {
20
+ return this.tickets;
21
+ }
22
+ /** Returns true if the ticket was added, false if it was a duplicate or dropped (old epoch). */
23
+ addTicket(epochIndex, ticket) {
24
+ if (this.currentEpochValue !== null && epochIndex < this.currentEpochValue) {
25
+ return false;
26
+ }
27
+ if (this.currentEpochValue !== null && epochIndex > this.currentEpochValue) {
28
+ logger.log `Epoch changed from ${this.currentEpochValue} to ${epochIndex}, clearing ${this.tickets.length} old tickets`;
29
+ this.tickets = [];
30
+ }
31
+ this.currentEpochValue = epochIndex;
32
+ const isDuplicate = this.tickets.some((pending) => pending.epochIndex === epochIndex && pending.ticket.signature.isEqualTo(ticket.signature));
33
+ if (isDuplicate) {
34
+ return false;
35
+ }
36
+ this.tickets.push({ epochIndex, ticket });
37
+ logger.info `[addTicket] Added ticket for epoch ${epochIndex}, total: ${this.tickets.length}`;
38
+ return true;
39
+ }
40
+ /**
41
+ * Replace the pool contents for the given epoch with the supplied tickets. Used when the
42
+ * authorship worker pushes an authoritative pool dump on an epoch boundary; any tickets
43
+ * that aren't in the dump are dropped, and dedup runs over the new set.
44
+ */
45
+ replace(epochIndex, tickets) {
46
+ this.tickets = [];
47
+ this.currentEpochValue = epochIndex;
48
+ for (const ticket of tickets) {
49
+ const isDuplicate = this.tickets.some((pending) => pending.ticket.signature.isEqualTo(ticket.signature));
50
+ if (!isDuplicate) {
51
+ this.tickets.push({ epochIndex, ticket });
52
+ }
53
+ }
54
+ logger.log `Pool replaced for epoch ${epochIndex} with ${this.tickets.length} tickets`;
55
+ }
56
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=pending-ticket-pool.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pending-ticket-pool.test.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/ticket-pool/pending-ticket-pool.test.ts"],"names":[],"mappings":""}