@aztec/sequencer-client 0.23.0 → 0.24.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/solo_block_builder.d.ts +2 -2
- package/dest/block_builder/solo_block_builder.d.ts.map +1 -1
- package/dest/block_builder/solo_block_builder.js +3 -3
- package/dest/prover/empty.d.ts +2 -2
- package/dest/prover/empty.d.ts.map +1 -1
- package/dest/prover/empty.js +1 -1
- package/dest/prover/index.d.ts +2 -2
- package/dest/prover/index.d.ts.map +1 -1
- package/dest/sequencer/abstract_phase_manager.d.ts +11 -11
- package/dest/sequencer/abstract_phase_manager.d.ts.map +1 -1
- package/dest/sequencer/abstract_phase_manager.js +18 -15
- package/dest/sequencer/application_logic_phase_manager.d.ts +3 -3
- package/dest/sequencer/application_logic_phase_manager.d.ts.map +1 -1
- package/dest/sequencer/application_logic_phase_manager.js +2 -2
- package/dest/sequencer/fee_distribution_phase_manager.d.ts +4 -4
- package/dest/sequencer/fee_distribution_phase_manager.d.ts.map +1 -1
- package/dest/sequencer/fee_distribution_phase_manager.js +2 -2
- package/dest/sequencer/fee_preparation_phase_manager.d.ts +3 -3
- package/dest/sequencer/fee_preparation_phase_manager.d.ts.map +1 -1
- package/dest/sequencer/fee_preparation_phase_manager.js +1 -2
- package/dest/sequencer/processed_tx.d.ts +3 -3
- package/dest/sequencer/processed_tx.d.ts.map +1 -1
- package/dest/sequencer/processed_tx.js +4 -4
- package/dest/sequencer/public_processor.d.ts.map +1 -1
- package/dest/sequencer/public_processor.js +1 -1
- package/dest/simulator/index.d.ts +3 -3
- package/dest/simulator/index.d.ts.map +1 -1
- package/dest/simulator/public_kernel.d.ts +3 -3
- package/dest/simulator/public_kernel.d.ts.map +1 -1
- package/dest/simulator/public_kernel.js +4 -4
- package/dest/simulator/rollup.js +2 -2
- package/package.json +12 -23
- package/src/block_builder/index.ts +24 -0
- package/src/block_builder/solo_block_builder.ts +715 -0
- package/src/block_builder/types.ts +8 -0
- package/src/client/index.ts +1 -0
- package/src/client/sequencer-client.ts +97 -0
- package/src/config.ts +86 -0
- package/src/global_variable_builder/config.ts +20 -0
- package/src/global_variable_builder/global_builder.ts +95 -0
- package/src/global_variable_builder/index.ts +16 -0
- package/src/global_variable_builder/viem-reader.ts +61 -0
- package/src/index.ts +15 -0
- package/src/mocks/verification_keys.ts +36 -0
- package/src/prover/empty.ts +74 -0
- package/src/prover/index.ts +53 -0
- package/src/publisher/config.ts +41 -0
- package/src/publisher/index.ts +14 -0
- package/src/publisher/l1-publisher.ts +365 -0
- package/src/publisher/viem-tx-sender.ts +241 -0
- package/src/receiver.ts +13 -0
- package/src/sequencer/abstract_phase_manager.ts +427 -0
- package/src/sequencer/application_logic_phase_manager.ts +107 -0
- package/src/sequencer/config.ts +1 -0
- package/src/sequencer/fee_distribution_phase_manager.ts +70 -0
- package/src/sequencer/fee_preparation_phase_manager.ts +79 -0
- package/src/sequencer/index.ts +2 -0
- package/src/sequencer/processed_tx.ts +95 -0
- package/src/sequencer/public_processor.ts +136 -0
- package/src/sequencer/sequencer.ts +462 -0
- package/src/simulator/index.ts +53 -0
- package/src/simulator/public_executor.ts +169 -0
- package/src/simulator/public_kernel.ts +58 -0
- package/src/simulator/rollup.ts +76 -0
- package/src/utils.ts +16 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { ExtendedContractData, Tx, TxHash, TxL2Logs } from '@aztec/circuit-types';
|
|
2
|
+
import {
|
|
3
|
+
CombinedAccumulatedData,
|
|
4
|
+
Fr,
|
|
5
|
+
Header,
|
|
6
|
+
Proof,
|
|
7
|
+
PublicKernelCircuitPublicInputs,
|
|
8
|
+
makeEmptyProof,
|
|
9
|
+
} from '@aztec/circuits.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Represents a tx that has been processed by the sequencer public processor,
|
|
13
|
+
* so its kernel circuit public inputs are filled in.
|
|
14
|
+
*/
|
|
15
|
+
export type ProcessedTx = Pick<Tx, 'proof' | 'encryptedLogs' | 'unencryptedLogs' | 'newContracts'> & {
|
|
16
|
+
/**
|
|
17
|
+
* Output of the public kernel circuit for this tx.
|
|
18
|
+
*/
|
|
19
|
+
data: PublicKernelCircuitPublicInputs;
|
|
20
|
+
/**
|
|
21
|
+
* Hash of the transaction.
|
|
22
|
+
*/
|
|
23
|
+
hash: TxHash;
|
|
24
|
+
/**
|
|
25
|
+
* Flag indicating the tx is 'empty' meaning it's a padding tx to take us to a power of 2.
|
|
26
|
+
*/
|
|
27
|
+
isEmpty: boolean;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Represents a tx that failed to be processed by the sequencer public processor.
|
|
32
|
+
*/
|
|
33
|
+
export type FailedTx = {
|
|
34
|
+
/**
|
|
35
|
+
* The failing transaction.
|
|
36
|
+
*/
|
|
37
|
+
tx: Tx;
|
|
38
|
+
/**
|
|
39
|
+
* The error that caused the tx to fail.
|
|
40
|
+
*/
|
|
41
|
+
error: Error;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Makes a processed tx out of source tx.
|
|
46
|
+
* @param tx - Source tx.
|
|
47
|
+
* @param kernelOutput - Output of the kernel circuit simulation for this tx.
|
|
48
|
+
* @param proof - Proof of the kernel circuit for this tx.
|
|
49
|
+
*/
|
|
50
|
+
export async function makeProcessedTx(
|
|
51
|
+
tx: Tx,
|
|
52
|
+
kernelOutput?: PublicKernelCircuitPublicInputs,
|
|
53
|
+
proof?: Proof,
|
|
54
|
+
): Promise<ProcessedTx> {
|
|
55
|
+
return {
|
|
56
|
+
hash: await tx.getTxHash(),
|
|
57
|
+
data:
|
|
58
|
+
kernelOutput ??
|
|
59
|
+
new PublicKernelCircuitPublicInputs(
|
|
60
|
+
tx.data.aggregationObject,
|
|
61
|
+
tx.data.endNonRevertibleData,
|
|
62
|
+
CombinedAccumulatedData.fromFinalAccumulatedData(tx.data.end),
|
|
63
|
+
tx.data.constants,
|
|
64
|
+
tx.data.isPrivate,
|
|
65
|
+
),
|
|
66
|
+
proof: proof ?? tx.proof,
|
|
67
|
+
encryptedLogs: tx.encryptedLogs,
|
|
68
|
+
unencryptedLogs: tx.unencryptedLogs,
|
|
69
|
+
newContracts: tx.newContracts,
|
|
70
|
+
isEmpty: false,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Makes an empty tx from an empty kernel circuit public inputs.
|
|
76
|
+
* @returns A processed empty tx.
|
|
77
|
+
*/
|
|
78
|
+
export function makeEmptyProcessedTx(header: Header, chainId: Fr, version: Fr): Promise<ProcessedTx> {
|
|
79
|
+
const emptyKernelOutput = PublicKernelCircuitPublicInputs.empty();
|
|
80
|
+
emptyKernelOutput.constants.historicalHeader = header;
|
|
81
|
+
emptyKernelOutput.constants.txContext.chainId = chainId;
|
|
82
|
+
emptyKernelOutput.constants.txContext.version = version;
|
|
83
|
+
const emptyProof = makeEmptyProof();
|
|
84
|
+
|
|
85
|
+
const hash = new TxHash(Fr.ZERO.toBuffer());
|
|
86
|
+
return Promise.resolve({
|
|
87
|
+
hash,
|
|
88
|
+
encryptedLogs: new TxL2Logs([]),
|
|
89
|
+
unencryptedLogs: new TxL2Logs([]),
|
|
90
|
+
data: emptyKernelOutput,
|
|
91
|
+
proof: emptyProof,
|
|
92
|
+
newContracts: [ExtendedContractData.empty()],
|
|
93
|
+
isEmpty: true,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { ContractDataSource, L1ToL2MessageSource, Tx } from '@aztec/circuit-types';
|
|
2
|
+
import { TxSequencerProcessingStats } from '@aztec/circuit-types/stats';
|
|
3
|
+
import { GlobalVariables, Header, Proof, PublicKernelCircuitPublicInputs } from '@aztec/circuits.js';
|
|
4
|
+
import { createDebugLogger } from '@aztec/foundation/log';
|
|
5
|
+
import { Timer } from '@aztec/foundation/timer';
|
|
6
|
+
import { PublicExecutor, PublicStateDB } from '@aztec/simulator';
|
|
7
|
+
import { MerkleTreeOperations } from '@aztec/world-state';
|
|
8
|
+
|
|
9
|
+
import { EmptyPublicProver } from '../prover/empty.js';
|
|
10
|
+
import { PublicProver } from '../prover/index.js';
|
|
11
|
+
import { PublicKernelCircuitSimulator } from '../simulator/index.js';
|
|
12
|
+
import { ContractsDataSourcePublicDB, WorldStateDB, WorldStatePublicDB } from '../simulator/public_executor.js';
|
|
13
|
+
import { RealPublicKernelCircuitSimulator } from '../simulator/public_kernel.js';
|
|
14
|
+
import { AbstractPhaseManager } from './abstract_phase_manager.js';
|
|
15
|
+
import { FeePreparationPhaseManager } from './fee_preparation_phase_manager.js';
|
|
16
|
+
import { FailedTx, ProcessedTx, makeEmptyProcessedTx, makeProcessedTx } from './processed_tx.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Creates new instances of PublicProcessor given the provided merkle tree db and contract data source.
|
|
20
|
+
*/
|
|
21
|
+
export class PublicProcessorFactory {
|
|
22
|
+
constructor(
|
|
23
|
+
private merkleTree: MerkleTreeOperations,
|
|
24
|
+
private contractDataSource: ContractDataSource,
|
|
25
|
+
private l1Tol2MessagesDataSource: L1ToL2MessageSource,
|
|
26
|
+
) {}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Creates a new instance of a PublicProcessor.
|
|
30
|
+
* @param historicalHeader - The header of a block previous to the one in which the tx is included.
|
|
31
|
+
* @param globalVariables - The global variables for the block being processed.
|
|
32
|
+
* @param newContracts - Provides access to contract bytecode for public executions.
|
|
33
|
+
* @returns A new instance of a PublicProcessor.
|
|
34
|
+
*/
|
|
35
|
+
public async create(
|
|
36
|
+
historicalHeader: Header | undefined,
|
|
37
|
+
globalVariables: GlobalVariables,
|
|
38
|
+
): Promise<PublicProcessor> {
|
|
39
|
+
historicalHeader = historicalHeader ?? (await this.merkleTree.buildInitialHeader());
|
|
40
|
+
|
|
41
|
+
const publicContractsDB = new ContractsDataSourcePublicDB(this.contractDataSource);
|
|
42
|
+
const worldStatePublicDB = new WorldStatePublicDB(this.merkleTree);
|
|
43
|
+
const worldStateDB = new WorldStateDB(this.merkleTree, this.l1Tol2MessagesDataSource);
|
|
44
|
+
const publicExecutor = new PublicExecutor(worldStatePublicDB, publicContractsDB, worldStateDB, historicalHeader);
|
|
45
|
+
return new PublicProcessor(
|
|
46
|
+
this.merkleTree,
|
|
47
|
+
publicExecutor,
|
|
48
|
+
new RealPublicKernelCircuitSimulator(),
|
|
49
|
+
new EmptyPublicProver(),
|
|
50
|
+
globalVariables,
|
|
51
|
+
historicalHeader,
|
|
52
|
+
publicContractsDB,
|
|
53
|
+
worldStatePublicDB,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Converts Txs lifted from the P2P module into ProcessedTx objects by executing
|
|
60
|
+
* any public function calls in them. Txs with private calls only are unaffected.
|
|
61
|
+
*/
|
|
62
|
+
export class PublicProcessor {
|
|
63
|
+
constructor(
|
|
64
|
+
protected db: MerkleTreeOperations,
|
|
65
|
+
protected publicExecutor: PublicExecutor,
|
|
66
|
+
protected publicKernel: PublicKernelCircuitSimulator,
|
|
67
|
+
protected publicProver: PublicProver,
|
|
68
|
+
protected globalVariables: GlobalVariables,
|
|
69
|
+
protected historicalHeader: Header,
|
|
70
|
+
protected publicContractsDB: ContractsDataSourcePublicDB,
|
|
71
|
+
protected publicStateDB: PublicStateDB,
|
|
72
|
+
|
|
73
|
+
private log = createDebugLogger('aztec:sequencer:public-processor'),
|
|
74
|
+
) {}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Run each tx through the public circuit and the public kernel circuit if needed.
|
|
78
|
+
* @param txs - Txs to process.
|
|
79
|
+
* @returns The list of processed txs with their circuit simulation outputs.
|
|
80
|
+
*/
|
|
81
|
+
public async process(txs: Tx[]): Promise<[ProcessedTx[], FailedTx[]]> {
|
|
82
|
+
// The processor modifies the tx objects in place, so we need to clone them.
|
|
83
|
+
txs = txs.map(tx => Tx.clone(tx));
|
|
84
|
+
const result: ProcessedTx[] = [];
|
|
85
|
+
const failed: FailedTx[] = [];
|
|
86
|
+
|
|
87
|
+
for (const tx of txs) {
|
|
88
|
+
let phase: AbstractPhaseManager | undefined = new FeePreparationPhaseManager(
|
|
89
|
+
this.db,
|
|
90
|
+
this.publicExecutor,
|
|
91
|
+
this.publicKernel,
|
|
92
|
+
this.publicProver,
|
|
93
|
+
this.globalVariables,
|
|
94
|
+
this.historicalHeader,
|
|
95
|
+
this.publicContractsDB,
|
|
96
|
+
this.publicStateDB,
|
|
97
|
+
);
|
|
98
|
+
let publicKernelOutput: PublicKernelCircuitPublicInputs | undefined = undefined;
|
|
99
|
+
let publicKernelProof: Proof | undefined = undefined;
|
|
100
|
+
const timer = new Timer();
|
|
101
|
+
try {
|
|
102
|
+
while (phase) {
|
|
103
|
+
const output = await phase.handle(tx, publicKernelOutput, publicKernelProof);
|
|
104
|
+
publicKernelOutput = output.publicKernelOutput;
|
|
105
|
+
publicKernelProof = output.publicKernelProof;
|
|
106
|
+
phase = phase.nextPhase();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const processedTransaction = await makeProcessedTx(tx, publicKernelOutput, publicKernelProof);
|
|
110
|
+
result.push(processedTransaction);
|
|
111
|
+
|
|
112
|
+
this.log(`Processed public part of ${tx.data.end.newNullifiers[0]}`, {
|
|
113
|
+
eventName: 'tx-sequencer-processing',
|
|
114
|
+
duration: timer.ms(),
|
|
115
|
+
publicDataUpdateRequests:
|
|
116
|
+
processedTransaction.data.end.publicDataUpdateRequests.filter(x => !x.leafSlot.isZero()).length ?? 0,
|
|
117
|
+
...tx.getStats(),
|
|
118
|
+
} satisfies TxSequencerProcessingStats);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
const failedTx = await phase!.rollback(tx, err);
|
|
121
|
+
failed.push(failedTx);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return [result, failed];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Makes an empty processed tx. Useful for padding a block to a power of two number of txs.
|
|
130
|
+
* @returns A processed tx with empty data.
|
|
131
|
+
*/
|
|
132
|
+
public makeEmptyProcessedTx(): Promise<ProcessedTx> {
|
|
133
|
+
const { chainId, version } = this.globalVariables;
|
|
134
|
+
return makeEmptyProcessedTx(this.historicalHeader, chainId, version);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
import { L1ToL2MessageSource, L2Block, L2BlockSource, MerkleTreeId, Tx } from '@aztec/circuit-types';
|
|
2
|
+
import { L2BlockBuiltStats } from '@aztec/circuit-types/stats';
|
|
3
|
+
import { AztecAddress, EthAddress, GlobalVariables } from '@aztec/circuits.js';
|
|
4
|
+
import { times } from '@aztec/foundation/collection';
|
|
5
|
+
import { Fr } from '@aztec/foundation/fields';
|
|
6
|
+
import { createDebugLogger } from '@aztec/foundation/log';
|
|
7
|
+
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
8
|
+
import { Timer, elapsed } from '@aztec/foundation/timer';
|
|
9
|
+
import { P2P } from '@aztec/p2p';
|
|
10
|
+
import { WorldStateStatus, WorldStateSynchronizer } from '@aztec/world-state';
|
|
11
|
+
|
|
12
|
+
import { BlockBuilder } from '../block_builder/index.js';
|
|
13
|
+
import { GlobalVariableBuilder } from '../global_variable_builder/global_builder.js';
|
|
14
|
+
import { L1Publisher } from '../publisher/l1-publisher.js';
|
|
15
|
+
import { ceilPowerOfTwo } from '../utils.js';
|
|
16
|
+
import { SequencerConfig } from './config.js';
|
|
17
|
+
import { ProcessedTx } from './processed_tx.js';
|
|
18
|
+
import { PublicProcessorFactory } from './public_processor.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Sequencer client
|
|
22
|
+
* - Wins a period of time to become the sequencer (depending on finalized protocol).
|
|
23
|
+
* - Chooses a set of txs from the tx pool to be in the rollup.
|
|
24
|
+
* - Simulate the rollup of txs.
|
|
25
|
+
* - Adds proof requests to the request pool (not for this milestone).
|
|
26
|
+
* - Receives results to those proofs from the network (repeats as necessary) (not for this milestone).
|
|
27
|
+
* - Publishes L1 tx(s) to the rollup contract via RollupPublisher.
|
|
28
|
+
*/
|
|
29
|
+
export class Sequencer {
|
|
30
|
+
private runningPromise?: RunningPromise;
|
|
31
|
+
private pollingIntervalMs: number = 1000;
|
|
32
|
+
private maxTxsPerBlock = 32;
|
|
33
|
+
private minTxsPerBLock = 1;
|
|
34
|
+
// TODO: zero values should not be allowed for the following 2 values in PROD
|
|
35
|
+
private _coinbase = EthAddress.ZERO;
|
|
36
|
+
private _feeRecipient = AztecAddress.ZERO;
|
|
37
|
+
private lastPublishedBlock = 0;
|
|
38
|
+
private state = SequencerState.STOPPED;
|
|
39
|
+
|
|
40
|
+
constructor(
|
|
41
|
+
private publisher: L1Publisher,
|
|
42
|
+
private globalsBuilder: GlobalVariableBuilder,
|
|
43
|
+
private p2pClient: P2P,
|
|
44
|
+
private worldState: WorldStateSynchronizer,
|
|
45
|
+
private blockBuilder: BlockBuilder,
|
|
46
|
+
private l2BlockSource: L2BlockSource,
|
|
47
|
+
private l1ToL2MessageSource: L1ToL2MessageSource,
|
|
48
|
+
private publicProcessorFactory: PublicProcessorFactory,
|
|
49
|
+
config: SequencerConfig = {},
|
|
50
|
+
private log = createDebugLogger('aztec:sequencer'),
|
|
51
|
+
) {
|
|
52
|
+
this.updateConfig(config);
|
|
53
|
+
this.log(`Initialized sequencer with ${this.minTxsPerBLock}-${this.maxTxsPerBlock} txs per block.`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Updates sequencer config.
|
|
58
|
+
* @param config - New parameters.
|
|
59
|
+
*/
|
|
60
|
+
public updateConfig(config: SequencerConfig) {
|
|
61
|
+
if (config.transactionPollingIntervalMS) {
|
|
62
|
+
this.pollingIntervalMs = config.transactionPollingIntervalMS;
|
|
63
|
+
}
|
|
64
|
+
if (config.maxTxsPerBlock) {
|
|
65
|
+
this.maxTxsPerBlock = config.maxTxsPerBlock;
|
|
66
|
+
}
|
|
67
|
+
if (config.minTxsPerBlock) {
|
|
68
|
+
this.minTxsPerBLock = config.minTxsPerBlock;
|
|
69
|
+
}
|
|
70
|
+
if (config.coinbase) {
|
|
71
|
+
this._coinbase = config.coinbase;
|
|
72
|
+
}
|
|
73
|
+
if (config.feeRecipient) {
|
|
74
|
+
this._feeRecipient = config.feeRecipient;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Starts the sequencer and moves to IDLE state. Blocks until the initial sync is complete.
|
|
80
|
+
*/
|
|
81
|
+
public async start() {
|
|
82
|
+
await this.initialSync();
|
|
83
|
+
|
|
84
|
+
this.runningPromise = new RunningPromise(this.work.bind(this), this.pollingIntervalMs);
|
|
85
|
+
this.runningPromise.start();
|
|
86
|
+
this.state = SequencerState.IDLE;
|
|
87
|
+
this.log('Sequencer started');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Stops the sequencer from processing txs and moves to STOPPED state.
|
|
92
|
+
*/
|
|
93
|
+
public async stop(): Promise<void> {
|
|
94
|
+
this.log(`Stopping sequencer`);
|
|
95
|
+
await this.runningPromise?.stop();
|
|
96
|
+
this.publisher.interrupt();
|
|
97
|
+
this.state = SequencerState.STOPPED;
|
|
98
|
+
this.log('Stopped sequencer');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Starts a previously stopped sequencer.
|
|
103
|
+
*/
|
|
104
|
+
public restart() {
|
|
105
|
+
this.log('Restarting sequencer');
|
|
106
|
+
this.publisher.restart();
|
|
107
|
+
this.runningPromise!.start();
|
|
108
|
+
this.state = SequencerState.IDLE;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Returns the current state of the sequencer.
|
|
113
|
+
* @returns An object with a state entry with one of SequencerState.
|
|
114
|
+
*/
|
|
115
|
+
public status() {
|
|
116
|
+
return { state: this.state };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
protected async initialSync() {
|
|
120
|
+
// TODO: Should we wait for world state to be ready, or is the caller expected to run await start?
|
|
121
|
+
this.lastPublishedBlock = await this.worldState.status().then((s: WorldStateStatus) => s.syncedToL2Block);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Grabs up to maxTxsPerBlock from the p2p client, constructs a block, and pushes it to L1.
|
|
126
|
+
*/
|
|
127
|
+
protected async work() {
|
|
128
|
+
try {
|
|
129
|
+
// Update state when the previous block has been synced
|
|
130
|
+
const prevBlockSynced = await this.isBlockSynced();
|
|
131
|
+
if (prevBlockSynced && this.state === SequencerState.PUBLISHING_BLOCK) {
|
|
132
|
+
this.log(`Block has been synced`);
|
|
133
|
+
this.state = SequencerState.IDLE;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Do not go forward with new block if the previous one has not been mined and processed
|
|
137
|
+
if (!prevBlockSynced) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const workTimer = new Timer();
|
|
142
|
+
this.state = SequencerState.WAITING_FOR_TXS;
|
|
143
|
+
|
|
144
|
+
// Get txs to build the new block
|
|
145
|
+
const pendingTxs = await this.p2pClient.getTxs();
|
|
146
|
+
if (pendingTxs.length < this.minTxsPerBLock) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
this.log.info(`Retrieved ${pendingTxs.length} txs from P2P pool`);
|
|
150
|
+
|
|
151
|
+
const historicalHeader = (await this.l2BlockSource.getBlock(-1))?.header;
|
|
152
|
+
const newBlockNumber =
|
|
153
|
+
(historicalHeader === undefined
|
|
154
|
+
? await this.l2BlockSource.getBlockNumber()
|
|
155
|
+
: Number(historicalHeader.globalVariables.blockNumber.toBigInt())) + 1;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* We'll call this function before running expensive operations to avoid wasted work.
|
|
159
|
+
*/
|
|
160
|
+
const assertBlockHeight = async () => {
|
|
161
|
+
const currentBlockNumber = await this.l2BlockSource.getBlockNumber();
|
|
162
|
+
if (currentBlockNumber + 1 !== newBlockNumber) {
|
|
163
|
+
throw new Error('New block was emitted while building block');
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const newGlobalVariables = await this.globalsBuilder.buildGlobalVariables(
|
|
168
|
+
new Fr(newBlockNumber),
|
|
169
|
+
this._coinbase,
|
|
170
|
+
this._feeRecipient,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
// Filter out invalid txs
|
|
174
|
+
// TODO: It should be responsibility of the P2P layer to validate txs before passing them on here
|
|
175
|
+
const validTxs = await this.takeValidTxs(pendingTxs, newGlobalVariables);
|
|
176
|
+
if (validTxs.length < this.minTxsPerBLock) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
this.log.info(`Building block ${newBlockNumber} with ${validTxs.length} transactions`);
|
|
181
|
+
this.state = SequencerState.CREATING_BLOCK;
|
|
182
|
+
|
|
183
|
+
// Process txs and drop the ones that fail processing
|
|
184
|
+
// We create a fresh processor each time to reset any cached state (eg storage writes)
|
|
185
|
+
const processor = await this.publicProcessorFactory.create(historicalHeader, newGlobalVariables);
|
|
186
|
+
const [publicProcessorDuration, [processedTxs, failedTxs]] = await elapsed(() => processor.process(validTxs));
|
|
187
|
+
if (failedTxs.length > 0) {
|
|
188
|
+
const failedTxData = failedTxs.map(fail => fail.tx);
|
|
189
|
+
this.log(`Dropping failed txs ${(await Tx.getHashes(failedTxData)).join(', ')}`);
|
|
190
|
+
await this.p2pClient.deleteTxs(await Tx.getHashes(failedTxData));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Only accept processed transactions that are not double-spends,
|
|
194
|
+
// public functions emitting nullifiers would pass earlier check but fail here.
|
|
195
|
+
// Note that we're checking all nullifiers generated in the private execution twice,
|
|
196
|
+
// we could store the ones already checked and skip them here as an optimization.
|
|
197
|
+
const processedValidTxs = await this.takeValidTxs(processedTxs, newGlobalVariables);
|
|
198
|
+
|
|
199
|
+
if (processedValidTxs.length === 0) {
|
|
200
|
+
this.log('No txs processed correctly to build block. Exiting');
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
await assertBlockHeight();
|
|
205
|
+
|
|
206
|
+
// Get l1 to l2 messages from the contract
|
|
207
|
+
this.log('Requesting L1 to L2 messages from contract');
|
|
208
|
+
const l1ToL2Messages = await this.getPendingL1ToL2Messages();
|
|
209
|
+
this.log('Successfully retrieved L1 to L2 messages from contract');
|
|
210
|
+
|
|
211
|
+
// Build the new block by running the rollup circuits
|
|
212
|
+
this.log(`Assembling block with txs ${processedValidTxs.map(tx => tx.hash).join(', ')}`);
|
|
213
|
+
|
|
214
|
+
await assertBlockHeight();
|
|
215
|
+
|
|
216
|
+
const emptyTx = await processor.makeEmptyProcessedTx();
|
|
217
|
+
const [rollupCircuitsDuration, block] = await elapsed(() =>
|
|
218
|
+
this.buildBlock(processedValidTxs, l1ToL2Messages, emptyTx, newGlobalVariables),
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
this.log(`Assembled block ${block.number}`, {
|
|
222
|
+
eventName: 'l2-block-built',
|
|
223
|
+
duration: workTimer.ms(),
|
|
224
|
+
publicProcessDuration: publicProcessorDuration,
|
|
225
|
+
rollupCircuitsDuration: rollupCircuitsDuration,
|
|
226
|
+
...block.getStats(),
|
|
227
|
+
} satisfies L2BlockBuiltStats);
|
|
228
|
+
|
|
229
|
+
await assertBlockHeight();
|
|
230
|
+
|
|
231
|
+
await this.publishExtendedContractData(processedValidTxs, block);
|
|
232
|
+
|
|
233
|
+
await assertBlockHeight();
|
|
234
|
+
|
|
235
|
+
await this.publishL2Block(block);
|
|
236
|
+
this.log.info(`Submitted rollup block ${block.number} with ${processedValidTxs.length} transactions`);
|
|
237
|
+
} catch (err) {
|
|
238
|
+
this.log.error(`Rolling back world state DB due to error assembling block`, (err as any).stack);
|
|
239
|
+
await this.worldState.getLatest().rollback();
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Gets new extended contract data from the txs and publishes it on chain.
|
|
245
|
+
* @param validTxs - The set of real transactions being published as part of the block.
|
|
246
|
+
* @param block - The L2Block to be published.
|
|
247
|
+
*/
|
|
248
|
+
protected async publishExtendedContractData(validTxs: ProcessedTx[], block: L2Block) {
|
|
249
|
+
// Publishes contract data for txs to the network and awaits the tx to be mined
|
|
250
|
+
this.state = SequencerState.PUBLISHING_CONTRACT_DATA;
|
|
251
|
+
const newContracts = validTxs.flatMap(tx => tx.newContracts).filter(cd => !cd.isEmpty());
|
|
252
|
+
|
|
253
|
+
if (newContracts.length === 0) {
|
|
254
|
+
this.log.debug(`No new contracts to publish in block ${block.number}`);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const blockCalldataHash = block.getCalldataHash();
|
|
259
|
+
this.log.info(`Publishing ${newContracts.length} contracts in block ${block.number}`);
|
|
260
|
+
|
|
261
|
+
const publishedContractData = await this.publisher.processNewContractData(
|
|
262
|
+
block.number,
|
|
263
|
+
blockCalldataHash,
|
|
264
|
+
newContracts,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
if (publishedContractData) {
|
|
268
|
+
this.log(`Successfully published new contract data for block ${block.number}`);
|
|
269
|
+
} else if (!publishedContractData && newContracts.length) {
|
|
270
|
+
this.log(`Failed to publish new contract data for block ${block.number}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Publishes the L2Block to the rollup contract.
|
|
276
|
+
* @param block - The L2Block to be published.
|
|
277
|
+
*/
|
|
278
|
+
protected async publishL2Block(block: L2Block) {
|
|
279
|
+
// Publishes new block to the network and awaits the tx to be mined
|
|
280
|
+
this.state = SequencerState.PUBLISHING_BLOCK;
|
|
281
|
+
const publishedL2Block = await this.publisher.processL2Block(block);
|
|
282
|
+
if (publishedL2Block) {
|
|
283
|
+
this.log(`Successfully published block ${block.number}`);
|
|
284
|
+
this.lastPublishedBlock = block.number;
|
|
285
|
+
} else {
|
|
286
|
+
throw new Error(`Failed to publish block`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
protected async takeValidTxs<T extends Tx | ProcessedTx>(txs: T[], globalVariables: GlobalVariables): Promise<T[]> {
|
|
291
|
+
const validTxs: T[] = [];
|
|
292
|
+
const txsToDelete = [];
|
|
293
|
+
const thisBlockNullifiers: Set<bigint> = new Set();
|
|
294
|
+
|
|
295
|
+
// Process txs until we get to maxTxsPerBlock, rejecting double spends in the process
|
|
296
|
+
for (const tx of txs) {
|
|
297
|
+
if (tx.data.constants.txContext.chainId.value !== globalVariables.chainId.value) {
|
|
298
|
+
this.log(
|
|
299
|
+
`Deleting tx for incorrect chain ${tx.data.constants.txContext.chainId.toString()}, tx hash ${await Tx.getHash(
|
|
300
|
+
tx,
|
|
301
|
+
)}`,
|
|
302
|
+
);
|
|
303
|
+
txsToDelete.push(tx);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
if (await this.isTxDoubleSpend(tx)) {
|
|
307
|
+
this.log(`Deleting double spend tx ${await Tx.getHash(tx)}`);
|
|
308
|
+
txsToDelete.push(tx);
|
|
309
|
+
continue;
|
|
310
|
+
} else if (this.isTxDoubleSpendSameBlock(tx, thisBlockNullifiers)) {
|
|
311
|
+
// We don't drop these txs from the p2p pool immediately since they become valid
|
|
312
|
+
// again if the current block fails to be published for some reason.
|
|
313
|
+
this.log(`Skipping tx with double-spend for this same block ${await Tx.getHash(tx)}`);
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
tx.data.end.newNullifiers.forEach(n => thisBlockNullifiers.add(n.value.toBigInt()));
|
|
318
|
+
validTxs.push(tx);
|
|
319
|
+
if (validTxs.length >= this.maxTxsPerBlock) {
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Make sure we remove these from the tx pool so we do not consider it again
|
|
325
|
+
if (txsToDelete.length > 0) {
|
|
326
|
+
await this.p2pClient.deleteTxs(await Tx.getHashes([...txsToDelete]));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return validTxs;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Returns whether the previous block sent has been mined, and all dependencies have caught up with it.
|
|
334
|
+
* @returns Boolean indicating if our dependencies are synced to the latest block.
|
|
335
|
+
*/
|
|
336
|
+
protected async isBlockSynced() {
|
|
337
|
+
const syncedBlocks = await Promise.all([
|
|
338
|
+
this.worldState.status().then((s: WorldStateStatus) => s.syncedToL2Block),
|
|
339
|
+
this.p2pClient.getStatus().then(s => s.syncedToL2Block),
|
|
340
|
+
this.l2BlockSource.getBlockNumber(),
|
|
341
|
+
this.l1ToL2MessageSource.getBlockNumber(),
|
|
342
|
+
]);
|
|
343
|
+
const min = Math.min(...syncedBlocks);
|
|
344
|
+
return min >= this.lastPublishedBlock;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Pads the set of txs to a power of two and assembles a block by calling the block builder.
|
|
349
|
+
* @param txs - Processed txs to include in the next block.
|
|
350
|
+
* @param newL1ToL2Messages - L1 to L2 messages to be part of the block.
|
|
351
|
+
* @param emptyTx - Empty tx to repeat at the end of the block to pad to a power of two.
|
|
352
|
+
* @param globalVariables - Global variables to use in the block.
|
|
353
|
+
* @returns The new block.
|
|
354
|
+
*/
|
|
355
|
+
protected async buildBlock(
|
|
356
|
+
txs: ProcessedTx[],
|
|
357
|
+
newL1ToL2Messages: Fr[],
|
|
358
|
+
emptyTx: ProcessedTx,
|
|
359
|
+
globalVariables: GlobalVariables,
|
|
360
|
+
) {
|
|
361
|
+
// Pad the txs array with empty txs to be a power of two, at least 2
|
|
362
|
+
const txsTargetSize = Math.max(ceilPowerOfTwo(txs.length), 2);
|
|
363
|
+
const emptyTxCount = txsTargetSize - txs.length;
|
|
364
|
+
|
|
365
|
+
const allTxs = [...txs, ...times(emptyTxCount, () => emptyTx)];
|
|
366
|
+
this.log(`Building block ${globalVariables.blockNumber}`);
|
|
367
|
+
|
|
368
|
+
const [block] = await this.blockBuilder.buildL2Block(globalVariables, allTxs, newL1ToL2Messages);
|
|
369
|
+
return block;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Calls the archiver to pull upto `NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP` message keys
|
|
374
|
+
* (archiver returns the top messages sorted by fees)
|
|
375
|
+
* @returns An array of L1 to L2 messages' messageKeys
|
|
376
|
+
*/
|
|
377
|
+
protected async getPendingL1ToL2Messages(): Promise<Fr[]> {
|
|
378
|
+
return await this.l1ToL2MessageSource.getPendingL1ToL2Messages();
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Returns true if one of the tx nullifiers exist on the block being built.
|
|
383
|
+
* @param tx - The tx to test.
|
|
384
|
+
* @param thisBlockNullifiers - The nullifiers added so far.
|
|
385
|
+
*/
|
|
386
|
+
protected isTxDoubleSpendSameBlock(tx: Tx | ProcessedTx, thisBlockNullifiers: Set<bigint>): boolean {
|
|
387
|
+
// We only consider non-empty nullifiers
|
|
388
|
+
const newNullifiers = tx.data.end.newNullifiers.filter(n => !n.isEmpty());
|
|
389
|
+
|
|
390
|
+
for (const nullifier of newNullifiers) {
|
|
391
|
+
if (thisBlockNullifiers.has(nullifier.value.toBigInt())) {
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Returns true if one of the transaction nullifiers exist.
|
|
400
|
+
* Nullifiers prevent double spends in a private context.
|
|
401
|
+
* @param tx - The transaction.
|
|
402
|
+
* @returns Whether this is a problematic double spend that the L1 contract would reject.
|
|
403
|
+
*/
|
|
404
|
+
protected async isTxDoubleSpend(tx: Tx | ProcessedTx): Promise<boolean> {
|
|
405
|
+
// We only consider non-empty nullifiers
|
|
406
|
+
const newNullifiers = tx.data.end.newNullifiers.filter(n => !n.isEmpty());
|
|
407
|
+
|
|
408
|
+
// Ditch this tx if it has a repeated nullifiers
|
|
409
|
+
const uniqNullifiers = new Set(newNullifiers.map(n => n.value.toBigInt()));
|
|
410
|
+
if (uniqNullifiers.size !== newNullifiers.length) {
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
for (const nullifier of newNullifiers) {
|
|
415
|
+
// TODO(AD): this is an exhaustive search currently
|
|
416
|
+
const db = this.worldState.getLatest();
|
|
417
|
+
const indexInDb = await db.findLeafIndex(MerkleTreeId.NULLIFIER_TREE, nullifier.toBuffer());
|
|
418
|
+
if (indexInDb !== undefined) {
|
|
419
|
+
return true;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
get coinbase(): EthAddress {
|
|
426
|
+
return this._coinbase;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
get feeRecipient(): AztecAddress {
|
|
430
|
+
return this._feeRecipient;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* State of the sequencer.
|
|
436
|
+
*/
|
|
437
|
+
export enum SequencerState {
|
|
438
|
+
/**
|
|
439
|
+
* Will move to WAITING_FOR_TXS after a configured amount of time.
|
|
440
|
+
*/
|
|
441
|
+
IDLE,
|
|
442
|
+
/**
|
|
443
|
+
* Polling the P2P module for txs to include in a block. Will move to CREATING_BLOCK if there are valid txs to include, or back to IDLE otherwise.
|
|
444
|
+
*/
|
|
445
|
+
WAITING_FOR_TXS,
|
|
446
|
+
/**
|
|
447
|
+
* Creating a new L2 block. Includes processing public function calls and running rollup circuits. Will move to PUBLISHING_CONTRACT_DATA.
|
|
448
|
+
*/
|
|
449
|
+
CREATING_BLOCK,
|
|
450
|
+
/**
|
|
451
|
+
* Sending the tx to L1 with encrypted logs and awaiting it to be mined. Will move back to PUBLISHING_BLOCK once finished.
|
|
452
|
+
*/
|
|
453
|
+
PUBLISHING_CONTRACT_DATA,
|
|
454
|
+
/**
|
|
455
|
+
* Sending the tx to L1 with the L2 block data and awaiting it to be mined. Will move to IDLE.
|
|
456
|
+
*/
|
|
457
|
+
PUBLISHING_BLOCK,
|
|
458
|
+
/**
|
|
459
|
+
* Sequencer is stopped and not processing any txs from the pool.
|
|
460
|
+
*/
|
|
461
|
+
STOPPED,
|
|
462
|
+
}
|