@typeberry/lib 0.6.0 → 0.6.1-3e97e3e
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 +2 -2
- package/packages/core/concurrent/parent.d.ts +3 -0
- package/packages/core/concurrent/parent.d.ts.map +1 -1
- package/packages/core/concurrent/parent.js +9 -6
- package/packages/core/pvm-interpreter-ananas/index.d.ts.map +1 -1
- package/packages/core/pvm-interpreter-ananas/index.js +4 -2
- package/packages/jam/jamnp-s/tasks/ticket-distribution.d.ts +7 -0
- package/packages/jam/jamnp-s/tasks/ticket-distribution.d.ts.map +1 -1
- package/packages/jam/jamnp-s/tasks/ticket-distribution.js +37 -4
- package/packages/jam/jamnp-s/tasks/ticket-distribution.test.js +40 -1
- package/packages/jam/node/main.d.ts.map +1 -1
- package/packages/jam/node/main.js +2 -1
- package/packages/workers/api-node/config.d.ts +9 -0
- package/packages/workers/api-node/config.d.ts.map +1 -1
- package/packages/workers/api-node/config.js +10 -0
- package/packages/workers/api-node/config.test.d.ts +2 -0
- package/packages/workers/api-node/config.test.d.ts.map +1 -0
- package/packages/workers/api-node/config.test.js +41 -0
- package/packages/workers/api-node/host-environment.d.ts +32 -0
- package/packages/workers/api-node/host-environment.d.ts.map +1 -0
- package/packages/workers/api-node/host-environment.js +106 -0
- package/packages/workers/api-node/index.d.ts +1 -0
- package/packages/workers/api-node/index.d.ts.map +1 -1
- package/packages/workers/api-node/index.js +1 -0
- package/packages/workers/api-node/protocol.d.ts.map +1 -1
- package/packages/workers/api-node/protocol.js +8 -4
- package/packages/workers/block-authorship/generator.d.ts +22 -3
- package/packages/workers/block-authorship/generator.d.ts.map +1 -1
- package/packages/workers/block-authorship/generator.js +39 -5
- package/packages/workers/block-authorship/generator.test.js +202 -1
- package/packages/workers/block-authorship/main.d.ts.map +1 -1
- package/packages/workers/block-authorship/main.js +175 -25
- package/packages/workers/comms-authorship-network/protocol.d.ts +13 -2
- package/packages/workers/comms-authorship-network/protocol.d.ts.map +1 -1
- package/packages/workers/comms-authorship-network/protocol.js +10 -3
- package/packages/workers/comms-authorship-network/tickets-message.d.ts +14 -0
- package/packages/workers/comms-authorship-network/tickets-message.d.ts.map +1 -1
- package/packages/workers/comms-authorship-network/tickets-message.js +17 -0
- package/packages/workers/importer/finality.js +4 -4
- package/packages/workers/importer/finality.test.js +186 -168
- package/packages/workers/jam-network/main.d.ts.map +1 -1
- package/packages/workers/jam-network/main.js +5 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Block, type TimeSlot, type ValidatorIndex } from "#@typeberry/block";
|
|
1
|
+
import { Block, type EntropyHash, type TimeSlot, type ValidatorIndex } from "#@typeberry/block";
|
|
2
2
|
import { type BlockView } from "#@typeberry/block/block.js";
|
|
3
|
+
import type { SignedTicket } from "#@typeberry/block/tickets.js";
|
|
3
4
|
import { BytesBlob } from "#@typeberry/bytes";
|
|
4
5
|
import type { ChainSpec } from "#@typeberry/config";
|
|
5
6
|
import { type BandersnatchSecretSeed } from "#@typeberry/crypto";
|
|
@@ -38,7 +39,10 @@ export declare class Generator {
|
|
|
38
39
|
static new(args: GeneratorArgs): Generator;
|
|
39
40
|
private constructor();
|
|
40
41
|
private getLastHeaderAndState;
|
|
41
|
-
nextBlockView(validatorIndex: ValidatorIndex, bandersnatchSecret: BandersnatchSecretSeed, sealPayload: BlockSealInput, timeSlot: TimeSlot
|
|
42
|
+
nextBlockView(validatorIndex: ValidatorIndex, bandersnatchSecret: BandersnatchSecretSeed, sealPayload: BlockSealInput, timeSlot: TimeSlot, pendingTickets?: {
|
|
43
|
+
ticket: SignedTicket;
|
|
44
|
+
id: EntropyHash;
|
|
45
|
+
}[]): Promise<BlockView>;
|
|
42
46
|
/**
|
|
43
47
|
* Returns y(H_S) part of the VRF signature.
|
|
44
48
|
*
|
|
@@ -50,6 +54,21 @@ export declare class Generator {
|
|
|
50
54
|
* data (i.e. the `aux_data`) so we are able to compute it beforehand.
|
|
51
55
|
*/
|
|
52
56
|
private getEntropyHash;
|
|
53
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Selects tickets to include in the extrinsic from the pending pool.
|
|
59
|
+
*
|
|
60
|
+
* Tickets were already verified at receipt time (IDs pre-computed). This method:
|
|
61
|
+
* 1. Filters out tickets whose IDs are already in `state.ticketsAccumulator` (already processed).
|
|
62
|
+
* 2. Sorts remaining tickets by ID ascending (required by Safrole).
|
|
63
|
+
* 3. Deduplicates by ID (pool dedup is best-effort; reorgs can produce duplicates).
|
|
64
|
+
* 4. Returns at most `chainSpec.maxTicketsPerExtrinsic` tickets.
|
|
65
|
+
*
|
|
66
|
+
* Called only during the contest period (slotInEpoch < contestLength).
|
|
67
|
+
*/
|
|
68
|
+
private prepareTicketsExtrinsic;
|
|
69
|
+
nextBlock(validatorIndex: ValidatorIndex, bandersnatchSecret: BandersnatchSecretSeed, sealPayload: BlockSealInput, timeSlot: TimeSlot, pendingTickets?: {
|
|
70
|
+
ticket: SignedTicket;
|
|
71
|
+
id: EntropyHash;
|
|
72
|
+
}[]): Promise<Block>;
|
|
54
73
|
}
|
|
55
74
|
//# sourceMappingURL=generator.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../../../../packages/workers/block-authorship/generator.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,
|
|
1
|
+
{"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../../../../packages/workers/block-authorship/generator.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,EACL,KAAK,WAAW,EAIhB,KAAK,QAAQ,EACb,KAAK,cAAc,EACpB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,KAAK,SAAS,EAAa,MAAM,2BAA2B,CAAC;AAEtE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EAAS,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAEpD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAoC,KAAK,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAClG,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC9D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAIvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,yCAAyC,CAAC;AAGhF,OAAO,EAAqB,KAAK,MAAM,EAAU,MAAM,kBAAkB,CAAC;AAM1E;;;;;;;;GAQG;AACH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;AAEvD,oDAAoD;AACpD,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,EAAE,SAAS,CAAC;IACrB,YAAY,EAAE,gBAAgB,CAAC;IAC/B,YAAY,EAAE,MAAM,CAAC,YAAY,CAAC;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,QAAQ,CAAC;IACjB,MAAM,EAAE,QAAQ,CAAC;CAClB,CAAC;AAEF,qBAAa,SAAS;IACpB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2C;IAEnE,SAAgB,SAAS,EAAE,SAAS,CAAC;IACrC,SAAgB,YAAY,EAAE,gBAAgB,CAAC;IAC/C,SAAgB,YAAY,EAAE,MAAM,CAAC,YAAY,CAAC;IAClD,SAAgB,OAAO,EAAE,OAAO,CAAC;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAW;IAClC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAW;IAElC,wDAAwD;IACxD,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,aAAa;IAI9B,OAAO;IAUP,OAAO,CAAC,qBAAqB;IAYvB,aAAa,CACjB,cAAc,EAAE,cAAc,EAC9B,kBAAkB,EAAE,sBAAsB,EAC1C,WAAW,EAAE,cAAc,EAC3B,QAAQ,EAAE,QAAQ,EAClB,cAAc,GAAE;QAAE,MAAM,EAAE,YAAY,CAAC;QAAC,EAAE,EAAE,WAAW,CAAA;KAAE,EAAO,GAC/D,OAAO,CAAC,SAAS,CAAC;IAKrB;;;;;;;;;OASG;YACW,cAAc;IAiB5B;;;;;;;;;;OAUG;IACH,OAAO,CAAC,uBAAuB;IA4BzB,SAAS,CACb,cAAc,EAAE,cAAc,EAC9B,kBAAkB,EAAE,sBAAsB,EAC1C,WAAW,EAAE,cAAc,EAC3B,QAAQ,EAAE,QAAQ,EAClB,cAAc,GAAE;QAAE,MAAM,EAAE,YAAY,CAAC;QAAC,EAAE,EAAE,WAAW,CAAA;KAAE,EAAO;CAqGnE"}
|
|
@@ -2,6 +2,7 @@ import { Block, encodeUnsealedHeader, Header, reencodeAsView, } from "#@typeberr
|
|
|
2
2
|
import { Extrinsic } from "#@typeberry/block/block.js";
|
|
3
3
|
import { DisputesExtrinsic } from "#@typeberry/block/disputes.js";
|
|
4
4
|
import { Bytes, BytesBlob } from "#@typeberry/bytes";
|
|
5
|
+
import { HashSet } from "#@typeberry/collections/hash-set.js";
|
|
5
6
|
import { BANDERSNATCH_VRF_SIGNATURE_BYTES } from "#@typeberry/crypto";
|
|
6
7
|
import { Logger } from "#@typeberry/logger";
|
|
7
8
|
import { Safrole } from "#@typeberry/safrole";
|
|
@@ -44,8 +45,8 @@ export class Generator {
|
|
|
44
45
|
lastState,
|
|
45
46
|
};
|
|
46
47
|
}
|
|
47
|
-
async nextBlockView(validatorIndex, bandersnatchSecret, sealPayload, timeSlot) {
|
|
48
|
-
const newBlock = await this.nextBlock(validatorIndex, bandersnatchSecret, sealPayload, timeSlot);
|
|
48
|
+
async nextBlockView(validatorIndex, bandersnatchSecret, sealPayload, timeSlot, pendingTickets = []) {
|
|
49
|
+
const newBlock = await this.nextBlock(validatorIndex, bandersnatchSecret, sealPayload, timeSlot, pendingTickets);
|
|
49
50
|
return reencodeAsView(Block.Codec, newBlock, this.chainSpec);
|
|
50
51
|
}
|
|
51
52
|
/**
|
|
@@ -65,7 +66,37 @@ export class Generator {
|
|
|
65
66
|
}
|
|
66
67
|
return entropyHashResult;
|
|
67
68
|
}
|
|
68
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Selects tickets to include in the extrinsic from the pending pool.
|
|
71
|
+
*
|
|
72
|
+
* Tickets were already verified at receipt time (IDs pre-computed). This method:
|
|
73
|
+
* 1. Filters out tickets whose IDs are already in `state.ticketsAccumulator` (already processed).
|
|
74
|
+
* 2. Sorts remaining tickets by ID ascending (required by Safrole).
|
|
75
|
+
* 3. Deduplicates by ID (pool dedup is best-effort; reorgs can produce duplicates).
|
|
76
|
+
* 4. Returns at most `chainSpec.maxTicketsPerExtrinsic` tickets.
|
|
77
|
+
*
|
|
78
|
+
* Called only during the contest period (slotInEpoch < contestLength).
|
|
79
|
+
*/
|
|
80
|
+
prepareTicketsExtrinsic(pendingTickets, state) {
|
|
81
|
+
if (pendingTickets.length === 0) {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
// Tickets are already verified at receipt time — just filter, sort and slice.
|
|
85
|
+
// Build a set of ticket IDs already in the state accumulator for fast lookup.
|
|
86
|
+
const accumulatedIds = HashSet.from(state.ticketsAccumulator.map((t) => t.id));
|
|
87
|
+
const filtered = pendingTickets.filter(({ id }) => !accumulatedIds.has(id));
|
|
88
|
+
// Sort by ID ascending
|
|
89
|
+
filtered.sort((a, b) => a.id.compare(b.id).value);
|
|
90
|
+
// Deduplicate by ID (pool dedup is best-effort; state may produce duplicates across reorgs)
|
|
91
|
+
const deduped = [];
|
|
92
|
+
for (const item of filtered) {
|
|
93
|
+
if (deduped.length === 0 || !deduped[deduped.length - 1].id.isEqualTo(item.id)) {
|
|
94
|
+
deduped.push(item);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return deduped.slice(0, this.chainSpec.maxTicketsPerExtrinsic).map(({ ticket }) => ticket);
|
|
98
|
+
}
|
|
99
|
+
async nextBlock(validatorIndex, bandersnatchSecret, sealPayload, timeSlot, pendingTickets = []) {
|
|
69
100
|
this.metrics.recordBlockAuthoringStarted(timeSlot);
|
|
70
101
|
const startTime = now();
|
|
71
102
|
// fetch latest data from the db.
|
|
@@ -85,9 +116,12 @@ export class Generator {
|
|
|
85
116
|
// retrieve data from previous block
|
|
86
117
|
const hasher = TransitionHasher.new(this.keccakHasher, this.blake2b);
|
|
87
118
|
const stateRoot = this.states.getStateRoot(lastState);
|
|
88
|
-
|
|
119
|
+
const slotInEpoch = timeSlot % this.chainSpec.epochLength;
|
|
120
|
+
const isContestPeriod = slotInEpoch < this.chainSpec.contestLength;
|
|
121
|
+
// Include tickets only during contest period
|
|
122
|
+
const ticketsForExtrinsic = isContestPeriod ? await this.prepareTicketsExtrinsic(pendingTickets, lastState) : [];
|
|
89
123
|
const extrinsic = Extrinsic.create({
|
|
90
|
-
tickets: asOpaqueType(
|
|
124
|
+
tickets: asOpaqueType(ticketsForExtrinsic),
|
|
91
125
|
preimages: [],
|
|
92
126
|
guarantees: asOpaqueType([]),
|
|
93
127
|
assurances: asOpaqueType([]),
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, it, mock } from "node:test";
|
|
2
2
|
import { Block, DisputesExtrinsic, EpochMarker, Extrinsic, Header, tryAsTimeSlot, tryAsValidatorIndex, ValidatorKeys, } from "#@typeberry/block";
|
|
3
|
+
import { SignedTicket, Ticket, tryAsTicketAttempt } from "#@typeberry/block/tickets.js";
|
|
3
4
|
import { Bytes, BytesBlob } from "#@typeberry/bytes";
|
|
4
5
|
import { asKnownSize, FixedSizeArray } from "#@typeberry/collections";
|
|
5
6
|
import { tinyChainSpec } from "#@typeberry/config";
|
|
6
|
-
import { BANDERSNATCH_KEY_BYTES, BANDERSNATCH_VRF_SIGNATURE_BYTES, BLS_KEY_BYTES, ED25519_KEY_BYTES, initWasm, } from "#@typeberry/crypto";
|
|
7
|
+
import { BANDERSNATCH_KEY_BYTES, BANDERSNATCH_PROOF_BYTES, BANDERSNATCH_VRF_SIGNATURE_BYTES, BLS_KEY_BYTES, ED25519_KEY_BYTES, initWasm, } from "#@typeberry/crypto";
|
|
7
8
|
import { BANDERSNATCH_RING_ROOT_BYTES } from "#@typeberry/crypto/bandersnatch.js";
|
|
8
9
|
import { Blake2b, HASH_SIZE, keccak } from "#@typeberry/hash";
|
|
9
10
|
import bandersnatchVrf from "#@typeberry/safrole/bandersnatch-vrf.js";
|
|
@@ -174,6 +175,206 @@ describe("Generator", () => {
|
|
|
174
175
|
});
|
|
175
176
|
deepEqual(block, expectedBlock);
|
|
176
177
|
});
|
|
178
|
+
it("should include sorted tickets during contest period", async () => {
|
|
179
|
+
// tinyChainSpec: contestLength = 10, so slot 1 is in contest period (1 < 10)
|
|
180
|
+
const state = createMockState(0);
|
|
181
|
+
const blocksDb = createMockBlocksDb(MOCK_PARENT_HASH);
|
|
182
|
+
const statesDb = createMockStatesDb(state);
|
|
183
|
+
const generator = Generator.new({
|
|
184
|
+
chainSpec: tinyChainSpec,
|
|
185
|
+
bandersnatch,
|
|
186
|
+
keccakHasher,
|
|
187
|
+
blake2b,
|
|
188
|
+
blocks: blocksDb,
|
|
189
|
+
states: statesDb,
|
|
190
|
+
});
|
|
191
|
+
// Create two tickets with different signatures
|
|
192
|
+
const sig1 = Bytes.zero(BANDERSNATCH_PROOF_BYTES);
|
|
193
|
+
sig1.raw[0] = 1;
|
|
194
|
+
const sig2 = Bytes.zero(BANDERSNATCH_PROOF_BYTES);
|
|
195
|
+
sig2.raw[0] = 2;
|
|
196
|
+
const ticket1 = SignedTicket.create({
|
|
197
|
+
attempt: tryAsTicketAttempt(0),
|
|
198
|
+
signature: sig1.asOpaque(),
|
|
199
|
+
});
|
|
200
|
+
const ticket2 = SignedTicket.create({
|
|
201
|
+
attempt: tryAsTicketAttempt(0),
|
|
202
|
+
signature: sig2.asOpaque(),
|
|
203
|
+
});
|
|
204
|
+
// ticket2 gets smaller ID (0x01...) and ticket1 gets larger ID (0x02...)
|
|
205
|
+
// so the sorted order should be [ticket2, ticket1]
|
|
206
|
+
const id1 = Bytes.fill(HASH_SIZE, 0x02).asOpaque();
|
|
207
|
+
const id2 = Bytes.fill(HASH_SIZE, 0x01).asOpaque();
|
|
208
|
+
const validatorIndex = tryAsValidatorIndex(0);
|
|
209
|
+
// Slot 1 is in contest period (1 < contestLength=10)
|
|
210
|
+
const timeSlot = tryAsTimeSlot(1);
|
|
211
|
+
// IDs are now pre-computed before passing to nextBlock
|
|
212
|
+
const block = await generator.nextBlock(validatorIndex, MOCK_BANDERSNATCH_SECRET, MOCK_SEAL_PAYLOAD, timeSlot, [
|
|
213
|
+
{ ticket: ticket1, id: id1 },
|
|
214
|
+
{ ticket: ticket2, id: id2 },
|
|
215
|
+
]);
|
|
216
|
+
// Tickets should be sorted by ID ascending: ticket2 (id=0x01) before ticket1 (id=0x02)
|
|
217
|
+
const tickets = block.extrinsic.tickets;
|
|
218
|
+
deepEqual(tickets.length, 2);
|
|
219
|
+
deepEqual(tickets[0].signature, sig2.asOpaque());
|
|
220
|
+
deepEqual(tickets[1].signature, sig1.asOpaque());
|
|
221
|
+
});
|
|
222
|
+
it("should exclude tickets outside contest period", async () => {
|
|
223
|
+
// tinyChainSpec: contestLength = 10, epochLength = 12
|
|
224
|
+
// Slot 10 is outside contest period (10 >= 10)
|
|
225
|
+
const state = createMockState(9);
|
|
226
|
+
const blocksDb = createMockBlocksDb(MOCK_PARENT_HASH);
|
|
227
|
+
const statesDb = createMockStatesDb(state);
|
|
228
|
+
const generator = Generator.new({
|
|
229
|
+
chainSpec: tinyChainSpec,
|
|
230
|
+
bandersnatch,
|
|
231
|
+
keccakHasher,
|
|
232
|
+
blake2b,
|
|
233
|
+
blocks: blocksDb,
|
|
234
|
+
states: statesDb,
|
|
235
|
+
});
|
|
236
|
+
const sig1 = Bytes.zero(BANDERSNATCH_PROOF_BYTES);
|
|
237
|
+
const ticket1 = SignedTicket.create({
|
|
238
|
+
attempt: tryAsTicketAttempt(0),
|
|
239
|
+
signature: sig1.asOpaque(),
|
|
240
|
+
});
|
|
241
|
+
const validatorIndex = tryAsValidatorIndex(0);
|
|
242
|
+
// Slot 10 is NOT in contest period (10 >= contestLength=10)
|
|
243
|
+
const timeSlot = tryAsTimeSlot(10);
|
|
244
|
+
const mockId = Bytes.fill(HASH_SIZE, 0x01).asOpaque();
|
|
245
|
+
const block = await generator.nextBlock(validatorIndex, MOCK_BANDERSNATCH_SECRET, MOCK_SEAL_PAYLOAD, timeSlot, [
|
|
246
|
+
{ ticket: ticket1, id: mockId },
|
|
247
|
+
]);
|
|
248
|
+
// No tickets should be included outside contest period
|
|
249
|
+
const tickets = block.extrinsic.tickets;
|
|
250
|
+
deepEqual(tickets.length, 0);
|
|
251
|
+
});
|
|
252
|
+
it("should filter out tickets already in ticketsAccumulator", async () => {
|
|
253
|
+
// Build a state that already has ticket with id=0x01 in its accumulator
|
|
254
|
+
const accumulatedId = Bytes.fill(HASH_SIZE, 0x01).asOpaque();
|
|
255
|
+
const accumulatedTicket = Ticket.create({
|
|
256
|
+
id: accumulatedId,
|
|
257
|
+
attempt: tryAsTicketAttempt(0),
|
|
258
|
+
});
|
|
259
|
+
const state = {
|
|
260
|
+
...createMockState(0),
|
|
261
|
+
ticketsAccumulator: asKnownSize([accumulatedTicket]),
|
|
262
|
+
};
|
|
263
|
+
const blocksDb = createMockBlocksDb(MOCK_PARENT_HASH);
|
|
264
|
+
const statesDb = createMockStatesDb(state);
|
|
265
|
+
const generator = Generator.new({
|
|
266
|
+
chainSpec: tinyChainSpec,
|
|
267
|
+
bandersnatch,
|
|
268
|
+
keccakHasher,
|
|
269
|
+
blake2b,
|
|
270
|
+
blocks: blocksDb,
|
|
271
|
+
states: statesDb,
|
|
272
|
+
});
|
|
273
|
+
const sig1 = Bytes.zero(BANDERSNATCH_PROOF_BYTES);
|
|
274
|
+
sig1.raw[0] = 1;
|
|
275
|
+
const sig2 = Bytes.zero(BANDERSNATCH_PROOF_BYTES);
|
|
276
|
+
sig2.raw[0] = 2;
|
|
277
|
+
const ticketAlreadyAccumulated = SignedTicket.create({
|
|
278
|
+
attempt: tryAsTicketAttempt(0),
|
|
279
|
+
signature: sig1.asOpaque(),
|
|
280
|
+
});
|
|
281
|
+
const ticketNew = SignedTicket.create({
|
|
282
|
+
attempt: tryAsTicketAttempt(0),
|
|
283
|
+
signature: sig2.asOpaque(),
|
|
284
|
+
});
|
|
285
|
+
// id=0x01 is already in accumulator, id=0x02 is new
|
|
286
|
+
const idAccumulated = Bytes.fill(HASH_SIZE, 0x01).asOpaque();
|
|
287
|
+
const idNew = Bytes.fill(HASH_SIZE, 0x02).asOpaque();
|
|
288
|
+
const validatorIndex = tryAsValidatorIndex(0);
|
|
289
|
+
const timeSlot = tryAsTimeSlot(1); // inside contest period
|
|
290
|
+
const block = await generator.nextBlock(validatorIndex, MOCK_BANDERSNATCH_SECRET, MOCK_SEAL_PAYLOAD, timeSlot, [
|
|
291
|
+
{ ticket: ticketAlreadyAccumulated, id: idAccumulated },
|
|
292
|
+
{ ticket: ticketNew, id: idNew },
|
|
293
|
+
]);
|
|
294
|
+
// Only the new ticket (not in accumulator) should be included
|
|
295
|
+
const tickets = block.extrinsic.tickets;
|
|
296
|
+
deepEqual(tickets.length, 1);
|
|
297
|
+
deepEqual(tickets[0].signature, sig2.asOpaque());
|
|
298
|
+
});
|
|
299
|
+
it("should deduplicate tickets by ID", async () => {
|
|
300
|
+
const state = createMockState(0);
|
|
301
|
+
const blocksDb = createMockBlocksDb(MOCK_PARENT_HASH);
|
|
302
|
+
const statesDb = createMockStatesDb(state);
|
|
303
|
+
const generator = Generator.new({
|
|
304
|
+
chainSpec: tinyChainSpec,
|
|
305
|
+
bandersnatch,
|
|
306
|
+
keccakHasher,
|
|
307
|
+
blake2b,
|
|
308
|
+
blocks: blocksDb,
|
|
309
|
+
states: statesDb,
|
|
310
|
+
});
|
|
311
|
+
const sig1 = Bytes.zero(BANDERSNATCH_PROOF_BYTES);
|
|
312
|
+
sig1.raw[0] = 1;
|
|
313
|
+
const sig2 = Bytes.zero(BANDERSNATCH_PROOF_BYTES);
|
|
314
|
+
sig2.raw[0] = 2;
|
|
315
|
+
// Two different SignedTicket objects but with the same ID (e.g. duplicate from reorg)
|
|
316
|
+
const ticketA = SignedTicket.create({
|
|
317
|
+
attempt: tryAsTicketAttempt(0),
|
|
318
|
+
signature: sig1.asOpaque(),
|
|
319
|
+
});
|
|
320
|
+
const ticketB = SignedTicket.create({
|
|
321
|
+
attempt: tryAsTicketAttempt(0),
|
|
322
|
+
signature: sig2.asOpaque(),
|
|
323
|
+
});
|
|
324
|
+
const duplicateId = Bytes.fill(HASH_SIZE, 0x05).asOpaque();
|
|
325
|
+
const validatorIndex = tryAsValidatorIndex(0);
|
|
326
|
+
const timeSlot = tryAsTimeSlot(1);
|
|
327
|
+
const block = await generator.nextBlock(validatorIndex, MOCK_BANDERSNATCH_SECRET, MOCK_SEAL_PAYLOAD, timeSlot, [
|
|
328
|
+
{ ticket: ticketA, id: duplicateId },
|
|
329
|
+
{ ticket: ticketB, id: duplicateId }, // same ID — should be deduplicated
|
|
330
|
+
]);
|
|
331
|
+
const tickets = block.extrinsic.tickets;
|
|
332
|
+
deepEqual(tickets.length, 1);
|
|
333
|
+
// First occurrence is kept after sort (both have same ID, ticketA comes first)
|
|
334
|
+
deepEqual(tickets[0].signature, sig1.asOpaque());
|
|
335
|
+
});
|
|
336
|
+
it("should include at most maxTicketsPerExtrinsic tickets", async () => {
|
|
337
|
+
// tinyChainSpec.maxTicketsPerExtrinsic = 3
|
|
338
|
+
const state = createMockState(0);
|
|
339
|
+
const blocksDb = createMockBlocksDb(MOCK_PARENT_HASH);
|
|
340
|
+
const statesDb = createMockStatesDb(state);
|
|
341
|
+
const generator = Generator.new({
|
|
342
|
+
chainSpec: tinyChainSpec,
|
|
343
|
+
bandersnatch,
|
|
344
|
+
keccakHasher,
|
|
345
|
+
blake2b,
|
|
346
|
+
blocks: blocksDb,
|
|
347
|
+
states: statesDb,
|
|
348
|
+
});
|
|
349
|
+
// Create 4 tickets — only 3 should be included (lowest IDs win)
|
|
350
|
+
const makeTicket = (sigByte, idByte) => {
|
|
351
|
+
const sig = Bytes.zero(BANDERSNATCH_PROOF_BYTES);
|
|
352
|
+
sig.raw[0] = sigByte;
|
|
353
|
+
return {
|
|
354
|
+
ticket: SignedTicket.create({ attempt: tryAsTicketAttempt(0), signature: sig.asOpaque() }),
|
|
355
|
+
id: Bytes.fill(HASH_SIZE, idByte).asOpaque(),
|
|
356
|
+
sig: sig.asOpaque(),
|
|
357
|
+
};
|
|
358
|
+
};
|
|
359
|
+
const t1 = makeTicket(1, 0x01); // lowest ID
|
|
360
|
+
const t2 = makeTicket(2, 0x02);
|
|
361
|
+
const t3 = makeTicket(3, 0x03);
|
|
362
|
+
const t4 = makeTicket(4, 0x04); // highest ID — should be excluded
|
|
363
|
+
const validatorIndex = tryAsValidatorIndex(0);
|
|
364
|
+
const timeSlot = tryAsTimeSlot(1);
|
|
365
|
+
const block = await generator.nextBlock(validatorIndex, MOCK_BANDERSNATCH_SECRET, MOCK_SEAL_PAYLOAD, timeSlot, [
|
|
366
|
+
{ ticket: t4.ticket, id: t4.id }, // pass out-of-order to verify sorting
|
|
367
|
+
{ ticket: t2.ticket, id: t2.id },
|
|
368
|
+
{ ticket: t3.ticket, id: t3.id },
|
|
369
|
+
{ ticket: t1.ticket, id: t1.id },
|
|
370
|
+
]);
|
|
371
|
+
const tickets = block.extrinsic.tickets;
|
|
372
|
+
deepEqual(tickets.length, 3); // maxTicketsPerExtrinsic = 3
|
|
373
|
+
// Should include the 3 lowest IDs, sorted ascending
|
|
374
|
+
deepEqual(tickets[0].signature, t1.sig);
|
|
375
|
+
deepEqual(tickets[1].signature, t2.sig);
|
|
376
|
+
deepEqual(tickets[2].signature, t3.sig);
|
|
377
|
+
});
|
|
177
378
|
it("should create block with epoch marker at epoch boundary", async () => {
|
|
178
379
|
// tinyChainSpec.epochLength = 12, so:
|
|
179
380
|
// - timeslot 11 is last slot of epoch 0
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../../../../packages/workers/block-authorship/main.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../../../../packages/workers/block-authorship/main.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qCAAqC,CAAC;AAiB3E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAE3D,OAAO,KAAK,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAK9E,KAAK,MAAM,GAAG,YAAY,CAAC,qBAAqB,CAAC,CAAC;AAwBlD,wBAAsB,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,EAAE,eAAe,EAAE,eAAe,iBAiapG"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { setTimeout } from "node:timers/promises";
|
|
2
2
|
import { tryAsEpoch, tryAsTimeSlot, tryAsValidatorIndex, } from "#@typeberry/block";
|
|
3
3
|
import { BytesBlob } from "#@typeberry/bytes";
|
|
4
|
+
import { HashDictionary } from "#@typeberry/collections/hash-dictionary.js";
|
|
4
5
|
import { HashSet } from "#@typeberry/collections/hash-set.js";
|
|
5
6
|
import { initWasm } from "#@typeberry/crypto";
|
|
6
7
|
import { deriveBandersnatchPublicKey, deriveEd25519PublicKey, } from "#@typeberry/crypto/key-derivation.js";
|
|
@@ -8,10 +9,11 @@ import { Blake2b, keccak } from "#@typeberry/hash";
|
|
|
8
9
|
import { Logger } from "#@typeberry/logger";
|
|
9
10
|
import { tryAsU64 } from "#@typeberry/numbers";
|
|
10
11
|
import { Safrole } from "#@typeberry/safrole";
|
|
12
|
+
import bandersnatchVrf from "#@typeberry/safrole/bandersnatch-vrf.js";
|
|
11
13
|
import { BandernsatchWasm } from "#@typeberry/safrole/bandersnatch-wasm.js";
|
|
12
14
|
import { JAM_FALLBACK_SEAL, JAM_TICKET_SEAL } from "#@typeberry/safrole/constants.js";
|
|
13
15
|
import { SafroleSealingKeysKind } from "#@typeberry/state";
|
|
14
|
-
import { asOpaqueType,
|
|
16
|
+
import { asOpaqueType, Result } from "#@typeberry/utils";
|
|
15
17
|
import { Generator } from "./generator.js";
|
|
16
18
|
import { generateTickets } from "./ticket-generator.js";
|
|
17
19
|
const logger = Logger.new(import.meta.filename, "author");
|
|
@@ -51,10 +53,22 @@ export async function main(config, comms, networkingComms) {
|
|
|
51
53
|
const initialHash = blocks.getBestHeaderHash();
|
|
52
54
|
const initialState = states.getState(initialHash);
|
|
53
55
|
logger.info `Block authorship validator keys: ${keys.map(({ bandersnatchPublic }, index) => `\n ${index}: ${bandersnatchPublic.toString()}`)}`;
|
|
56
|
+
// Per-epoch cache for Tickets mode: index corresponds to position in sealingKeySeries.tickets.
|
|
57
|
+
// null entry means none of our keys match that slot.
|
|
58
|
+
// Rebuilt once per epoch via buildTicketAuthorshipCache().
|
|
59
|
+
// Declared here (before the eager startup build below) so its TDZ doesn't fire
|
|
60
|
+
// when `buildTicketAuthorshipCache` runs during initialisation.
|
|
61
|
+
let ticketAuthorshipCache = null;
|
|
54
62
|
if (initialState !== null) {
|
|
55
|
-
const
|
|
63
|
+
const isEpochStart = startTimeSlot % chainSpec.epochLength === 0;
|
|
64
|
+
const initialKeys = await getSealingKeySeries(isEpochStart, startTimeSlot, initialState);
|
|
56
65
|
if (initialKeys.isOk) {
|
|
57
66
|
logEpochBlockCreation(tryAsEpoch(Math.floor(startTimeSlot / chainSpec.epochLength)), initialKeys.ok);
|
|
67
|
+
// Build the cache eagerly so the first slot of a session doesn't need an
|
|
68
|
+
// on-the-fly VRF scan. After this, `buildTicketAuthorshipCache` is only
|
|
69
|
+
// re-run on epoch boundaries.
|
|
70
|
+
const initialEntropy = isEpochStart ? initialState.entropy[2] : initialState.entropy[3];
|
|
71
|
+
await buildTicketAuthorshipCache(initialKeys.ok, initialEntropy);
|
|
58
72
|
}
|
|
59
73
|
}
|
|
60
74
|
function getTime() {
|
|
@@ -63,14 +77,6 @@ export async function main(config, comms, networkingComms) {
|
|
|
63
77
|
const slotDurationMs = BigInt(chainSpec.slotDuration * 1000);
|
|
64
78
|
return tryAsU64(BigInt(startTimeSlot) * slotDurationMs + timeFromStart + slotDurationMs);
|
|
65
79
|
}
|
|
66
|
-
function getKeyForCurrentSlot(sealingKeySeries, keys, timeSlot) {
|
|
67
|
-
if (sealingKeySeries.kind === SafroleSealingKeysKind.Keys) {
|
|
68
|
-
const indexForCurrentSlot = timeSlot % sealingKeySeries.keys.length;
|
|
69
|
-
const sealingKey = sealingKeySeries.keys[indexForCurrentSlot];
|
|
70
|
-
return keys.find((x) => x.bandersnatchPublic.isEqualTo(sealingKey)) ?? null;
|
|
71
|
-
}
|
|
72
|
-
throw new Error("Tickets mode is not supported yet");
|
|
73
|
-
}
|
|
74
80
|
function getValidatorIndex(key, currentValidatorData) {
|
|
75
81
|
const index = currentValidatorData.findIndex((data) => data.bandersnatch.isEqualTo(key.bandersnatchPublic));
|
|
76
82
|
if (index < 0) {
|
|
@@ -78,14 +84,65 @@ export async function main(config, comms, networkingComms) {
|
|
|
78
84
|
}
|
|
79
85
|
return tryAsValidatorIndex(index);
|
|
80
86
|
}
|
|
81
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Precomputes which slots we are the author of for the current epoch (Tickets mode).
|
|
89
|
+
*/
|
|
90
|
+
async function buildTicketAuthorshipCache(sealingKeySeries, entropy) {
|
|
91
|
+
if (sealingKeySeries.kind !== SafroleSealingKeysKind.Tickets) {
|
|
92
|
+
ticketAuthorshipCache = null;
|
|
93
|
+
return;
|
|
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
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const cache = sealingKeySeries.tickets.map((ticket) => ownTickets.get(ticket.id.asOpaque()) ?? null);
|
|
106
|
+
ticketAuthorshipCache = cache;
|
|
107
|
+
const ours = cache.filter(Boolean).length;
|
|
108
|
+
logger.info `Built ticket authorship cache: ${ours}/${cache.length} slots assigned to us this epoch.`;
|
|
109
|
+
}
|
|
110
|
+
function getTicketSealPayload(entropy, attempt) {
|
|
111
|
+
return BytesBlob.blobFromParts(JAM_TICKET_SEAL, entropy.raw, new Uint8Array([attempt]));
|
|
112
|
+
}
|
|
113
|
+
function getFallbackSealPayload(entropy) {
|
|
114
|
+
return asOpaqueType(BytesBlob.blobFromParts(JAM_FALLBACK_SEAL, entropy.raw));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Returns the validator key and seal payload for the current slot, or null if we are not the author.
|
|
118
|
+
*
|
|
119
|
+
* Keys mode (fallback): matches our key against the slot's assigned bandersnatch key.
|
|
120
|
+
* Tickets mode: O(1) lookup against the per-epoch authorship cache (built eagerly at
|
|
121
|
+
* startup and on every epoch transition, so we never fall back to on-the-fly VRF).
|
|
122
|
+
*/
|
|
123
|
+
function getSealData(sealingKeySeries, keys, timeSlot, entropy) {
|
|
82
124
|
if (sealingKeySeries.kind === SafroleSealingKeysKind.Keys) {
|
|
83
|
-
|
|
125
|
+
const indexForCurrentSlot = timeSlot % sealingKeySeries.keys.length;
|
|
126
|
+
const sealingKey = sealingKeySeries.keys[indexForCurrentSlot];
|
|
127
|
+
const key = keys.find((x) => x.bandersnatchPublic.isEqualTo(sealingKey)) ?? null;
|
|
128
|
+
if (key === null) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
key,
|
|
133
|
+
sealPayload: getFallbackSealPayload(entropy),
|
|
134
|
+
logId: `key ${key.bandersnatchPublic}`,
|
|
135
|
+
};
|
|
84
136
|
}
|
|
85
|
-
|
|
86
|
-
|
|
137
|
+
// Tickets mode: each slot is sealed by the validator who can produce the VRF output
|
|
138
|
+
// matching the ticket's ID for that slot.
|
|
139
|
+
const index = timeSlot % sealingKeySeries.tickets.length;
|
|
140
|
+
const ticket = sealingKeySeries.tickets.at(index) ?? null;
|
|
141
|
+
const cached = ticketAuthorshipCache?.at(index) ?? null;
|
|
142
|
+
if (ticket === null || cached === null) {
|
|
143
|
+
return null;
|
|
87
144
|
}
|
|
88
|
-
|
|
145
|
+
return { ...cached, logId: `ticket ${ticket.id} (attempt ${ticket.attempt})` };
|
|
89
146
|
}
|
|
90
147
|
function isEpochChanged(lastTimeslot, currentTimeslot) {
|
|
91
148
|
const lastEpoch = Math.floor(lastTimeslot / chainSpec.epochLength);
|
|
@@ -93,11 +150,17 @@ export async function main(config, comms, networkingComms) {
|
|
|
93
150
|
return currentEpoch > lastEpoch;
|
|
94
151
|
}
|
|
95
152
|
function logEpochBlockCreation(epoch, sealingKeySeries) {
|
|
153
|
+
if (sealingKeySeries.kind === SafroleSealingKeysKind.Tickets) {
|
|
154
|
+
logger.info `[EPOCH ${epoch}] Tickets mode active with ${sealingKeySeries.tickets.length} tickets.`;
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
96
157
|
let isCreating = false;
|
|
97
158
|
const epochStart = epoch * chainSpec.epochLength;
|
|
98
159
|
const epochEnd = epochStart + chainSpec.epochLength;
|
|
99
160
|
for (let slot = epochStart; slot < epochEnd; slot++) {
|
|
100
|
-
const
|
|
161
|
+
const indexForCurrentSlot = slot % sealingKeySeries.keys.length;
|
|
162
|
+
const sealingKey = sealingKeySeries.keys[indexForCurrentSlot];
|
|
163
|
+
const key = keys.find((x) => x.bandersnatchPublic.isEqualTo(sealingKey)) ?? null;
|
|
101
164
|
if (key !== null) {
|
|
102
165
|
isCreating = true;
|
|
103
166
|
logger.info `[EPOCH ${epoch}] Validator ${key.bandersnatchPublic.toString()} will author block at slot ${slot}`;
|
|
@@ -118,13 +181,86 @@ export async function main(config, comms, networkingComms) {
|
|
|
118
181
|
}
|
|
119
182
|
return Result.ok(state.sealingKeySeries);
|
|
120
183
|
}
|
|
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
|
+
/**
|
|
189
|
+
* Adds pre-verified tickets to the in-memory ticket pool for the given epoch.
|
|
190
|
+
*
|
|
191
|
+
* Clears the pool when the epoch changes (we only ever need tickets for one epoch at a time).
|
|
192
|
+
* Deduplicates by ticket ID using a HashSet for O(1) lookup — prevents double-counting
|
|
193
|
+
* tickets received from multiple peers or via both CE-131 and CE-132 paths.
|
|
194
|
+
*/
|
|
195
|
+
function addToPool(epochIndex, verifiedTickets) {
|
|
196
|
+
if (ticketPool.size > 0 && !ticketPool.has(epochIndex)) {
|
|
197
|
+
ticketPool.clear();
|
|
198
|
+
ticketIdSets.clear();
|
|
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);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Returns the correct tickets entropy for verification given the current state.
|
|
216
|
+
*
|
|
217
|
+
* When `state` is from epoch E-1 (i.e. we haven't produced epoch E's first block yet),
|
|
218
|
+
* the ticket entropy for epoch E is at index 1 (not yet shifted).
|
|
219
|
+
* After the epoch transition it moves to index 2.
|
|
220
|
+
*/
|
|
221
|
+
function getTicketEntropy(epochIndex, state) {
|
|
222
|
+
const stateEpoch = Math.floor(state.timeslot / chainSpec.epochLength);
|
|
223
|
+
return epochIndex > stateEpoch ? state.entropy[1] : state.entropy[2];
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Verifies tickets against the ring commitment and current epoch entropy, then adds valid
|
|
227
|
+
* ones to the pool with their computed IDs.
|
|
228
|
+
*
|
|
229
|
+
* Called both for own generated tickets and for tickets relayed from peers.
|
|
230
|
+
* Verification computes the ticket ID (entropyHash) which is then used for
|
|
231
|
+
* deduplication in the pool and later when building the extrinsic.
|
|
232
|
+
*/
|
|
233
|
+
async function verifyAndAddToPool(epochIndex, tickets, state) {
|
|
234
|
+
const results = await bandersnatchVrf.verifyTickets(bandersnatch, state.designatedValidatorData.length, state.epochRoot, tickets, getTicketEntropy(epochIndex, state));
|
|
235
|
+
if (results.length !== tickets.length) {
|
|
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;
|
|
244
|
+
}
|
|
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
|
+
});
|
|
121
257
|
const isFastForward = config.workerParams.isFastForward;
|
|
122
258
|
let lastGeneratedSlot = startTimeSlot;
|
|
123
259
|
let ticketsGeneratedForEpoch = -1;
|
|
124
260
|
while (!isFinished) {
|
|
125
261
|
const hash = blocks.getBestHeaderHash();
|
|
126
262
|
const state = states.getState(hash);
|
|
127
|
-
const currentValidatorData = state?.currentValidatorData;
|
|
263
|
+
const currentValidatorData = state?.currentValidatorData ?? null;
|
|
128
264
|
if (state === null) {
|
|
129
265
|
continue;
|
|
130
266
|
}
|
|
@@ -165,6 +301,8 @@ export async function main(config, comms, networkingComms) {
|
|
|
165
301
|
}
|
|
166
302
|
else {
|
|
167
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);
|
|
168
306
|
// Send directly to network worker (bypasses main thread)
|
|
169
307
|
await networkingComms.sendTickets({ epochIndex: epoch, tickets: ticketsResult.ok });
|
|
170
308
|
}
|
|
@@ -175,19 +313,31 @@ export async function main(config, comms, networkingComms) {
|
|
|
175
313
|
if (selingKeySeriesResult.isError) {
|
|
176
314
|
continue;
|
|
177
315
|
}
|
|
178
|
-
|
|
179
|
-
|
|
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);
|
|
180
330
|
}
|
|
181
|
-
const
|
|
182
|
-
if (
|
|
331
|
+
const sealData = getSealData(selingKeySeriesResult.ok, keys, timeSlot, entropy);
|
|
332
|
+
if (sealData !== null && currentValidatorData !== null) {
|
|
333
|
+
const { key, sealPayload } = sealData;
|
|
183
334
|
const validatorIndex = getValidatorIndex(key, currentValidatorData);
|
|
184
335
|
if (validatorIndex === null) {
|
|
185
336
|
continue;
|
|
186
337
|
}
|
|
187
|
-
logger.log `Attempting to create a block using
|
|
188
|
-
const
|
|
189
|
-
const
|
|
190
|
-
const newBlock = await generator.nextBlockView(validatorIndex, key.bandersnatchSecret, sealPayload, timeSlot);
|
|
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);
|
|
191
341
|
counter += 1;
|
|
192
342
|
lastGeneratedSlot = timeSlot;
|
|
193
343
|
logger.trace `Sending block ${counter}`;
|