@typeberry/lib 0.8.4-faebc7a → 0.9.0-c9f9e4d

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 (90) hide show
  1. package/package.json +1 -1
  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/jamnp-s/tasks/ticket-distribution.js +1 -1
  18. package/packages/jam/safrole/bandersnatch-vrf.d.ts +22 -2
  19. package/packages/jam/safrole/bandersnatch-vrf.d.ts.map +1 -1
  20. package/packages/jam/safrole/bandersnatch-vrf.js +54 -20
  21. package/packages/jam/safrole/bandersnatch-vrf.test.js +3 -2
  22. package/packages/jam/safrole/bandersnatch-wasm.d.ts +10 -0
  23. package/packages/jam/safrole/bandersnatch-wasm.d.ts.map +1 -1
  24. package/packages/jam/safrole/bandersnatch-wasm.js +12 -0
  25. package/packages/jam/ticket-pool/ticket-validator.d.ts +5 -4
  26. package/packages/jam/ticket-pool/ticket-validator.d.ts.map +1 -1
  27. package/packages/jam/ticket-pool/ticket-validator.js +8 -3
  28. package/packages/jam/ticket-pool/ticket-validator.test.js +5 -4
  29. package/packages/jam/ticket-pool/verified-ticket-pool.d.ts +2 -0
  30. package/packages/jam/ticket-pool/verified-ticket-pool.d.ts.map +1 -1
  31. package/packages/jam/ticket-pool/verified-ticket-pool.js +4 -0
  32. package/packages/jam/ticket-pool/verified-ticket-pool.test.js +5 -5
  33. package/packages/workers/block-authorship/{generator.d.ts → block-generator.d.ts} +5 -5
  34. package/packages/workers/block-authorship/block-generator.d.ts.map +1 -0
  35. package/packages/workers/block-authorship/{generator.js → block-generator.js} +3 -3
  36. package/packages/workers/block-authorship/block-generator.test.d.ts +2 -0
  37. package/packages/workers/block-authorship/block-generator.test.d.ts.map +1 -0
  38. package/packages/workers/block-authorship/{generator.test.js → block-generator.test.js} +8 -8
  39. package/packages/workers/block-authorship/epoch-authoring-slots.d.ts +35 -0
  40. package/packages/workers/block-authorship/epoch-authoring-slots.d.ts.map +1 -0
  41. package/packages/workers/block-authorship/epoch-authoring-slots.js +86 -0
  42. package/packages/workers/block-authorship/epoch-tracker.d.ts +29 -0
  43. package/packages/workers/block-authorship/epoch-tracker.d.ts.map +1 -0
  44. package/packages/workers/block-authorship/epoch-tracker.js +80 -0
  45. package/packages/workers/block-authorship/index.d.ts.map +1 -1
  46. package/packages/workers/block-authorship/index.js +1 -1
  47. package/packages/workers/block-authorship/main.d.ts +3 -0
  48. package/packages/workers/block-authorship/main.d.ts.map +1 -1
  49. package/packages/workers/block-authorship/main.js +193 -261
  50. package/packages/workers/block-authorship/ticket-generator/bootstrap-main.d.ts +2 -0
  51. package/packages/workers/block-authorship/ticket-generator/bootstrap-main.d.ts.map +1 -0
  52. package/packages/workers/block-authorship/ticket-generator/bootstrap-main.js +23 -0
  53. package/packages/workers/block-authorship/ticket-generator/index.d.ts +16 -0
  54. package/packages/workers/block-authorship/ticket-generator/index.d.ts.map +1 -0
  55. package/packages/workers/block-authorship/ticket-generator/index.js +62 -0
  56. package/packages/workers/block-authorship/ticket-generator/protocol.d.ts +50 -0
  57. package/packages/workers/block-authorship/ticket-generator/protocol.d.ts.map +1 -0
  58. package/packages/workers/block-authorship/ticket-generator/protocol.js +54 -0
  59. package/packages/workers/block-authorship/{ticket-generator.d.ts → ticket-generator/ticket-generator.d.ts} +4 -0
  60. package/packages/workers/block-authorship/ticket-generator/ticket-generator.d.ts.map +1 -0
  61. package/packages/workers/block-authorship/{ticket-generator.js → ticket-generator/ticket-generator.js} +19 -9
  62. package/packages/workers/block-authorship/ticket-generator/ticket-generator.test.d.ts.map +1 -0
  63. package/packages/workers/block-authorship/{ticket-generator.test.js → ticket-generator/ticket-generator.test.js} +13 -9
  64. package/packages/workers/block-authorship/ticket-generator/worker-pool.d.ts +36 -0
  65. package/packages/workers/block-authorship/ticket-generator/worker-pool.d.ts.map +1 -0
  66. package/packages/workers/block-authorship/ticket-generator/worker-pool.js +111 -0
  67. package/packages/workers/block-authorship/ticket-validator.d.ts +7 -8
  68. package/packages/workers/block-authorship/ticket-validator.d.ts.map +1 -1
  69. package/packages/workers/block-authorship/ticket-validator.js +26 -23
  70. package/packages/workers/comms-authorship-network/protocol.d.ts +4 -4
  71. package/packages/workers/comms-authorship-network/protocol.d.ts.map +1 -1
  72. package/packages/workers/comms-authorship-network/protocol.js +4 -5
  73. package/packages/workers/comms-authorship-network/tickets-message.d.ts +0 -14
  74. package/packages/workers/comms-authorship-network/tickets-message.d.ts.map +1 -1
  75. package/packages/workers/comms-authorship-network/tickets-message.js +0 -17
  76. package/packages/workers/importer/importer.d.ts +2 -2
  77. package/packages/workers/importer/importer.d.ts.map +1 -1
  78. package/packages/workers/importer/importer.js +5 -5
  79. package/packages/workers/importer/stats.d.ts +1 -3
  80. package/packages/workers/importer/stats.d.ts.map +1 -1
  81. package/packages/workers/importer/stats.js +12 -12
  82. package/packages/workers/jam-network/main.d.ts.map +1 -1
  83. package/packages/workers/jam-network/main.js +8 -3
  84. package/packages/workers/block-authorship/generator.d.ts.map +0 -1
  85. package/packages/workers/block-authorship/generator.test.d.ts +0 -2
  86. package/packages/workers/block-authorship/generator.test.d.ts.map +0 -1
  87. package/packages/workers/block-authorship/ticket-generator.d.ts.map +0 -1
  88. package/packages/workers/block-authorship/ticket-generator.test.d.ts.map +0 -1
  89. /package/packages/configs/{typeberry-dev.json → typeberry-dev-tiny.json} +0 -0
  90. /package/packages/workers/block-authorship/{ticket-generator.test.d.ts → ticket-generator/ticket-generator.test.d.ts} +0 -0
