@aztec/sequencer-client 0.69.0 → 0.69.1-devnet
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/client/sequencer-client.d.ts +4 -0
- package/dest/client/sequencer-client.d.ts.map +1 -1
- package/dest/client/sequencer-client.js +4 -5
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +11 -1
- package/dest/index.d.ts +3 -1
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +4 -2
- package/dest/publisher/config.d.ts +4 -0
- package/dest/publisher/config.d.ts.map +1 -1
- package/dest/publisher/config.js +6 -1
- package/dest/publisher/index.d.ts +0 -1
- package/dest/publisher/index.d.ts.map +1 -1
- package/dest/publisher/index.js +1 -2
- package/dest/publisher/l1-publisher.d.ts +43 -12
- package/dest/publisher/l1-publisher.d.ts.map +1 -1
- package/dest/publisher/l1-publisher.js +184 -132
- package/dest/sequencer/index.d.ts +1 -0
- package/dest/sequencer/index.d.ts.map +1 -1
- package/dest/sequencer/index.js +2 -1
- package/dest/sequencer/sequencer.d.ts +22 -29
- package/dest/sequencer/sequencer.d.ts.map +1 -1
- package/dest/sequencer/sequencer.js +68 -132
- package/dest/sequencer/utils.d.ts +2 -2
- package/dest/sequencer/utils.d.ts.map +1 -1
- package/dest/sequencer/utils.js +3 -3
- package/dest/slasher/factory.d.ts +11 -0
- package/dest/slasher/factory.d.ts.map +1 -0
- package/dest/slasher/factory.js +10 -0
- package/dest/slasher/index.d.ts +3 -0
- package/dest/slasher/index.d.ts.map +1 -0
- package/dest/slasher/index.js +3 -0
- package/dest/slasher/slasher_client.d.ts +127 -0
- package/dest/slasher/slasher_client.d.ts.map +1 -0
- package/dest/slasher/slasher_client.js +305 -0
- package/dest/test/index.d.ts +18 -0
- package/dest/test/index.d.ts.map +1 -0
- package/dest/test/index.js +8 -0
- package/dest/{publisher → test}/test-l1-publisher.d.ts +1 -1
- package/dest/test/test-l1-publisher.d.ts.map +1 -0
- package/dest/test/test-l1-publisher.js +11 -0
- package/dest/tx_validator/archive_cache.d.ts +14 -0
- package/dest/tx_validator/archive_cache.d.ts.map +1 -0
- package/dest/tx_validator/archive_cache.js +22 -0
- package/dest/tx_validator/gas_validator.d.ts +2 -3
- package/dest/tx_validator/gas_validator.d.ts.map +1 -1
- package/dest/tx_validator/gas_validator.js +9 -22
- package/dest/tx_validator/nullifier_cache.d.ts +16 -0
- package/dest/tx_validator/nullifier_cache.d.ts.map +1 -0
- package/dest/tx_validator/nullifier_cache.js +24 -0
- package/dest/tx_validator/phases_validator.d.ts +2 -3
- package/dest/tx_validator/phases_validator.d.ts.map +1 -1
- package/dest/tx_validator/phases_validator.js +15 -24
- package/dest/tx_validator/tx_validator_factory.d.ts +15 -14
- package/dest/tx_validator/tx_validator_factory.d.ts.map +1 -1
- package/dest/tx_validator/tx_validator_factory.js +41 -24
- package/package.json +24 -20
- package/src/client/sequencer-client.ts +9 -3
- package/src/config.ts +10 -0
- package/src/index.ts +3 -1
- package/src/publisher/config.ts +10 -0
- package/src/publisher/index.ts +0 -1
- package/src/publisher/l1-publisher.ts +222 -137
- package/src/sequencer/index.ts +1 -0
- package/src/sequencer/sequencer.ts +91 -195
- package/src/sequencer/utils.ts +2 -2
- package/src/slasher/factory.ts +22 -0
- package/src/slasher/index.ts +2 -0
- package/src/slasher/slasher_client.ts +402 -0
- package/src/test/index.ts +23 -0
- package/src/{publisher → test}/test-l1-publisher.ts +1 -1
- package/src/tx_validator/archive_cache.ts +27 -0
- package/src/tx_validator/gas_validator.ts +11 -24
- package/src/tx_validator/nullifier_cache.ts +29 -0
- package/src/tx_validator/phases_validator.ts +22 -33
- package/src/tx_validator/tx_validator_factory.ts +89 -40
- package/dest/publisher/test-l1-publisher.d.ts.map +0 -1
- package/dest/publisher/test-l1-publisher.js +0 -11
- package/dest/publisher/utils.d.ts +0 -2
- package/dest/publisher/utils.d.ts.map +0 -1
- package/dest/publisher/utils.js +0 -13
- package/src/publisher/utils.ts +0 -14
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type L2Block,
|
|
3
|
+
type L2BlockId,
|
|
4
|
+
type L2BlockSource,
|
|
5
|
+
L2BlockStream,
|
|
6
|
+
type L2BlockStreamEvent,
|
|
7
|
+
type L2Tips,
|
|
8
|
+
} from '@aztec/circuit-types';
|
|
9
|
+
import { INITIAL_L2_BLOCK_NUM } from '@aztec/circuits.js/constants';
|
|
10
|
+
import { type L1ContractsConfig, type L1ReaderConfig, createEthereumChain } from '@aztec/ethereum';
|
|
11
|
+
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
12
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
13
|
+
import { type AztecKVStore, type AztecMap, type AztecSingleton } from '@aztec/kv-store';
|
|
14
|
+
import { SlashFactoryAbi } from '@aztec/l1-artifacts';
|
|
15
|
+
import { type TelemetryClient, WithTracer } from '@aztec/telemetry-client';
|
|
16
|
+
import { NoopTelemetryClient } from '@aztec/telemetry-client/noop';
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
type Chain,
|
|
20
|
+
type GetContractReturnType,
|
|
21
|
+
type HttpTransport,
|
|
22
|
+
type PublicClient,
|
|
23
|
+
createPublicClient,
|
|
24
|
+
getAddress,
|
|
25
|
+
getContract,
|
|
26
|
+
http,
|
|
27
|
+
} from 'viem';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Enum defining the possible states of the Slasher client.
|
|
31
|
+
*/
|
|
32
|
+
export enum SlasherClientState {
|
|
33
|
+
IDLE,
|
|
34
|
+
SYNCHING,
|
|
35
|
+
RUNNING,
|
|
36
|
+
STOPPED,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The synchronization status of the Slasher client.
|
|
41
|
+
*/
|
|
42
|
+
export interface SlasherSyncState {
|
|
43
|
+
/**
|
|
44
|
+
* The current state of the slasher client.
|
|
45
|
+
*/
|
|
46
|
+
state: SlasherClientState;
|
|
47
|
+
/**
|
|
48
|
+
* The block number that the slasher client is synced to.
|
|
49
|
+
*/
|
|
50
|
+
syncedToL2Block: L2BlockId;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface SlasherConfig {
|
|
54
|
+
blockCheckIntervalMS: number;
|
|
55
|
+
blockRequestBatchSize: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type SlashEvent = {
|
|
59
|
+
epoch: bigint;
|
|
60
|
+
amount: bigint;
|
|
61
|
+
lifetime: bigint;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @notice A Hypomeiones slasher client implementation
|
|
66
|
+
*
|
|
67
|
+
* Hypomeiones: a class of individuals in ancient Sparta who were considered inferior or lesser citizens compared
|
|
68
|
+
* to the full Spartan citizens.
|
|
69
|
+
*
|
|
70
|
+
* The implementation here is less than ideal. It exists, not to be the end all be all, but to show that
|
|
71
|
+
* slashing can be done with this mechanism.
|
|
72
|
+
*
|
|
73
|
+
* The implementation is VERY brute in the sense that it only looks for pruned blocks and then tries to slash
|
|
74
|
+
* the full committee of that.
|
|
75
|
+
* If it sees a prune, it will mark the full epoch as "to be slashed".
|
|
76
|
+
*
|
|
77
|
+
* Also, it is not particularly smart around what it should if there were to be multiple slashing events.
|
|
78
|
+
*
|
|
79
|
+
* A few improvements:
|
|
80
|
+
* - Only vote on the proposal if it is possible to reach, e.g., if 6 votes are needed and only 4 slots are left don't vote.
|
|
81
|
+
* - Stop voting on a payload once it is processed.
|
|
82
|
+
* - Only vote on the proposal if it have not already been executed
|
|
83
|
+
* - Caveat, we need to fully decide if it is acceptable to have the same payload address multiple times. In the current
|
|
84
|
+
* slash factory that could mean slashing the same committee for the same error multiple times.
|
|
85
|
+
* - Decide how to deal with multiple slashing events in the same round.
|
|
86
|
+
* - This could be that multiple epochs are pruned in the same round, but with the current naive implementation we could end up
|
|
87
|
+
* slashing only the first, because the "lifetime" of the second would have passed after that vote
|
|
88
|
+
*/
|
|
89
|
+
export class SlasherClient extends WithTracer {
|
|
90
|
+
private currentState = SlasherClientState.IDLE;
|
|
91
|
+
private syncPromise = Promise.resolve();
|
|
92
|
+
private syncResolve?: () => void = undefined;
|
|
93
|
+
private latestBlockNumberAtStart = -1;
|
|
94
|
+
private provenBlockNumberAtStart = -1;
|
|
95
|
+
|
|
96
|
+
private synchedBlockHashes: AztecMap<number, string>;
|
|
97
|
+
private synchedLatestBlockNumber: AztecSingleton<number>;
|
|
98
|
+
private synchedProvenBlockNumber: AztecSingleton<number>;
|
|
99
|
+
|
|
100
|
+
private blockStream;
|
|
101
|
+
|
|
102
|
+
private slashEvents: SlashEvent[] = [];
|
|
103
|
+
|
|
104
|
+
protected slashFactoryContract?: GetContractReturnType<typeof SlashFactoryAbi, PublicClient<HttpTransport, Chain>> =
|
|
105
|
+
undefined;
|
|
106
|
+
|
|
107
|
+
// The amount to slash for a prune.
|
|
108
|
+
// Note that we set it to 0, such that no actual slashing will happen, but the event will be fired,
|
|
109
|
+
// showing that the slashing mechanism is working.
|
|
110
|
+
private slashingAmount: bigint = 0n;
|
|
111
|
+
|
|
112
|
+
constructor(
|
|
113
|
+
private config: SlasherConfig & L1ContractsConfig & L1ReaderConfig,
|
|
114
|
+
private store: AztecKVStore,
|
|
115
|
+
private l2BlockSource: L2BlockSource,
|
|
116
|
+
telemetry: TelemetryClient = new NoopTelemetryClient(),
|
|
117
|
+
private log = createLogger('slasher'),
|
|
118
|
+
) {
|
|
119
|
+
super(telemetry, 'slasher');
|
|
120
|
+
|
|
121
|
+
this.blockStream = new L2BlockStream(l2BlockSource, this, this, createLogger('slasher:block_stream'), {
|
|
122
|
+
batchSize: config.blockRequestBatchSize,
|
|
123
|
+
pollIntervalMS: config.blockCheckIntervalMS,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
this.synchedBlockHashes = store.openMap('slasher_block_hashes');
|
|
127
|
+
this.synchedLatestBlockNumber = store.openSingleton('slasher_last_l2_block');
|
|
128
|
+
this.synchedProvenBlockNumber = store.openSingleton('slasher_last_proven_l2_block');
|
|
129
|
+
|
|
130
|
+
if (config.l1Contracts.slashFactoryAddress && config.l1Contracts.slashFactoryAddress !== EthAddress.ZERO) {
|
|
131
|
+
const chain = createEthereumChain(config.l1RpcUrl, config.l1ChainId);
|
|
132
|
+
const publicClient = createPublicClient({
|
|
133
|
+
chain: chain.chainInfo,
|
|
134
|
+
transport: http(chain.rpcUrl),
|
|
135
|
+
pollingInterval: config.viemPollingIntervalMS,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
this.slashFactoryContract = getContract({
|
|
139
|
+
address: getAddress(config.l1Contracts.slashFactoryAddress.toString()),
|
|
140
|
+
abi: SlashFactoryAbi,
|
|
141
|
+
client: publicClient,
|
|
142
|
+
});
|
|
143
|
+
} else {
|
|
144
|
+
this.log.warn('No slash factory address found, slashing will not be enabled');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
this.log.info(`Slasher client initialized`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// This is where we should put a bunch of the improvements mentioned earlier.
|
|
151
|
+
public async getSlashPayload(slotNumber: bigint): Promise<EthAddress | undefined> {
|
|
152
|
+
if (!this.slashFactoryContract) {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// As long as the slot is greater than the lifetime, we want to keep deleting the first element
|
|
157
|
+
// since it will not make sense to include anymore.
|
|
158
|
+
while (this.slashEvents.length > 0 && this.slashEvents[0].lifetime < slotNumber) {
|
|
159
|
+
this.slashEvents.shift();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (this.slashEvents.length == 0) {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const slashEvent = this.slashEvents[0];
|
|
167
|
+
|
|
168
|
+
const [payloadAddress, isDeployed] = await this.slashFactoryContract.read.getAddressAndIsDeployed([
|
|
169
|
+
slashEvent.epoch,
|
|
170
|
+
slashEvent.amount,
|
|
171
|
+
]);
|
|
172
|
+
|
|
173
|
+
if (!isDeployed) {
|
|
174
|
+
// The proposal cannot be executed until it is deployed
|
|
175
|
+
this.log.verbose(`Voting on not yet deployed payload: ${payloadAddress}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return EthAddress.fromString(payloadAddress);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
public getL2BlockHash(number: number): Promise<string | undefined> {
|
|
182
|
+
return Promise.resolve(this.synchedBlockHashes.get(number));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
public getL2Tips(): Promise<L2Tips> {
|
|
186
|
+
const latestBlockNumber = this.getSyncedLatestBlockNum();
|
|
187
|
+
let latestBlockHash: string | undefined;
|
|
188
|
+
const provenBlockNumber = this.getSyncedProvenBlockNum();
|
|
189
|
+
let provenBlockHash: string | undefined;
|
|
190
|
+
|
|
191
|
+
if (latestBlockNumber > 0) {
|
|
192
|
+
latestBlockHash = this.synchedBlockHashes.get(latestBlockNumber);
|
|
193
|
+
if (typeof latestBlockHash === 'undefined') {
|
|
194
|
+
this.log.warn(`Block hash for latest block ${latestBlockNumber} not found`);
|
|
195
|
+
throw new Error();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (provenBlockNumber > 0) {
|
|
200
|
+
provenBlockHash = this.synchedBlockHashes.get(provenBlockNumber);
|
|
201
|
+
if (typeof provenBlockHash === 'undefined') {
|
|
202
|
+
this.log.warn(`Block hash for proven block ${provenBlockNumber} not found`);
|
|
203
|
+
throw new Error();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return Promise.resolve({
|
|
208
|
+
latest: { hash: latestBlockHash!, number: latestBlockNumber },
|
|
209
|
+
proven: { hash: provenBlockHash!, number: provenBlockNumber },
|
|
210
|
+
finalized: { hash: provenBlockHash!, number: provenBlockNumber },
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
public async handleBlockStreamEvent(event: L2BlockStreamEvent): Promise<void> {
|
|
215
|
+
this.log.debug(`Handling block stream event ${event.type}`);
|
|
216
|
+
switch (event.type) {
|
|
217
|
+
case 'blocks-added':
|
|
218
|
+
await this.handleLatestL2Blocks(event.blocks);
|
|
219
|
+
break;
|
|
220
|
+
case 'chain-finalized':
|
|
221
|
+
// TODO (alexg): I think we can prune the block hashes map here
|
|
222
|
+
break;
|
|
223
|
+
case 'chain-proven': {
|
|
224
|
+
const from = this.getSyncedProvenBlockNum() + 1;
|
|
225
|
+
const limit = event.blockNumber - from + 1;
|
|
226
|
+
await this.handleProvenL2Blocks(await this.l2BlockSource.getBlocks(from, limit));
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
case 'chain-pruned':
|
|
230
|
+
await this.handlePruneL2Blocks(event.blockNumber);
|
|
231
|
+
break;
|
|
232
|
+
default: {
|
|
233
|
+
const _: never = event;
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
public async start() {
|
|
240
|
+
if (this.currentState === SlasherClientState.STOPPED) {
|
|
241
|
+
throw new Error('Slasher already stopped');
|
|
242
|
+
}
|
|
243
|
+
if (this.currentState !== SlasherClientState.IDLE) {
|
|
244
|
+
return this.syncPromise;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// get the current latest block numbers
|
|
248
|
+
this.latestBlockNumberAtStart = await this.l2BlockSource.getBlockNumber();
|
|
249
|
+
this.provenBlockNumberAtStart = await this.l2BlockSource.getProvenBlockNumber();
|
|
250
|
+
|
|
251
|
+
const syncedLatestBlock = this.getSyncedLatestBlockNum() + 1;
|
|
252
|
+
const syncedProvenBlock = this.getSyncedProvenBlockNum() + 1;
|
|
253
|
+
|
|
254
|
+
// if there are blocks to be retrieved, go to a synching state
|
|
255
|
+
if (syncedLatestBlock <= this.latestBlockNumberAtStart || syncedProvenBlock <= this.provenBlockNumberAtStart) {
|
|
256
|
+
this.setCurrentState(SlasherClientState.SYNCHING);
|
|
257
|
+
this.syncPromise = new Promise(resolve => {
|
|
258
|
+
this.syncResolve = resolve;
|
|
259
|
+
});
|
|
260
|
+
this.log.verbose(`Starting sync from ${syncedLatestBlock} (last proven ${syncedProvenBlock})`);
|
|
261
|
+
} else {
|
|
262
|
+
// if no blocks to be retrieved, go straight to running
|
|
263
|
+
this.setCurrentState(SlasherClientState.RUNNING);
|
|
264
|
+
this.syncPromise = Promise.resolve();
|
|
265
|
+
this.log.verbose(`Block ${syncedLatestBlock} (proven ${syncedProvenBlock}) already beyond current block`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
this.blockStream.start();
|
|
269
|
+
this.log.verbose(`Started block downloader from block ${syncedLatestBlock}`);
|
|
270
|
+
|
|
271
|
+
return this.syncPromise;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Allows consumers to stop the instance of the slasher client.
|
|
276
|
+
* 'ready' will now return 'false' and the running promise that keeps the client synced is interrupted.
|
|
277
|
+
*/
|
|
278
|
+
public async stop() {
|
|
279
|
+
this.log.debug('Stopping Slasher client...');
|
|
280
|
+
await this.blockStream.stop();
|
|
281
|
+
this.log.debug('Stopped block downloader');
|
|
282
|
+
this.setCurrentState(SlasherClientState.STOPPED);
|
|
283
|
+
this.log.info('Slasher client stopped.');
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Public function to check if the slasher client is fully synced and ready to receive txs.
|
|
288
|
+
* @returns True if the slasher client is ready to receive txs.
|
|
289
|
+
*/
|
|
290
|
+
public isReady() {
|
|
291
|
+
return this.currentState === SlasherClientState.RUNNING;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Public function to check the latest block number that the slasher client is synced to.
|
|
296
|
+
* @returns Block number of latest L2 Block we've synced with.
|
|
297
|
+
*/
|
|
298
|
+
public getSyncedLatestBlockNum() {
|
|
299
|
+
return this.synchedLatestBlockNumber.get() ?? INITIAL_L2_BLOCK_NUM - 1;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Public function to check the latest proven block number that the slasher client is synced to.
|
|
304
|
+
* @returns Block number of latest proven L2 Block we've synced with.
|
|
305
|
+
*/
|
|
306
|
+
public getSyncedProvenBlockNum() {
|
|
307
|
+
return this.synchedProvenBlockNumber.get() ?? INITIAL_L2_BLOCK_NUM - 1;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Method to check the status of the slasher client.
|
|
312
|
+
* @returns Information about slasher client status: state & syncedToBlockNum.
|
|
313
|
+
*/
|
|
314
|
+
public async getStatus(): Promise<SlasherSyncState> {
|
|
315
|
+
const blockNumber = this.getSyncedLatestBlockNum();
|
|
316
|
+
const blockHash =
|
|
317
|
+
blockNumber == 0
|
|
318
|
+
? ''
|
|
319
|
+
: await this.l2BlockSource.getBlockHeader(blockNumber).then(header => header?.hash().toString());
|
|
320
|
+
return Promise.resolve({
|
|
321
|
+
state: this.currentState,
|
|
322
|
+
syncedToL2Block: { number: blockNumber, hash: blockHash },
|
|
323
|
+
} as SlasherSyncState);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Handles new blocks
|
|
328
|
+
* @param blocks - A list of blocks that the slasher client needs to store block hashes for
|
|
329
|
+
* @returns Empty promise.
|
|
330
|
+
*/
|
|
331
|
+
private async handleLatestL2Blocks(blocks: L2Block[]): Promise<void> {
|
|
332
|
+
if (!blocks.length) {
|
|
333
|
+
return Promise.resolve();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const lastBlockNum = blocks[blocks.length - 1].number;
|
|
337
|
+
await Promise.all(blocks.map(block => this.synchedBlockHashes.set(block.number, block.hash().toString())));
|
|
338
|
+
await this.synchedLatestBlockNumber.set(lastBlockNum);
|
|
339
|
+
this.log.debug(`Synched to latest block ${lastBlockNum}`);
|
|
340
|
+
this.startServiceIfSynched();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Handles new proven blocks by updating the proven block number
|
|
345
|
+
* @param blocks - A list of proven L2 blocks.
|
|
346
|
+
* @returns Empty promise.
|
|
347
|
+
*/
|
|
348
|
+
private async handleProvenL2Blocks(blocks: L2Block[]): Promise<void> {
|
|
349
|
+
if (!blocks.length) {
|
|
350
|
+
return Promise.resolve();
|
|
351
|
+
}
|
|
352
|
+
const lastBlockNum = blocks[blocks.length - 1].number;
|
|
353
|
+
await this.synchedProvenBlockNumber.set(lastBlockNum);
|
|
354
|
+
this.log.debug(`Synched to proven block ${lastBlockNum}`);
|
|
355
|
+
|
|
356
|
+
this.startServiceIfSynched();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
private async handlePruneL2Blocks(latestBlock: number): Promise<void> {
|
|
360
|
+
const blockHeader = await this.l2BlockSource.getBlockHeader(latestBlock);
|
|
361
|
+
const slotNumber = blockHeader ? blockHeader.globalVariables.slotNumber.toBigInt() : BigInt(0);
|
|
362
|
+
const epochNumber = slotNumber / BigInt(this.config.aztecEpochDuration);
|
|
363
|
+
this.log.info(`Detected chain prune. Punishing the validators at epoch ${epochNumber}`);
|
|
364
|
+
|
|
365
|
+
// Set the lifetime such that we have a full round that we could vote throughout.
|
|
366
|
+
const slotsIntoRound = slotNumber % BigInt(this.config.slashingRoundSize);
|
|
367
|
+
const toNext = slotsIntoRound == 0n ? 0n : BigInt(this.config.slashingRoundSize) - slotsIntoRound;
|
|
368
|
+
|
|
369
|
+
const lifetime = slotNumber + toNext + BigInt(this.config.slashingRoundSize);
|
|
370
|
+
|
|
371
|
+
this.slashEvents.push({
|
|
372
|
+
epoch: epochNumber,
|
|
373
|
+
amount: this.slashingAmount,
|
|
374
|
+
lifetime,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
await this.synchedLatestBlockNumber.set(latestBlock);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private startServiceIfSynched() {
|
|
381
|
+
if (
|
|
382
|
+
this.currentState === SlasherClientState.SYNCHING &&
|
|
383
|
+
this.getSyncedLatestBlockNum() >= this.latestBlockNumberAtStart &&
|
|
384
|
+
this.getSyncedProvenBlockNum() >= this.provenBlockNumberAtStart
|
|
385
|
+
) {
|
|
386
|
+
this.log.debug(`Synched to blocks at start`);
|
|
387
|
+
this.setCurrentState(SlasherClientState.RUNNING);
|
|
388
|
+
if (this.syncResolve !== undefined) {
|
|
389
|
+
this.syncResolve();
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Method to set the value of the current state.
|
|
396
|
+
* @param newState - New state value.
|
|
397
|
+
*/
|
|
398
|
+
private setCurrentState(newState: SlasherClientState) {
|
|
399
|
+
this.currentState = newState;
|
|
400
|
+
this.log.debug(`Moved to state ${SlasherClientState[this.currentState]}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type PublicProcessorFactory } from '@aztec/simulator';
|
|
2
|
+
|
|
3
|
+
import { SequencerClient } from '../client/sequencer-client.js';
|
|
4
|
+
import { type L1Publisher } from '../publisher/l1-publisher.js';
|
|
5
|
+
import { Sequencer } from '../sequencer/sequencer.js';
|
|
6
|
+
import { type SequencerState } from '../sequencer/utils.js';
|
|
7
|
+
|
|
8
|
+
class TestSequencer_ extends Sequencer {
|
|
9
|
+
public override publicProcessorFactory!: PublicProcessorFactory;
|
|
10
|
+
public override timeTable!: Record<SequencerState, number>;
|
|
11
|
+
public override processTxTime!: number;
|
|
12
|
+
public override publisher!: L1Publisher;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type TestSequencer = TestSequencer_;
|
|
16
|
+
|
|
17
|
+
class TestSequencerClient_ extends SequencerClient {
|
|
18
|
+
public override sequencer!: TestSequencer;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type TestSequencerClient = TestSequencerClient_;
|
|
22
|
+
|
|
23
|
+
export * from './test-l1-publisher.js';
|
|
@@ -3,7 +3,7 @@ import { type Delayer, withDelayer } from '@aztec/ethereum/test';
|
|
|
3
3
|
|
|
4
4
|
import { type Chain, type HttpTransport, type PrivateKeyAccount, type WalletClient } from 'viem';
|
|
5
5
|
|
|
6
|
-
import { L1Publisher } from '
|
|
6
|
+
import { L1Publisher } from '../publisher/l1-publisher.js';
|
|
7
7
|
|
|
8
8
|
export class TestL1Publisher extends L1Publisher {
|
|
9
9
|
public delayer: Delayer | undefined;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { MerkleTreeId, type MerkleTreeReadOperations } from '@aztec/circuit-types';
|
|
2
|
+
import { type Fr } from '@aztec/circuits.js';
|
|
3
|
+
import { type ArchiveSource } from '@aztec/p2p';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Implements an archive source by checking a DB and an in-memory collection.
|
|
7
|
+
* Intended for validating transactions as they are added to a block.
|
|
8
|
+
*/
|
|
9
|
+
export class ArchiveCache implements ArchiveSource {
|
|
10
|
+
archives: Map<string, bigint>;
|
|
11
|
+
|
|
12
|
+
constructor(private db: MerkleTreeReadOperations) {
|
|
13
|
+
this.archives = new Map<string, bigint>();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public async getArchiveIndices(archives: Fr[]): Promise<(bigint | undefined)[]> {
|
|
17
|
+
const toCheckDb = archives.filter(n => !this.archives.has(n.toString()));
|
|
18
|
+
const dbHits = await this.db.findLeafIndices(MerkleTreeId.ARCHIVE, toCheckDb);
|
|
19
|
+
dbHits.forEach((x, index) => {
|
|
20
|
+
if (x !== undefined) {
|
|
21
|
+
this.archives.set(toCheckDb[index].toString(), x);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return archives.map(n => this.archives.get(n.toString()));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Tx, TxExecutionPhase, type TxValidator } from '@aztec/circuit-types';
|
|
1
|
+
import { type Tx, TxExecutionPhase, type TxValidationResult, type TxValidator } from '@aztec/circuit-types';
|
|
2
2
|
import { type AztecAddress, type Fr, FunctionSelector, type GasFees } from '@aztec/circuits.js';
|
|
3
3
|
import { createLogger } from '@aztec/foundation/log';
|
|
4
4
|
import { computeFeePayerBalanceStorageSlot, getExecutionRequestsByPhase } from '@aztec/simulator';
|
|
@@ -27,25 +27,10 @@ export class GasTxValidator implements TxValidator<Tx> {
|
|
|
27
27
|
this.#gasFees = gasFees;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const skippedTxs: Tx[] = [];
|
|
34
|
-
|
|
35
|
-
for (const tx of txs) {
|
|
36
|
-
if (this.#shouldSkip(tx)) {
|
|
37
|
-
skippedTxs.push(tx);
|
|
38
|
-
} else if (await this.#validateTxFee(tx)) {
|
|
39
|
-
validTxs.push(tx);
|
|
40
|
-
} else {
|
|
41
|
-
invalidTxs.push(tx);
|
|
42
|
-
}
|
|
30
|
+
validateTx(tx: Tx): Promise<TxValidationResult> {
|
|
31
|
+
if (this.#shouldSkip(tx)) {
|
|
32
|
+
return Promise.resolve({ result: 'skipped', reason: ['Insufficient fee per gas'] });
|
|
43
33
|
}
|
|
44
|
-
|
|
45
|
-
return [validTxs, invalidTxs, skippedTxs];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
validateTx(tx: Tx): Promise<boolean> {
|
|
49
34
|
return this.#validateTxFee(tx);
|
|
50
35
|
}
|
|
51
36
|
|
|
@@ -57,20 +42,22 @@ export class GasTxValidator implements TxValidator<Tx> {
|
|
|
57
42
|
const notEnoughMaxFees =
|
|
58
43
|
maxFeesPerGas.feePerDaGas.lt(this.#gasFees.feePerDaGas) ||
|
|
59
44
|
maxFeesPerGas.feePerL2Gas.lt(this.#gasFees.feePerL2Gas);
|
|
45
|
+
|
|
60
46
|
if (notEnoughMaxFees) {
|
|
61
47
|
this.#log.warn(`Skipping transaction ${tx.getTxHash()} due to insufficient fee per gas`);
|
|
62
48
|
}
|
|
63
49
|
return notEnoughMaxFees;
|
|
64
50
|
}
|
|
65
51
|
|
|
66
|
-
async #validateTxFee(tx: Tx): Promise<
|
|
52
|
+
async #validateTxFee(tx: Tx): Promise<TxValidationResult> {
|
|
67
53
|
const feePayer = tx.data.feePayer;
|
|
68
54
|
// TODO(@spalladino) Eventually remove the is_zero condition as we should always charge fees to every tx
|
|
69
55
|
if (feePayer.isZero()) {
|
|
70
56
|
if (this.#enforceFees) {
|
|
71
57
|
this.#log.warn(`Rejecting transaction ${tx.getTxHash()} due to missing fee payer`);
|
|
58
|
+
return { result: 'invalid', reason: ['Missing fee payer'] };
|
|
72
59
|
} else {
|
|
73
|
-
return
|
|
60
|
+
return { result: 'valid' };
|
|
74
61
|
}
|
|
75
62
|
}
|
|
76
63
|
|
|
@@ -98,13 +85,13 @@ export class GasTxValidator implements TxValidator<Tx> {
|
|
|
98
85
|
|
|
99
86
|
const balance = claimFunctionCall ? initialBalance.add(claimFunctionCall.args[2]) : initialBalance;
|
|
100
87
|
if (balance.lt(feeLimit)) {
|
|
101
|
-
this.#log.
|
|
88
|
+
this.#log.warn(`Rejecting transaction due to not enough fee payer balance`, {
|
|
102
89
|
feePayer,
|
|
103
90
|
balance: balance.toBigInt(),
|
|
104
91
|
feeLimit: feeLimit.toBigInt(),
|
|
105
92
|
});
|
|
106
|
-
return
|
|
93
|
+
return { result: 'invalid', reason: ['Insufficient fee payer balance'] };
|
|
107
94
|
}
|
|
108
|
-
return
|
|
95
|
+
return { result: 'valid' };
|
|
109
96
|
}
|
|
110
97
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { MerkleTreeId, type MerkleTreeReadOperations } from '@aztec/circuit-types';
|
|
2
|
+
import { type NullifierSource } from '@aztec/p2p';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Implements a nullifier source by checking a DB and an in-memory collection.
|
|
6
|
+
* Intended for validating transactions as they are added to a block.
|
|
7
|
+
*/
|
|
8
|
+
export class NullifierCache implements NullifierSource {
|
|
9
|
+
nullifiers: Set<string>;
|
|
10
|
+
|
|
11
|
+
constructor(private db: MerkleTreeReadOperations) {
|
|
12
|
+
this.nullifiers = new Set();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public async nullifiersExist(nullifiers: Buffer[]): Promise<boolean[]> {
|
|
16
|
+
const cacheResults = nullifiers.map(n => this.nullifiers.has(n.toString()));
|
|
17
|
+
const toCheckDb = nullifiers.filter((_n, index) => !cacheResults[index]);
|
|
18
|
+
const dbHits = await this.db.findLeafIndices(MerkleTreeId.NULLIFIER_TREE, toCheckDb);
|
|
19
|
+
|
|
20
|
+
let dbIndex = 0;
|
|
21
|
+
return nullifiers.map((_n, index) => cacheResults[index] || dbHits[dbIndex++] !== undefined);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public addNullifiers(nullifiers: Buffer[]) {
|
|
25
|
+
for (const nullifier of nullifiers) {
|
|
26
|
+
this.nullifiers.add(nullifier.toString());
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
type PublicExecutionRequest,
|
|
4
4
|
Tx,
|
|
5
5
|
TxExecutionPhase,
|
|
6
|
+
type TxValidationResult,
|
|
6
7
|
type TxValidator,
|
|
7
8
|
} from '@aztec/circuit-types';
|
|
8
9
|
import { type ContractDataSource } from '@aztec/circuits.js';
|
|
@@ -17,48 +18,36 @@ export class PhasesTxValidator implements TxValidator<Tx> {
|
|
|
17
18
|
this.contractDataSource = new ContractsDataSourcePublicDB(contracts);
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
async
|
|
21
|
-
|
|
22
|
-
const invalidTxs: Tx[] = [];
|
|
23
|
-
|
|
24
|
-
for (const tx of txs) {
|
|
21
|
+
async validateTx(tx: Tx): Promise<TxValidationResult> {
|
|
22
|
+
try {
|
|
25
23
|
// TODO(@spalladino): We add this just to handle public authwit-check calls during setup
|
|
26
24
|
// which are needed for public FPC flows, but fail if the account contract hasnt been deployed yet,
|
|
27
25
|
// which is what we're trying to do as part of the current txs.
|
|
28
26
|
await this.contractDataSource.addNewContracts(tx);
|
|
29
27
|
|
|
30
|
-
if (
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
invalidTxs.push(tx);
|
|
28
|
+
if (!tx.data.forPublic) {
|
|
29
|
+
this.#log.debug(`Tx ${Tx.getHash(tx)} does not contain enqueued public functions. Skipping phases validation.`);
|
|
30
|
+
return { result: 'valid' };
|
|
34
31
|
}
|
|
35
32
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const setupFns = getExecutionRequestsByPhase(tx, TxExecutionPhase.SETUP);
|
|
49
|
-
for (const setupFn of setupFns) {
|
|
50
|
-
if (!(await this.isOnAllowList(setupFn, this.setupAllowList))) {
|
|
51
|
-
this.#log.warn(
|
|
52
|
-
`Rejecting tx ${Tx.getHash(tx)} because it calls setup function not on allow list: ${
|
|
53
|
-
setupFn.callContext.contractAddress
|
|
54
|
-
}:${setupFn.callContext.functionSelector}`,
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
return false;
|
|
33
|
+
const setupFns = getExecutionRequestsByPhase(tx, TxExecutionPhase.SETUP);
|
|
34
|
+
for (const setupFn of setupFns) {
|
|
35
|
+
if (!(await this.isOnAllowList(setupFn, this.setupAllowList))) {
|
|
36
|
+
this.#log.warn(
|
|
37
|
+
`Rejecting tx ${Tx.getHash(tx)} because it calls setup function not on allow list: ${
|
|
38
|
+
setupFn.callContext.contractAddress
|
|
39
|
+
}:${setupFn.callContext.functionSelector}`,
|
|
40
|
+
{ allowList: this.setupAllowList },
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return { result: 'invalid', reason: ['Setup function not on allow list'] };
|
|
44
|
+
}
|
|
58
45
|
}
|
|
59
|
-
}
|
|
60
46
|
|
|
61
|
-
|
|
47
|
+
return { result: 'valid' };
|
|
48
|
+
} finally {
|
|
49
|
+
await this.contractDataSource.removeNewContracts(tx);
|
|
50
|
+
}
|
|
62
51
|
}
|
|
63
52
|
|
|
64
53
|
async isOnAllowList(publicCall: PublicExecutionRequest, allowList: AllowedElement[]): Promise<boolean> {
|