@typeberry/lib 0.5.11 → 0.6.0-40f6423
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/pvm-interpreter-ananas/index.js +1 -1
- 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/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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@typeberry/lib",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0-40f6423",
|
|
4
4
|
"description": "Typeberry Library",
|
|
5
5
|
"main": "./bin/lib/index.js",
|
|
6
6
|
"types": "./bin/lib/index.d.ts",
|
|
@@ -264,7 +264,7 @@
|
|
|
264
264
|
"#@typeberry/jam-network/*": "./packages/workers/jam-network/*"
|
|
265
265
|
},
|
|
266
266
|
"dependencies": {
|
|
267
|
-
"@fluffylabs/anan-as": "^1.
|
|
267
|
+
"@fluffylabs/anan-as": "^1.3.0",
|
|
268
268
|
"@noble/ed25519": "2.2.3",
|
|
269
269
|
"hash-wasm": "4.12.0",
|
|
270
270
|
"@typeberry/native": "0.2.0-74dd7d7",
|
|
@@ -109,7 +109,7 @@ export class AnanasInterpreter {
|
|
|
109
109
|
const programArr = lowerBytes(program);
|
|
110
110
|
const argsArr = lowerBytes(args);
|
|
111
111
|
this.gas.initialGas = gas;
|
|
112
|
-
this.instance.resetJAM(programArr, pc, BigInt(gas), argsArr, true);
|
|
112
|
+
this.instance.resetJAM(programArr, pc, BigInt(gas), argsArr, true, false);
|
|
113
113
|
}
|
|
114
114
|
resetGeneric(program, _pc, gas) {
|
|
115
115
|
const programArr = lowerBytes(program);
|
|
@@ -29,6 +29,13 @@ export declare class TicketDistributionTask {
|
|
|
29
29
|
* Deduplicates tickets based on signature.
|
|
30
30
|
*/
|
|
31
31
|
addTicket(epochIndex: Epoch, ticket: SignedTicket): void;
|
|
32
|
+
private onTicketReceivedCallback;
|
|
33
|
+
/**
|
|
34
|
+
* Register a callback that validates a received ticket.
|
|
35
|
+
* The ticket is only added to the redistribution pool if the callback returns `true`.
|
|
36
|
+
* This prevents redistribution of invalid tickets (e.g. those with a tampered `attempt` field).
|
|
37
|
+
*/
|
|
38
|
+
setOnTicketReceived(cb: (epochIndex: Epoch, ticket: SignedTicket) => Promise<boolean>): void;
|
|
32
39
|
private onTicketReceived;
|
|
33
40
|
}
|
|
34
41
|
//# sourceMappingURL=ticket-distribution.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ticket-distribution.d.ts","sourceRoot":"","sources":["../../../../../../packages/jam/jamnp-s/tasks/ticket-distribution.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAGnD,OAAO,KAAK,EAAW,WAAW,EAAE,MAAM,aAAa,CAAC;AAExD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAe1D;;;;;;GAMG;AACH,qBAAa,sBAAsB;IAuB/B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,WAAW;IAvB9B,MAAM,CAAC,KAAK,CAAC,aAAa,EAAE,aAAa,EAAE,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS;IAgBzF,yDAAyD;IACzD,OAAO,CAAC,cAAc,CAA0D;IAChF,+DAA+D;IAC/D,OAAO,CAAC,YAAY,CAAsB;IAE1C,OAAO;IAKP;;OAEG;IACH,oBAAoB;IAkDpB;;;;OAIG;IACH,SAAS,CAAC,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY;
|
|
1
|
+
{"version":3,"file":"ticket-distribution.d.ts","sourceRoot":"","sources":["../../../../../../packages/jam/jamnp-s/tasks/ticket-distribution.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAGnD,OAAO,KAAK,EAAW,WAAW,EAAE,MAAM,aAAa,CAAC;AAExD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAe1D;;;;;;GAMG;AACH,qBAAa,sBAAsB;IAuB/B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,WAAW;IAvB9B,MAAM,CAAC,KAAK,CAAC,aAAa,EAAE,aAAa,EAAE,WAAW,EAAE,WAAW,EAAE,SAAS,EAAE,SAAS;IAgBzF,yDAAyD;IACzD,OAAO,CAAC,cAAc,CAA0D;IAChF,+DAA+D;IAC/D,OAAO,CAAC,YAAY,CAAsB;IAE1C,OAAO;IAKP;;OAEG;IACH,oBAAoB;IAkDpB;;;;OAIG;IACH,SAAS,CAAC,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY;IAsCjD,OAAO,CAAC,wBAAwB,CAAgF;IAEhH;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,EAAE,CAAC,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,KAAK,OAAO,CAAC,OAAO,CAAC;IAIrF,OAAO,CAAC,gBAAgB;CAsBzB"}
|
|
@@ -82,8 +82,13 @@ export class TicketDistributionTask {
|
|
|
82
82
|
* Deduplicates tickets based on signature.
|
|
83
83
|
*/
|
|
84
84
|
addTicket(epochIndex, ticket) {
|
|
85
|
-
//
|
|
86
|
-
|
|
85
|
+
// Drop tickets for older epochs (can happen when a delayed validation callback completes
|
|
86
|
+
// after the epoch has already advanced — accepting it would roll back currentEpoch).
|
|
87
|
+
if (this.currentEpoch !== null && epochIndex < this.currentEpoch) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
// Epoch advanced — clear old tickets
|
|
91
|
+
if (this.currentEpoch !== null && epochIndex > this.currentEpoch) {
|
|
87
92
|
logger.log `[addTicket] Epoch changed from ${this.currentEpoch} to ${epochIndex}, clearing ${this.pendingTickets.length} old tickets`;
|
|
88
93
|
this.pendingTickets = [];
|
|
89
94
|
// Note: We don't need to clear aux data for all peers here.
|
|
@@ -107,9 +112,37 @@ export class TicketDistributionTask {
|
|
|
107
112
|
logger.info `[addTicket] Added ticket for epoch ${epochIndex}, total: ${this.pendingTickets.length}`;
|
|
108
113
|
}
|
|
109
114
|
}
|
|
115
|
+
onTicketReceivedCallback = null;
|
|
116
|
+
/**
|
|
117
|
+
* Register a callback that validates a received ticket.
|
|
118
|
+
* The ticket is only added to the redistribution pool if the callback returns `true`.
|
|
119
|
+
* This prevents redistribution of invalid tickets (e.g. those with a tampered `attempt` field).
|
|
120
|
+
*/
|
|
121
|
+
setOnTicketReceived(cb) {
|
|
122
|
+
this.onTicketReceivedCallback = cb;
|
|
123
|
+
}
|
|
110
124
|
onTicketReceived(epochIndex, ticket) {
|
|
111
125
|
logger.trace `Received ticket for epoch ${epochIndex}, attempt ${ticket.attempt}`;
|
|
112
|
-
|
|
113
|
-
|
|
126
|
+
if (this.onTicketReceivedCallback !== null) {
|
|
127
|
+
// Validate first; only redistribute if valid to avoid spreading tampered tickets.
|
|
128
|
+
// Wrap with Promise.resolve().then() to catch both sync throws and async rejections.
|
|
129
|
+
const cb = this.onTicketReceivedCallback;
|
|
130
|
+
Promise.resolve()
|
|
131
|
+
.then(() => cb(epochIndex, ticket))
|
|
132
|
+
.then((isValid) => {
|
|
133
|
+
if (isValid) {
|
|
134
|
+
this.addTicket(epochIndex, ticket);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
logger.warn `Dropping invalid ticket for epoch ${epochIndex} (validation failed)`;
|
|
138
|
+
}
|
|
139
|
+
})
|
|
140
|
+
.catch((error) => {
|
|
141
|
+
logger.error `Error validating ticket for epoch ${epochIndex}, attempt ${ticket.attempt}: ${error}`;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
this.addTicket(epochIndex, ticket);
|
|
146
|
+
}
|
|
114
147
|
}
|
|
115
148
|
}
|
|
@@ -208,7 +208,7 @@ describe("TicketDistributionTask", () => {
|
|
|
208
208
|
peer1.ticketTask.addTicket(TEST_EPOCH, ticket);
|
|
209
209
|
peer1.ticketTask.maintainDistribution();
|
|
210
210
|
await tick();
|
|
211
|
-
// Self receives the ticket (via onTicketReceived -> addTicket)
|
|
211
|
+
// Self receives the ticket (via onTicketReceived -> addTicket, no callback set)
|
|
212
212
|
assert.strictEqual(self.receivedTickets.length, 1);
|
|
213
213
|
// Self should re-distribute to peer2 (and peer1, but peer1 already has it)
|
|
214
214
|
self.ticketTask.maintainDistribution();
|
|
@@ -217,4 +217,43 @@ describe("TicketDistributionTask", () => {
|
|
|
217
217
|
assert.strictEqual(peer2.receivedTickets.length, 1);
|
|
218
218
|
assert.deepStrictEqual(peer2.receivedTickets[0].ticket, ticket);
|
|
219
219
|
});
|
|
220
|
+
it("should NOT redistribute ticket if validation callback returns false", async () => {
|
|
221
|
+
const self = await init("self");
|
|
222
|
+
const peer1 = await init("peer1");
|
|
223
|
+
const peer2 = await init("peer2");
|
|
224
|
+
self.openConnection(peer1);
|
|
225
|
+
self.openConnection(peer2);
|
|
226
|
+
await tick();
|
|
227
|
+
// Validation always rejects
|
|
228
|
+
self.ticketTask.setOnTicketReceived(async () => false);
|
|
229
|
+
const ticket = createTestTicket(0);
|
|
230
|
+
peer1.ticketTask.addTicket(TEST_EPOCH, ticket);
|
|
231
|
+
peer1.ticketTask.maintainDistribution();
|
|
232
|
+
await tick();
|
|
233
|
+
// self.addTicket was NOT called (callback returned false), so nothing to redistribute
|
|
234
|
+
assert.strictEqual(self.receivedTickets.length, 0);
|
|
235
|
+
self.ticketTask.maintainDistribution();
|
|
236
|
+
await tick();
|
|
237
|
+
assert.strictEqual(peer2.receivedTickets.length, 0);
|
|
238
|
+
});
|
|
239
|
+
it("should redistribute ticket if validation callback returns true", async () => {
|
|
240
|
+
const self = await init("self");
|
|
241
|
+
const peer1 = await init("peer1");
|
|
242
|
+
const peer2 = await init("peer2");
|
|
243
|
+
self.openConnection(peer1);
|
|
244
|
+
self.openConnection(peer2);
|
|
245
|
+
await tick();
|
|
246
|
+
// Validation always accepts
|
|
247
|
+
self.ticketTask.setOnTicketReceived(async () => true);
|
|
248
|
+
const ticket = createTestTicket(0);
|
|
249
|
+
peer1.ticketTask.addTicket(TEST_EPOCH, ticket);
|
|
250
|
+
peer1.ticketTask.maintainDistribution();
|
|
251
|
+
await tick();
|
|
252
|
+
// self.addTicket WAS called (callback returned true)
|
|
253
|
+
assert.strictEqual(self.receivedTickets.length, 1);
|
|
254
|
+
self.ticketTask.maintainDistribution();
|
|
255
|
+
await tick();
|
|
256
|
+
assert.strictEqual(peer2.receivedTickets.length, 1);
|
|
257
|
+
assert.deepStrictEqual(peer2.receivedTickets[0].ticket, ticket);
|
|
258
|
+
});
|
|
220
259
|
});
|
|
@@ -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"}
|