@@ -1,24 +1,19 @@
1
1
  import { setTimeout } from "node:timers/promises";
2
- import { tryAsEpoch, tryAsTimeSlot, tryAsValidatorIndex, } from "#@typeberry/block";
3
- import { BytesBlob } from "#@typeberry/bytes";
4
- import { HashDictionary } from "#@typeberry/collections/hash-dictionary.js";
5
- import { HashSet } from "#@typeberry/collections/hash-set.js";
2
+ import { tryAsTimeSlot, tryAsValidatorIndex, } from "#@typeberry/block";
6
3
  import { initWasm } from "#@typeberry/crypto";
7
- import { deriveBandersnatchPublicKey, deriveEd25519PublicKey, } from "#@typeberry/crypto/key-derivation.js";
8
4
  import { Blake2b, keccak } from "#@typeberry/hash";
9
5
  import { Logger } from "#@typeberry/logger";
10
6
  import { tryAsU64 } from "#@typeberry/numbers";
11
- import { Safrole } from "#@typeberry/safrole";
12
- import bandersnatchVrf from "#@typeberry/safrole/bandersnatch-vrf.js";
13
7
  import { BandernsatchWasm } from "#@typeberry/safrole/bandersnatch-wasm.js";
14
- import { JAM_FALLBACK_SEAL, JAM_TICKET_SEAL } from "#@typeberry/safrole/constants.js";
15
- import { SafroleSealingKeysKind } from "#@typeberry/state";
16
8
  import { VerifiedTicketPool } from "#@typeberry/ticket-pool";
17
- import { asOpaqueType, Result } from "#@typeberry/utils";
18
- import { Generator } from "./generator.js";
19
- import { generateTickets } from "./ticket-generator.js";
9
+ import { BlockGenerator } from "./block-generator.js";
10
+ import { EpochTracker } from "./epoch-tracker.js";
11
+ import { TicketGenerator } from "./ticket-generator/index.js";
20
12
  import { BandersnatchTicketValidator } from "./ticket-validator.js";
21
13
  const logger = Logger.new(import.meta.filename, "author");
