@aztec/sequencer-client 0.57.0 → 0.59.0
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/dest/block_builder/index.d.ts +2 -2
- package/dest/block_builder/index.d.ts.map +1 -1
- package/dest/block_builder/light.d.ts +3 -3
- package/dest/block_builder/light.d.ts.map +1 -1
- package/dest/block_builder/light.js +6 -5
- package/dest/block_builder/orchestrator.d.ts +3 -3
- package/dest/block_builder/orchestrator.d.ts.map +1 -1
- package/dest/block_builder/orchestrator.js +1 -1
- package/dest/client/sequencer-client.d.ts +2 -2
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +3 -4
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +4 -5
- package/dest/publisher/l1-publisher-metrics.d.ts.map +1 -1
- package/dest/publisher/l1-publisher-metrics.js +2 -1
- package/dest/publisher/l1-publisher.d.ts +9 -4
- package/dest/publisher/l1-publisher.d.ts.map +1 -1
- package/dest/publisher/l1-publisher.js +71 -25
- package/dest/sequencer/sequencer.d.ts +3 -4
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +98 -80
- package/dest/tx_validator/phases_validator.d.ts +1 -1
- package/dest/tx_validator/phases_validator.d.ts.map +1 -1
- package/dest/tx_validator/phases_validator.js +1 -1
- package/dest/tx_validator/tx_validator_factory.d.ts +9 -7
- package/dest/tx_validator/tx_validator_factory.d.ts.map +1 -1
- package/dest/tx_validator/tx_validator_factory.js +20 -10
- package/package.json +19 -19
- package/src/block_builder/index.ts +2 -2
- package/src/block_builder/light.ts +7 -6
- package/src/block_builder/orchestrator.ts +8 -3
- package/src/client/sequencer-client.ts +3 -9
- package/src/config.ts +3 -4
- package/src/publisher/l1-publisher-metrics.ts +1 -0
- package/src/publisher/l1-publisher.ts +90 -26
- package/src/sequencer/sequencer.ts +117 -100
- package/src/tx_validator/phases_validator.ts +1 -1
- package/src/tx_validator/tx_validator_factory.ts +39 -15
|
@@ -8,10 +8,13 @@ import {
|
|
|
8
8
|
Tx,
|
|
9
9
|
type TxHash,
|
|
10
10
|
type TxValidator,
|
|
11
|
-
type WorldStateStatus,
|
|
12
11
|
type WorldStateSynchronizer,
|
|
13
12
|
} from '@aztec/circuit-types';
|
|
14
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
type AllowedElement,
|
|
15
|
+
BlockProofError,
|
|
16
|
+
type WorldStateSynchronizerStatus,
|
|
17
|
+
} from '@aztec/circuit-types/interfaces';
|
|
15
18
|
import { type L2BlockBuiltStats } from '@aztec/circuit-types/stats';
|
|
16
19
|
import {
|
|
17
20
|
AppendOnlyTreeSnapshot,
|
|
@@ -67,7 +70,6 @@ export class Sequencer {
|
|
|
67
70
|
// TODO: zero values should not be allowed for the following 2 values in PROD
|
|
68
71
|
private _coinbase = EthAddress.ZERO;
|
|
69
72
|
private _feeRecipient = AztecAddress.ZERO;
|
|
70
|
-
private lastPublishedBlock = 0;
|
|
71
73
|
private state = SequencerState.STOPPED;
|
|
72
74
|
private allowedInSetup: AllowedElement[] = [];
|
|
73
75
|
private allowedInTeardown: AllowedElement[] = [];
|
|
@@ -142,13 +144,12 @@ export class Sequencer {
|
|
|
142
144
|
/**
|
|
143
145
|
* Starts the sequencer and moves to IDLE state. Blocks until the initial sync is complete.
|
|
144
146
|
*/
|
|
145
|
-
public
|
|
146
|
-
await this.initialSync();
|
|
147
|
-
|
|
147
|
+
public start() {
|
|
148
148
|
this.runningPromise = new RunningPromise(this.work.bind(this), this.pollingIntervalMs);
|
|
149
149
|
this.runningPromise.start();
|
|
150
150
|
this.state = SequencerState.IDLE;
|
|
151
151
|
this.log.info('Sequencer started');
|
|
152
|
+
return Promise.resolve();
|
|
152
153
|
}
|
|
153
154
|
|
|
154
155
|
/**
|
|
@@ -180,11 +181,6 @@ export class Sequencer {
|
|
|
180
181
|
return { state: this.state };
|
|
181
182
|
}
|
|
182
183
|
|
|
183
|
-
protected async initialSync() {
|
|
184
|
-
// TODO: Should we wait for world state to be ready, or is the caller expected to run await start?
|
|
185
|
-
this.lastPublishedBlock = await this.worldState.status().then((s: WorldStateStatus) => s.syncedToL2Block);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
184
|
/**
|
|
189
185
|
* @notice Performs most of the sequencer duties:
|
|
190
186
|
* - Checks if we are up to date
|
|
@@ -279,15 +275,14 @@ export class Sequencer {
|
|
|
279
275
|
// @note It is very important that the following function will FAIL and not just return early
|
|
280
276
|
// if it have made any state changes. If not, we won't rollback the state, and you will
|
|
281
277
|
// be in for a world of pain.
|
|
282
|
-
await this.
|
|
278
|
+
await this.buildBlockAndAttemptToPublish(validTxs, proposalHeader, historicalHeader);
|
|
283
279
|
} catch (err) {
|
|
284
280
|
if (BlockProofError.isBlockProofError(err)) {
|
|
285
281
|
const txHashes = err.txHashes.filter(h => !h.isZero());
|
|
286
282
|
this.log.warn(`Proving block failed, removing ${txHashes.length} txs from pool`);
|
|
287
283
|
await this.p2pClient.deleteTxs(txHashes);
|
|
288
284
|
}
|
|
289
|
-
this.log.error(`
|
|
290
|
-
await this.worldState.getLatest().rollback();
|
|
285
|
+
this.log.error(`Error assembling block`, (err as any).stack);
|
|
291
286
|
}
|
|
292
287
|
}
|
|
293
288
|
|
|
@@ -314,6 +309,7 @@ export class Sequencer {
|
|
|
314
309
|
this.log.debug(`Can propose block ${proposalBlockNumber} at slot ${slot}`);
|
|
315
310
|
return slot;
|
|
316
311
|
} catch (err) {
|
|
312
|
+
this.log.verbose(`Rejected from being able to propose at next block with ${tipArchive}`);
|
|
317
313
|
prettyLogViemError(err, this.log);
|
|
318
314
|
throw err;
|
|
319
315
|
}
|
|
@@ -395,10 +391,10 @@ export class Sequencer {
|
|
|
395
391
|
* @param proposalHeader - The partial header constructed for the proposal
|
|
396
392
|
* @param historicalHeader - The historical header of the parent
|
|
397
393
|
*/
|
|
398
|
-
@trackSpan('Sequencer.
|
|
394
|
+
@trackSpan('Sequencer.buildBlockAndAttemptToPublish', (_validTxs, proposalHeader, _historicalHeader) => ({
|
|
399
395
|
[Attributes.BLOCK_NUMBER]: proposalHeader.globalVariables.blockNumber.toNumber(),
|
|
400
396
|
}))
|
|
401
|
-
private async
|
|
397
|
+
private async buildBlockAndAttemptToPublish(
|
|
402
398
|
validTxs: Tx[],
|
|
403
399
|
proposalHeader: Header,
|
|
404
400
|
historicalHeader: Header | undefined,
|
|
@@ -419,85 +415,90 @@ export class Sequencer {
|
|
|
419
415
|
`Retrieved ${l1ToL2Messages.length} L1 to L2 messages for block ${newGlobalVariables.blockNumber.toNumber()}`,
|
|
420
416
|
);
|
|
421
417
|
|
|
422
|
-
// We create a fresh processor each time to reset any cached state (eg storage writes)
|
|
423
|
-
const processor = this.publicProcessorFactory.create(historicalHeader, newGlobalVariables);
|
|
424
|
-
|
|
425
418
|
const numRealTxs = validTxs.length;
|
|
426
419
|
const blockSize = Math.max(2, numRealTxs);
|
|
427
420
|
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
!this.shouldProposeBlock(historicalHeader, {
|
|
445
|
-
validTxsCount: validTxs.length,
|
|
446
|
-
processedTxsCount: processedTxs.length,
|
|
447
|
-
})
|
|
448
|
-
) {
|
|
449
|
-
// TODO: Roll back changes to world state
|
|
450
|
-
throw new Error('Should not propose the block');
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
// All real transactions have been added, set the block as full and complete the proving.
|
|
454
|
-
const block = await blockBuilder.setBlockCompleted();
|
|
421
|
+
const fork = await this.worldState.fork();
|
|
422
|
+
try {
|
|
423
|
+
// We create a fresh processor each time to reset any cached state (eg storage writes)
|
|
424
|
+
const processor = this.publicProcessorFactory.create(fork, historicalHeader, newGlobalVariables);
|
|
425
|
+
const blockBuildingTimer = new Timer();
|
|
426
|
+
const blockBuilder = this.blockBuilderFactory.create(fork);
|
|
427
|
+
await blockBuilder.startNewBlock(blockSize, newGlobalVariables, l1ToL2Messages);
|
|
428
|
+
|
|
429
|
+
const [publicProcessorDuration, [processedTxs, failedTxs]] = await elapsed(() =>
|
|
430
|
+
processor.process(validTxs, blockSize, blockBuilder, this.txValidatorFactory.validatorForProcessedTxs(fork)),
|
|
431
|
+
);
|
|
432
|
+
if (failedTxs.length > 0) {
|
|
433
|
+
const failedTxData = failedTxs.map(fail => fail.tx);
|
|
434
|
+
this.log.debug(`Dropping failed txs ${Tx.getHashes(failedTxData).join(', ')}`);
|
|
435
|
+
await this.p2pClient.deleteTxs(Tx.getHashes(failedTxData));
|
|
436
|
+
}
|
|
455
437
|
|
|
456
|
-
|
|
457
|
-
// block being published before ours instead of just waiting on our block
|
|
438
|
+
await this.publisher.validateBlockForSubmission(proposalHeader);
|
|
458
439
|
|
|
459
|
-
|
|
440
|
+
if (
|
|
441
|
+
!this.shouldProposeBlock(historicalHeader, {
|
|
442
|
+
validTxsCount: validTxs.length,
|
|
443
|
+
processedTxsCount: processedTxs.length,
|
|
444
|
+
})
|
|
445
|
+
) {
|
|
446
|
+
// TODO: Roll back changes to world state
|
|
447
|
+
throw new Error('Should not propose the block');
|
|
448
|
+
}
|
|
460
449
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
450
|
+
// All real transactions have been added, set the block as full and complete the proving.
|
|
451
|
+
const block = await blockBuilder.setBlockCompleted();
|
|
452
|
+
|
|
453
|
+
// TODO(@PhilWindle) We should probably periodically check for things like another
|
|
454
|
+
// block being published before ours instead of just waiting on our block
|
|
455
|
+
|
|
456
|
+
await this.publisher.validateBlockForSubmission(block.header);
|
|
457
|
+
|
|
458
|
+
const workDuration = workTimer.ms();
|
|
459
|
+
this.log.verbose(
|
|
460
|
+
`Assembled block ${block.number} (txEffectsHash: ${block.header.contentCommitment.txsEffectsHash.toString(
|
|
461
|
+
'hex',
|
|
462
|
+
)})`,
|
|
463
|
+
{
|
|
464
|
+
eventName: 'l2-block-built',
|
|
465
|
+
creator: this.publisher.getSenderAddress().toString(),
|
|
466
|
+
duration: workDuration,
|
|
467
|
+
publicProcessDuration: publicProcessorDuration,
|
|
468
|
+
rollupCircuitsDuration: blockBuildingTimer.ms(),
|
|
469
|
+
...block.getStats(),
|
|
470
|
+
} satisfies L2BlockBuiltStats,
|
|
471
|
+
);
|
|
474
472
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
473
|
+
if (this.isFlushing) {
|
|
474
|
+
this.log.verbose(`Flushing completed`);
|
|
475
|
+
}
|
|
478
476
|
|
|
479
|
-
|
|
477
|
+
const txHashes = validTxs.map(tx => tx.getTxHash());
|
|
480
478
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
479
|
+
this.isFlushing = false;
|
|
480
|
+
this.log.verbose('Collecting attestations');
|
|
481
|
+
const attestations = await this.collectAttestations(block, txHashes);
|
|
482
|
+
this.log.verbose('Attestations collected');
|
|
485
483
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
484
|
+
this.log.verbose('Collecting proof quotes');
|
|
485
|
+
const proofQuote = await this.createProofClaimForPreviousEpoch(newGlobalVariables.slotNumber.toBigInt());
|
|
486
|
+
this.log.verbose(proofQuote ? `Using proof quote ${inspect(proofQuote.payload)}` : 'No proof quote available');
|
|
489
487
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
488
|
+
try {
|
|
489
|
+
await this.publishL2Block(block, attestations, txHashes, proofQuote);
|
|
490
|
+
this.metrics.recordPublishedBlock(workDuration);
|
|
491
|
+
this.log.info(
|
|
492
|
+
`Submitted rollup block ${block.number} with ${processedTxs.length} transactions duration=${Math.ceil(
|
|
493
|
+
workDuration,
|
|
494
|
+
)}ms (Submitter: ${this.publisher.getSenderAddress()})`,
|
|
495
|
+
);
|
|
496
|
+
} catch (err) {
|
|
497
|
+
this.metrics.recordFailedBlock();
|
|
498
|
+
throw err;
|
|
499
|
+
}
|
|
500
|
+
} finally {
|
|
501
|
+
await fork.close();
|
|
501
502
|
}
|
|
502
503
|
}
|
|
503
504
|
|
|
@@ -506,6 +507,11 @@ export class Sequencer {
|
|
|
506
507
|
this.isFlushing = true;
|
|
507
508
|
}
|
|
508
509
|
|
|
510
|
+
@trackSpan('Sequencer.collectAttestations', (block, txHashes) => ({
|
|
511
|
+
[Attributes.BLOCK_NUMBER]: block.number,
|
|
512
|
+
[Attributes.BLOCK_ARCHIVE]: block.archive.toString(),
|
|
513
|
+
[Attributes.BLOCK_TXS_COUNT]: txHashes.length,
|
|
514
|
+
}))
|
|
509
515
|
protected async collectAttestations(block: L2Block, txHashes: TxHash[]): Promise<Signature[] | undefined> {
|
|
510
516
|
// TODO(https://github.com/AztecProtocol/aztec-packages/issues/7962): inefficient to have a round trip in here - this should be cached
|
|
511
517
|
const committee = await this.publisher.getCurrentEpochCommittee();
|
|
@@ -600,9 +606,7 @@ export class Sequencer {
|
|
|
600
606
|
this.state = SequencerState.PUBLISHING_BLOCK;
|
|
601
607
|
|
|
602
608
|
const publishedL2Block = await this.publisher.proposeL2Block(block, attestations, txHashes, proofQuote);
|
|
603
|
-
if (publishedL2Block) {
|
|
604
|
-
this.lastPublishedBlock = block.number;
|
|
605
|
-
} else {
|
|
609
|
+
if (!publishedL2Block) {
|
|
606
610
|
throw new Error(`Failed to publish block ${block.number}`);
|
|
607
611
|
}
|
|
608
612
|
}
|
|
@@ -638,24 +642,37 @@ export class Sequencer {
|
|
|
638
642
|
}
|
|
639
643
|
|
|
640
644
|
/**
|
|
641
|
-
* Returns whether
|
|
645
|
+
* Returns whether all dependencies have caught up.
|
|
646
|
+
* We don't check against the previous block submitted since it may have been reorg'd out.
|
|
642
647
|
* @returns Boolean indicating if our dependencies are synced to the latest block.
|
|
643
648
|
*/
|
|
644
649
|
protected async isBlockSynced() {
|
|
645
650
|
const syncedBlocks = await Promise.all([
|
|
646
|
-
this.worldState.status().then((s:
|
|
647
|
-
this.
|
|
648
|
-
this.
|
|
651
|
+
this.worldState.status().then((s: WorldStateSynchronizerStatus) => s.syncedToL2Block),
|
|
652
|
+
this.l2BlockSource.getL2Tips().then(t => t.latest),
|
|
653
|
+
this.p2pClient.getStatus().then(s => s.syncedToL2Block.number),
|
|
649
654
|
this.l1ToL2MessageSource.getBlockNumber(),
|
|
650
|
-
]);
|
|
651
|
-
const
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
655
|
+
] as const);
|
|
656
|
+
const [worldState, l2BlockSource, p2p, l1ToL2MessageSource] = syncedBlocks;
|
|
657
|
+
const result =
|
|
658
|
+
// check that world state has caught up with archiver
|
|
659
|
+
// note that the archiver reports undefined hash for the genesis block
|
|
660
|
+
// because it doesn't have access to world state to compute it (facepalm)
|
|
661
|
+
(l2BlockSource.hash === undefined || worldState.hash === l2BlockSource.hash) &&
|
|
662
|
+
// and p2p client and message source are at least at the same block
|
|
663
|
+
// this should change to hashes once p2p client handles reorgs
|
|
664
|
+
// and once we stop pretending that the l1tol2message source is not
|
|
665
|
+
// just the archiver under a different name
|
|
666
|
+
p2p >= l2BlockSource.number &&
|
|
667
|
+
l1ToL2MessageSource >= l2BlockSource.number;
|
|
668
|
+
|
|
669
|
+
this.log.verbose(`Sequencer sync check ${result ? 'succeeded' : 'failed'}`, {
|
|
670
|
+
worldStateNumber: worldState.number,
|
|
671
|
+
worldStateHash: worldState.hash,
|
|
672
|
+
l2BlockSourceNumber: l2BlockSource.number,
|
|
673
|
+
l2BlockSourceHash: l2BlockSource.hash,
|
|
674
|
+
p2pNumber: p2p,
|
|
675
|
+
l1ToL2MessageSourceNumber: l1ToL2MessageSource,
|
|
659
676
|
});
|
|
660
677
|
return result;
|
|
661
678
|
}
|
|
@@ -5,9 +5,9 @@ import {
|
|
|
5
5
|
Tx,
|
|
6
6
|
type TxValidator,
|
|
7
7
|
} from '@aztec/circuit-types';
|
|
8
|
+
import { type ContractDataSource } from '@aztec/circuits.js';
|
|
8
9
|
import { createDebugLogger } from '@aztec/foundation/log';
|
|
9
10
|
import { ContractsDataSourcePublicDB, EnqueuedCallsProcessor } from '@aztec/simulator';
|
|
10
|
-
import { type ContractDataSource } from '@aztec/types/contracts';
|
|
11
11
|
|
|
12
12
|
export class PhasesTxValidator implements TxValidator<Tx> {
|
|
13
13
|
#log = createDebugLogger('aztec:sequencer:tx_validator:tx_phases');
|
|
@@ -1,33 +1,57 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import {
|
|
2
|
+
type AllowedElement,
|
|
3
|
+
MerkleTreeId,
|
|
4
|
+
type MerkleTreeReadOperations,
|
|
5
|
+
type ProcessedTx,
|
|
6
|
+
type Tx,
|
|
7
|
+
type TxValidator,
|
|
8
|
+
} from '@aztec/circuit-types';
|
|
9
|
+
import { type ContractDataSource, type GlobalVariables } from '@aztec/circuits.js';
|
|
10
|
+
import {
|
|
11
|
+
AggregateTxValidator,
|
|
12
|
+
DataTxValidator,
|
|
13
|
+
DoubleSpendTxValidator,
|
|
14
|
+
MetadataTxValidator,
|
|
15
|
+
type NullifierSource,
|
|
16
|
+
} from '@aztec/p2p';
|
|
17
|
+
import { ProtocolContractAddress } from '@aztec/protocol-contracts';
|
|
18
|
+
import { readPublicState } from '@aztec/simulator';
|
|
8
19
|
|
|
9
|
-
import { GasTxValidator } from './gas_validator.js';
|
|
20
|
+
import { GasTxValidator, type PublicStateSource } from './gas_validator.js';
|
|
10
21
|
import { PhasesTxValidator } from './phases_validator.js';
|
|
11
22
|
|
|
12
23
|
export class TxValidatorFactory {
|
|
24
|
+
nullifierSource: NullifierSource;
|
|
25
|
+
publicStateSource: PublicStateSource;
|
|
13
26
|
constructor(
|
|
14
|
-
private
|
|
27
|
+
private committedDb: MerkleTreeReadOperations,
|
|
15
28
|
private contractDataSource: ContractDataSource,
|
|
16
29
|
private enforceFees: boolean,
|
|
17
|
-
) {
|
|
30
|
+
) {
|
|
31
|
+
this.nullifierSource = {
|
|
32
|
+
getNullifierIndex: nullifier => this.committedDb.findLeafIndex(MerkleTreeId.NULLIFIER_TREE, nullifier.toBuffer()),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
this.publicStateSource = {
|
|
36
|
+
storageRead: (contractAddress, slot) => {
|
|
37
|
+
return readPublicState(this.committedDb, contractAddress, slot);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
18
41
|
|
|
19
42
|
validatorForNewTxs(globalVariables: GlobalVariables, setupAllowList: AllowedElement[]): TxValidator<Tx> {
|
|
20
|
-
const worldStateDB = new WorldStateDB(this.merkleTreeDb, this.contractDataSource);
|
|
21
43
|
return new AggregateTxValidator(
|
|
22
44
|
new DataTxValidator(),
|
|
23
45
|
new MetadataTxValidator(globalVariables.chainId, globalVariables.blockNumber),
|
|
24
|
-
new DoubleSpendTxValidator(
|
|
46
|
+
new DoubleSpendTxValidator(this.nullifierSource),
|
|
25
47
|
new PhasesTxValidator(this.contractDataSource, setupAllowList),
|
|
26
|
-
new GasTxValidator(
|
|
48
|
+
new GasTxValidator(this.publicStateSource, ProtocolContractAddress.FeeJuice, this.enforceFees),
|
|
27
49
|
);
|
|
28
50
|
}
|
|
29
51
|
|
|
30
|
-
validatorForProcessedTxs(): TxValidator<ProcessedTx> {
|
|
31
|
-
return new DoubleSpendTxValidator(
|
|
52
|
+
validatorForProcessedTxs(fork: MerkleTreeReadOperations): TxValidator<ProcessedTx> {
|
|
53
|
+
return new DoubleSpendTxValidator({
|
|
54
|
+
getNullifierIndex: nullifier => fork.findLeafIndex(MerkleTreeId.NULLIFIER_TREE, nullifier.toBuffer()),
|
|
55
|
+
});
|
|
32
56
|
}
|
|
33
57
|
}
|