@aztec/sequencer-client 0.0.0-test.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/README.md +45 -0
- package/dest/client/index.d.ts +2 -0
- package/dest/client/index.d.ts.map +1 -0
- package/dest/client/index.js +1 -0
- package/dest/client/sequencer-client.d.ts +71 -0
- package/dest/client/sequencer-client.d.ts.map +1 -0
- package/dest/client/sequencer-client.js +117 -0
- package/dest/config.d.ts +29 -0
- package/dest/config.d.ts.map +1 -0
- package/dest/config.js +143 -0
- package/dest/global_variable_builder/global_builder.d.ts +32 -0
- package/dest/global_variable_builder/global_builder.d.ts.map +1 -0
- package/dest/global_variable_builder/global_builder.js +79 -0
- package/dest/global_variable_builder/index.d.ts +2 -0
- package/dest/global_variable_builder/index.d.ts.map +1 -0
- package/dest/global_variable_builder/index.js +1 -0
- package/dest/index.d.ts +8 -0
- package/dest/index.d.ts.map +1 -0
- package/dest/index.js +9 -0
- package/dest/publisher/config.d.ts +31 -0
- package/dest/publisher/config.d.ts.map +1 -0
- package/dest/publisher/config.js +35 -0
- package/dest/publisher/index.d.ts +2 -0
- package/dest/publisher/index.d.ts.map +1 -0
- package/dest/publisher/index.js +1 -0
- package/dest/publisher/sequencer-publisher-metrics.d.ts +25 -0
- package/dest/publisher/sequencer-publisher-metrics.d.ts.map +1 -0
- package/dest/publisher/sequencer-publisher-metrics.js +129 -0
- package/dest/publisher/sequencer-publisher.d.ts +152 -0
- package/dest/publisher/sequencer-publisher.d.ts.map +1 -0
- package/dest/publisher/sequencer-publisher.js +481 -0
- package/dest/sequencer/allowed.d.ts +3 -0
- package/dest/sequencer/allowed.d.ts.map +1 -0
- package/dest/sequencer/allowed.js +27 -0
- package/dest/sequencer/config.d.ts +2 -0
- package/dest/sequencer/config.d.ts.map +1 -0
- package/dest/sequencer/config.js +1 -0
- package/dest/sequencer/index.d.ts +4 -0
- package/dest/sequencer/index.d.ts.map +1 -0
- package/dest/sequencer/index.js +3 -0
- package/dest/sequencer/metrics.d.ts +24 -0
- package/dest/sequencer/metrics.d.ts.map +1 -0
- package/dest/sequencer/metrics.js +102 -0
- package/dest/sequencer/sequencer.d.ts +180 -0
- package/dest/sequencer/sequencer.d.ts.map +1 -0
- package/dest/sequencer/sequencer.js +623 -0
- package/dest/sequencer/timetable.d.ts +38 -0
- package/dest/sequencer/timetable.d.ts.map +1 -0
- package/dest/sequencer/timetable.js +110 -0
- package/dest/sequencer/utils.d.ts +48 -0
- package/dest/sequencer/utils.d.ts.map +1 -0
- package/dest/sequencer/utils.js +53 -0
- package/dest/slasher/factory.d.ts +7 -0
- package/dest/slasher/factory.d.ts.map +1 -0
- package/dest/slasher/factory.js +8 -0
- package/dest/slasher/index.d.ts +3 -0
- package/dest/slasher/index.d.ts.map +1 -0
- package/dest/slasher/index.js +2 -0
- package/dest/slasher/slasher_client.d.ts +75 -0
- package/dest/slasher/slasher_client.d.ts.map +1 -0
- package/dest/slasher/slasher_client.js +132 -0
- package/dest/test/index.d.ts +17 -0
- package/dest/test/index.d.ts.map +1 -0
- package/dest/test/index.js +10 -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 +14 -0
- package/dest/tx_validator/gas_validator.d.ts.map +1 -0
- package/dest/tx_validator/gas_validator.js +78 -0
- 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 +12 -0
- package/dest/tx_validator/phases_validator.d.ts.map +1 -0
- package/dest/tx_validator/phases_validator.js +80 -0
- package/dest/tx_validator/test_utils.d.ts +23 -0
- package/dest/tx_validator/test_utils.d.ts.map +1 -0
- package/dest/tx_validator/test_utils.js +26 -0
- package/dest/tx_validator/tx_validator_factory.d.ts +18 -0
- package/dest/tx_validator/tx_validator_factory.d.ts.map +1 -0
- package/dest/tx_validator/tx_validator_factory.js +50 -0
- package/package.json +121 -0
- package/src/client/index.ts +1 -0
- package/src/client/sequencer-client.ts +219 -0
- package/src/config.ts +179 -0
- package/src/global_variable_builder/global_builder.ts +108 -0
- package/src/global_variable_builder/index.ts +1 -0
- package/src/index.ts +10 -0
- package/src/publisher/config.ts +75 -0
- package/src/publisher/index.ts +1 -0
- package/src/publisher/sequencer-publisher-metrics.ts +176 -0
- package/src/publisher/sequencer-publisher.ts +625 -0
- package/src/sequencer/allowed.ts +36 -0
- package/src/sequencer/config.ts +1 -0
- package/src/sequencer/index.ts +3 -0
- package/src/sequencer/metrics.ts +137 -0
- package/src/sequencer/sequencer.ts +759 -0
- package/src/sequencer/timetable.ts +123 -0
- package/src/sequencer/utils.ts +74 -0
- package/src/slasher/factory.ts +15 -0
- package/src/slasher/index.ts +2 -0
- package/src/slasher/slasher_client.ts +193 -0
- package/src/test/index.ts +20 -0
- package/src/tx_validator/archive_cache.ts +28 -0
- package/src/tx_validator/gas_validator.ts +101 -0
- package/src/tx_validator/nullifier_cache.ts +30 -0
- package/src/tx_validator/phases_validator.ts +98 -0
- package/src/tx_validator/test_utils.ts +48 -0
- package/src/tx_validator/tx_validator_factory.ts +120 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { createLogger } from '@aztec/aztec.js';
|
|
2
|
+
import { SequencerState } from './utils.js';
|
|
3
|
+
export class SequencerTimetable {
|
|
4
|
+
ethereumSlotDuration;
|
|
5
|
+
aztecSlotDuration;
|
|
6
|
+
maxL1TxInclusionTimeIntoSlot;
|
|
7
|
+
enforce;
|
|
8
|
+
metrics;
|
|
9
|
+
log;
|
|
10
|
+
/** How late into the slot can we be to start working */ initialTime;
|
|
11
|
+
/** How long it takes to get ready to start building */ blockPrepareTime;
|
|
12
|
+
/** How long it takes to for proposals and attestations to travel across the p2p layer (one-way) */ attestationPropagationTime;
|
|
13
|
+
/** How much time we spend validating and processing a block after building it, and assembling the proposal to send to attestors */ blockValidationTime;
|
|
14
|
+
/**
|
|
15
|
+
* How long it takes to get a published block into L1. L1 builders typically accept txs up to 4 seconds into their slot,
|
|
16
|
+
* but we'll timeout sooner to give it more time to propagate (remember we also have blobs!). Still, when working in anvil,
|
|
17
|
+
* we can just post in the very last second of the L1 slot and still expect the tx to be accepted.
|
|
18
|
+
*/ l1PublishingTime;
|
|
19
|
+
constructor(ethereumSlotDuration, aztecSlotDuration, maxL1TxInclusionTimeIntoSlot, enforce = true, metrics, log = createLogger('sequencer:timetable')){
|
|
20
|
+
this.ethereumSlotDuration = ethereumSlotDuration;
|
|
21
|
+
this.aztecSlotDuration = aztecSlotDuration;
|
|
22
|
+
this.maxL1TxInclusionTimeIntoSlot = maxL1TxInclusionTimeIntoSlot;
|
|
23
|
+
this.enforce = enforce;
|
|
24
|
+
this.metrics = metrics;
|
|
25
|
+
this.log = log;
|
|
26
|
+
this.initialTime = 3;
|
|
27
|
+
this.blockPrepareTime = 1;
|
|
28
|
+
this.attestationPropagationTime = 2;
|
|
29
|
+
this.blockValidationTime = 1;
|
|
30
|
+
this.l1PublishingTime = this.ethereumSlotDuration - this.maxL1TxInclusionTimeIntoSlot;
|
|
31
|
+
}
|
|
32
|
+
get afterBlockBuildingTimeNeededWithoutReexec() {
|
|
33
|
+
return this.blockValidationTime + this.attestationPropagationTime * 2 + this.l1PublishingTime;
|
|
34
|
+
}
|
|
35
|
+
getBlockProposalExecTimeEnd(secondsIntoSlot) {
|
|
36
|
+
// We are N seconds into the slot. We need to account for `afterBlockBuildingTimeNeededWithoutReexec` seconds,
|
|
37
|
+
// send then split the remaining time between the re-execution and the block building.
|
|
38
|
+
const maxAllowed = this.aztecSlotDuration - this.afterBlockBuildingTimeNeededWithoutReexec;
|
|
39
|
+
const available = maxAllowed - secondsIntoSlot;
|
|
40
|
+
const executionTimeEnd = secondsIntoSlot + available / 2;
|
|
41
|
+
this.log.debug(`Block proposal execution time deadline is ${executionTimeEnd}`, {
|
|
42
|
+
secondsIntoSlot,
|
|
43
|
+
maxAllowed,
|
|
44
|
+
available,
|
|
45
|
+
executionTimeEnd
|
|
46
|
+
});
|
|
47
|
+
return executionTimeEnd;
|
|
48
|
+
}
|
|
49
|
+
get afterBlockReexecTimeNeeded() {
|
|
50
|
+
return this.attestationPropagationTime + this.l1PublishingTime;
|
|
51
|
+
}
|
|
52
|
+
getValidatorReexecTimeEnd(secondsIntoSlot) {
|
|
53
|
+
// We need to leave for `afterBlockReexecTimeNeeded` seconds available.
|
|
54
|
+
const validationTimeEnd = this.aztecSlotDuration - this.afterBlockReexecTimeNeeded;
|
|
55
|
+
this.log.debug(`Validator re-execution time deadline is ${validationTimeEnd}`, {
|
|
56
|
+
secondsIntoSlot,
|
|
57
|
+
validationTimeEnd
|
|
58
|
+
});
|
|
59
|
+
return validationTimeEnd;
|
|
60
|
+
}
|
|
61
|
+
getMaxAllowedTime(state) {
|
|
62
|
+
switch(state){
|
|
63
|
+
case SequencerState.STOPPED:
|
|
64
|
+
case SequencerState.IDLE:
|
|
65
|
+
case SequencerState.SYNCHRONIZING:
|
|
66
|
+
case SequencerState.PROPOSER_CHECK:
|
|
67
|
+
return; // We don't really care about times for this states
|
|
68
|
+
case SequencerState.INITIALIZING_PROPOSAL:
|
|
69
|
+
return this.initialTime;
|
|
70
|
+
case SequencerState.CREATING_BLOCK:
|
|
71
|
+
return this.initialTime + this.blockPrepareTime;
|
|
72
|
+
case SequencerState.COLLECTING_ATTESTATIONS:
|
|
73
|
+
return this.aztecSlotDuration - this.l1PublishingTime - 2 * this.attestationPropagationTime;
|
|
74
|
+
case SequencerState.PUBLISHING_BLOCK:
|
|
75
|
+
return this.aztecSlotDuration - this.l1PublishingTime;
|
|
76
|
+
default:
|
|
77
|
+
{
|
|
78
|
+
const _exhaustiveCheck = state;
|
|
79
|
+
throw new Error(`Unexpected state: ${state}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
assertTimeLeft(newState, secondsIntoSlot) {
|
|
84
|
+
if (!this.enforce) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const maxAllowedTime = this.getMaxAllowedTime(newState);
|
|
88
|
+
if (maxAllowedTime === undefined) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const bufferSeconds = maxAllowedTime - secondsIntoSlot;
|
|
92
|
+
if (bufferSeconds < 0) {
|
|
93
|
+
throw new SequencerTooSlowError(newState, maxAllowedTime, secondsIntoSlot);
|
|
94
|
+
}
|
|
95
|
+
this.metrics?.recordStateTransitionBufferMs(Math.floor(bufferSeconds * 1000), newState);
|
|
96
|
+
this.log.trace(`Enough time to transition to ${newState}`, {
|
|
97
|
+
maxAllowedTime,
|
|
98
|
+
secondsIntoSlot
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export class SequencerTooSlowError extends Error {
|
|
103
|
+
proposedState;
|
|
104
|
+
maxAllowedTime;
|
|
105
|
+
currentTime;
|
|
106
|
+
constructor(proposedState, maxAllowedTime, currentTime){
|
|
107
|
+
super(`Too far into slot for ${proposedState} (time into slot ${currentTime}s greater than ${maxAllowedTime}s allowance)`), this.proposedState = proposedState, this.maxAllowedTime = maxAllowedTime, this.currentTime = currentTime;
|
|
108
|
+
this.name = 'SequencerTooSlowError';
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
2
|
+
import { Signature } from '@aztec/foundation/eth-signature';
|
|
3
|
+
import type { BlockAttestation } from '@aztec/stdlib/p2p';
|
|
4
|
+
export declare enum SequencerState {
|
|
5
|
+
/**
|
|
6
|
+
* Sequencer is stopped and not processing any txs from the pool.
|
|
7
|
+
*/
|
|
8
|
+
STOPPED = "STOPPED",
|
|
9
|
+
/**
|
|
10
|
+
* Sequencer is awaiting the next call to work().
|
|
11
|
+
*/
|
|
12
|
+
IDLE = "IDLE",
|
|
13
|
+
/**
|
|
14
|
+
* Synchronizing with the L2 chain.
|
|
15
|
+
*/
|
|
16
|
+
SYNCHRONIZING = "SYNCHRONIZING",
|
|
17
|
+
/**
|
|
18
|
+
* Checking if we are the proposer for the current slot.
|
|
19
|
+
*/
|
|
20
|
+
PROPOSER_CHECK = "PROPOSER_CHECK",
|
|
21
|
+
/**
|
|
22
|
+
* Initializing the block proposal. Will move to CREATING_BLOCK if there are valid txs to include, or back to SYNCHRONIZING otherwise.
|
|
23
|
+
*/
|
|
24
|
+
INITIALIZING_PROPOSAL = "INITIALIZING_PROPOSAL",
|
|
25
|
+
/**
|
|
26
|
+
* Creating a new L2 block. Includes processing public function calls and running rollup circuits. Will move to PUBLISHING_CONTRACT_DATA.
|
|
27
|
+
*/
|
|
28
|
+
CREATING_BLOCK = "CREATING_BLOCK",
|
|
29
|
+
/**
|
|
30
|
+
* Collecting attestations from its peers. Will move to PUBLISHING_BLOCK.
|
|
31
|
+
*/
|
|
32
|
+
COLLECTING_ATTESTATIONS = "COLLECTING_ATTESTATIONS",
|
|
33
|
+
/**
|
|
34
|
+
* Sending the tx to L1 with the L2 block data and awaiting it to be mined. Will move to SYNCHRONIZING.
|
|
35
|
+
*/
|
|
36
|
+
PUBLISHING_BLOCK = "PUBLISHING_BLOCK"
|
|
37
|
+
}
|
|
38
|
+
export type SequencerStateCallback = () => SequencerState;
|
|
39
|
+
export declare function sequencerStateToNumber(state: SequencerState): number;
|
|
40
|
+
/** Order Attestations
|
|
41
|
+
*
|
|
42
|
+
* Returns attestation signatures in the order of a series of provided ethereum addresses
|
|
43
|
+
* The rollup smart contract expects attestations to appear in the order of the committee
|
|
44
|
+
*
|
|
45
|
+
* @todo: perform this logic within the memory attestation store instead?
|
|
46
|
+
*/
|
|
47
|
+
export declare function orderAttestations(attestations: BlockAttestation[], orderAddresses: EthAddress[]): Promise<Signature[]>;
|
|
48
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/sequencer/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAChE,OAAO,EAAE,SAAS,EAAE,MAAM,iCAAiC,CAAC;AAC5D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAE1D,oBAAY,cAAc;IACxB;;OAEG;IACH,OAAO,YAAY;IACnB;;OAEG;IACH,IAAI,SAAS;IACb;;OAEG;IACH,aAAa,kBAAkB;IAC/B;;OAEG;IACH,cAAc,mBAAmB;IACjC;;OAEG;IACH,qBAAqB,0BAA0B;IAC/C;;OAEG;IACH,cAAc,mBAAmB;IACjC;;OAEG;IACH,uBAAuB,4BAA4B;IACnD;;OAEG;IACH,gBAAgB,qBAAqB;CACtC;AAED,MAAM,MAAM,sBAAsB,GAAG,MAAM,cAAc,CAAC;AAE1D,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAEpE;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,YAAY,EAAE,gBAAgB,EAAE,EAChC,cAAc,EAAE,UAAU,EAAE,GAC3B,OAAO,CAAC,SAAS,EAAE,CAAC,CAkBtB"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Signature } from '@aztec/foundation/eth-signature';
|
|
2
|
+
export var SequencerState = /*#__PURE__*/ function(SequencerState) {
|
|
3
|
+
/**
|
|
4
|
+
* Sequencer is stopped and not processing any txs from the pool.
|
|
5
|
+
*/ SequencerState["STOPPED"] = "STOPPED";
|
|
6
|
+
/**
|
|
7
|
+
* Sequencer is awaiting the next call to work().
|
|
8
|
+
*/ SequencerState["IDLE"] = "IDLE";
|
|
9
|
+
/**
|
|
10
|
+
* Synchronizing with the L2 chain.
|
|
11
|
+
*/ SequencerState["SYNCHRONIZING"] = "SYNCHRONIZING";
|
|
12
|
+
/**
|
|
13
|
+
* Checking if we are the proposer for the current slot.
|
|
14
|
+
*/ SequencerState["PROPOSER_CHECK"] = "PROPOSER_CHECK";
|
|
15
|
+
/**
|
|
16
|
+
* Initializing the block proposal. Will move to CREATING_BLOCK if there are valid txs to include, or back to SYNCHRONIZING otherwise.
|
|
17
|
+
*/ SequencerState["INITIALIZING_PROPOSAL"] = "INITIALIZING_PROPOSAL";
|
|
18
|
+
/**
|
|
19
|
+
* Creating a new L2 block. Includes processing public function calls and running rollup circuits. Will move to PUBLISHING_CONTRACT_DATA.
|
|
20
|
+
*/ SequencerState["CREATING_BLOCK"] = "CREATING_BLOCK";
|
|
21
|
+
/**
|
|
22
|
+
* Collecting attestations from its peers. Will move to PUBLISHING_BLOCK.
|
|
23
|
+
*/ SequencerState["COLLECTING_ATTESTATIONS"] = "COLLECTING_ATTESTATIONS";
|
|
24
|
+
/**
|
|
25
|
+
* Sending the tx to L1 with the L2 block data and awaiting it to be mined. Will move to SYNCHRONIZING.
|
|
26
|
+
*/ SequencerState["PUBLISHING_BLOCK"] = "PUBLISHING_BLOCK";
|
|
27
|
+
return SequencerState;
|
|
28
|
+
}({});
|
|
29
|
+
export function sequencerStateToNumber(state) {
|
|
30
|
+
return Object.values(SequencerState).indexOf(state);
|
|
31
|
+
}
|
|
32
|
+
/** Order Attestations
|
|
33
|
+
*
|
|
34
|
+
* Returns attestation signatures in the order of a series of provided ethereum addresses
|
|
35
|
+
* The rollup smart contract expects attestations to appear in the order of the committee
|
|
36
|
+
*
|
|
37
|
+
* @todo: perform this logic within the memory attestation store instead?
|
|
38
|
+
*/ export async function orderAttestations(attestations, orderAddresses) {
|
|
39
|
+
// Create a map of sender addresses to BlockAttestations
|
|
40
|
+
const attestationMap = new Map();
|
|
41
|
+
for (const attestation of attestations){
|
|
42
|
+
const sender = await attestation.getSender();
|
|
43
|
+
if (sender) {
|
|
44
|
+
attestationMap.set(sender.toString(), attestation);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Create the ordered array based on the orderAddresses, else return an empty signature
|
|
48
|
+
const orderedAttestations = orderAddresses.map((address)=>{
|
|
49
|
+
const addressString = address.toString();
|
|
50
|
+
return attestationMap.get(addressString)?.signature || Signature.empty();
|
|
51
|
+
});
|
|
52
|
+
return orderedAttestations;
|
|
53
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { L1ContractsConfig, L1ReaderConfig } from '@aztec/ethereum';
|
|
2
|
+
import type { L2BlockSourceEventEmitter } from '@aztec/stdlib/block';
|
|
3
|
+
import { type TelemetryClient } from '@aztec/telemetry-client';
|
|
4
|
+
import { SlasherClient } from './slasher_client.js';
|
|
5
|
+
import type { SlasherConfig } from './slasher_client.js';
|
|
6
|
+
export declare const createSlasherClient: (_config: SlasherConfig & L1ContractsConfig & L1ReaderConfig, l2BlockSource: L2BlockSourceEventEmitter, telemetry?: TelemetryClient) => SlasherClient;
|
|
7
|
+
//# sourceMappingURL=factory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"factory.d.ts","sourceRoot":"","sources":["../../src/slasher/factory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACzE,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AACrE,OAAO,EAAE,KAAK,eAAe,EAAsB,MAAM,yBAAyB,CAAC;AAEnF,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,eAAO,MAAM,mBAAmB,YACrB,aAAa,GAAG,iBAAiB,GAAG,cAAc,iBAC5C,yBAAyB,cAC7B,eAAe,kBAI3B,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { getTelemetryClient } from '@aztec/telemetry-client';
|
|
2
|
+
import { SlasherClient } from './slasher_client.js';
|
|
3
|
+
export const createSlasherClient = (_config, l2BlockSource, telemetry = getTelemetryClient())=>{
|
|
4
|
+
const config = {
|
|
5
|
+
..._config
|
|
6
|
+
};
|
|
7
|
+
return new SlasherClient(config, l2BlockSource, telemetry);
|
|
8
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/slasher/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,OAAO,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { type L1ContractsConfig, type L1ReaderConfig, type ViemPublicClient } from '@aztec/ethereum';
|
|
2
|
+
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
3
|
+
import { SlashFactoryAbi } from '@aztec/l1-artifacts';
|
|
4
|
+
import { type L2BlockId, type L2BlockSourceEvent, type L2BlockSourceEventEmitter } from '@aztec/stdlib/block';
|
|
5
|
+
import { type TelemetryClient, WithTracer } from '@aztec/telemetry-client';
|
|
6
|
+
import { type GetContractReturnType } from 'viem';
|
|
7
|
+
/**
|
|
8
|
+
* Enum defining the possible states of the Slasher client.
|
|
9
|
+
*/
|
|
10
|
+
export declare enum SlasherClientState {
|
|
11
|
+
IDLE = 0,
|
|
12
|
+
RUNNING = 1,
|
|
13
|
+
STOPPED = 2
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The synchronization status of the Slasher client.
|
|
17
|
+
*/
|
|
18
|
+
export interface SlasherSyncState {
|
|
19
|
+
/**
|
|
20
|
+
* The current state of the slasher client.
|
|
21
|
+
*/
|
|
22
|
+
state: SlasherClientState;
|
|
23
|
+
/**
|
|
24
|
+
* The block number that the slasher client is synced to.
|
|
25
|
+
*/
|
|
26
|
+
syncedToL2Block: L2BlockId;
|
|
27
|
+
}
|
|
28
|
+
export interface SlasherConfig {
|
|
29
|
+
blockCheckIntervalMS: number;
|
|
30
|
+
blockRequestBatchSize: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* @notice A Hypomeiones slasher client implementation
|
|
34
|
+
*
|
|
35
|
+
* Hypomeiones: a class of individuals in ancient Sparta who were considered inferior or lesser citizens compared
|
|
36
|
+
* to the full Spartan citizens.
|
|
37
|
+
*
|
|
38
|
+
* The implementation here is less than ideal. It exists, not to be the end all be all, but to show that
|
|
39
|
+
* slashing can be done with this mechanism.
|
|
40
|
+
*
|
|
41
|
+
* The implementation is VERY brute in the sense that it only looks for pruned blocks and then tries to slash
|
|
42
|
+
* the full committee of that.
|
|
43
|
+
* If it sees a prune, it will mark the full epoch as "to be slashed".
|
|
44
|
+
*
|
|
45
|
+
* Also, it is not particularly smart around what it should if there were to be multiple slashing events.
|
|
46
|
+
*
|
|
47
|
+
* A few improvements:
|
|
48
|
+
* - 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.
|
|
49
|
+
* - Stop voting on a payload once it is processed.
|
|
50
|
+
* - Only vote on the proposal if it have not already been executed
|
|
51
|
+
* - Caveat, we need to fully decide if it is acceptable to have the same payload address multiple times. In the current
|
|
52
|
+
* slash factory that could mean slashing the same committee for the same error multiple times.
|
|
53
|
+
* - Decide how to deal with multiple slashing events in the same round.
|
|
54
|
+
* - This could be that multiple epochs are pruned in the same round, but with the current naive implementation we could end up
|
|
55
|
+
* slashing only the first, because the "lifetime" of the second would have passed after that vote
|
|
56
|
+
*/
|
|
57
|
+
export declare class SlasherClient extends WithTracer {
|
|
58
|
+
private config;
|
|
59
|
+
private l2BlockSource;
|
|
60
|
+
private log;
|
|
61
|
+
private slashEvents;
|
|
62
|
+
protected slashFactoryContract?: GetContractReturnType<typeof SlashFactoryAbi, ViemPublicClient>;
|
|
63
|
+
private slashingAmount;
|
|
64
|
+
constructor(config: SlasherConfig & L1ContractsConfig & L1ReaderConfig, l2BlockSource: L2BlockSourceEventEmitter, telemetry?: TelemetryClient, log?: import("@aztec/foundation/log").Logger);
|
|
65
|
+
start(): void;
|
|
66
|
+
getSlashPayload(slotNumber: bigint): Promise<EthAddress | undefined>;
|
|
67
|
+
handleBlockStreamEvent(event: L2BlockSourceEvent): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Allows consumers to stop the instance of the slasher client.
|
|
70
|
+
* 'ready' will now return 'false' and the running promise that keeps the client synced is interrupted.
|
|
71
|
+
*/
|
|
72
|
+
stop(): void;
|
|
73
|
+
private handlePruneL2Blocks;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=slasher_client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slasher_client.d.ts","sourceRoot":"","sources":["../../src/slasher/slasher_client.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EAEtB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAE3D,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EACL,KAAK,SAAS,EACd,KAAK,kBAAkB,EACvB,KAAK,yBAAyB,EAE/B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,KAAK,eAAe,EAAE,UAAU,EAAsB,MAAM,yBAAyB,CAAC;AAE/F,OAAO,EAAE,KAAK,qBAAqB,EAA+D,MAAM,MAAM,CAAC;AAE/G;;GAEG;AACH,oBAAY,kBAAkB;IAC5B,IAAI,IAAA;IACJ,OAAO,IAAA;IACP,OAAO,IAAA;CACR;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;OAEG;IACH,KAAK,EAAE,kBAAkB,CAAC;IAC1B;;OAEG;IACH,eAAe,EAAE,SAAS,CAAC;CAC5B;AAED,MAAM,WAAW,aAAa;IAC5B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAQD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,qBAAa,aAAc,SAAQ,UAAU;IAWzC,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,aAAa;IAErB,OAAO,CAAC,GAAG;IAbb,OAAO,CAAC,WAAW,CAAoB;IAEvC,SAAS,CAAC,oBAAoB,CAAC,EAAE,qBAAqB,CAAC,OAAO,eAAe,EAAE,gBAAgB,CAAC,CAAa;IAK7G,OAAO,CAAC,cAAc,CAAc;gBAG1B,MAAM,EAAE,aAAa,GAAG,iBAAiB,GAAG,cAAc,EAC1D,aAAa,EAAE,yBAAyB,EAChD,SAAS,GAAE,eAAsC,EACzC,GAAG,yCAA0B;IAwBhC,KAAK;IAMC,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,SAAS,CAAC;IA8B1E,sBAAsB,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC;IAavE;;;OAGG;IACI,IAAI;IAOX,OAAO,CAAC,mBAAmB;CAgB5B"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { createEthereumChain } from '@aztec/ethereum';
|
|
2
|
+
import { EthAddress } from '@aztec/foundation/eth-address';
|
|
3
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
4
|
+
import { SlashFactoryAbi } from '@aztec/l1-artifacts';
|
|
5
|
+
import { L2BlockSourceEvents } from '@aztec/stdlib/block';
|
|
6
|
+
import { WithTracer, getTelemetryClient } from '@aztec/telemetry-client';
|
|
7
|
+
import { createPublicClient, fallback, getAddress, getContract, http } from 'viem';
|
|
8
|
+
/**
|
|
9
|
+
* Enum defining the possible states of the Slasher client.
|
|
10
|
+
*/ export var SlasherClientState = /*#__PURE__*/ function(SlasherClientState) {
|
|
11
|
+
SlasherClientState[SlasherClientState["IDLE"] = 0] = "IDLE";
|
|
12
|
+
SlasherClientState[SlasherClientState["RUNNING"] = 1] = "RUNNING";
|
|
13
|
+
SlasherClientState[SlasherClientState["STOPPED"] = 2] = "STOPPED";
|
|
14
|
+
return SlasherClientState;
|
|
15
|
+
}({});
|
|
16
|
+
/**
|
|
17
|
+
* @notice A Hypomeiones slasher client implementation
|
|
18
|
+
*
|
|
19
|
+
* Hypomeiones: a class of individuals in ancient Sparta who were considered inferior or lesser citizens compared
|
|
20
|
+
* to the full Spartan citizens.
|
|
21
|
+
*
|
|
22
|
+
* The implementation here is less than ideal. It exists, not to be the end all be all, but to show that
|
|
23
|
+
* slashing can be done with this mechanism.
|
|
24
|
+
*
|
|
25
|
+
* The implementation is VERY brute in the sense that it only looks for pruned blocks and then tries to slash
|
|
26
|
+
* the full committee of that.
|
|
27
|
+
* If it sees a prune, it will mark the full epoch as "to be slashed".
|
|
28
|
+
*
|
|
29
|
+
* Also, it is not particularly smart around what it should if there were to be multiple slashing events.
|
|
30
|
+
*
|
|
31
|
+
* A few improvements:
|
|
32
|
+
* - 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.
|
|
33
|
+
* - Stop voting on a payload once it is processed.
|
|
34
|
+
* - Only vote on the proposal if it have not already been executed
|
|
35
|
+
* - Caveat, we need to fully decide if it is acceptable to have the same payload address multiple times. In the current
|
|
36
|
+
* slash factory that could mean slashing the same committee for the same error multiple times.
|
|
37
|
+
* - Decide how to deal with multiple slashing events in the same round.
|
|
38
|
+
* - This could be that multiple epochs are pruned in the same round, but with the current naive implementation we could end up
|
|
39
|
+
* slashing only the first, because the "lifetime" of the second would have passed after that vote
|
|
40
|
+
*/ export class SlasherClient extends WithTracer {
|
|
41
|
+
config;
|
|
42
|
+
l2BlockSource;
|
|
43
|
+
log;
|
|
44
|
+
slashEvents;
|
|
45
|
+
slashFactoryContract;
|
|
46
|
+
// The amount to slash for a prune.
|
|
47
|
+
// Note that we set it to 0, such that no actual slashing will happen, but the event will be fired,
|
|
48
|
+
// showing that the slashing mechanism is working.
|
|
49
|
+
slashingAmount;
|
|
50
|
+
constructor(config, l2BlockSource, telemetry = getTelemetryClient(), log = createLogger('slasher')){
|
|
51
|
+
super(telemetry, 'slasher'), this.config = config, this.l2BlockSource = l2BlockSource, this.log = log, this.slashEvents = [], this.slashFactoryContract = undefined, this.slashingAmount = 0n;
|
|
52
|
+
if (config.l1Contracts.slashFactoryAddress && config.l1Contracts.slashFactoryAddress !== EthAddress.ZERO) {
|
|
53
|
+
const chain = createEthereumChain(config.l1RpcUrls, config.l1ChainId);
|
|
54
|
+
const publicClient = createPublicClient({
|
|
55
|
+
chain: chain.chainInfo,
|
|
56
|
+
transport: fallback(chain.rpcUrls.map((url)=>http(url))),
|
|
57
|
+
pollingInterval: config.viemPollingIntervalMS
|
|
58
|
+
});
|
|
59
|
+
this.slashFactoryContract = getContract({
|
|
60
|
+
address: getAddress(config.l1Contracts.slashFactoryAddress.toString()),
|
|
61
|
+
abi: SlashFactoryAbi,
|
|
62
|
+
client: publicClient
|
|
63
|
+
});
|
|
64
|
+
} else {
|
|
65
|
+
this.log.warn('No slash factory address found, slashing will not be enabled');
|
|
66
|
+
}
|
|
67
|
+
this.log.info(`Slasher client initialized`);
|
|
68
|
+
}
|
|
69
|
+
start() {
|
|
70
|
+
this.log.info('Starting Slasher client...');
|
|
71
|
+
this.l2BlockSource.on(L2BlockSourceEvents.L2PruneDetected, this.handlePruneL2Blocks.bind(this));
|
|
72
|
+
}
|
|
73
|
+
// This is where we should put a bunch of the improvements mentioned earlier.
|
|
74
|
+
async getSlashPayload(slotNumber) {
|
|
75
|
+
if (!this.slashFactoryContract) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
// As long as the slot is greater than the lifetime, we want to keep deleting the first element
|
|
79
|
+
// since it will not make sense to include anymore.
|
|
80
|
+
while(this.slashEvents.length > 0 && this.slashEvents[0].lifetime < slotNumber){
|
|
81
|
+
this.slashEvents.shift();
|
|
82
|
+
}
|
|
83
|
+
if (this.slashEvents.length == 0) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
const slashEvent = this.slashEvents[0];
|
|
87
|
+
const [payloadAddress, isDeployed] = await this.slashFactoryContract.read.getAddressAndIsDeployed([
|
|
88
|
+
slashEvent.epoch,
|
|
89
|
+
slashEvent.amount
|
|
90
|
+
]);
|
|
91
|
+
if (!isDeployed) {
|
|
92
|
+
// The proposal cannot be executed until it is deployed
|
|
93
|
+
this.log.verbose(`Voting on not yet deployed payload: ${payloadAddress}`);
|
|
94
|
+
}
|
|
95
|
+
return EthAddress.fromString(payloadAddress);
|
|
96
|
+
}
|
|
97
|
+
handleBlockStreamEvent(event) {
|
|
98
|
+
this.log.debug(`Handling block stream event ${event.type}`);
|
|
99
|
+
switch(event.type){
|
|
100
|
+
case L2BlockSourceEvents.L2PruneDetected:
|
|
101
|
+
this.handlePruneL2Blocks(event);
|
|
102
|
+
break;
|
|
103
|
+
default:
|
|
104
|
+
{
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return Promise.resolve();
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Allows consumers to stop the instance of the slasher client.
|
|
112
|
+
* 'ready' will now return 'false' and the running promise that keeps the client synced is interrupted.
|
|
113
|
+
*/ stop() {
|
|
114
|
+
this.log.debug('Stopping Slasher client...');
|
|
115
|
+
this.l2BlockSource.removeListener(L2BlockSourceEvents.L2PruneDetected, this.handlePruneL2Blocks.bind(this));
|
|
116
|
+
this.log.info('Slasher client stopped.');
|
|
117
|
+
}
|
|
118
|
+
// I need to get the slot number from the block that was just pruned
|
|
119
|
+
handlePruneL2Blocks(event) {
|
|
120
|
+
const { slotNumber, epochNumber } = event;
|
|
121
|
+
this.log.info(`Detected chain prune. Punishing the validators at epoch ${epochNumber}`);
|
|
122
|
+
// Set the lifetime such that we have a full round that we could vote throughout.
|
|
123
|
+
const slotsIntoRound = slotNumber % BigInt(this.config.slashingRoundSize);
|
|
124
|
+
const toNext = slotsIntoRound == 0n ? 0n : BigInt(this.config.slashingRoundSize) - slotsIntoRound;
|
|
125
|
+
const lifetime = slotNumber + toNext + BigInt(this.config.slashingRoundSize);
|
|
126
|
+
this.slashEvents.push({
|
|
127
|
+
epoch: epochNumber,
|
|
128
|
+
amount: this.slashingAmount,
|
|
129
|
+
lifetime
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PublicProcessorFactory } from '@aztec/simulator/server';
|
|
2
|
+
import { SequencerClient } from '../client/sequencer-client.js';
|
|
3
|
+
import type { SequencerPublisher } from '../publisher/sequencer-publisher.js';
|
|
4
|
+
import { Sequencer } from '../sequencer/sequencer.js';
|
|
5
|
+
import type { SequencerTimetable } from '../sequencer/timetable.js';
|
|
6
|
+
declare class TestSequencer_ extends Sequencer {
|
|
7
|
+
publicProcessorFactory: PublicProcessorFactory;
|
|
8
|
+
timetable: SequencerTimetable;
|
|
9
|
+
publisher: SequencerPublisher;
|
|
10
|
+
}
|
|
11
|
+
export type TestSequencer = TestSequencer_;
|
|
12
|
+
declare class TestSequencerClient_ extends SequencerClient {
|
|
13
|
+
sequencer: TestSequencer;
|
|
14
|
+
}
|
|
15
|
+
export type TestSequencerClient = TestSequencerClient_;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/test/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAC;AAEtE,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAChE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qCAAqC,CAAC;AAC9E,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AACtD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAEpE,cAAM,cAAe,SAAQ,SAAS;IACpB,sBAAsB,EAAG,sBAAsB,CAAC;IAChD,SAAS,EAAG,kBAAkB,CAAC;IAC/B,SAAS,EAAG,kBAAkB,CAAC;CAChD;AAED,MAAM,MAAM,aAAa,GAAG,cAAc,CAAC;AAE3C,cAAM,oBAAqB,SAAQ,eAAe;IAChC,SAAS,EAAG,aAAa,CAAC;CAC3C;AAED,MAAM,MAAM,mBAAmB,GAAG,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { SequencerClient } from '../client/sequencer-client.js';
|
|
2
|
+
import { Sequencer } from '../sequencer/sequencer.js';
|
|
3
|
+
class TestSequencer_ extends Sequencer {
|
|
4
|
+
publicProcessorFactory;
|
|
5
|
+
timetable;
|
|
6
|
+
publisher;
|
|
7
|
+
}
|
|
8
|
+
class TestSequencerClient_ extends SequencerClient {
|
|
9
|
+
sequencer;
|
|
10
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Fr } from '@aztec/foundation/fields';
|
|
2
|
+
import type { ArchiveSource } from '@aztec/p2p';
|
|
3
|
+
import type { MerkleTreeReadOperations } from '@aztec/stdlib/interfaces/server';
|
|
4
|
+
/**
|
|
5
|
+
* Implements an archive 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 declare class ArchiveCache implements ArchiveSource {
|
|
9
|
+
private db;
|
|
10
|
+
archives: Map<string, bigint>;
|
|
11
|
+
constructor(db: MerkleTreeReadOperations);
|
|
12
|
+
getArchiveIndices(archives: Fr[]): Promise<(bigint | undefined)[]>;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=archive_cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"archive_cache.d.ts","sourceRoot":"","sources":["../../src/tx_validator/archive_cache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,0BAA0B,CAAC;AACnD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,iCAAiC,CAAC;AAGhF;;;GAGG;AACH,qBAAa,YAAa,YAAW,aAAa;IAGpC,OAAO,CAAC,EAAE;IAFtB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;gBAEV,EAAE,EAAE,wBAAwB;IAInC,iBAAiB,CAAC,QAAQ,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,CAAC;CAWhF"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { MerkleTreeId } from '@aztec/stdlib/trees';
|
|
2
|
+
/**
|
|
3
|
+
* Implements an archive source by checking a DB and an in-memory collection.
|
|
4
|
+
* Intended for validating transactions as they are added to a block.
|
|
5
|
+
*/ export class ArchiveCache {
|
|
6
|
+
db;
|
|
7
|
+
archives;
|
|
8
|
+
constructor(db){
|
|
9
|
+
this.db = db;
|
|
10
|
+
this.archives = new Map();
|
|
11
|
+
}
|
|
12
|
+
async getArchiveIndices(archives) {
|
|
13
|
+
const toCheckDb = archives.filter((n)=>!this.archives.has(n.toString()));
|
|
14
|
+
const dbHits = await this.db.findLeafIndices(MerkleTreeId.ARCHIVE, toCheckDb);
|
|
15
|
+
dbHits.forEach((x, index)=>{
|
|
16
|
+
if (x !== undefined) {
|
|
17
|
+
this.archives.set(toCheckDb[index].toString(), x);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
return archives.map((n)=>this.archives.get(n.toString()));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Fr } from '@aztec/foundation/fields';
|
|
2
|
+
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
3
|
+
import type { GasFees } from '@aztec/stdlib/gas';
|
|
4
|
+
import { type Tx, type TxValidationResult, type TxValidator } from '@aztec/stdlib/tx';
|
|
5
|
+
/** Provides a view into public contract state */
|
|
6
|
+
export interface PublicStateSource {
|
|
7
|
+
storageRead: (contractAddress: AztecAddress, slot: Fr) => Promise<Fr>;
|
|
8
|
+
}
|
|
9
|
+
export declare class GasTxValidator implements TxValidator<Tx> {
|
|
10
|
+
#private;
|
|
11
|
+
constructor(publicDataSource: PublicStateSource, feeJuiceAddress: AztecAddress, gasFees: GasFees);
|
|
12
|
+
validateTx(tx: Tx): Promise<TxValidationResult>;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=gas_validator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gas_validator.d.ts","sourceRoot":"","sources":["../../src/tx_validator/gas_validator.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,MAAM,0BAA0B,CAAC;AAK9C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,KAAK,EAAE,EAAoB,KAAK,kBAAkB,EAAE,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAExG,iDAAiD;AACjD,MAAM,WAAW,iBAAiB;IAChC,WAAW,EAAE,CAAC,eAAe,EAAE,YAAY,EAAE,IAAI,EAAE,EAAE,KAAK,OAAO,CAAC,EAAE,CAAC,CAAC;CACvE;AAED,qBAAa,cAAe,YAAW,WAAW,CAAC,EAAE,CAAC;;gBAMxC,gBAAgB,EAAE,iBAAiB,EAAE,eAAe,EAAE,YAAY,EAAE,OAAO,EAAE,OAAO;IAM1F,UAAU,CAAC,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,kBAAkB,CAAC;CA0EtD"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { createLogger } from '@aztec/foundation/log';
|
|
2
|
+
import { computeFeePayerBalanceStorageSlot } from '@aztec/protocol-contracts/fee-juice';
|
|
3
|
+
import { getExecutionRequestsByPhase } from '@aztec/simulator/server';
|
|
4
|
+
import { FunctionSelector } from '@aztec/stdlib/abi';
|
|
5
|
+
import { TxExecutionPhase } from '@aztec/stdlib/tx';
|
|
6
|
+
export class GasTxValidator {
|
|
7
|
+
#log = createLogger('sequencer:tx_validator:tx_gas');
|
|
8
|
+
#publicDataSource;
|
|
9
|
+
#feeJuiceAddress;
|
|
10
|
+
#gasFees;
|
|
11
|
+
constructor(publicDataSource, feeJuiceAddress, gasFees){
|
|
12
|
+
this.#publicDataSource = publicDataSource;
|
|
13
|
+
this.#feeJuiceAddress = feeJuiceAddress;
|
|
14
|
+
this.#gasFees = gasFees;
|
|
15
|
+
}
|
|
16
|
+
async validateTx(tx) {
|
|
17
|
+
if (await this.#shouldSkip(tx)) {
|
|
18
|
+
return Promise.resolve({
|
|
19
|
+
result: 'skipped',
|
|
20
|
+
reason: [
|
|
21
|
+
'Insufficient fee per gas'
|
|
22
|
+
]
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return this.#validateTxFee(tx);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Check whether the tx's max fees are valid for the current block, and skip if not.
|
|
29
|
+
* We skip instead of invalidating since the tx may become eligible later.
|
|
30
|
+
* Note that circuits check max fees even if fee payer is unset, so we
|
|
31
|
+
* keep this validation even if the tx does not pay fees.
|
|
32
|
+
*/ async #shouldSkip(tx) {
|
|
33
|
+
const gasSettings = tx.data.constants.txContext.gasSettings;
|
|
34
|
+
// Skip the tx if its max fees are not enough for the current block's gas fees.
|
|
35
|
+
const maxFeesPerGas = gasSettings.maxFeesPerGas;
|
|
36
|
+
const notEnoughMaxFees = maxFeesPerGas.feePerDaGas.lt(this.#gasFees.feePerDaGas) || maxFeesPerGas.feePerL2Gas.lt(this.#gasFees.feePerL2Gas);
|
|
37
|
+
if (notEnoughMaxFees) {
|
|
38
|
+
this.#log.warn(`Skipping transaction ${await tx.getTxHash()} due to insufficient fee per gas`, {
|
|
39
|
+
txMaxFeesPerGas: maxFeesPerGas.toInspect(),
|
|
40
|
+
currentGasFees: this.#gasFees.toInspect()
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return notEnoughMaxFees;
|
|
44
|
+
}
|
|
45
|
+
async #validateTxFee(tx) {
|
|
46
|
+
const feePayer = tx.data.feePayer;
|
|
47
|
+
// Compute the maximum fee that this tx may pay, based on its gasLimits and maxFeePerGas
|
|
48
|
+
const feeLimit = tx.data.constants.txContext.gasSettings.getFeeLimit();
|
|
49
|
+
// Read current balance of the feePayer
|
|
50
|
+
const initialBalance = await this.#publicDataSource.storageRead(this.#feeJuiceAddress, await computeFeePayerBalanceStorageSlot(feePayer));
|
|
51
|
+
// If there is a claim in this tx that increases the fee payer balance in Fee Juice, add it to balance
|
|
52
|
+
const setupFns = getExecutionRequestsByPhase(tx, TxExecutionPhase.SETUP);
|
|
53
|
+
const increasePublicBalanceSelector = await FunctionSelector.fromSignature('_increase_public_balance((Field),u128)');
|
|
54
|
+
const claimFunctionCall = setupFns.find((fn)=>fn.callContext.contractAddress.equals(this.#feeJuiceAddress) && fn.callContext.msgSender.equals(this.#feeJuiceAddress) && fn.args.length > 2 && // Public functions get routed through the dispatch function, whose first argument is the target function selector.
|
|
55
|
+
fn.args[0].equals(increasePublicBalanceSelector.toField()) && fn.args[1].equals(feePayer.toField()) && !fn.callContext.isStaticCall);
|
|
56
|
+
// The claim amount is at index 2 in the args array because:
|
|
57
|
+
// - Index 0: Target function selector (due to dispatch routing)
|
|
58
|
+
// - Index 1: Amount recipient
|
|
59
|
+
// - Index 2: Amount being claimed
|
|
60
|
+
const balance = claimFunctionCall ? initialBalance.add(claimFunctionCall.args[2]) : initialBalance;
|
|
61
|
+
if (balance.lt(feeLimit)) {
|
|
62
|
+
this.#log.warn(`Rejecting transaction due to not enough fee payer balance`, {
|
|
63
|
+
feePayer,
|
|
64
|
+
balance: balance.toBigInt(),
|
|
65
|
+
feeLimit: feeLimit.toBigInt()
|
|
66
|
+
});
|
|
67
|
+
return {
|
|
68
|
+
result: 'invalid',
|
|
69
|
+
reason: [
|
|
70
|
+
'Insufficient fee payer balance'
|
|
71
|
+
]
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
result: 'valid'
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|