14
+ /**
15
+ * The `BlockAuthorship` should create new blocks and send them as signals to the main thread.
16
+ */
22
17
  export async function main(config, comms, networkingComms) {
23
18
  await initWasm();
24
19
  logger.info `🎁 Block Authorship running`;
@@ -26,19 +21,19 @@ export async function main(config, comms, networkingComms) {
26
21
  const db = config.openDatabase();
27
22
  const blocks = db.getBlocksDb();
28
23
  const states = db.getStatesDb();
29
- let isFinished = false;
30
- comms.setOnFinish(async () => {
31
- isFinished = true;
32
- });
33
- // Generate blocks until the close signal is received.
34
- let counter = 0;
24
+ const getBestState = () => {
25
+ const state = states.getState(blocks.getBestHeaderHash());
26
+ if (state === null) {
27
+ throw new Error("Authorship: State for the best block is missing. Terminating.");
28
+ }
29
+ return state;
30
+ };
35
31
  const blake2bHasher = await Blake2b.createHasher();
36
32
  const bandersnatch = await BandernsatchWasm.new();
37
33
  const keccakHasher = await keccak.KeccakHasher.create();
38
- const hash = blocks.getBestHeaderHash();
39
- const startTime = tryAsU64(process.hrtime.bigint() / 1000000n);
40
- const startTimeSlot = states.getState(hash)?.timeslot ?? tryAsTimeSlot(0);
41
- const generator = Generator.new({
34
+ const epochTracker = await EpochTracker.new(chainSpec, bandersnatch, blake2bHasher, config.workerParams.keys);
35
+ logger.info `👛 Authoring with: ${epochTracker.authoring.getBandersnatchPublicKeys().map((bandersnatchPublic, index) => `\n ${index}: ${bandersnatchPublic}`)}`;
36
+ const generator = BlockGenerator.new({
42
37
  chainSpec,
43
38
  bandersnatch,
44
39
  keccakHasher,
@@ -46,261 +41,198 @@ export async function main(config, comms, networkingComms) {
46
41
  blocks,
47
42
  states,
48
43
  });
49
- const keys = await Promise.all(config.workerParams.keys.map(async (secrets) => ({
50
- bandersnatchSecret: secrets.bandersnatch,
51
- bandersnatchPublic: deriveBandersnatchPublicKey(secrets.bandersnatch),
52
- ed25519Secret: secrets.ed25519,
53
- ed25519Public: await deriveEd25519PublicKey(secrets.ed25519),
54
- })));
55
- const initialHash = blocks.getBestHeaderHash();
56
- const initialState = states.getState(initialHash);
57
- logger.info `Block authorship validator keys: ${keys.map(({ bandersnatchPublic }, index) => `\n ${index}: ${bandersnatchPublic.toString()}`)}`;
58
- // Per-epoch cache for Tickets mode: index corresponds to position in sealingKeySeries.tickets.
59
- // null entry means none of our keys match that slot.
60
- // Rebuilt once per epoch via buildTicketAuthorshipCache().
61
- // Declared here (before the eager startup build below) so its TDZ doesn't fire
62
- // when `buildTicketAuthorshipCache` runs during initialisation.
63
- let ticketAuthorshipCache = null;
64
- if (initialState !== null) {
65
- const isEpochStart = startTimeSlot % chainSpec.epochLength === 0;
66
- const initialKeys = await getSealingKeySeries(isEpochStart, startTimeSlot, initialState);
67
- if (initialKeys.isOk) {
68
- logEpochBlockCreation(tryAsEpoch(Math.floor(startTimeSlot / chainSpec.epochLength)), initialKeys.ok);
69
- // Build the cache eagerly so the first slot of a session doesn't need an
70
- // on-the-fly VRF scan. After this, `buildTicketAuthorshipCache` is only
71
- // re-run on epoch boundaries.
72
- const initialEntropy = isEpochStart ? initialState.entropy[2] : initialState.entropy[3];
73
- await buildTicketAuthorshipCache(initialKeys.ok, initialEntropy);
74
- }
75
- }
76
- function getTime() {
77
- const currentTime = process.hrtime.bigint() / 1000000n;
78
- const timeFromStart = currentTime - startTime;
79
- const slotDurationMs = BigInt(chainSpec.slotDuration * 1000);
80
- return tryAsU64(BigInt(startTimeSlot) * slotDurationMs + timeFromStart + slotDurationMs);
81
- }
82
- function getValidatorIndex(key, currentValidatorData) {
83
- const index = currentValidatorData.findIndex((data) => data.bandersnatch.isEqualTo(key.bandersnatchPublic));
84
- if (index < 0) {
85
- return null;
86
- }
87
- return tryAsValidatorIndex(index);
88
- }
89
- /**
90
- * Precomputes which slots we are the author of for the current epoch (Tickets mode).
91
- */
92
- async function buildTicketAuthorshipCache(sealingKeySeries, entropy) {
93
- if (sealingKeySeries.kind !== SafroleSealingKeysKind.Tickets) {
94
- ticketAuthorshipCache = null;
95
- return;
44
+ // Verified tickets for the next epoch, keyed by entropy hash (ticket id).
45
+ const verifiedPool = VerifiedTicketPool.new();
46
+ const ticketValidator = BandersnatchTicketValidator.new(chainSpec, bandersnatch, getBestState);
47
+ const keys = epochTracker.authoring.getValidatorKeys().map((x) => ({
48
+ public: x.bandersnatchPublic,
49
+ secret: x.bandersnatchSecret,
50
+ }));
51
+ const ticketGenerator = await TicketGenerator.new(chainSpec, keys);
52
+ // handling incoming tickets
53
+ const onEpochTickets = async (epochIndex, tickets, source) => {
54
+ logger.log `[E${epochIndex}] Received (${tickets.length}) tickets from ${source}`;
55
+ const result = await ticketValidator.validate(epochIndex, tickets);
56
+ // add to our pool as well
57
+ if (result.isOk) {
58
+ verifiedPool.add(epochIndex, result.ok);
96
59
  }
97
- const ownTickets = new HashDictionary();
98
- for (let attempt = 0; attempt < chainSpec.ticketsPerValidator; attempt++) {
99
- const payload = getTicketSealPayload(entropy, attempt);
100
- for (const key of keys) {
101
- const result = await bandersnatchVrf.getVrfOutputHash(bandersnatch, key.bandersnatchSecret, payload);
102
- if (result.isOk) {
103
- ownTickets.set(result.ok.asOpaque(), { key, sealPayload: asOpaqueType(payload) });
104
- }
60
+ return result.isOk;
61
+ };
62
+ // Receive tickets from networking.
63
+ networkingComms.setOnReceivedTickets(async ({ epochIndex, tickets }) => {
64
+ return await onEpochTickets(epochIndex, tickets, "network");
65
+ });
66
+ const state = getBestState();
67
+ const timeSlotHandler = TimeSlotHandler.new(config.workerParams.isFastForward, chainSpec, state.timeslot);
68
+ // per-epoch cached data
69
+ let epochData = null;
70
+ // Generate blocks until the close signal is received.
71
+ let isFinished = false;
72
+ comms.setOnFinish(async () => {
73
+ isFinished = true;
74
+ });
75
+ let ticketGeneratorDone = Promise.resolve();
76
+ while (!isFinished) {
77
+ const state = getBestState();
78
+ // query current expected time slot
79
+ const stateTimeSlot = state.timeslot;
80
+ const newTimeSlot = timeSlotHandler.getCurrentTimeSlot(stateTimeSlot);
81
+ const epochPhase = newTimeSlot % chainSpec.epochLength;
82
+ // Seems that the epoch is changing, let's transition
83
+ if (epochData === null || epochTracker.isEpochChanged(stateTimeSlot, newTimeSlot)) {
84
+ const oldEpochData = epochData;
85
+ const epochDataResult = await epochTracker.getEpochData(logger, state, newTimeSlot);
86
+ if (epochDataResult.isError) {
87
+ // Couldn't compute the sealing keys for this epoch — wait and retry rather
88
+ // than crashing the worker (`epochData` keeps its previous value, if any).
89
+ logger.warn `[#${newTimeSlot}] Could not compute epoch data: ${epochDataResult.details()}`;
90
+ await timeSlotHandler.waitForNextSlot(false, epochPhase, ticketGeneratorDone);
91
+ continue;
105
92
  }
106
- }
107
- const cache = sealingKeySeries.tickets.map((ticket) => ownTickets.get(ticket.id.asOpaque()) ?? null);
108
- ticketAuthorshipCache = cache;
109
- const ours = cache.filter(Boolean).length;
110
- logger.info `Built ticket authorship cache: ${ours}/${cache.length} slots assigned to us this epoch.`;
111
- }
112
- function getTicketSealPayload(entropy, attempt) {
113
- return BytesBlob.blobFromParts(JAM_TICKET_SEAL, entropy.raw, new Uint8Array([attempt]));
114
- }
115
- function getFallbackSealPayload(entropy) {
116
- return asOpaqueType(BytesBlob.blobFromParts(JAM_FALLBACK_SEAL, entropy.raw));
117
- }
118
- /**
119
- * Returns the validator key and seal payload for the current slot, or null if we are not the author.
120
- *
121
- * Keys mode (fallback): matches our key against the slot's assigned bandersnatch key.
122
- * Tickets mode: O(1) lookup against the per-epoch authorship cache (built eagerly at
123
- * startup and on every epoch transition, so we never fall back to on-the-fly VRF).
124
- */
125
- function getSealData(sealingKeySeries, keys, timeSlot, entropy) {
126
- if (sealingKeySeries.kind === SafroleSealingKeysKind.Keys) {
127
- const indexForCurrentSlot = timeSlot % sealingKeySeries.keys.length;
128
- const sealingKey = sealingKeySeries.keys[indexForCurrentSlot];
129
- const key = keys.find((x) => x.bandersnatchPublic.isEqualTo(sealingKey)) ?? null;
130
- if (key === null) {
131
- return null;
93
+ epochData = epochDataResult.ok;
94
+ const epochIndex = epochData.epoch;
95
+ if (oldEpochData === null) {
96
+ logger.info `🎁 [E${epochIndex}#${newTimeSlot}] starting authorship (state at #${stateTimeSlot})`;
132
97
  }
133
- return {
134
- key,
135
- sealPayload: getFallbackSealPayload(entropy),
136
- logId: `key ${key.bandersnatchPublic}`,
137
- };
138
- }
139
- // Tickets mode: each slot is sealed by the validator who can produce the VRF output
140
- // matching the ticket's ID for that slot.
141
- const index = timeSlot % sealingKeySeries.tickets.length;
142
- const ticket = sealingKeySeries.tickets.at(index) ?? null;
143
- const cached = ticketAuthorshipCache?.at(index) ?? null;
144
- if (ticket === null || cached === null) {
145
- return null;
146
- }
147
- return { ...cached, logId: `ticket ${ticket.id} (attempt ${ticket.attempt})` };
148
- }
149
- function isEpochChanged(lastTimeslot, currentTimeslot) {
150
- const lastEpoch = Math.floor(lastTimeslot / chainSpec.epochLength);
151
- const currentEpoch = Math.floor(currentTimeslot / chainSpec.epochLength);
152
- return currentEpoch > lastEpoch;
153
- }
154
- function logEpochBlockCreation(epoch, sealingKeySeries) {
155
- if (sealingKeySeries.kind === SafroleSealingKeysKind.Tickets) {
156
- logger.info `[EPOCH ${epoch}] Tickets mode active with ${sealingKeySeries.tickets.length} tickets.`;
157
- return;
158
- }
159
- let isCreating = false;
160
- const epochStart = epoch * chainSpec.epochLength;
161
- const epochEnd = epochStart + chainSpec.epochLength;
162
- for (let slot = epochStart; slot < epochEnd; slot++) {
163
- const indexForCurrentSlot = slot % sealingKeySeries.keys.length;
164
- const sealingKey = sealingKeySeries.keys[indexForCurrentSlot];
165
- const key = keys.find((x) => x.bandersnatchPublic.isEqualTo(sealingKey)) ?? null;
166
- if (key !== null) {
167
- isCreating = true;
168
- logger.info `[EPOCH ${epoch}] Validator ${key.bandersnatchPublic.toString()} will author block at slot ${slot}`;
98
+ else {
99
+ logger.info `🎁 [E${oldEpochData.epoch}#${stateTimeSlot} -> E${epochIndex}#${newTimeSlot}] epoch transition`;
169
100
  }
170
- }
171
- if (isCreating === false) {
172
- logger.info `[EPOCH ${epoch}] No blocks to author for this epoch.`;
173
- }
174
- }
175
- async function getSealingKeySeries(isNewEpoch, timeSlot, state) {
176
- if (isNewEpoch) {
177
- const safrole = new Safrole(chainSpec, blake2bHasher, state);
178
- return await safrole.getSealingKeySeries({
179
- entropy: state.entropy[1],
180
- slot: timeSlot,
181
- punishSet: state.disputesRecords.punishSet,
101
+ // On every epoch boundary, push the authoritative ticket pool to networking so it
102
+ // can replace its redistribution set; this keeps the two sides from drifting.
103
+ const tickets = verifiedPool.getForEpoch(epochIndex).map((entry) => entry.ticket);
104
+ await networkingComms.sendReplaceTicketPool({
105
+ epochIndex,
106
+ tickets,
182
107
  });
183
- }
184
- return Result.ok(state.sealingKeySeries);
185
- }
186
- // Verified tickets for the current epoch, keyed by entropy hash (ticket id).
187
- // Tickets enter via `validator.validate(...)` which both verifies and inserts.
188
- const verifiedPool = new VerifiedTicketPool();
189
- const ticketValidator = new BandersnatchTicketValidator(bandersnatch, chainSpec, verifiedPool, () => states.getState(blocks.getBestHeaderHash()));
190
- // Receive a single ticket from peers (via jam-network worker).
191
- // Returns true if the ticket passed validation so jam-network can decide whether to redistribute it.
192
- networkingComms.setOnReceivedTickets(async ({ epochIndex, ticket }) => {
193
- logger.log `Received ticket from peer for epoch ${epochIndex}`;
194
- const result = await ticketValidator.validate(epochIndex, ticket);
195
- return result.isOk;
196
- });
197
- const isFastForward = config.workerParams.isFastForward;
198
- let lastGeneratedSlot = startTimeSlot;
199
- let ticketsGeneratedForEpoch = -1;
200
- while (!isFinished) {
201
- const hash = blocks.getBestHeaderHash();
202
- const state = states.getState(hash);
203
- const currentValidatorData = state?.currentValidatorData ?? null;
204
- if (state === null) {
205
- continue;
206
- }
207
- const lastTimeSlot = state.timeslot;
208
- /**
209
- * In fastForward mode, use simulated time (next slot after current state).
210
- * In normal mode, use wall clock time.
211
- * Assuming `slotDuration` is 6 sec it is safe till year 2786.
212
- * If `slotDuration` is 1 sec then it is safe till 2106.
213
- */
214
- const timeSlot = isFastForward === true
215
- ? tryAsTimeSlot(lastTimeSlot + 1)
216
- : tryAsTimeSlot(Number(getTime() / 1000n / BigInt(chainSpec.slotDuration)));
217
- // In fastForward mode, skip if we already generated for this slot (waiting for import)
218
- if (isFastForward === true && timeSlot <= lastGeneratedSlot) {
219
- continue;
220
- }
221
- const isNewEpoch = isEpochChanged(lastTimeSlot, timeSlot);
222
- // Generate tickets if within contest period and not yet generated for this epoch
223
- const epoch = tryAsEpoch(Math.floor(timeSlot / chainSpec.epochLength));
224
- const slotInEpoch = timeSlot % chainSpec.epochLength;
225
- const shouldGenerateTickets = slotInEpoch < chainSpec.contestLength && ticketsGeneratedForEpoch !== epoch;
226
- if (shouldGenerateTickets) {
227
- const designatedValidatorData = state.designatedValidatorData;
228
- const ringKeys = designatedValidatorData.map((data) => data.bandersnatch);
229
- const designatedKeySet = HashSet.from(ringKeys);
230
- const validatorKeys = keys
231
- .filter((k) => designatedKeySet.has(k.bandersnatchPublic))
232
- .map((k) => ({ secret: k.bandersnatchSecret, public: k.bandersnatchPublic }));
233
- if (validatorKeys.length > 0) {
234
- // If state is from the previous epoch, entropy hasn't been shifted yet (index 1).
235
- // After epoch change, it has been shifted to index 2.
236
- const ticketEntropy = isNewEpoch ? state.entropy[1] : state.entropy[2];
237
- logger.info `Epoch ${epoch}, slot ${slotInEpoch}/${chainSpec.contestLength}. Generating tickets for ${validatorKeys.length} validators...`;
238
- const ticketsResult = await generateTickets(bandersnatch, ringKeys, validatorKeys, ticketEntropy, chainSpec.ticketsPerValidator);
239
- if (ticketsResult.isError) {
240
- logger.warn `Failed to generate tickets for epoch ${epoch}: ${ticketsResult.error}`;
241
- }
242
- else {
243
- logger.log `Generated ${ticketsResult.ok.length} tickets for epoch ${epoch}. Distributing...`;
244
- // Verify own tickets (validator stores them in the pool with computed ids).
245
- for (const ticket of ticketsResult.ok) {
246
- await ticketValidator.validate(epoch, ticket);
108
+ // Let's generate some tickets for the next epoch if we still have time
109
+ if (epochPhase < chainSpec.contestLength) {
110
+ const generatingForEpoch = epochData.epoch;
111
+ const isEpochStart = epochPhase === 0;
112
+ ticketGeneratorDone = ticketGenerator.generateTickets(state, isEpochStart, async (tickets) => {
113
+ // too late!
114
+ if (generatingForEpoch !== epochData?.epoch) {
115
+ return;
247
116
  }
248
- // Send directly to network worker (bypasses main thread)
249
- await networkingComms.sendTickets({ epochIndex: epoch, tickets: ticketsResult.ok });
250
- }
251
- }
252
- ticketsGeneratedForEpoch = epoch;
253
- }
254
- const selingKeySeriesResult = await getSealingKeySeries(isNewEpoch, timeSlot, state);
255
- if (selingKeySeriesResult.isError) {
256
- continue;
257
- }
258
- // On a new epoch, `state.entropy[2]` is the epoch-E entropy (pre-transition);
259
- // mid-epoch, it has already shifted to `entropy[3]`.
260
- const entropy = isNewEpoch ? state.entropy[2] : state.entropy[3];
261
- // Rebuild the authorship cache on each epoch boundary, and also catch the case
262
- // where the startup prebuild was skipped (e.g. initialState was null or the
263
- // initial sealing-key transition errored) so we don't silently miss Tickets-mode
264
- // slots until the next epoch boundary.
265
- const needsCacheRebuild = isNewEpoch ||
266
- (selingKeySeriesResult.ok.kind === SafroleSealingKeysKind.Tickets && ticketAuthorshipCache === null);
267
- if (needsCacheRebuild) {
268
- if (isNewEpoch) {
269
- logEpochBlockCreation(epoch, selingKeySeriesResult.ok);
117
+ const isValid = await onEpochTickets(generatingForEpoch, tickets, "generator");
118
+ // Push our freshly generated tickets to networking so they're redistributed
119
+ // to peers (who include them in their blocks). Without this, a multi-node
120
+ // network never shares tickets and accumulators only ever hold local ones.
121
+ if (isValid) {
122
+ await networkingComms.sendTickets({ epochIndex: generatingForEpoch, tickets });
123
+ }
124
+ });
270
125
  }
271
- await buildTicketAuthorshipCache(selingKeySeriesResult.ok, entropy);
272
- }
273
- // On every epoch boundary, push the authoritative ticket pool to networking so it
274
- // can replace its redistribution set; this keeps the two sides from drifting.
275
- if (isNewEpoch) {
276
- const dumpTickets = verifiedPool.getForEpoch(epoch).map((entry) => entry.ticket);
277
- await networkingComms.sendReplaceTicketPool({ epochIndex: epoch, tickets: dumpTickets });
278
126
  }
279
- const sealData = getSealData(selingKeySeriesResult.ok, keys, timeSlot, entropy);
280
- if (sealData !== null && currentValidatorData !== null) {
281
- const { key, sealPayload } = sealData;
282
- const validatorIndex = getValidatorIndex(key, currentValidatorData);
127
+ const logPrefix = `[E${epochData.epoch}#${newTimeSlot}]`;
128
+ // author a block if we are assigned to that slot
129
+ const currentSlot = epochData.slots[epochPhase];
130
+ if (currentSlot !== null) {
131
+ const { logId, key, sealPayload } = currentSlot;
132
+ // figure out validator index
133
+ const validatorIndex = getValidatorIndex(key.bandersnatchPublic, state.currentValidatorData);
283
134
  if (validatorIndex === null) {
135
+ logger.log `${logPrefix} Not currently validator, yet ${currentSlot.logId} is present.`;
136
+ // Don't spin: wait for the next slot before re-checking (otherwise this is
137
+ // a tight hot loop until some other component advances the DB).
138
+ await timeSlotHandler.waitForNextSlot(false, epochPhase, ticketGeneratorDone);
284
139
  continue;
285
140
  }
286
- logger.log `Attempting to create a block using ${sealData.logId} located at validator index ${validatorIndex}.`;
287
- const currentEpochTickets = verifiedPool.getForEpoch(epoch);
288
- const newBlock = await generator.nextBlockView(validatorIndex, key.bandersnatchSecret, sealPayload, timeSlot,
141
+ logger.log `${logPrefix} Creating block using ${logId} (valIdx: ${validatorIndex})`;
142
+ // retrieve epoch tickets to include
143
+ const currentEpochTickets = verifiedPool.getForEpoch(epochData.epoch);
144
+ const newBlock = await generator.nextBlockView(validatorIndex, key.bandersnatchSecret, sealPayload, newTimeSlot,
289
145
  // VerifiedTicket has the same `{ ticket, id }` shape the generator expects.
290
146
  [...currentEpochTickets]);
291
- counter += 1;
292
- lastGeneratedSlot = timeSlot;
293
- logger.trace `Sending block ${counter}`;
147
+ logger.trace `${logPrefix} sending block`;
294
148
  await comms.sendBlock(newBlock);
295
149
  }
296
- else if (isFastForward === true) {
297
- // In fast-forward mode, if this slot is not ours, wait briefly for other validators to produce blocks
298
- await setTimeout(10);
299
- }
300
- if (isFastForward === false) {
301
- await setTimeout(chainSpec.slotDuration * 1000);
302
- }
150
+ logger.trace `${logPrefix} awaiting next slot`;
151
+ await timeSlotHandler.waitForNextSlot(currentSlot !== null, epochPhase, ticketGeneratorDone);
303
152
  }
304
153
  logger.info `🎁 Block Authorship finished. Closing channel.`;
305
154
  await db.close();
306
155
  }
156
+ function getValidatorIndex(key, currentValidatorData) {
157
+ const index = currentValidatorData.findIndex((data) => data.bandersnatch.isEqualTo(key));
158
+ if (index < 0) {
159
+ return null;
160
+ }
161
+ return tryAsValidatorIndex(index);
162
+ }
163
+ /**
164
+ * How many slots before the end of the contest period we force-await the ticket
165
+ * generator in fast-forward mode. Without this, blocks are produced faster than
166
+ * tickets are generated and the accumulator never fills (→ Keys-mode fallback).
167
+ *
168
+ * Derived so that, after the wait completes, there are enough remaining contest
169
+ * slots to include a full accumulator worth of tickets (`epochLength` tickets at
170
+ * `maxTicketsPerExtrinsic` per block), plus a small buffer.
171
+ */
172
+ function ticketInclusionMargin(chainSpec) {
173
+ return Math.ceil(chainSpec.epochLength / chainSpec.maxTicketsPerExtrinsic) + 4;
174
+ }
175
+ function systemTimeMs() {
176
+ return process.hrtime.bigint() / 1000000n;
177
+ }
178
+ class TimeSlotHandler {
179
+ initialStateTimeSlot;
180
+ slotDurationMs;
181
+ isFastForward;
182
+ contestLength;
183
+ inclusionMargin;
184
+ systemStartTimeMs;
185
+ stateStartTime;
186
+ static new(isFastForward, chainSpec, stateTimeSlot) {
187
+ const slotDurationMs = BigInt(chainSpec.slotDuration) * 1000n;
188
+ return new TimeSlotHandler(stateTimeSlot, slotDurationMs, isFastForward, chainSpec.contestLength, ticketInclusionMargin(chainSpec));
189
+ }
190
+ constructor(initialStateTimeSlot, slotDurationMs, isFastForward, contestLength, inclusionMargin) {
191
+ this.initialStateTimeSlot = initialStateTimeSlot;
192
+ this.slotDurationMs = slotDurationMs;
193
+ this.isFastForward = isFastForward;
194
+ this.contestLength = contestLength;
195
+ this.inclusionMargin = inclusionMargin;
196
+ this.systemStartTimeMs = systemTimeMs();
197
+ this.stateStartTime = BigInt(initialStateTimeSlot) * slotDurationMs;
198
+ }
199
+ /**
200
+ * In fastForward mode, use simulated time (next slot after current state).
201
+ * In normal mode, use wall clock time.
202
+ * Assuming `slotDuration` is 6 sec it is safe till year 2786.
203
+ * If `slotDuration` is 1 sec then it is safe till 2106.
204
+ */
205
+ getCurrentTimeSlot(stateTimeSlot) {
206
+ return this.isFastForward === true
207
+ ? tryAsTimeSlot(stateTimeSlot + 1)
208
+ : tryAsTimeSlot(Number(this.getVirtualTimeMs() / this.slotDurationMs));
209
+ }
210
+ async waitForNextSlot(wasAuthoring, epochPhase, ticketGeneratorDone) {
211
+ if (this.isFastForward) {
212
+ // when we approach the end of the contest period make sure to wait for all tickets
213
+ if (epochPhase < this.contestLength && epochPhase + this.inclusionMargin > this.contestLength) {
214
+ await ticketGeneratorDone;
215
+ }
216
+ // return as fast as possible
217
+ if (wasAuthoring) {
218
+ return;
219
+ }
220
+ // or wait for other nodes to produce a block
221
+ return await setTimeout(100);
222
+ }
223
+ // Sleep until the next slot boundary (not a full slot from "now") so the
224
+ // wakeup doesn't drift later and later as block work eats into each slot.
225
+ const elapsedInSlot = this.getVirtualTimeMs() % this.slotDurationMs;
226
+ const waitMs = elapsedInSlot === 0n ? this.slotDurationMs : this.slotDurationMs - elapsedInSlot;
227
+ await setTimeout(Number(waitMs));
228
+ }
229
+ /**
230
+ * We assume there is no gap between system time and the initial state time.
231
+ *
232
+ * I.e. we can resume any database by moving the state time to the future.
233
+ */
234
+ getVirtualTimeMs() {
235
+ const timeFromStart = systemTimeMs() - this.systemStartTimeMs;
236
+ return tryAsU64(this.stateStartTime + timeFromStart + this.slotDurationMs);
237
+ }
238
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=bootstrap-main.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bootstrap-main.d.ts","sourceRoot":"","sources":["../../../../../../packages/workers/block-authorship/ticket-generator/bootstrap-main.ts"],"names":[],"mappings":""}
@@ -0,0 +1,23 @@
1
+ // biome-ignore-all lint/suspicious/noConsole: worker bootstrap
2
+ //
3
+ // Worker-thread entry point for parallel ticket generation. Spawned by
4
+ // `TicketGeneratorPool` (via the `.mjs` bootstrap), it initialises the native
5
+ // bandersnatch binding once and then answers shard requests by running
6
+ // `batchGenerateRingVrfForValidators` and returning the raw signature bytes.
7
+ import { ConcurrentWorker } from "#@typeberry/concurrent";
8
+ import { initWasm } from "#@typeberry/crypto";
9
+ import { BandernsatchWasm } from "#@typeberry/safrole/bandersnatch-wasm.js";
10
+ import { TicketGenShardResult } from "./protocol.js";
11
+ async function main() {
12
+ await initWasm();
13
+ const bandersnatch = await BandernsatchWasm.new();
14
+ const worker = ConcurrentWorker.new(async (params, bs) => {
15
+ const signatures = await bs.batchGenerateRingVrfForValidators(params.ringKeysData, params.proverKeyIndices, params.secretSeedsData, params.secretSeedDataLen, params.inputsData, params.vrfInputDataLen);
16
+ return new TicketGenShardResult(signatures);
17
+ }, bandersnatch);
18
+ worker.listenToParentPort();
19
+ }
20
+ main().catch((e) => {
21
+ console.error("ticket-generator worker failed to start:", e);
22
+ process.exit(1);
23
+ });
@@ -0,0 +1,16 @@
1
+ import { type SignedTicket } from "#@typeberry/block";
2
+ import type { ChainSpec } from "#@typeberry/config";
3
+ import type { State } from "#@typeberry/state";
4
+ import type { ValidatorKey } from "./ticket-generator.js";
5
+ export type TicketGeneratorOptions = {
6
+ useWorkerPool: boolean;
7
+ };
8
+ export declare class TicketGenerator {
9
+ private readonly chainSpec;
10
+ private readonly keys;
11
+ private readonly pool;
12
+ static new(chainSpec: ChainSpec, keys: ValidatorKey[]): Promise<TicketGenerator>;
13
+ private constructor();
14
+ generateTickets(state: State, isEpochStart: boolean, onTickets: (tickets: SignedTicket[]) => Promise<void>): Promise<void>;
15
+ }
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../../packages/workers/block-authorship/ticket-generator/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,YAAY,EAAc,MAAM,kBAAkB,CAAC;AAEjE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAEnD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAE9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AA2B1D,MAAM,MAAM,sBAAsB,GAAG;IACnC,aAAa,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF,qBAAa,eAAe;IAOxB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,IAAI;WARV,GAAG,CAAC,SAAS,EAAE,SAAS,EAAE,IAAI,EAAE,YAAY,EAAE;IAK3D,OAAO;IAMD,eAAe,CAAC,KAAK,EAAE,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,OAAO,EAAE,YAAY,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC;CAyBjH"}
@@ -0,0 +1,62 @@
1
+ import os from "node:os";
2
+ import { tryAsEpoch } from "#@typeberry/block";
3
+ import { HashSet } from "#@typeberry/collections";
4
+ import { Logger } from "#@typeberry/logger";
5
+ import { measure } from "#@typeberry/utils";
6
+ import { TicketGeneratorPool } from "./worker-pool.js";
7
+ /**
8
+ * Extra validators to generate tickets for, beyond the minimum needed to fill the
9
+ * accumulator. Filling requires `epochLength` distinct valid tickets; each validator
10
+ * yields `ticketsPerValidator`. The margin guards against a few tickets failing to
11
+ * land (extra tickets are simply dropped by the accumulator).
12
+ */
13
+ const TICKET_GENERATION_VALIDATOR_MARGIN = 8;
14
+ /** Leave this many cores for the main thread, importer, network and the OS. */
15
+ const TICKET_POOL_RESERVED_CORES = 4;
16
+ /** Hard cap on ticket-generation worker threads. */
17
+ const TICKET_POOL_MAX_WORKERS = 8;
18
+ const logger = Logger.new(import.meta.filename, "tickets");
19
+ const measureTicketGen = measure("ticket:gen");
20
+ /** Number of worker threads to use for parallel ticket generation. */
21
+ function ticketPoolWorkerCount(validators) {
22
+ const cores = os.availableParallelism?.() ?? os.cpus().length;
23
+ const availableCores = Math.min(cores - TICKET_POOL_RESERVED_CORES, TICKET_POOL_MAX_WORKERS);
24
+ // never reserve more cores than we have validators (makes no sense)
25
+ const requiredCores = Math.min(validators, availableCores);
26
+ return Math.max(1, requiredCores);
27
+ }
28
+ export class TicketGenerator {
29
+ chainSpec;
30
+ keys;
31
+ pool;
32
+ static async new(chainSpec, keys) {
33
+ const pool = await TicketGeneratorPool.create(ticketPoolWorkerCount(keys.length));
34
+ return new TicketGenerator(chainSpec, keys, pool);
35
+ }
36
+ constructor(chainSpec, keys, pool) {
37
+ this.chainSpec = chainSpec;
38
+ this.keys = keys;
39
+ this.pool = pool;
40
+ }
41
+ async generateTickets(state, isEpochStart, onTickets) {
42
+ // Pick the right entropy and validator set
43
+ const validators = isEpochStart ? state.designatedValidatorData : state.nextValidatorData;
44
+ const entropy = isEpochStart ? state.entropy[1] : state.entropy[2];
45
+ const epoch = tryAsEpoch(Math.floor(state.timeslot / this.chainSpec.epochLength));
46
+ const ringKeys = validators.map((d) => d.bandersnatch);
47
+ const nextKeySet = HashSet.from(ringKeys);
48
+ const validatorKeys = this.keys.filter((k) => nextKeySet.has(k.public));
49
+ // Generate just enough validators to fill the accumulator, plus a margin.
50
+ const needed = Math.ceil(this.chainSpec.epochLength / this.chainSpec.ticketsPerValidator) + TICKET_GENERATION_VALIDATOR_MARGIN;
51
+ const selected = validatorKeys.slice(0, Math.min(validatorKeys.length, needed));
52
+ const ticketGen = measureTicketGen();
53
+ logger.info `🎫 [E${epoch}] generating tickets for ${selected.length} validators across ${this.pool.workerCount} worker threads…`;
54
+ try {
55
+ await this.pool.generate(ringKeys, selected, entropy, this.chainSpec.ticketsPerValidator, onTickets);
56
+ logger.info `🎫 [E${epoch}] ${ticketGen}`;
57
+ }
58
+ catch (e) {
59
+ logger.warn `🎫 [E${epoch}] ticket generation failed: ${e}`;
60
+ }
61
+ }
62
+ }