@typeberry/lib 0.8.4-7a69737 → 0.8.4-7d1719f
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 +5 -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/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 +16 -1
- package/packages/jam/node/main-importer.d.ts +6 -3
- package/packages/jam/node/main-importer.d.ts.map +1 -1
- package/packages/jam/node/main-importer.js +3 -2
- 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 +46 -0
- package/packages/jam/ticket-pool/ticket-validator.d.ts.map +1 -0
- package/packages/jam/ticket-pool/ticket-validator.js +29 -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 +34 -0
- package/packages/jam/ticket-pool/verified-ticket-pool.d.ts +24 -0
- package/packages/jam/ticket-pool/verified-ticket-pool.d.ts.map +1 -0
- package/packages/jam/ticket-pool/verified-ticket-pool.js +37 -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/workers/api-node/config.d.ts +12 -5
- package/packages/workers/api-node/config.d.ts.map +1 -1
- package/packages/workers/api-node/config.js +20 -17
- package/packages/workers/api-node/config.test.js +38 -1
- package/packages/workers/block-authorship/main.d.ts.map +1 -1
- package/packages/workers/block-authorship/main.js +22 -74
- package/packages/workers/block-authorship/ticket-validator.d.ts +32 -0
- package/packages/workers/block-authorship/ticket-validator.d.ts.map +1 -0
- package/packages/workers/block-authorship/ticket-validator.js +56 -0
- package/packages/workers/comms-authorship-network/protocol.d.ts +10 -0
- package/packages/workers/comms-authorship-network/protocol.d.ts.map +1 -1
- package/packages/workers/comms-authorship-network/protocol.js +8 -1
- package/packages/workers/jam-network/main.d.ts.map +1 -1
- package/packages/workers/jam-network/main.js +20 -4
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Logger } from "#@typeberry/logger";
|
|
2
|
+
import { DenyTicketsValidator, PendingTicketPool } from "#@typeberry/ticket-pool";
|
|
2
3
|
import { OK } from "#@typeberry/utils";
|
|
3
4
|
import { ce131 } from "../protocol/index.js";
|
|
4
5
|
const logger = Logger.new(import.meta.filename, "net:tickets");
|
|
@@ -12,6 +13,11 @@ const TICKET_AUX = {
|
|
|
12
13
|
* Uses CE-132 (proxy-to-all) for direct broadcast to all peers.
|
|
13
14
|
* Implements a maintain pattern similar to SyncTask: tickets are collected
|
|
14
15
|
* and periodically distributed to peers that haven't received them yet.
|
|
16
|
+
*
|
|
17
|
+
* Incoming tickets from peers are first run through a {@link TicketValidator};
|
|
18
|
+
* only validated tickets are added to the redistribution pool. The default
|
|
19
|
+
* validator denies everything, so callers must wire a real one via
|
|
20
|
+
* {@link setTicketValidator} before any networked ticket can be redistributed.
|
|
15
21
|
*/
|
|
16
22
|
export class TicketDistributionTask {
|
|
17
23
|
streamManager;
|
|
@@ -26,10 +32,8 @@ export class TicketDistributionTask {
|
|
|
26
32
|
streamManager.registerOutgoingHandlers(ce131.ClientHandler.new(chainSpec, ce131.STREAM_KIND_PROXY_TO_ALL));
|
|
27
33
|
return task;
|
|
28
34
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
/** Current epoch being tracked (cleared when epoch changes) */
|
|
32
|
-
currentEpoch = null;
|
|
35
|
+
pool = new PendingTicketPool();
|
|
36
|
+
validator = new DenyTicketsValidator();
|
|
33
37
|
constructor(streamManager, connections) {
|
|
34
38
|
this.streamManager = streamManager;
|
|
35
39
|
this.connections = connections;
|
|
@@ -38,14 +42,13 @@ export class TicketDistributionTask {
|
|
|
38
42
|
* Should be called periodically to distribute pending tickets to connected peers.
|
|
39
43
|
*/
|
|
40
44
|
maintainDistribution() {
|
|
41
|
-
|
|
42
|
-
|
|
45
|
+
const currentEpoch = this.pool.currentEpoch;
|
|
46
|
+
if (currentEpoch === null) {
|
|
47
|
+
return;
|
|
43
48
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
for (let ticketIdx = 0; ticketIdx < this.pendingTickets.length; ticketIdx++) {
|
|
48
|
-
const { epochIndex, ticket } = this.pendingTickets[ticketIdx];
|
|
49
|
+
const tickets = this.pool.getTickets();
|
|
50
|
+
for (let ticketIdx = 0; ticketIdx < tickets.length; ticketIdx++) {
|
|
51
|
+
const { epochIndex, ticket } = tickets[ticketIdx];
|
|
49
52
|
// Try to send to each connected peer
|
|
50
53
|
for (const peerInfo of this.connections.getConnectedPeers()) {
|
|
51
54
|
this.connections.withAuxData(peerInfo.peerId, TICKET_AUX, (maybeAux) => {
|
|
@@ -77,72 +80,45 @@ export class TicketDistributionTask {
|
|
|
77
80
|
}
|
|
78
81
|
}
|
|
79
82
|
/**
|
|
80
|
-
* Add a ticket to the
|
|
83
|
+
* Add a ticket to the redistribution pool.
|
|
81
84
|
* Clears pending tickets when epoch changes.
|
|
82
85
|
* Deduplicates tickets based on signature.
|
|
83
86
|
*/
|
|
84
87
|
addTicket(epochIndex, ticket) {
|
|
85
|
-
|
|
86
|
-
// after the epoch has already advanced — accepting it would roll back currentEpoch).
|
|
87
|
-
if (this.currentEpoch !== null && epochIndex < this.currentEpoch) {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
// Epoch advanced — clear old tickets
|
|
91
|
-
if (this.currentEpoch !== null && epochIndex > this.currentEpoch) {
|
|
92
|
-
logger.log `[addTicket] Epoch changed from ${this.currentEpoch} to ${epochIndex}, clearing ${this.pendingTickets.length} old tickets`;
|
|
93
|
-
this.pendingTickets = [];
|
|
94
|
-
// Note: We don't need to clear aux data for all peers here.
|
|
95
|
-
// The aux data contains the epoch, so maintainDistribution will lazily
|
|
96
|
-
// reset it when it detects an epoch mismatch. This handles both connected
|
|
97
|
-
// and disconnected peers correctly.
|
|
98
|
-
}
|
|
99
|
-
this.currentEpoch = epochIndex;
|
|
100
|
-
/**
|
|
101
|
-
* Deduplicate: check if a ticket with the same signature already exists
|
|
102
|
-
*
|
|
103
|
-
* Here we are risking "poisoning" the local pendingTickets - i.e:
|
|
104
|
-
* 1. The adversary sees a signature and swaps the ticket attempt to something different.
|
|
105
|
-
* 2. This creates an invalid ticket, but prevents a valid ticket with the same signature from being included and distributed.
|
|
106
|
-
*
|
|
107
|
-
* TODO [MaSi]: The poisoning risk should be fixed during implementation of ticket validation.
|
|
108
|
-
*/
|
|
109
|
-
const isDuplicate = this.pendingTickets.some((pending) => pending.epochIndex === epochIndex && pending.ticket.signature.isEqualTo(ticket.signature));
|
|
110
|
-
if (!isDuplicate) {
|
|
111
|
-
this.pendingTickets.push({ epochIndex, ticket });
|
|
112
|
-
logger.info `[addTicket] Added ticket for epoch ${epochIndex}, total: ${this.pendingTickets.length}`;
|
|
113
|
-
}
|
|
88
|
+
this.pool.addTicket(epochIndex, ticket);
|
|
114
89
|
}
|
|
115
|
-
onTicketReceivedCallback = null;
|
|
116
90
|
/**
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* This prevents redistribution of invalid tickets (e.g. those with a tampered `attempt` field).
|
|
91
|
+
* Replace the redistribution pool for the given epoch with the supplied tickets.
|
|
92
|
+
* Used when the authorship worker dumps the authoritative pool on an epoch boundary.
|
|
120
93
|
*/
|
|
121
|
-
|
|
122
|
-
this.
|
|
94
|
+
replacePool(epochIndex, tickets) {
|
|
95
|
+
this.pool.replace(epochIndex, tickets);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Register the validator that decides whether tickets received from peers should be
|
|
99
|
+
* accepted (and therefore redistributed). The default is {@link DenyTicketsValidator},
|
|
100
|
+
* so the caller must install a real validator for any peer ticket to make it through.
|
|
101
|
+
*/
|
|
102
|
+
setTicketValidator(validator) {
|
|
103
|
+
this.validator = validator;
|
|
123
104
|
}
|
|
124
105
|
onTicketReceived(epochIndex, ticket) {
|
|
125
106
|
logger.trace `Received ticket for epoch ${epochIndex}, attempt ${ticket.attempt}`;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
this.addTicket(epochIndex, ticket);
|
|
146
|
-
}
|
|
107
|
+
const validator = this.validator;
|
|
108
|
+
// Wrap with Promise.resolve().then() so a synchronous throw inside the validator
|
|
109
|
+
// funnels into the same .catch() as an async rejection.
|
|
110
|
+
Promise.resolve()
|
|
111
|
+
.then(() => validator.validate(epochIndex, ticket))
|
|
112
|
+
.then((result) => {
|
|
113
|
+
if (result.isOk) {
|
|
114
|
+
this.addTicket(epochIndex, ticket);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
logger.trace `Dropping ticket for epoch ${epochIndex}: ${result.error}`;
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
.catch((error) => {
|
|
121
|
+
logger.error `Error validating ticket for epoch ${epochIndex}, attempt ${ticket.attempt}: ${error}`;
|
|
122
|
+
});
|
|
147
123
|
}
|
|
148
124
|
}
|
|
@@ -8,7 +8,8 @@ import { tinyChainSpec } from "#@typeberry/config";
|
|
|
8
8
|
import { BANDERSNATCH_PROOF_BYTES } from "#@typeberry/crypto";
|
|
9
9
|
import { Logger } from "#@typeberry/logger";
|
|
10
10
|
import { createTestPeerPair, MockNetwork } from "#@typeberry/networking/testing.js";
|
|
11
|
-
import {
|
|
11
|
+
import { AcceptTicketsValidator, ValidationError } from "#@typeberry/ticket-pool";
|
|
12
|
+
import { OK, Result } from "#@typeberry/utils";
|
|
12
13
|
import { Connections } from "../peers.js";
|
|
13
14
|
import { StreamManager } from "../stream-manager.js";
|
|
14
15
|
import { TicketDistributionTask } from "./ticket-distribution.js";
|
|
@@ -34,6 +35,9 @@ describe("TicketDistributionTask", () => {
|
|
|
34
35
|
const receivedTickets = [];
|
|
35
36
|
// Use real TicketDistributionTask
|
|
36
37
|
const ticketTask = TicketDistributionTask.start(streamManager, connections, tinyChainSpec);
|
|
38
|
+
// Default validator accepts every ticket so the test asserts purely on distribution
|
|
39
|
+
// behaviour. Tests that exercise the rejection path overwrite this.
|
|
40
|
+
ticketTask.setTicketValidator(new AcceptTicketsValidator());
|
|
37
41
|
// Intercept received tickets by wrapping onTicketReceived behavior
|
|
38
42
|
// The task already adds received tickets to pending queue via addTicket,
|
|
39
43
|
// so we can track them by checking the pending queue growth or by
|
|
@@ -217,7 +221,7 @@ describe("TicketDistributionTask", () => {
|
|
|
217
221
|
assert.strictEqual(peer2.receivedTickets.length, 1);
|
|
218
222
|
assert.deepStrictEqual(peer2.receivedTickets[0].ticket, ticket);
|
|
219
223
|
});
|
|
220
|
-
it("should NOT redistribute ticket if
|
|
224
|
+
it("should NOT redistribute ticket if validator rejects", async () => {
|
|
221
225
|
const self = await init("self");
|
|
222
226
|
const peer1 = await init("peer1");
|
|
223
227
|
const peer2 = await init("peer2");
|
|
@@ -225,35 +229,53 @@ describe("TicketDistributionTask", () => {
|
|
|
225
229
|
self.openConnection(peer2);
|
|
226
230
|
await tick();
|
|
227
231
|
// Validation always rejects
|
|
228
|
-
self.ticketTask.
|
|
232
|
+
self.ticketTask.setTicketValidator({
|
|
233
|
+
validate: async () => Result.error(ValidationError.InvalidProof, () => "rejected"),
|
|
234
|
+
});
|
|
229
235
|
const ticket = createTestTicket(0);
|
|
230
236
|
peer1.ticketTask.addTicket(TEST_EPOCH, ticket);
|
|
231
237
|
peer1.ticketTask.maintainDistribution();
|
|
232
238
|
await tick();
|
|
233
|
-
// self.addTicket was NOT called (
|
|
239
|
+
// self.addTicket was NOT called (validator rejected), so nothing to redistribute
|
|
234
240
|
assert.strictEqual(self.receivedTickets.length, 0);
|
|
235
241
|
self.ticketTask.maintainDistribution();
|
|
236
242
|
await tick();
|
|
237
243
|
assert.strictEqual(peer2.receivedTickets.length, 0);
|
|
238
244
|
});
|
|
239
|
-
it("should redistribute ticket if
|
|
245
|
+
it("should redistribute ticket if validator accepts", async () => {
|
|
240
246
|
const self = await init("self");
|
|
241
247
|
const peer1 = await init("peer1");
|
|
242
248
|
const peer2 = await init("peer2");
|
|
243
249
|
self.openConnection(peer1);
|
|
244
250
|
self.openConnection(peer2);
|
|
245
251
|
await tick();
|
|
246
|
-
//
|
|
247
|
-
self.ticketTask.setOnTicketReceived(async () => true);
|
|
252
|
+
// Default init() already wires an AcceptTicketsValidator
|
|
248
253
|
const ticket = createTestTicket(0);
|
|
249
254
|
peer1.ticketTask.addTicket(TEST_EPOCH, ticket);
|
|
250
255
|
peer1.ticketTask.maintainDistribution();
|
|
251
256
|
await tick();
|
|
252
|
-
// self.addTicket WAS called
|
|
257
|
+
// self.addTicket WAS called
|
|
253
258
|
assert.strictEqual(self.receivedTickets.length, 1);
|
|
254
259
|
self.ticketTask.maintainDistribution();
|
|
255
260
|
await tick();
|
|
256
261
|
assert.strictEqual(peer2.receivedTickets.length, 1);
|
|
257
262
|
assert.deepStrictEqual(peer2.receivedTickets[0].ticket, ticket);
|
|
258
263
|
});
|
|
264
|
+
it("replacePool overwrites the redistribution pool", async () => {
|
|
265
|
+
const self = await init("self");
|
|
266
|
+
const peer1 = await init("peer1");
|
|
267
|
+
self.openConnection(peer1);
|
|
268
|
+
await tick();
|
|
269
|
+
// Locally added tickets first
|
|
270
|
+
self.ticketTask.addTicket(TEST_EPOCH, createTestTicket(0));
|
|
271
|
+
self.ticketTask.addTicket(TEST_EPOCH, createTestTicket(1));
|
|
272
|
+
// Pool dump replaces with a different set
|
|
273
|
+
const dump = [createTestTicket(2), createTestTicket(3)];
|
|
274
|
+
self.ticketTask.replacePool(TEST_EPOCH, dump);
|
|
275
|
+
self.ticketTask.maintainDistribution();
|
|
276
|
+
await tick();
|
|
277
|
+
assert.strictEqual(peer1.receivedTickets.length, 2);
|
|
278
|
+
assert.deepStrictEqual(peer1.receivedTickets[0].ticket, dump[0]);
|
|
279
|
+
assert.deepStrictEqual(peer1.receivedTickets[1].ticket, dump[1]);
|
|
280
|
+
});
|
|
259
281
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main-fuzz.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/node/main-fuzz.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,WAAW,EAAmB,MAAM,oBAAoB,CAAC;AACvE,OAAO,EAAE,EAAE,IAAI,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAOrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAIjD,MAAM,MAAM,UAAU,GAAG;IACvB,OAAO,EAAE,WAAW,CAAC;IACrB,aAAa,EAAE,SAAS,CAAC;IACzB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,uBAAuB,EAAE,OAAO,CAAC;CAClC,CAAC;
|
|
1
|
+
{"version":3,"file":"main-fuzz.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/node/main-fuzz.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,KAAK,WAAW,EAAmB,MAAM,oBAAoB,CAAC;AACvE,OAAO,EAAE,EAAE,IAAI,MAAM,EAAE,MAAM,uBAAuB,CAAC;AAOrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAIjD,MAAM,MAAM,UAAU,GAAG;IACvB,OAAO,EAAE,WAAW,CAAC;IACrB,aAAa,EAAE,SAAS,CAAC;IACzB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,uBAAuB,EAAE,OAAO,CAAC;CAClC,CAAC;AAWF;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CASpF;AAED,iFAAiF;AACjF,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE5D;AAED,wBAAgB,cAAc;;;;EAM7B;AAED,wBAAsB,QAAQ,CAAC,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,uBAqHxF"}
|
|
@@ -14,6 +14,9 @@ import { mainImporter } from "./main-importer.js";
|
|
|
14
14
|
const logger = Logger.new(import.meta.filename, "fuzztarget");
|
|
15
15
|
/** Dedicated subdirectory under the configured base path that the fuzzer owns and wipes. */
|
|
16
16
|
const FUZZ_DB_SUBDIR = "typeberry-fuzz-db";
|
|
17
|
+
const FUZZ_DB_FJALL = "fjall-hybrid";
|
|
18
|
+
const FUZZ_DB_LMDB = "lmdb-hybrid";
|
|
19
|
+
const FUZZ_DB_OPTIONS = [FUZZ_DB_FJALL, FUZZ_DB_LMDB];
|
|
17
20
|
/**
|
|
18
21
|
* Resolve the directory the fuzzer should use for its on-disk database, or
|
|
19
22
|
* `undefined` for an in-memory database. The dedicated `FUZZ_DB_SUBDIR` is
|
|
@@ -50,6 +53,15 @@ export async function mainFuzz(fuzzConfig, withRelPath) {
|
|
|
50
53
|
logHostEnvironment(logger);
|
|
51
54
|
const { jamNodeConfig: config } = fuzzConfig;
|
|
52
55
|
const fuzzDbBase = resolveFuzzDbBase(config.node.databaseBasePath);
|
|
56
|
+
const rawFuzzDb = process.env.JAM_FUZZ_DB?.trim() ?? "";
|
|
57
|
+
// Using experimental fjall-hybrid by default, with an option to test lmdb as well.
|
|
58
|
+
const hybridStateBackend = rawFuzzDb === "" ? FUZZ_DB_FJALL : rawFuzzDb;
|
|
59
|
+
if (!isValidStateBackend(hybridStateBackend)) {
|
|
60
|
+
throw new Error(`JAM_FUZZ_DB must be one of: ${FUZZ_DB_OPTIONS} (got: "${rawFuzzDb}").`);
|
|
61
|
+
}
|
|
62
|
+
if (fuzzDbBase !== undefined) {
|
|
63
|
+
logger.info `🗄️ Fuzz persistent backend: ${hybridStateBackend}.`;
|
|
64
|
+
}
|
|
53
65
|
let runningNode = null;
|
|
54
66
|
const chainSpec = getChainSpec(config.node.flavor);
|
|
55
67
|
const closeFuzzTarget = startFuzzTarget(fuzzConfig.version, fuzzConfig.socket, {
|
|
@@ -106,7 +118,7 @@ export async function mainFuzz(fuzzConfig, withRelPath) {
|
|
|
106
118
|
// spec only, where values are big) bounds its on-disk/page-cache size.
|
|
107
119
|
// Tiny stays uncompressed since its db is small and speed matters more.
|
|
108
120
|
ephemeral: isPersistent,
|
|
109
|
-
stateBackend: isPersistent ?
|
|
121
|
+
stateBackend: isPersistent ? hybridStateBackend : "lmdb",
|
|
110
122
|
});
|
|
111
123
|
};
|
|
112
124
|
if (fuzzDbBase !== undefined) {
|
|
@@ -136,3 +148,6 @@ export async function mainFuzz(fuzzConfig, withRelPath) {
|
|
|
136
148
|
}
|
|
137
149
|
};
|
|
138
150
|
}
|
|
151
|
+
function isValidStateBackend(val) {
|
|
152
|
+
return FUZZ_DB_OPTIONS.indexOf(val) !== -1;
|
|
153
|
+
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import type { JamConfig } from "./jam-config.js";
|
|
2
2
|
import type { NodeApi } from "./main.js";
|
|
3
|
+
export type StateBackend = "lmdb" | "lmdb-hybrid" | "fjall-hybrid";
|
|
3
4
|
export type ImporterOptions = {
|
|
4
5
|
initGenesisFromAncestry?: boolean;
|
|
5
6
|
dummyFinalityDepth?: number;
|
|
6
7
|
pruneBlocks?: boolean;
|
|
7
|
-
/** Open the
|
|
8
|
+
/** Open the database without fsync/compression. Only safe for throwaway dbs (e.g. fuzzing). */
|
|
8
9
|
ephemeral?: boolean;
|
|
9
|
-
/**
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Persistent backend to use when `databaseBasePath` is set. Defaults to full LMDB.
|
|
12
|
+
*/
|
|
13
|
+
stateBackend?: StateBackend;
|
|
11
14
|
};
|
|
12
15
|
export declare function mainImporter(config: JamConfig, withRelPath: (v: string) => string, options?: ImporterOptions): Promise<NodeApi>;
|
|
13
16
|
//# sourceMappingURL=main-importer.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main-importer.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/node/main-importer.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIzC,MAAM,MAAM,eAAe,GAAG;IAC5B,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB
|
|
1
|
+
{"version":3,"file":"main-importer.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/node/main-importer.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIzC,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,aAAa,GAAG,cAAc,CAAC;AAEnE,MAAM,MAAM,eAAe,GAAG;IAC5B,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAClC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,+FAA+F;IAC/F,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;OAEG;IACH,YAAY,CAAC,EAAE,YAAY,CAAC;CAC7B,CAAC;AAEF,wBAAsB,YAAY,CAChC,MAAM,EAAE,SAAS,EACjB,WAAW,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,EAClC,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,OAAO,CAAC,CAqGlB"}
|
|
@@ -39,8 +39,8 @@ export async function mainImporter(config, withRelPath, options = {}) {
|
|
|
39
39
|
blake2b,
|
|
40
40
|
workerParams,
|
|
41
41
|
})
|
|
42
|
-
: dbBackend === "hybrid"
|
|
43
|
-
? HybridWorkerConfig.new({
|
|
42
|
+
: dbBackend === "lmdb-hybrid" || dbBackend === "fjall-hybrid"
|
|
43
|
+
? await HybridWorkerConfig.new({
|
|
44
44
|
nodeName,
|
|
45
45
|
chainSpec,
|
|
46
46
|
blake2b,
|
|
@@ -48,6 +48,7 @@ export async function mainImporter(config, withRelPath, options = {}) {
|
|
|
48
48
|
workerParams,
|
|
49
49
|
ephemeral,
|
|
50
50
|
compression,
|
|
51
|
+
backend: dbBackend === "lmdb-hybrid" ? "lmdb" : "fjall",
|
|
51
52
|
})
|
|
52
53
|
: LmdbWorkerConfig.new({
|
|
53
54
|
nodeName,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/ticket-pool/index.ts"],"names":[],"mappings":"AAAA,cAAc,0BAA0B,CAAC;AACzC,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Epoch } from "#@typeberry/block";
|
|
2
|
+
import type { SignedTicket } from "#@typeberry/block/tickets.js";
|
|
3
|
+
/**
|
|
4
|
+
* An ordered, signature-deduplicated pool of tickets waiting to be redistributed to peers.
|
|
5
|
+
*
|
|
6
|
+
* Used on the networking side. Indices are stable within an epoch so callers can track
|
|
7
|
+
* per-peer "sent" sets by index. The pool is cleared whenever a new epoch is observed,
|
|
8
|
+
* and tickets for older epochs are dropped (can happen when an async validation completes
|
|
9
|
+
* after the epoch already advanced).
|
|
10
|
+
*/
|
|
11
|
+
export declare class PendingTicketPool {
|
|
12
|
+
private tickets;
|
|
13
|
+
private currentEpochValue;
|
|
14
|
+
/** Epoch the pool is currently holding tickets for, or `null` if empty. */
|
|
15
|
+
get currentEpoch(): Epoch | null;
|
|
16
|
+
/** Returns the ordered tickets currently in the pool. Caller must not mutate the array. */
|
|
17
|
+
getTickets(): readonly {
|
|
18
|
+
epochIndex: Epoch;
|
|
19
|
+
ticket: SignedTicket;
|
|
20
|
+
}[];
|
|
21
|
+
/** Returns true if the ticket was added, false if it was a duplicate or dropped (old epoch). */
|
|
22
|
+
addTicket(epochIndex: Epoch, ticket: SignedTicket): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Replace the pool contents for the given epoch with the supplied tickets. Used when the
|
|
25
|
+
* authorship worker pushes an authoritative pool dump on an epoch boundary; any tickets
|
|
26
|
+
* that aren't in the dump are dropped, and dedup runs over the new set.
|
|
27
|
+
*/
|
|
28
|
+
replace(epochIndex: Epoch, tickets: readonly SignedTicket[]): void;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=pending-ticket-pool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pending-ticket-pool.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/ticket-pool/pending-ticket-pool.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAKhE;;;;;;;GAOG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,OAAO,CAA0D;IACzE,OAAO,CAAC,iBAAiB,CAAsB;IAE/C,2EAA2E;IAC3E,IAAI,YAAY,IAAI,KAAK,GAAG,IAAI,CAE/B;IAED,2FAA2F;IAC3F,UAAU,IAAI,SAAS;QAAE,UAAU,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,YAAY,CAAA;KAAE,EAAE;IAIpE,gGAAgG;IAChG,SAAS,CAAC,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO;IAyB3D;;;;OAIG;IACH,OAAO,CAAC,UAAU,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,YAAY,EAAE,GAAG,IAAI;CAWnE"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Logger } from "#@typeberry/logger";
|
|
2
|
+
const logger = Logger.new(import.meta.filename, "pending-pool");
|
|
3
|
+
/**
|
|
4
|
+
* An ordered, signature-deduplicated pool of tickets waiting to be redistributed to peers.
|
|
5
|
+
*
|
|
6
|
+
* Used on the networking side. Indices are stable within an epoch so callers can track
|
|
7
|
+
* per-peer "sent" sets by index. The pool is cleared whenever a new epoch is observed,
|
|
8
|
+
* and tickets for older epochs are dropped (can happen when an async validation completes
|
|
9
|
+
* after the epoch already advanced).
|
|
10
|
+
*/
|
|
11
|
+
export class PendingTicketPool {
|
|
12
|
+
tickets = [];
|
|
13
|
+
currentEpochValue = null;
|
|
14
|
+
/** Epoch the pool is currently holding tickets for, or `null` if empty. */
|
|
15
|
+
get currentEpoch() {
|
|
16
|
+
return this.currentEpochValue;
|
|
17
|
+
}
|
|
18
|
+
/** Returns the ordered tickets currently in the pool. Caller must not mutate the array. */
|
|
19
|
+
getTickets() {
|
|
20
|
+
return this.tickets;
|
|
21
|
+
}
|
|
22
|
+
/** Returns true if the ticket was added, false if it was a duplicate or dropped (old epoch). */
|
|
23
|
+
addTicket(epochIndex, ticket) {
|
|
24
|
+
if (this.currentEpochValue !== null && epochIndex < this.currentEpochValue) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
if (this.currentEpochValue !== null && epochIndex > this.currentEpochValue) {
|
|
28
|
+
logger.log `Epoch changed from ${this.currentEpochValue} to ${epochIndex}, clearing ${this.tickets.length} old tickets`;
|
|
29
|
+
this.tickets = [];
|
|
30
|
+
}
|
|
31
|
+
this.currentEpochValue = epochIndex;
|
|
32
|
+
const isDuplicate = this.tickets.some((pending) => pending.epochIndex === epochIndex && pending.ticket.signature.isEqualTo(ticket.signature));
|
|
33
|
+
if (isDuplicate) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
this.tickets.push({ epochIndex, ticket });
|
|
37
|
+
logger.info `[addTicket] Added ticket for epoch ${epochIndex}, total: ${this.tickets.length}`;
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Replace the pool contents for the given epoch with the supplied tickets. Used when the
|
|
42
|
+
* authorship worker pushes an authoritative pool dump on an epoch boundary; any tickets
|
|
43
|
+
* that aren't in the dump are dropped, and dedup runs over the new set.
|
|
44
|
+
*/
|
|
45
|
+
replace(epochIndex, tickets) {
|
|
46
|
+
this.tickets = [];
|
|
47
|
+
this.currentEpochValue = epochIndex;
|
|
48
|
+
for (const ticket of tickets) {
|
|
49
|
+
const isDuplicate = this.tickets.some((pending) => pending.ticket.signature.isEqualTo(ticket.signature));
|
|
50
|
+
if (!isDuplicate) {
|
|
51
|
+
this.tickets.push({ epochIndex, ticket });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
logger.log `Pool replaced for epoch ${epochIndex} with ${this.tickets.length} tickets`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pending-ticket-pool.test.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/ticket-pool/pending-ticket-pool.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { tryAsEpoch } from "#@typeberry/block";
|
|
4
|
+
import { SignedTicket, tryAsTicketAttempt } from "#@typeberry/block/tickets.js";
|
|
5
|
+
import { Bytes } from "#@typeberry/bytes";
|
|
6
|
+
import { BANDERSNATCH_PROOF_BYTES } from "#@typeberry/crypto";
|
|
7
|
+
import { PendingTicketPool } from "./pending-ticket-pool.js";
|
|
8
|
+
const E1 = tryAsEpoch(1);
|
|
9
|
+
const E2 = tryAsEpoch(2);
|
|
10
|
+
function makeTicket(seed, attempt = 0) {
|
|
11
|
+
const sig = Bytes.zero(BANDERSNATCH_PROOF_BYTES);
|
|
12
|
+
sig.raw[0] = seed;
|
|
13
|
+
return SignedTicket.create({
|
|
14
|
+
attempt: tryAsTicketAttempt(attempt),
|
|
15
|
+
signature: sig.asOpaque(),
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
describe("PendingTicketPool", () => {
|
|
19
|
+
it("starts empty with no current epoch", () => {
|
|
20
|
+
const pool = new PendingTicketPool();
|
|
21
|
+
assert.strictEqual(pool.currentEpoch, null);
|
|
22
|
+
assert.deepStrictEqual(pool.getTickets(), []);
|
|
23
|
+
});
|
|
24
|
+
it("adds a ticket and tracks the epoch", () => {
|
|
25
|
+
const pool = new PendingTicketPool();
|
|
26
|
+
const t = makeTicket(1);
|
|
27
|
+
assert.strictEqual(pool.addTicket(E1, t), true);
|
|
28
|
+
assert.strictEqual(pool.currentEpoch, E1);
|
|
29
|
+
assert.strictEqual(pool.getTickets().length, 1);
|
|
30
|
+
});
|
|
31
|
+
it("dedups by signature within an epoch", () => {
|
|
32
|
+
const pool = new PendingTicketPool();
|
|
33
|
+
const t = makeTicket(1);
|
|
34
|
+
pool.addTicket(E1, t);
|
|
35
|
+
assert.strictEqual(pool.addTicket(E1, t), false);
|
|
36
|
+
assert.strictEqual(pool.getTickets().length, 1);
|
|
37
|
+
});
|
|
38
|
+
it("clears tickets when a newer epoch arrives", () => {
|
|
39
|
+
const pool = new PendingTicketPool();
|
|
40
|
+
pool.addTicket(E1, makeTicket(1));
|
|
41
|
+
pool.addTicket(E1, makeTicket(2));
|
|
42
|
+
pool.addTicket(E2, makeTicket(3));
|
|
43
|
+
const tickets = pool.getTickets();
|
|
44
|
+
assert.strictEqual(tickets.length, 1);
|
|
45
|
+
assert.strictEqual(tickets[0].epochIndex, E2);
|
|
46
|
+
assert.strictEqual(pool.currentEpoch, E2);
|
|
47
|
+
});
|
|
48
|
+
it("drops late tickets for older epochs", () => {
|
|
49
|
+
const pool = new PendingTicketPool();
|
|
50
|
+
pool.addTicket(E2, makeTicket(1));
|
|
51
|
+
assert.strictEqual(pool.addTicket(E1, makeTicket(2)), false);
|
|
52
|
+
assert.strictEqual(pool.getTickets().length, 1);
|
|
53
|
+
assert.strictEqual(pool.currentEpoch, E2);
|
|
54
|
+
});
|
|
55
|
+
it("replace clears existing tickets and dedups the new set", () => {
|
|
56
|
+
const pool = new PendingTicketPool();
|
|
57
|
+
pool.addTicket(E1, makeTicket(1));
|
|
58
|
+
pool.addTicket(E1, makeTicket(2));
|
|
59
|
+
const dump = [makeTicket(3), makeTicket(4), makeTicket(3)];
|
|
60
|
+
pool.replace(E2, dump);
|
|
61
|
+
const tickets = pool.getTickets();
|
|
62
|
+
assert.strictEqual(tickets.length, 2);
|
|
63
|
+
assert.strictEqual(pool.currentEpoch, E2);
|
|
64
|
+
assert.strictEqual(tickets[0].ticket.signature.raw[0], 3);
|
|
65
|
+
assert.strictEqual(tickets[1].ticket.signature.raw[0], 4);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { EntropyHash, Epoch } from "#@typeberry/block";
|
|
2
|
+
import type { SignedTicket } from "#@typeberry/block/tickets.js";
|
|
3
|
+
import { Result } from "#@typeberry/utils";
|
|
4
|
+
/**
|
|
5
|
+
* Outcome of a successful validation.
|
|
6
|
+
*
|
|
7
|
+
* `id` is the entropy hash the validator computed for this ticket. It is `null` when the
|
|
8
|
+
* concrete validator doesn't actually verify (e.g. {@link AcceptTicketsValidator}) or when
|
|
9
|
+
* it delegates to another process that doesn't bother to send the id back over the wire.
|
|
10
|
+
*/
|
|
11
|
+
export type ValidatedTicket = {
|
|
12
|
+
id: EntropyHash | null;
|
|
13
|
+
};
|
|
14
|
+
/** Reasons a ticket may fail validation. */
|
|
15
|
+
export declare enum ValidationError {
|
|
16
|
+
/** Verifier rejected the signature / proof. */
|
|
17
|
+
InvalidProof = "invalid_proof",
|
|
18
|
+
/** Validator could not run (e.g. state unavailable, transient internal failure). */
|
|
19
|
+
ValidatorUnavailable = "validator_unavailable",
|
|
20
|
+
/** Ticket is for an epoch outside the validator's window of interest. */
|
|
21
|
+
WrongEpoch = "wrong_epoch"
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Strategy for verifying tickets arriving from peers.
|
|
25
|
+
*
|
|
26
|
+
* The concrete implementation may call into the bandersnatch verifier, defer to another
|
|
27
|
+
* worker via IPC, or short-circuit (Accept/Deny defaults for tests).
|
|
28
|
+
*/
|
|
29
|
+
export interface TicketValidator {
|
|
30
|
+
validate(epochIndex: Epoch, ticket: SignedTicket): Promise<Result<ValidatedTicket, ValidationError>>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Accepts every ticket without inspection. Useful for unit tests where the validator
|
|
34
|
+
* isn't the subject under test. Must never be used in production.
|
|
35
|
+
*/
|
|
36
|
+
export declare class AcceptTicketsValidator implements TicketValidator {
|
|
37
|
+
validate(_epochIndex: Epoch, _ticket: SignedTicket): Promise<Result<ValidatedTicket, ValidationError>>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Rejects every ticket. Used as the default for any task that needs an explicit, real
|
|
41
|
+
* validator wired in before it will accept anything from the network.
|
|
42
|
+
*/
|
|
43
|
+
export declare class DenyTicketsValidator implements TicketValidator {
|
|
44
|
+
validate(_epochIndex: Epoch, _ticket: SignedTicket): Promise<Result<ValidatedTicket, ValidationError>>;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=ticket-validator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ticket-validator.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/ticket-pool/ticket-validator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE1C;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,EAAE,EAAE,WAAW,GAAG,IAAI,CAAC;CACxB,CAAC;AAEF,4CAA4C;AAC5C,oBAAY,eAAe;IACzB,+CAA+C;IAC/C,YAAY,kBAAkB;IAC9B,oFAAoF;IACpF,oBAAoB,0BAA0B;IAC9C,yEAAyE;IACzE,UAAU,gBAAgB;CAC3B;AAED;;;;;GAKG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,eAAe,CAAC,CAAC,CAAC;CACtG;AAED;;;GAGG;AACH,qBAAa,sBAAuB,YAAW,eAAe;IACtD,QAAQ,CAAC,WAAW,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,eAAe,CAAC,CAAC;CAG7G;AAED;;;GAGG;AACH,qBAAa,oBAAqB,YAAW,eAAe;IACpD,QAAQ,CAAC,WAAW,EAAE,KAAK,EAAE,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,eAAe,CAAC,CAAC;CAG7G"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Result } from "#@typeberry/utils";
|
|
2
|
+
/** Reasons a ticket may fail validation. */
|
|
3
|
+
export var ValidationError;
|
|
4
|
+
(function (ValidationError) {
|
|
5
|
+
/** Verifier rejected the signature / proof. */
|
|
6
|
+
ValidationError["InvalidProof"] = "invalid_proof";
|
|
7
|
+
/** Validator could not run (e.g. state unavailable, transient internal failure). */
|
|
8
|
+
ValidationError["ValidatorUnavailable"] = "validator_unavailable";
|
|
9
|
+
/** Ticket is for an epoch outside the validator's window of interest. */
|
|
10
|
+
ValidationError["WrongEpoch"] = "wrong_epoch";
|
|
11
|
+
})(ValidationError || (ValidationError = {}));
|
|
12
|
+
/**
|
|
13
|
+
* Accepts every ticket without inspection. Useful for unit tests where the validator
|
|
14
|
+
* isn't the subject under test. Must never be used in production.
|
|
15
|
+
*/
|
|
16
|
+
export class AcceptTicketsValidator {
|
|
17
|
+
async validate(_epochIndex, _ticket) {
|
|
18
|
+
return Result.ok({ id: null });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Rejects every ticket. Used as the default for any task that needs an explicit, real
|
|
23
|
+
* validator wired in before it will accept anything from the network.
|
|
24
|
+
*/
|
|
25
|
+
export class DenyTicketsValidator {
|
|
26
|
+
async validate(_epochIndex, _ticket) {
|
|
27
|
+
return Result.error(ValidationError.ValidatorUnavailable, () => "no ticket validator wired");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ticket-validator.test.d.ts","sourceRoot":"","sources":["../../../../../packages/jam/ticket-pool/ticket-validator.test.ts"],"names":[],"mappings":""}
|