@typeberry/lib 0.8.3 → 0.8.4-70b1490
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -4
- package/packages/configs/index.d.ts +30 -1
- package/packages/configs/index.d.ts.map +1 -1
- package/packages/configs/index.js +4 -2
- package/packages/configs/typeberry-dev-full.json +29 -0
- package/packages/core/bytes/bytes.d.ts +1 -0
- package/packages/core/bytes/bytes.d.ts.map +1 -1
- package/packages/core/bytes/bytes.js +8 -0
- package/packages/core/utils/debug.d.ts +16 -4
- package/packages/core/utils/debug.d.ts.map +1 -1
- package/packages/core/utils/debug.js +39 -17
- package/packages/core/utils/debug.test.js +12 -6
- package/packages/jam/config-node/node-config.d.ts +2 -1
- package/packages/jam/config-node/node-config.d.ts.map +1 -1
- package/packages/jam/config-node/node-config.js +8 -3
- package/packages/jam/config-node/node-config.test.js +3 -3
- package/packages/jam/database/states.d.ts +7 -0
- package/packages/jam/database/states.d.ts.map +1 -1
- package/packages/jam/database-fjall/hybrid-states.d.ts +45 -0
- package/packages/jam/database-fjall/hybrid-states.d.ts.map +1 -0
- package/packages/jam/database-fjall/hybrid-states.js +113 -0
- package/packages/jam/database-fjall/hybrid-states.test.d.ts +2 -0
- package/packages/jam/database-fjall/hybrid-states.test.d.ts.map +1 -0
- package/packages/jam/database-fjall/hybrid-states.test.js +83 -0
- package/packages/jam/database-fjall/index.d.ts +3 -0
- package/packages/jam/database-fjall/index.d.ts.map +1 -0
- package/packages/jam/database-fjall/index.js +2 -0
- package/packages/jam/database-fjall/root.d.ts +52 -0
- package/packages/jam/database-fjall/root.d.ts.map +1 -0
- package/packages/jam/database-fjall/root.js +85 -0
- package/packages/jam/database-lmdb/hybrid-states.d.ts +3 -1
- package/packages/jam/database-lmdb/hybrid-states.d.ts.map +1 -1
- package/packages/jam/database-lmdb/hybrid-states.js +5 -2
- package/packages/jam/database-lmdb/root.d.ts +14 -1
- package/packages/jam/database-lmdb/root.d.ts.map +1 -1
- package/packages/jam/database-lmdb/root.js +25 -5
- package/packages/jam/database-lmdb/states.d.ts +1 -0
- package/packages/jam/database-lmdb/states.d.ts.map +1 -1
- package/packages/jam/database-lmdb/states.js +3 -0
- package/packages/jam/database-lmdb/states.test.js +4 -4
- package/packages/jam/jamnp-s/tasks/ticket-distribution.d.ts +18 -10
- package/packages/jam/jamnp-s/tasks/ticket-distribution.d.ts.map +1 -1
- package/packages/jam/jamnp-s/tasks/ticket-distribution.js +44 -68
- package/packages/jam/jamnp-s/tasks/ticket-distribution.test.js +30 -8
- package/packages/jam/node/main-fuzz.d.ts.map +1 -1
- package/packages/jam/node/main-fuzz.js +21 -4
- package/packages/jam/node/main-importer.d.ts +7 -4
- package/packages/jam/node/main-importer.d.ts.map +1 -1
- package/packages/jam/node/main-importer.js +10 -4
- package/packages/jam/safrole/bandersnatch-vrf.d.ts +24 -4
- package/packages/jam/safrole/bandersnatch-vrf.d.ts.map +1 -1
- package/packages/jam/safrole/bandersnatch-vrf.js +92 -40
- package/packages/jam/safrole/bandersnatch-vrf.test.js +12 -9
- package/packages/jam/safrole/bandersnatch-wasm.d.ts +10 -0
- package/packages/jam/safrole/bandersnatch-wasm.d.ts.map +1 -1
- package/packages/jam/safrole/bandersnatch-wasm.js +12 -0
- package/packages/jam/safrole/safrole.js +5 -5
- package/packages/jam/safrole/safrole.test.js +13 -13
- package/packages/jam/ticket-pool/index.d.ts +4 -0
- package/packages/jam/ticket-pool/index.d.ts.map +1 -0
- package/packages/jam/ticket-pool/index.js +3 -0
- package/packages/jam/ticket-pool/pending-ticket-pool.d.ts +30 -0
- package/packages/jam/ticket-pool/pending-ticket-pool.d.ts.map +1 -0
- package/packages/jam/ticket-pool/pending-ticket-pool.js +56 -0
- package/packages/jam/ticket-pool/pending-ticket-pool.test.d.ts +2 -0
- package/packages/jam/ticket-pool/pending-ticket-pool.test.d.ts.map +1 -0
- package/packages/jam/ticket-pool/pending-ticket-pool.test.js +67 -0
- package/packages/jam/ticket-pool/ticket-validator.d.ts +47 -0
- package/packages/jam/ticket-pool/ticket-validator.d.ts.map +1 -0
- package/packages/jam/ticket-pool/ticket-validator.js +34 -0
- package/packages/jam/ticket-pool/ticket-validator.test.d.ts +2 -0
- package/packages/jam/ticket-pool/ticket-validator.test.d.ts.map +1 -0
- package/packages/jam/ticket-pool/ticket-validator.test.js +35 -0
- package/packages/jam/ticket-pool/verified-ticket-pool.d.ts +26 -0
- package/packages/jam/ticket-pool/verified-ticket-pool.d.ts.map +1 -0
- package/packages/jam/ticket-pool/verified-ticket-pool.js +41 -0
- package/packages/jam/ticket-pool/verified-ticket-pool.test.d.ts +2 -0
- package/packages/jam/ticket-pool/verified-ticket-pool.test.d.ts.map +1 -0
- package/packages/jam/ticket-pool/verified-ticket-pool.test.js +54 -0
- package/packages/jam/transition/chain-stf.d.ts +2 -1
- package/packages/jam/transition/chain-stf.d.ts.map +1 -1
- package/packages/jam/transition/chain-stf.js +15 -3
- package/packages/workers/api-node/config.d.ts +14 -5
- package/packages/workers/api-node/config.d.ts.map +1 -1
- package/packages/workers/api-node/config.js +29 -20
- package/packages/workers/api-node/config.test.js +38 -1
- package/packages/workers/block-authorship/{generator.d.ts → block-generator.d.ts} +5 -5
- package/packages/workers/block-authorship/block-generator.d.ts.map +1 -0
- package/packages/workers/block-authorship/{generator.js → block-generator.js} +3 -3
- package/packages/workers/block-authorship/block-generator.test.d.ts +2 -0
- package/packages/workers/block-authorship/block-generator.test.d.ts.map +1 -0
- package/packages/workers/block-authorship/{generator.test.js → block-generator.test.js} +8 -8
- package/packages/workers/block-authorship/epoch-authoring-slots.d.ts +35 -0
- package/packages/workers/block-authorship/epoch-authoring-slots.d.ts.map +1 -0
- package/packages/workers/block-authorship/epoch-authoring-slots.js +86 -0
- package/packages/workers/block-authorship/epoch-tracker.d.ts +29 -0
- package/packages/workers/block-authorship/epoch-tracker.d.ts.map +1 -0
- package/packages/workers/block-authorship/epoch-tracker.js +80 -0
- package/packages/workers/block-authorship/index.d.ts.map +1 -1
- package/packages/workers/block-authorship/index.js +1 -1
- package/packages/workers/block-authorship/main.d.ts +3 -0
- package/packages/workers/block-authorship/main.d.ts.map +1 -1
- package/packages/workers/block-authorship/main.js +197 -315
- package/packages/workers/block-authorship/ticket-generator/bootstrap-main.d.ts +2 -0
- package/packages/workers/block-authorship/ticket-generator/bootstrap-main.d.ts.map +1 -0
- package/packages/workers/block-authorship/ticket-generator/bootstrap-main.js +23 -0
- package/packages/workers/block-authorship/ticket-generator/index.d.ts +16 -0
- package/packages/workers/block-authorship/ticket-generator/index.d.ts.map +1 -0
- package/packages/workers/block-authorship/ticket-generator/index.js +62 -0
- package/packages/workers/block-authorship/ticket-generator/protocol.d.ts +50 -0
- package/packages/workers/block-authorship/ticket-generator/protocol.d.ts.map +1 -0
- package/packages/workers/block-authorship/ticket-generator/protocol.js +54 -0
- package/packages/workers/block-authorship/{ticket-generator.d.ts → ticket-generator/ticket-generator.d.ts} +4 -0
- package/packages/workers/block-authorship/ticket-generator/ticket-generator.d.ts.map +1 -0
- package/packages/workers/block-authorship/{ticket-generator.js → ticket-generator/ticket-generator.js} +19 -9
- package/packages/workers/block-authorship/ticket-generator/ticket-generator.test.d.ts.map +1 -0
- package/packages/workers/block-authorship/{ticket-generator.test.js → ticket-generator/ticket-generator.test.js} +13 -9
- package/packages/workers/block-authorship/ticket-generator/worker-pool.d.ts +36 -0
- package/packages/workers/block-authorship/ticket-generator/worker-pool.d.ts.map +1 -0
- package/packages/workers/block-authorship/ticket-generator/worker-pool.js +111 -0
- package/packages/workers/block-authorship/ticket-validator.d.ts +31 -0
- package/packages/workers/block-authorship/ticket-validator.d.ts.map +1 -0
- package/packages/workers/block-authorship/ticket-validator.js +59 -0
- package/packages/workers/comms-authorship-network/protocol.d.ts +14 -4
- package/packages/workers/comms-authorship-network/protocol.d.ts.map +1 -1
- package/packages/workers/comms-authorship-network/protocol.js +12 -6
- package/packages/workers/comms-authorship-network/tickets-message.d.ts +0 -14
- package/packages/workers/comms-authorship-network/tickets-message.d.ts.map +1 -1
- package/packages/workers/comms-authorship-network/tickets-message.js +0 -17
- package/packages/workers/importer/importer.d.ts +5 -3
- package/packages/workers/importer/importer.d.ts.map +1 -1
- package/packages/workers/importer/importer.js +43 -35
- package/packages/workers/importer/stats.d.ts +36 -0
- package/packages/workers/importer/stats.d.ts.map +1 -0
- package/packages/workers/importer/stats.js +69 -0
- package/packages/workers/jam-network/main.d.ts.map +1 -1
- package/packages/workers/jam-network/main.js +25 -4
- package/packages/workers/block-authorship/generator.d.ts.map +0 -1
- package/packages/workers/block-authorship/generator.test.d.ts +0 -2
- package/packages/workers/block-authorship/generator.test.d.ts.map +0 -1
- package/packages/workers/block-authorship/ticket-generator.d.ts.map +0 -1
- package/packages/workers/block-authorship/ticket-generator.test.d.ts.map +0 -1
- /package/packages/configs/{typeberry-dev.json → typeberry-dev-tiny.json} +0 -0
- /package/packages/workers/block-authorship/{ticket-generator.test.d.ts → ticket-generator/ticket-generator.test.d.ts} +0 -0
|
@@ -1,22 +1,19 @@
|
|
|
1
1
|
import { setTimeout } from "node:timers/promises";
|
|
2
|
-
import {
|
|
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 {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
8
|
+
import { VerifiedTicketPool } from "#@typeberry/ticket-pool";
|
|
9
|
+
import { BlockGenerator } from "./block-generator.js";
|
|
10
|
+
import { EpochTracker } from "./epoch-tracker.js";
|
|
11
|
+
import { TicketGenerator } from "./ticket-generator/index.js";
|
|
12
|
+
import { BandersnatchTicketValidator } from "./ticket-validator.js";
|
|
19
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
|
+
*/
|
|
20
17
|
export async function main(config, comms, networkingComms) {
|
|
21
18
|
await initWasm();
|
|
22
19
|
logger.info `🎁 Block Authorship running`;
|
|
@@ -24,19 +21,19 @@ export async function main(config, comms, networkingComms) {
|
|
|
24
21
|
const db = config.openDatabase();
|
|
25
22
|
const blocks = db.getBlocksDb();
|
|
26
23
|
const states = db.getStatesDb();
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
+
};
|
|
33
31
|
const blake2bHasher = await Blake2b.createHasher();
|
|
34
32
|
const bandersnatch = await BandernsatchWasm.new();
|
|
35
33
|
const keccakHasher = await keccak.KeccakHasher.create();
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
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({
|
|
40
37
|
chainSpec,
|
|
41
38
|
bandersnatch,
|
|
42
39
|
keccakHasher,
|
|
@@ -44,313 +41,198 @@ export async function main(config, comms, networkingComms) {
|
|
|
44
41
|
blocks,
|
|
45
42
|
states,
|
|
46
43
|
});
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const ownTickets = new HashDictionary();
|
|
96
|
-
for (let attempt = 0; attempt < chainSpec.ticketsPerValidator; attempt++) {
|
|
97
|
-
const payload = getTicketSealPayload(entropy, attempt);
|
|
98
|
-
for (const key of keys) {
|
|
99
|
-
const result = await bandersnatchVrf.getVrfOutputHash(bandersnatch, key.bandersnatchSecret, payload);
|
|
100
|
-
if (result.isOk) {
|
|
101
|
-
ownTickets.set(result.ok.asOpaque(), { key, sealPayload: asOpaqueType(payload) });
|
|
102
|
-
}
|
|
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);
|
|
59
|
+
}
|
|
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;
|
|
103
92
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
93
|
+
epochData = epochDataResult.ok;
|
|
94
|
+
const epochIndex = epochData.epoch;
|
|
95
|
+
if (oldEpochData === null) {
|
|
96
|
+
logger.info `🎁 [E${epochIndex}#${newTimeSlot}] starting authorship (state at #${stateTimeSlot})`;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
logger.info `🎁 [E${oldEpochData.epoch}#${stateTimeSlot} -> E${epochIndex}#${newTimeSlot}] epoch transition`;
|
|
100
|
+
}
|
|
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,
|
|
107
|
+
});
|
|
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;
|
|
116
|
+
}
|
|
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
|
+
});
|
|
130
125
|
}
|
|
131
|
-
return {
|
|
132
|
-
key,
|
|
133
|
-
sealPayload: getFallbackSealPayload(entropy),
|
|
134
|
-
logId: `key ${key.bandersnatchPublic}`,
|
|
135
|
-
};
|
|
136
126
|
}
|
|
137
|
-
|
|
138
|
-
//
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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);
|
|
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);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
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,
|
|
145
|
+
// VerifiedTicket has the same `{ ticket, id }` shape the generator expects.
|
|
146
|
+
[...currentEpochTickets]);
|
|
147
|
+
logger.trace `${logPrefix} sending block`;
|
|
148
|
+
await comms.sendBlock(newBlock);
|
|
144
149
|
}
|
|
145
|
-
|
|
150
|
+
logger.trace `${logPrefix} awaiting next slot`;
|
|
151
|
+
await timeSlotHandler.waitForNextSlot(currentSlot !== null, epochPhase, ticketGeneratorDone);
|
|
146
152
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
153
|
+
logger.info `🎁 Block Authorship finished. Closing channel.`;
|
|
154
|
+
await db.close();
|
|
155
|
+
}
|
|
156
|
+
function getValidatorIndex(key, currentValidatorData) {
|
|
157
|
+
const index = currentValidatorData.findIndex((data) => data.bandersnatch.isEqualTo(key));
|
|
158
|
+
if (index < 0) {
|
|
159
|
+
return null;
|
|
151
160
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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));
|
|
172
189
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
182
|
-
return Result.ok(state.sealingKeySeries);
|
|
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;
|
|
183
198
|
}
|
|
184
|
-
// Ticket pool: epochIndex -> {ticket, id}[]
|
|
185
|
-
// IDs (entropyHash) are computed at receipt time via verifyTickets(), enabling O(1) dedup by ID.
|
|
186
|
-
const ticketPool = new Map();
|
|
187
|
-
const ticketIdSets = new Map();
|
|
188
199
|
/**
|
|
189
|
-
*
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
* tickets received from multiple peers or via both CE-131 and CE-132 paths.
|
|
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.
|
|
194
204
|
*/
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
200
|
-
const existing = ticketPool.get(epochIndex) ?? [];
|
|
201
|
-
let idSet = ticketIdSets.get(epochIndex) ?? null;
|
|
202
|
-
if (idSet === null) {
|
|
203
|
-
idSet = HashSet.new();
|
|
204
|
-
ticketIdSets.set(epochIndex, idSet);
|
|
205
|
-
}
|
|
206
|
-
for (const entry of verifiedTickets) {
|
|
207
|
-
if (!idSet.has(entry.id)) {
|
|
208
|
-
existing.push(entry);
|
|
209
|
-
idSet.insert(entry.id);
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
ticketPool.set(epochIndex, existing);
|
|
205
|
+
getCurrentTimeSlot(stateTimeSlot) {
|
|
206
|
+
return this.isFastForward === true
|
|
207
|
+
? tryAsTimeSlot(stateTimeSlot + 1)
|
|
208
|
+
: tryAsTimeSlot(Number(this.getVirtualTimeMs() / this.slotDurationMs));
|
|
213
209
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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));
|
|
224
228
|
}
|
|
225
229
|
/**
|
|
226
|
-
*
|
|
227
|
-
* ones to the pool with their computed IDs.
|
|
230
|
+
* We assume there is no gap between system time and the initial state time.
|
|
228
231
|
*
|
|
229
|
-
*
|
|
230
|
-
* Verification computes the ticket ID (entropyHash) which is then used for
|
|
231
|
-
* deduplication in the pool and later when building the extrinsic.
|
|
232
|
+
* I.e. we can resume any database by moving the state time to the future.
|
|
232
233
|
*/
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
logger.error `verifyTickets returned ${results.length} results for ${tickets.length} tickets`;
|
|
237
|
-
return false;
|
|
238
|
-
}
|
|
239
|
-
const verified = tickets
|
|
240
|
-
.map((ticket, i) => ({ ticket, id: results[i].entropyHash }))
|
|
241
|
-
.filter((_, i) => results[i].isValid);
|
|
242
|
-
addToPool(epochIndex, verified);
|
|
243
|
-
return verified.length > 0;
|
|
234
|
+
getVirtualTimeMs() {
|
|
235
|
+
const timeFromStart = systemTimeMs() - this.systemStartTimeMs;
|
|
236
|
+
return tryAsU64(this.stateStartTime + timeFromStart + this.slotDurationMs);
|
|
244
237
|
}
|
|
245
|
-
// Receive a single ticket from peers (via jam-network worker).
|
|
246
|
-
// Returns true if the ticket passed validation so jam-network can decide whether to redistribute it.
|
|
247
|
-
networkingComms.setOnReceivedTickets(async ({ epochIndex, ticket }) => {
|
|
248
|
-
logger.log `Received ticket from peer for epoch ${epochIndex}`;
|
|
249
|
-
const hash = blocks.getBestHeaderHash();
|
|
250
|
-
const state = states.getState(hash);
|
|
251
|
-
if (state === null) {
|
|
252
|
-
logger.warn `Cannot verify received ticket: no state available`;
|
|
253
|
-
return false;
|
|
254
|
-
}
|
|
255
|
-
return await verifyAndAddToPool(epochIndex, [ticket], state);
|
|
256
|
-
});
|
|
257
|
-
const isFastForward = config.workerParams.isFastForward;
|
|
258
|
-
let lastGeneratedSlot = startTimeSlot;
|
|
259
|
-
let ticketsGeneratedForEpoch = -1;
|
|
260
|
-
while (!isFinished) {
|
|
261
|
-
const hash = blocks.getBestHeaderHash();
|
|
262
|
-
const state = states.getState(hash);
|
|
263
|
-
const currentValidatorData = state?.currentValidatorData ?? null;
|
|
264
|
-
if (state === null) {
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
const lastTimeSlot = state.timeslot;
|
|
268
|
-
/**
|
|
269
|
-
* In fastForward mode, use simulated time (next slot after current state).
|
|
270
|
-
* In normal mode, use wall clock time.
|
|
271
|
-
* Assuming `slotDuration` is 6 sec it is safe till year 2786.
|
|
272
|
-
* If `slotDuration` is 1 sec then it is safe till 2106.
|
|
273
|
-
*/
|
|
274
|
-
const timeSlot = isFastForward === true
|
|
275
|
-
? tryAsTimeSlot(lastTimeSlot + 1)
|
|
276
|
-
: tryAsTimeSlot(Number(getTime() / 1000n / BigInt(chainSpec.slotDuration)));
|
|
277
|
-
// In fastForward mode, skip if we already generated for this slot (waiting for import)
|
|
278
|
-
if (isFastForward === true && timeSlot <= lastGeneratedSlot) {
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
const isNewEpoch = isEpochChanged(lastTimeSlot, timeSlot);
|
|
282
|
-
// Generate tickets if within contest period and not yet generated for this epoch
|
|
283
|
-
const epoch = tryAsEpoch(Math.floor(timeSlot / chainSpec.epochLength));
|
|
284
|
-
const slotInEpoch = timeSlot % chainSpec.epochLength;
|
|
285
|
-
const shouldGenerateTickets = slotInEpoch < chainSpec.contestLength && ticketsGeneratedForEpoch !== epoch;
|
|
286
|
-
if (shouldGenerateTickets) {
|
|
287
|
-
const designatedValidatorData = state.designatedValidatorData;
|
|
288
|
-
const ringKeys = designatedValidatorData.map((data) => data.bandersnatch);
|
|
289
|
-
const designatedKeySet = HashSet.from(ringKeys);
|
|
290
|
-
const validatorKeys = keys
|
|
291
|
-
.filter((k) => designatedKeySet.has(k.bandersnatchPublic))
|
|
292
|
-
.map((k) => ({ secret: k.bandersnatchSecret, public: k.bandersnatchPublic }));
|
|
293
|
-
if (validatorKeys.length > 0) {
|
|
294
|
-
// If state is from the previous epoch, entropy hasn't been shifted yet (index 1).
|
|
295
|
-
// After epoch change, it has been shifted to index 2.
|
|
296
|
-
const ticketEntropy = isNewEpoch ? state.entropy[1] : state.entropy[2];
|
|
297
|
-
logger.info `Epoch ${epoch}, slot ${slotInEpoch}/${chainSpec.contestLength}. Generating tickets for ${validatorKeys.length} validators...`;
|
|
298
|
-
const ticketsResult = await generateTickets(bandersnatch, ringKeys, validatorKeys, ticketEntropy, chainSpec.ticketsPerValidator);
|
|
299
|
-
if (ticketsResult.isError) {
|
|
300
|
-
logger.warn `Failed to generate tickets for epoch ${epoch}: ${ticketsResult.error}`;
|
|
301
|
-
}
|
|
302
|
-
else {
|
|
303
|
-
logger.log `Generated ${ticketsResult.ok.length} tickets for epoch ${epoch}. Distributing...`;
|
|
304
|
-
// Verify own tickets to get IDs, then add to pool
|
|
305
|
-
await verifyAndAddToPool(epoch, ticketsResult.ok, state);
|
|
306
|
-
// Send directly to network worker (bypasses main thread)
|
|
307
|
-
await networkingComms.sendTickets({ epochIndex: epoch, tickets: ticketsResult.ok });
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
ticketsGeneratedForEpoch = epoch;
|
|
311
|
-
}
|
|
312
|
-
const selingKeySeriesResult = await getSealingKeySeries(isNewEpoch, timeSlot, state);
|
|
313
|
-
if (selingKeySeriesResult.isError) {
|
|
314
|
-
continue;
|
|
315
|
-
}
|
|
316
|
-
// On a new epoch, `state.entropy[2]` is the epoch-E entropy (pre-transition);
|
|
317
|
-
// mid-epoch, it has already shifted to `entropy[3]`.
|
|
318
|
-
const entropy = isNewEpoch ? state.entropy[2] : state.entropy[3];
|
|
319
|
-
// Rebuild the authorship cache on each epoch boundary, and also catch the case
|
|
320
|
-
// where the startup prebuild was skipped (e.g. initialState was null or the
|
|
321
|
-
// initial sealing-key transition errored) so we don't silently miss Tickets-mode
|
|
322
|
-
// slots until the next epoch boundary.
|
|
323
|
-
const needsCacheRebuild = isNewEpoch ||
|
|
324
|
-
(selingKeySeriesResult.ok.kind === SafroleSealingKeysKind.Tickets && ticketAuthorshipCache === null);
|
|
325
|
-
if (needsCacheRebuild) {
|
|
326
|
-
if (isNewEpoch) {
|
|
327
|
-
logEpochBlockCreation(epoch, selingKeySeriesResult.ok);
|
|
328
|
-
}
|
|
329
|
-
await buildTicketAuthorshipCache(selingKeySeriesResult.ok, entropy);
|
|
330
|
-
}
|
|
331
|
-
const sealData = getSealData(selingKeySeriesResult.ok, keys, timeSlot, entropy);
|
|
332
|
-
if (sealData !== null && currentValidatorData !== null) {
|
|
333
|
-
const { key, sealPayload } = sealData;
|
|
334
|
-
const validatorIndex = getValidatorIndex(key, currentValidatorData);
|
|
335
|
-
if (validatorIndex === null) {
|
|
336
|
-
continue;
|
|
337
|
-
}
|
|
338
|
-
logger.log `Attempting to create a block using ${sealData.logId} located at validator index ${validatorIndex}.`;
|
|
339
|
-
const currentEpochTickets = ticketPool.get(epoch) ?? [];
|
|
340
|
-
const newBlock = await generator.nextBlockView(validatorIndex, key.bandersnatchSecret, sealPayload, timeSlot, currentEpochTickets);
|
|
341
|
-
counter += 1;
|
|
342
|
-
lastGeneratedSlot = timeSlot;
|
|
343
|
-
logger.trace `Sending block ${counter}`;
|
|
344
|
-
await comms.sendBlock(newBlock);
|
|
345
|
-
}
|
|
346
|
-
else if (isFastForward === true) {
|
|
347
|
-
// In fast-forward mode, if this slot is not ours, wait briefly for other validators to produce blocks
|
|
348
|
-
await setTimeout(10);
|
|
349
|
-
}
|
|
350
|
-
if (isFastForward === false) {
|
|
351
|
-
await setTimeout(chainSpec.slotDuration * 1000);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
logger.info `🎁 Block Authorship finished. Closing channel.`;
|
|
355
|
-
await db.close();
|
|
356
238
|
}
|
|
@@ -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"}
|