diff --git a/l1-contracts/src/core/Leonidas.sol b/l1-contracts/src/core/Leonidas.sol index 28113a64fc9..77244bec628 100644 --- a/l1-contracts/src/core/Leonidas.sol +++ b/l1-contracts/src/core/Leonidas.sol @@ -83,16 +83,42 @@ contract Leonidas is Staking, TimeFns, ILeonidas { ); } - function deposit(address _attester, address _proposer, address _withdrawer, uint256 _amount) - public - override(Staking) + /** + * @notice Get the committee for a given timestamp + * + * @param _ts - The timestamp to get the committee for + * + * @return The committee for the given timestamp + */ + function getCommitteeAt(Timestamp _ts) + external + view + override(ILeonidas) + returns (address[] memory) { - setupEpoch(); - require( - _attester != address(0) && _proposer != address(0), - Errors.Leonidas__InvalidDeposit(_attester, _proposer) + return LeonidasLib.getCommitteeAt( + leonidasStore, stakingStore, getEpochAt(_ts), TARGET_COMMITTEE_SIZE ); - super.deposit(_attester, _proposer, _withdrawer, _amount); + } + + /** + * @notice Get the sample seed for a given timestamp + * + * @param _ts - The timestamp to get the sample seed for + * + * @return The sample seed for the given timestamp + */ + function getSampleSeedAt(Timestamp _ts) external view override(ILeonidas) returns (uint256) { + return LeonidasLib.getSampleSeed(leonidasStore, getEpochAt(_ts)); + } + + /** + * @notice Get the sample seed for the current epoch + * + * @return The sample seed for the current epoch + */ + function getCurrentSampleSeed() external view override(ILeonidas) returns (uint256) { + return LeonidasLib.getSampleSeed(leonidasStore, getCurrentEpoch()); } function initiateWithdraw(address _attester, address _recipient) @@ -106,6 +132,18 @@ contract Leonidas is Staking, TimeFns, ILeonidas { return super.initiateWithdraw(_attester, _recipient); } + function deposit(address _attester, address _proposer, address _withdrawer, uint256 _amount) + public + override(Staking) + { + setupEpoch(); + require( + _attester != address(0) && _proposer != address(0), + Errors.Leonidas__InvalidDeposit(_attester, _proposer) + ); + super.deposit(_attester, _proposer, _withdrawer, _amount); + } + /** * @notice Performs a setup of an epoch if needed. The setup will * - Sample the validator set for the epoch diff --git a/l1-contracts/src/core/interfaces/ILeonidas.sol b/l1-contracts/src/core/interfaces/ILeonidas.sol index 9624ead5043..7c9aeef404b 100644 --- a/l1-contracts/src/core/interfaces/ILeonidas.sol +++ b/l1-contracts/src/core/interfaces/ILeonidas.sol @@ -39,9 +39,13 @@ interface ILeonidas { // Likely removal of these to replace with a size and indiviual getter // Get the current epoch committee function getCurrentEpochCommittee() external view returns (address[] memory); + function getCommitteeAt(Timestamp _ts) external view returns (address[] memory); function getEpochCommittee(Epoch _epoch) external view returns (address[] memory); function getAttesters() external view returns (address[] memory); + function getSampleSeedAt(Timestamp _ts) external view returns (uint256); + function getCurrentSampleSeed() external view returns (uint256); + function getEpochAt(Timestamp _ts) external view returns (Epoch); function getSlotAt(Timestamp _ts) external view returns (Slot); function getEpochAtSlot(Slot _slotNumber) external view returns (Epoch); diff --git a/scripts/run_interleaved.sh b/scripts/run_interleaved.sh index 85449570cb4..5d18e8cb486 100755 --- a/scripts/run_interleaved.sh +++ b/scripts/run_interleaved.sh @@ -40,9 +40,11 @@ trap cleanup SIGINT SIGTERM EXIT # Function to run a command and prefix the output with color function run_command() { local cmd="$1" + # Take first 3 parts of command to display inline + local cmd_prefix=$(echo "$cmd" | awk '{print $1" "$2" "$3}') local color="$2" $cmd 2>&1 | while IFS= read -r line; do - echo -e "${color}[$cmd]\e[0m $line" + echo -e "${color}[$cmd_prefix]\e[0m $line" done } diff --git a/scripts/run_native_testnet.sh b/scripts/run_native_testnet.sh index 00b08f53b29..4cf6d83900a 100755 --- a/scripts/run_native_testnet.sh +++ b/scripts/run_native_testnet.sh @@ -32,6 +32,7 @@ PROVER_SCRIPT="\"./prover-node.sh 8078 false\"" NUM_VALIDATORS=3 INTERLEAVED=false METRICS=false +LOG_LEVEL="info" OTEL_COLLECTOR_ENDPOINT=${OTEL_COLLECTOR_ENDPOINT:-"http://localhost:4318"} # Function to display help message @@ -120,7 +121,7 @@ cd $(git rev-parse --show-toplevel) # Base command BASE_CMD="INTERLEAVED=$INTERLEAVED ./yarn-project/end-to-end/scripts/native_network_test.sh \ $TEST_SCRIPT \ - ./deploy-l1-contracts.sh \ + \"./deploy-l1-contracts.sh $NUM_VALIDATORS\" \ ./deploy-l2-contracts.sh \ ./boot-node.sh \ ./ethereum.sh \ diff --git a/yarn-project/Earthfile b/yarn-project/Earthfile index c937bc7f915..0fed665d9da 100644 --- a/yarn-project/Earthfile +++ b/yarn-project/Earthfile @@ -328,7 +328,7 @@ network-test: ENV LOG_LEVEL=verbose RUN INTERLEAVED=true end-to-end/scripts/native_network_test.sh \ "$test" \ - ./deploy-l1-contracts.sh \ + "./deploy-l1-contracts.sh $validators" \ ./deploy-l2-contracts.sh \ ./boot-node.sh \ ./ethereum.sh \ diff --git a/yarn-project/archiver/src/archiver/archiver.ts b/yarn-project/archiver/src/archiver/archiver.ts index 83510020b72..f3b25d266b4 100644 --- a/yarn-project/archiver/src/archiver/archiver.ts +++ b/yarn-project/archiver/src/archiver/archiver.ts @@ -2,6 +2,7 @@ import { type GetUnencryptedLogsResponse, type InBlock, type InboxLeaf, + type L1RollupConstants, type L1ToL2MessageSource, type L2Block, type L2BlockId, @@ -15,6 +16,10 @@ import { type TxReceipt, type TxScopedL2Log, type UnencryptedL2Log, + getEpochNumberAtTimestamp, + getSlotAtTimestamp, + getSlotRangeForEpoch, + getTimestampRangeForEpoch, } from '@aztec/circuit-types'; import { type ContractClassPublic, @@ -62,12 +67,6 @@ import { import { type ArchiverDataStore, type ArchiverL1SynchPoint } from './archiver_store.js'; import { type ArchiverConfig } from './config.js'; import { retrieveBlocksFromRollup, retrieveL1ToL2Messages } from './data_retrieval.js'; -import { - getEpochNumberAtTimestamp, - getSlotAtTimestamp, - getSlotRangeForEpoch, - getTimestampRangeForEpoch, -} from './epoch_helpers.js'; import { ArchiverInstrumentation } from './instrumentation.js'; import { type DataRetrieval } from './structs/data_retrieval.js'; import { type L1Published } from './structs/published.js'; @@ -1100,11 +1099,3 @@ class ArchiverStoreHelper return this.store.estimateSize(); } } - -type L1RollupConstants = { - l1StartBlock: bigint; - l1GenesisTime: bigint; - slotDuration: number; - epochDuration: number; - ethereumSlotDuration: number; -}; diff --git a/yarn-project/archiver/src/test/mock_l2_block_source.ts b/yarn-project/archiver/src/test/mock_l2_block_source.ts index cbd2e3363d3..86c120fcb2e 100644 --- a/yarn-project/archiver/src/test/mock_l2_block_source.ts +++ b/yarn-project/archiver/src/test/mock_l2_block_source.ts @@ -7,12 +7,11 @@ import { TxReceipt, TxStatus, } from '@aztec/circuit-types'; +import { getSlotRangeForEpoch } from '@aztec/circuit-types'; import { EthAddress, type Header } from '@aztec/circuits.js'; import { DefaultL1ContractsConfig } from '@aztec/ethereum'; import { createDebugLogger } from '@aztec/foundation/log'; -import { getSlotRangeForEpoch } from '../archiver/epoch_helpers.js'; - /** * A mocked implementation of L2BlockSource to be used in tests. */ diff --git a/yarn-project/aztec-node/src/aztec-node/server.ts b/yarn-project/aztec-node/src/aztec-node/server.ts index 57690bd78d9..87d74936a4a 100644 --- a/yarn-project/aztec-node/src/aztec-node/server.ts +++ b/yarn-project/aztec-node/src/aztec-node/server.ts @@ -171,7 +171,7 @@ export class AztecNodeService implements AztecNode { // start both and wait for them to sync from the block source await Promise.all([p2pClient.start(), worldStateSynchronizer.start()]); - const validatorClient = createValidatorClient(config, p2pClient, telemetry); + const validatorClient = await createValidatorClient(config, config.l1Contracts.rollupAddress, p2pClient, telemetry); // now create the sequencer const sequencer = config.disableValidator diff --git a/yarn-project/aztec.js/src/index.ts b/yarn-project/aztec.js/src/index.ts index 3d3ac097559..31cd10e926f 100644 --- a/yarn-project/aztec.js/src/index.ts +++ b/yarn-project/aztec.js/src/index.ts @@ -32,9 +32,9 @@ export { type ContractNotes, type ContractStorageLayout, type DeployOptions, + type ProfileResult, type SendMethodOptions, type WaitOpts, - type ProfileResult, } from './contract/index.js'; export { ContractDeployer } from './deployment/index.js'; @@ -59,8 +59,8 @@ export { type FieldLike, type FunctionSelectorLike, type L2AmountClaim, - type L2Claim, type L2AmountClaimWithRecipient, + type L2Claim, type WrappedFieldLike, } from './utils/index.js'; @@ -138,10 +138,12 @@ export { UnencryptedL2Log, UniqueNote, createAztecNodeClient, + getTimestampRangeForEpoch, merkleTreeIds, mockEpochProofQuote, mockTx, type AztecNode, + type EpochConstants, type LogFilter, type PXE, type PartialAddress, @@ -164,7 +166,7 @@ export { elapsed } from '@aztec/foundation/timer'; export { type FieldsOf } from '@aztec/foundation/types'; export { fileURLToPath } from '@aztec/foundation/url'; -export { type DeployL1Contracts, EthCheatCodes, deployL1Contract, deployL1Contracts } from '@aztec/ethereum'; +export { EthCheatCodes, deployL1Contract, deployL1Contracts, type DeployL1Contracts } from '@aztec/ethereum'; // Start of section that exports public api via granular api. // Here you *can* do `export *` as the granular api defacto exports things explicitly. diff --git a/yarn-project/aztec/package.json b/yarn-project/aztec/package.json index f8732bff89b..cf8c8e07636 100644 --- a/yarn-project/aztec/package.json +++ b/yarn-project/aztec/package.json @@ -114,4 +114,4 @@ "engines": { "node": ">=18" } -} \ No newline at end of file +} diff --git a/yarn-project/archiver/src/archiver/epoch_helpers.ts b/yarn-project/circuit-types/src/epoch-helpers/index.ts similarity index 84% rename from yarn-project/archiver/src/archiver/epoch_helpers.ts rename to yarn-project/circuit-types/src/epoch-helpers/index.ts index 55fe28e2f0e..50c698e1160 100644 --- a/yarn-project/archiver/src/archiver/epoch_helpers.ts +++ b/yarn-project/circuit-types/src/epoch-helpers/index.ts @@ -1,4 +1,18 @@ -// REFACTOR: This file should go in a package lower in the dependency graph. +export type L1RollupConstants = { + l1StartBlock: bigint; + l1GenesisTime: bigint; + slotDuration: number; + epochDuration: number; + ethereumSlotDuration: number; +}; + +export const EmptyL1RollupConstants: L1RollupConstants = { + l1StartBlock: 0n, + l1GenesisTime: 0n, + epochDuration: 1, // Not 0 to pervent division by zero + slotDuration: 1, + ethereumSlotDuration: 1, +}; export type EpochConstants = { l1GenesisBlock: bigint; diff --git a/yarn-project/circuit-types/src/index.ts b/yarn-project/circuit-types/src/index.ts index 829e43cf8e3..87db2e5e2dd 100644 --- a/yarn-project/circuit-types/src/index.ts +++ b/yarn-project/circuit-types/src/index.ts @@ -25,3 +25,4 @@ export * from './tx_execution_request.js'; export * from './in_block.js'; export * from './nullifier_with_block_source.js'; export * from './proving_error.js'; +export * from './epoch-helpers/index.js'; diff --git a/yarn-project/circuit-types/src/p2p/block_proposal.ts b/yarn-project/circuit-types/src/p2p/block_proposal.ts index 5d2ad7f7f46..207312ba4a1 100644 --- a/yarn-project/circuit-types/src/p2p/block_proposal.ts +++ b/yarn-project/circuit-types/src/p2p/block_proposal.ts @@ -49,6 +49,10 @@ export class BlockProposal extends Gossipable { return this.payload.archive; } + get slotNumber(): Fr { + return this.payload.header.globalVariables.slotNumber; + } + static async createProposalFromSigner( payload: ConsensusPayload, payloadSigner: (payload: Buffer32) => Promise, diff --git a/yarn-project/cli-wallet/package.json b/yarn-project/cli-wallet/package.json index e33bf41c805..7973038a447 100644 --- a/yarn-project/cli-wallet/package.json +++ b/yarn-project/cli-wallet/package.json @@ -100,4 +100,4 @@ "engines": { "node": ">=18" } -} \ No newline at end of file +} diff --git a/yarn-project/end-to-end/package.json b/yarn-project/end-to-end/package.json index 047609c4015..59533e26d89 100644 --- a/yarn-project/end-to-end/package.json +++ b/yarn-project/end-to-end/package.json @@ -98,6 +98,7 @@ "devDependencies": { "0x": "^5.7.0", "@jest/globals": "^29.5.0", + "@sinonjs/fake-timers": "^13.0.5", "@types/jest": "^29.5.0", "@types/js-yaml": "^4.0.9", "@types/lodash.chunk": "^4.2.9", @@ -156,4 +157,4 @@ "testRegex": "./src/.*\\.test\\.(js|mjs|ts)$", "rootDir": "./src" } -} \ No newline at end of file +} diff --git a/yarn-project/end-to-end/scripts/native-network/deploy-l1-contracts.sh b/yarn-project/end-to-end/scripts/native-network/deploy-l1-contracts.sh index 014a1b2b07d..2f1d670620c 100755 --- a/yarn-project/end-to-end/scripts/native-network/deploy-l1-contracts.sh +++ b/yarn-project/end-to-end/scripts/native-network/deploy-l1-contracts.sh @@ -2,6 +2,7 @@ # Get the name of the script without the path and extension SCRIPT_NAME=$(basename "$0" .sh) +REPO=$(git rev-parse --show-toplevel) # Redirect stdout and stderr to .log while also printing to the console exec > >(tee -a "$(dirname $0)/logs/${SCRIPT_NAME}.log") 2> >(tee -a "$(dirname $0)/logs/${SCRIPT_NAME}.log" >&2) @@ -13,7 +14,9 @@ set -eu # Check for validator addresses if [ $# -gt 0 ]; then INIT_VALIDATORS="true" - VALIDATOR_ADDRESSES="$1" + NUMBER_OF_VALIDATORS="$1" + # Generate validator keys, this will set the VALIDATOR_ADDRESSES variable + source $REPO/yarn-project/end-to-end/scripts/native-network/generate-aztec-validator-keys.sh $NUMBER_OF_VALIDATORS else INIT_VALIDATORS="false" fi diff --git a/yarn-project/end-to-end/scripts/native-network/generate-aztec-validator-keys.sh b/yarn-project/end-to-end/scripts/native-network/generate-aztec-validator-keys.sh new file mode 100755 index 00000000000..85fb8ac8a89 --- /dev/null +++ b/yarn-project/end-to-end/scripts/native-network/generate-aztec-validator-keys.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +# Generate Aztec validator keys + +NUMBER_OF_KEYS=${1:-16} +MNEMONIC=${2:-"test test test test test test test test test test test junk"} + +# Initialize arrays to store private keys and addresses +declare -a VALIDATOR_PRIVATE_KEYS +declare -a VALIDATOR_ADDRESSES_LIST + +for i in $(seq 0 $(($NUMBER_OF_KEYS - 1))); do + # Get private key and store it in array + PRIVATE_KEY=$(cast wallet private-key "$MNEMONIC" --mnemonic-index $i) + VALIDATOR_PRIVATE_KEYS+=("$PRIVATE_KEY") + + # Get address from private key and store it in array + ADDRESS=$(cast wallet address "$PRIVATE_KEY") + VALIDATOR_ADDRESSES_LIST+=("$ADDRESS") +done + +# Join addresses with commas +VALIDATOR_ADDRESSES=$(IFS=, ; echo "${VALIDATOR_ADDRESSES_LIST[*]}") + +# Optionally, if you need the arrays for further use, you can export them: +export VALIDATOR_PRIVATE_KEYS +export VALIDATOR_ADDRESSES_LIST +export VALIDATOR_ADDRESSES diff --git a/yarn-project/end-to-end/scripts/native-network/validator.sh b/yarn-project/end-to-end/scripts/native-network/validator.sh index 207952f9b0d..ecc04b6eedb 100755 --- a/yarn-project/end-to-end/scripts/native-network/validator.sh +++ b/yarn-project/end-to-end/scripts/native-network/validator.sh @@ -86,11 +86,12 @@ else node --no-warnings "$REPO"/yarn-project/aztec/dest/bin/index.js add-l1-validator --validator $ADDRESS --rollup $ROLLUP_CONTRACT_ADDRESS && break sleep 1 done -fi -# Fast forward epochs if we're on an anvil chain -if [ "$IS_ANVIL" = "true" ]; then - node --no-warnings "$REPO"/yarn-project/aztec/dest/bin/index.js fast-forward-epochs --rollup $ROLLUP_CONTRACT_ADDRESS --count 1 + # Fast forward epochs if we're on an anvil chain + if [ "$IS_ANVIL" = "true" ]; then + node --no-warnings "$REPO"/yarn-project/aztec/dest/bin/index.js fast-forward-epochs --rollup $ROLLUP_CONTRACT_ADDRESS --count 1 + fi fi + # Start the Validator Node with the sequencer and archiver node --no-warnings "$REPO"/yarn-project/aztec/dest/bin/index.js start --port="$PORT" --node --archiver --sequencer diff --git a/yarn-project/end-to-end/scripts/native-network/validators.sh b/yarn-project/end-to-end/scripts/native-network/validators.sh index c2454b87481..b3a75886368 100755 --- a/yarn-project/end-to-end/scripts/native-network/validators.sh +++ b/yarn-project/end-to-end/scripts/native-network/validators.sh @@ -4,6 +4,7 @@ set -eu # Get the name of the script without the path and extension SCRIPT_NAME=$(basename "$0" .sh) +REPO=$(git rev-parse --show-toplevel) # Redirect stdout and stderr to .log while also printing to the console exec > >(tee -a "$(dirname $0)/logs/${SCRIPT_NAME}.log") 2> >(tee -a "$(dirname $0)/logs/${SCRIPT_NAME}.log" >&2) @@ -15,17 +16,17 @@ cd "$(dirname "${BASH_SOURCE[0]}")" CMD=() +# Generate validator private keys +source $REPO/yarn-project/end-to-end/scripts/native-network/generate-aztec-validator-keys.sh $NUM_VALIDATORS + # Generate validator commands for ((i = 0; i < NUM_VALIDATORS; i++)); do PORT=$((8081 + i)) P2P_PORT=$((40401 + i)) - IDX=$((i + 1)) - # These variables should be set in public networks if we have funded validators already. Leave empty for test environments. - ADDRESS_VAR="ADDRESS_${IDX}" - PRIVATE_KEY_VAR="VALIDATOR_PRIVATE_KEY_${IDX}" - ADDRESS="${!ADDRESS_VAR:-}" - VALIDATOR_PRIVATE_KEY="${!PRIVATE_KEY_VAR:-}" + # Use the arrays generated from generate-aztec-validator-keys.sh + ADDRESS="${VALIDATOR_ADDRESSES_LIST[$i]}" + VALIDATOR_PRIVATE_KEY="${VALIDATOR_PRIVATE_KEYS[$i]}" CMD+=("./validator.sh $PORT $P2P_PORT $ADDRESS $VALIDATOR_PRIVATE_KEY") done diff --git a/yarn-project/end-to-end/src/e2e_epochs.test.ts b/yarn-project/end-to-end/src/e2e_epochs.test.ts index 3ac4e07afd8..ee0dc3197ee 100644 --- a/yarn-project/end-to-end/src/e2e_epochs.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs.test.ts @@ -1,5 +1,5 @@ -import { type EpochConstants, getTimestampRangeForEpoch } from '@aztec/archiver/epoch'; -import { type DebugLogger, retryUntil } from '@aztec/aztec.js'; +// eslint-disable-next-line no-restricted-imports +import { type DebugLogger, type EpochConstants, getTimestampRangeForEpoch, retryUntil } from '@aztec/aztec.js'; import { RollupContract } from '@aztec/ethereum/contracts'; import { type Delayer, waitUntilL1Timestamp } from '@aztec/ethereum/test'; diff --git a/yarn-project/end-to-end/src/e2e_p2p/gossip_network.test.ts b/yarn-project/end-to-end/src/e2e_p2p/gossip_network.test.ts index 6f1c7096705..afa5cdbb1cd 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/gossip_network.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/gossip_network.test.ts @@ -37,9 +37,9 @@ describe('e2e_p2p_network', () => { testName: 'e2e_p2p_network', numberOfNodes: NUM_NODES, basePort: BOOT_NODE_UDP_PORT, - // To collect metrics - run in aztec-packages `docker compose --profile metrics up` and set COLLECT_METRICS=true metricsPort: shouldCollectMetrics(), }); + await t.applyBaseSnapshots(); await t.setup(); await t.removeInitialNode(); diff --git a/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts b/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts index 0fb208a4932..12ad8e7623d 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/p2p_network.ts @@ -1,7 +1,7 @@ import { getSchnorrAccount } from '@aztec/accounts/schnorr'; import { type AztecNodeConfig, type AztecNodeService } from '@aztec/aztec-node'; -import { type AccountWalletWithSecretKey, EthCheatCodes } from '@aztec/aztec.js'; -import { MINIMUM_STAKE, getL1ContractsConfigEnvVars } from '@aztec/ethereum'; +import { type AccountWalletWithSecretKey } from '@aztec/aztec.js'; +import { EthCheatCodes, MINIMUM_STAKE, getL1ContractsConfigEnvVars } from '@aztec/ethereum'; import { type DebugLogger, createDebugLogger } from '@aztec/foundation/log'; import { RollupAbi, TestERC20Abi } from '@aztec/l1-artifacts'; import { SpamContract } from '@aztec/noir-contracts.js'; @@ -50,6 +50,8 @@ export class P2PNetworkTest { public wallet?: AccountWalletWithSecretKey; public spamContract?: SpamContract; + private cleanupInterval: NodeJS.Timeout | undefined = undefined; + constructor( testName: string, public bootstrapNode: BootstrapNode, @@ -77,6 +79,35 @@ export class P2PNetworkTest { }); } + /** + * Start a loop to sync the mock system time with the L1 block time + */ + public startSyncMockSystemTimeInterval() { + this.cleanupInterval = setInterval(async () => { + await this.syncMockSystemTime(); + }, l1ContractsConfig.aztecSlotDuration * 1000); + } + + /** + * When using fake timers, we need to keep the system and anvil clocks in sync. + */ + public async syncMockSystemTime() { + this.logger.info('Syncing mock system time'); + const { timer, deployL1ContractsValues } = this.ctx!; + // Send a tx and only update the time after the tx is mined, as eth time is not continuous + const tx = await deployL1ContractsValues.walletClient.sendTransaction({ + to: this.baseAccount.address, + value: 1n, + account: this.baseAccount, + }); + const receipt = await deployL1ContractsValues.publicClient.waitForTransactionReceipt({ + hash: tx, + }); + const timestamp = await deployL1ContractsValues.publicClient.getBlock({ blockNumber: receipt.blockNumber }); + timer.setSystemTime(Number(timestamp.timestamp) * 1000); + this.logger.info(`Synced mock system time to ${timestamp.timestamp * 1000n}`); + } + static async create({ testName, numberOfNodes, @@ -100,74 +131,81 @@ export class P2PNetworkTest { } async applyBaseSnapshots() { - await this.snapshotManager.snapshot('add-validators', async ({ deployL1ContractsValues, aztecNodeConfig }) => { - const rollup = getContract({ - address: deployL1ContractsValues.l1ContractAddresses.rollupAddress.toString(), - abi: RollupAbi, - client: deployL1ContractsValues.walletClient, - }); - - this.logger.verbose(`Adding ${this.numberOfNodes} validators`); - - const stakingAsset = getContract({ - address: deployL1ContractsValues.l1ContractAddresses.stakingAssetAddress.toString(), - abi: TestERC20Abi, - client: deployL1ContractsValues.walletClient, - }); - - const stakeNeeded = MINIMUM_STAKE * BigInt(this.numberOfNodes); - await Promise.all( - [ - await stakingAsset.write.mint([deployL1ContractsValues.walletClient.account.address, stakeNeeded], {} as any), - await stakingAsset.write.approve( - [deployL1ContractsValues.l1ContractAddresses.rollupAddress.toString(), stakeNeeded], - {} as any, - ), - ].map(txHash => deployL1ContractsValues.publicClient.waitForTransactionReceipt({ hash: txHash })), - ); - - const validators = []; - - for (let i = 0; i < this.numberOfNodes; i++) { - const attester = privateKeyToAccount(this.attesterPrivateKeys[i]!); - const proposer = privateKeyToAccount(this.proposerPrivateKeys[i]!); - validators.push({ - attester: attester.address, - proposer: proposer.address, - withdrawer: attester.address, - amount: MINIMUM_STAKE, - } as const); - - this.logger.verbose( - `Adding (attester, proposer) pair: (${attester.address}, ${proposer.address}) as validator`, + await this.snapshotManager.snapshot( + 'add-validators', + async ({ deployL1ContractsValues, aztecNodeConfig, timer }) => { + const rollup = getContract({ + address: deployL1ContractsValues.l1ContractAddresses.rollupAddress.toString(), + abi: RollupAbi, + client: deployL1ContractsValues.walletClient, + }); + + this.logger.verbose(`Adding ${this.numberOfNodes} validators`); + + const stakingAsset = getContract({ + address: deployL1ContractsValues.l1ContractAddresses.stakingAssetAddress.toString(), + abi: TestERC20Abi, + client: deployL1ContractsValues.walletClient, + }); + + const stakeNeeded = MINIMUM_STAKE * BigInt(this.numberOfNodes); + await Promise.all( + [ + await stakingAsset.write.mint( + [deployL1ContractsValues.walletClient.account.address, stakeNeeded], + {} as any, + ), + await stakingAsset.write.approve( + [deployL1ContractsValues.l1ContractAddresses.rollupAddress.toString(), stakeNeeded], + {} as any, + ), + ].map(txHash => deployL1ContractsValues.publicClient.waitForTransactionReceipt({ hash: txHash })), ); - } - - await deployL1ContractsValues.publicClient.waitForTransactionReceipt({ - hash: await rollup.write.cheat__InitialiseValidatorSet([validators]), - }); - - //@note Now we jump ahead to the next epoch such that the validator committee is picked - // INTERVAL MINING: If we are using anvil interval mining this will NOT progress the time! - // Which means that the validator set will still be empty! So anyone can propose. - const slotsInEpoch = await rollup.read.EPOCH_DURATION(); - const timestamp = await rollup.read.getTimestampForSlot([slotsInEpoch]); - const cheatCodes = new EthCheatCodes(aztecNodeConfig.l1RpcUrl); - try { - await cheatCodes.warp(Number(timestamp)); - } catch (err) { - this.logger.debug('Warp failed, time already satisfied'); - } - - // Send and await a tx to make sure we mine a block for the warp to correctly progress. - await deployL1ContractsValues.publicClient.waitForTransactionReceipt({ - hash: await deployL1ContractsValues.walletClient.sendTransaction({ - to: this.baseAccount.address, - value: 1n, - account: this.baseAccount, - }), - }); - }); + + const validators = []; + + for (let i = 0; i < this.numberOfNodes; i++) { + const attester = privateKeyToAccount(this.attesterPrivateKeys[i]!); + const proposer = privateKeyToAccount(this.proposerPrivateKeys[i]!); + validators.push({ + attester: attester.address, + proposer: proposer.address, + withdrawer: attester.address, + amount: MINIMUM_STAKE, + } as const); + + this.logger.verbose( + `Adding (attester, proposer) pair: (${attester.address}, ${proposer.address}) as validator`, + ); + } + + await deployL1ContractsValues.publicClient.waitForTransactionReceipt({ + hash: await rollup.write.cheat__InitialiseValidatorSet([validators]), + }); + + const slotsInEpoch = await rollup.read.EPOCH_DURATION(); + const timestamp = await rollup.read.getTimestampForSlot([slotsInEpoch]); + const cheatCodes = new EthCheatCodes(aztecNodeConfig.l1RpcUrl); + try { + await cheatCodes.warp(Number(timestamp)); + } catch (err) { + this.logger.debug('Warp failed, time already satisfied'); + } + + // Send and await a tx to make sure we mine a block for the warp to correctly progress. + await deployL1ContractsValues.publicClient.waitForTransactionReceipt({ + hash: await deployL1ContractsValues.walletClient.sendTransaction({ + to: this.baseAccount.address, + value: 1n, + account: this.baseAccount, + }), + }); + + // Set the system time in the node, only after we have warped the time and waited for a block + // Time is only set in the NEXT block + timer.setSystemTime(Number(timestamp) * 1000); + }, + ); } async setupAccount() { @@ -204,13 +242,30 @@ export class P2PNetworkTest { } async removeInitialNode() { - await this.snapshotManager.snapshot('remove-inital-validator', async () => { - await this.ctx.aztecNode.stop(); - }); + await this.snapshotManager.snapshot( + 'remove-inital-validator', + async ({ deployL1ContractsValues, aztecNode, timer }) => { + // Send and await a tx to make sure we mine a block for the warp to correctly progress. + const receipt = await deployL1ContractsValues.publicClient.waitForTransactionReceipt({ + hash: await deployL1ContractsValues.walletClient.sendTransaction({ + to: this.baseAccount.address, + value: 1n, + account: this.baseAccount, + }), + }); + const block = await deployL1ContractsValues.publicClient.getBlock({ + blockNumber: receipt.blockNumber, + }); + timer.setSystemTime(Number(block.timestamp) * 1000); + + await aztecNode.stop(); + }, + ); } async setup() { this.ctx = await this.snapshotManager.setup(); + this.startSyncMockSystemTimeInterval(); } async stopNodes(nodes: AztecNodeService[]) { @@ -230,5 +285,8 @@ export class P2PNetworkTest { async teardown() { await this.bootstrapNode.stop(); await this.snapshotManager.teardown(); + if (this.cleanupInterval) { + clearInterval(this.cleanupInterval); + } } } diff --git a/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts index 8aa2a934b33..6477c47acf0 100644 --- a/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts +++ b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts @@ -24,6 +24,7 @@ import { type ProverNode } from '@aztec/prover-node'; import { type PXEService, createPXEService, getPXEServiceConfig } from '@aztec/pxe'; import { createAndStartTelemetryClient, getConfigEnvVars as getTelemetryConfig } from '@aztec/telemetry-client/start'; +import { type InstalledClock, install } from '@sinonjs/fake-timers'; import { type Anvil } from '@viem/anvil'; import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; import { copySync, removeSync } from 'fs-extra/esm'; @@ -49,6 +50,7 @@ export type SubsystemsContext = { proverNode?: ProverNode; watcher: AnvilTestWatcher; cheatCodes: CheatCodes; + timer: InstalledClock; }; type SnapshotEntry = { @@ -247,6 +249,7 @@ async function teardown(context: SubsystemsContext | undefined) { await context.acvmConfig?.cleanup(); await context.anvil.stop(); await context.watcher.stop(); + context.timer?.uninstall(); } /** @@ -265,6 +268,9 @@ async function setupFromFresh( ): Promise { logger.verbose(`Initializing state...`); + // Use sinonjs fake timers + const timer = install({ shouldAdvanceTime: true, advanceTimeDelta: 20, toFake: ['Date'] }); + // Fetch the AztecNode config. // TODO: For some reason this is currently the union of a bunch of subsystems. That needs fixing. const aztecNodeConfig: AztecNodeConfig & SetupOptions = { ...getConfigEnvVars(), ...opts }; @@ -382,6 +388,7 @@ async function setupFromFresh( proverNode, watcher, cheatCodes, + timer, }; } @@ -391,6 +398,9 @@ async function setupFromFresh( async function setupFromState(statePath: string, logger: Logger): Promise { logger.verbose(`Initializing with saved state at ${statePath}...`); + // TODO: make one function + const timer = install({ shouldAdvanceTime: true, advanceTimeDelta: 20, toFake: ['Date'] }); + // TODO: For some reason this is currently the union of a bunch of subsystems. That needs fixing. const aztecNodeConfig: AztecNodeConfig & SetupOptions = JSON.parse( readFileSync(`${statePath}/aztec_node_config.json`, 'utf-8'), @@ -463,6 +473,7 @@ async function setupFromState(statePath: string, logger: Logger): Promise=18" + } +} diff --git a/yarn-project/epoch-cache/src/config.ts b/yarn-project/epoch-cache/src/config.ts new file mode 100644 index 00000000000..3da15946771 --- /dev/null +++ b/yarn-project/epoch-cache/src/config.ts @@ -0,0 +1,12 @@ +import { + type L1ContractsConfig, + type L1ReaderConfig, + getL1ContractsConfigEnvVars, + getL1ReaderConfigFromEnv, +} from '@aztec/ethereum'; + +export type EpochCacheConfig = L1ReaderConfig & L1ContractsConfig; + +export function getEpochCacheConfigEnvVars(): EpochCacheConfig { + return { ...getL1ReaderConfigFromEnv(), ...getL1ContractsConfigEnvVars() }; +} diff --git a/yarn-project/epoch-cache/src/epoch_cache.test.ts b/yarn-project/epoch-cache/src/epoch_cache.test.ts new file mode 100644 index 00000000000..4cde40eff1e --- /dev/null +++ b/yarn-project/epoch-cache/src/epoch_cache.test.ts @@ -0,0 +1,117 @@ +import { type RollupContract } from '@aztec/ethereum'; +import { EthAddress } from '@aztec/foundation/eth-address'; + +import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals'; +import { type MockProxy, mock } from 'jest-mock-extended'; + +import { EpochCache } from './epoch_cache.js'; + +describe('EpochCache', () => { + let rollupContract: MockProxy; + let epochCache: EpochCache; + + // Test constants + const SLOT_DURATION = 12; + const EPOCH_DURATION = 32; // 384 seconds + // const L1_GENESIS_TIME = 1000n; + let l1GenesisTime: bigint; + + const testCommittee = [ + EthAddress.fromString('0x0000000000000000000000000000000000000001'), + EthAddress.fromString('0x0000000000000000000000000000000000000002'), + EthAddress.fromString('0x0000000000000000000000000000000000000003'), + ]; + + const extraTestValidator = EthAddress.fromString('0x0000000000000000000000000000000000000004'); + + beforeEach(() => { + rollupContract = mock(); + + // Mock the getCommitteeAt method + rollupContract.getCommitteeAt.mockResolvedValue(testCommittee.map(v => v.toString())); + rollupContract.getSampleSeedAt.mockResolvedValue(0n); + + l1GenesisTime = BigInt(Math.floor(Date.now() / 1000)); + + // Setup fake timers + jest.useFakeTimers(); + + // Initialize with test constants + const testConstants = { + l1StartBlock: 0n, + l1GenesisTime, + slotDuration: SLOT_DURATION, + ethereumSlotDuration: SLOT_DURATION, + epochDuration: EPOCH_DURATION, + }; + + epochCache = new EpochCache(rollupContract, testCommittee, 0n, testConstants); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('should cache the validator set for the length of an epoch', async () => { + // Initial call to get validators + const initialCommittee = await epochCache.getCommittee(); + expect(initialCommittee).toEqual(testCommittee); + // Not called as we should cache with the initial validator set + expect(rollupContract.getCommitteeAt).toHaveBeenCalledTimes(0); + + // Move time forward within the same epoch (less than EPOCH_DURATION) (x 1000 for milliseconds) + jest.setSystemTime(Date.now() + (Number(EPOCH_DURATION * SLOT_DURATION) / 4) * 1000); + + // Add another validator to the set + rollupContract.getCommitteeAt.mockResolvedValue([...testCommittee, extraTestValidator].map(v => v.toString())); + + // Should use cached validators + const midEpochCommittee = await epochCache.getCommittee(); + expect(midEpochCommittee).toEqual(testCommittee); + expect(rollupContract.getCommitteeAt).toHaveBeenCalledTimes(0); // Still cached + + // Move time forward to next epoch (x 1000 for milliseconds) + jest.setSystemTime(Date.now() + Number(EPOCH_DURATION * SLOT_DURATION) * 1000); + + // Should fetch new validator + const nextEpochCommittee = await epochCache.getCommittee(); + expect(nextEpochCommittee).toEqual([...testCommittee, extraTestValidator]); + expect(rollupContract.getCommitteeAt).toHaveBeenCalledTimes(1); // Called again for new epoch + }); + + it('should correctly get current validator based on slot number', async () => { + // Set initial time to a known slot + const initialTime = Number(l1GenesisTime) * 1000; // Convert to milliseconds + jest.setSystemTime(initialTime); + + // The valid proposer has been calculated in advance to be [1,1,0] for the slots chosen + // Hence the chosen values for testCommittee below + + // Get validator for slot 0 + let [currentValidator] = await epochCache.getProposerInCurrentOrNextSlot(); + expect(currentValidator).toEqual(testCommittee[1]); + + // Move to next slot + jest.setSystemTime(initialTime + Number(SLOT_DURATION) * 1000); + [currentValidator] = await epochCache.getProposerInCurrentOrNextSlot(); + expect(currentValidator).toEqual(testCommittee[1]); + + // Move to slot that wraps around validator set + jest.setSystemTime(initialTime + Number(SLOT_DURATION) * 3 * 1000); + [currentValidator] = await epochCache.getProposerInCurrentOrNextSlot(); + expect(currentValidator).toEqual(testCommittee[0]); + }); + + it('Should request to update the validator set when on the epoch boundary', async () => { + // Set initial time to a known slot + const initialTime = Number(l1GenesisTime) * 1000; // Convert to milliseconds + jest.setSystemTime(initialTime); + + // Move forward to slot before the epoch boundary + jest.setSystemTime(initialTime + Number(SLOT_DURATION) * (EPOCH_DURATION - 1) * 1000); + + // Should request to update the validator set + await epochCache.getProposerInCurrentOrNextSlot(); + expect(rollupContract.getCommitteeAt).toHaveBeenCalledTimes(1); + }); +}); diff --git a/yarn-project/epoch-cache/src/epoch_cache.ts b/yarn-project/epoch-cache/src/epoch_cache.ts new file mode 100644 index 00000000000..7ebee68ef14 --- /dev/null +++ b/yarn-project/epoch-cache/src/epoch_cache.ts @@ -0,0 +1,192 @@ +import { + EmptyL1RollupConstants, + type L1RollupConstants, + getEpochNumberAtTimestamp, + getSlotAtTimestamp, +} from '@aztec/circuit-types'; +import { RollupContract, createEthereumChain } from '@aztec/ethereum'; +import { EthAddress } from '@aztec/foundation/eth-address'; +import { type Logger, createDebugLogger } from '@aztec/foundation/log'; + +import { createPublicClient, encodeAbiParameters, http, keccak256 } from 'viem'; + +import { type EpochCacheConfig, getEpochCacheConfigEnvVars } from './config.js'; + +type EpochAndSlot = { + epoch: bigint; + slot: bigint; + ts: bigint; +}; + +/** + * Epoch cache + * + * This class is responsible for managing traffic to the l1 node, by caching the validator set. + * It also provides a method to get the current or next proposer, and to check who is in the current slot. + * + * If the epoch changes, then we update the stored validator set. + * + * Note: This class is very dependent on the system clock being in sync. + */ +export class EpochCache { + private committee: EthAddress[]; + private cachedEpoch: bigint; + private cachedSampleSeed: bigint; + private readonly log: Logger = createDebugLogger('aztec:EpochCache'); + + constructor( + private rollup: RollupContract, + initialValidators: EthAddress[] = [], + initialSampleSeed: bigint = 0n, + private readonly l1constants: L1RollupConstants = EmptyL1RollupConstants, + ) { + this.committee = initialValidators; + this.cachedSampleSeed = initialSampleSeed; + + this.log.debug(`Initialized EpochCache with constants and validators`, { l1constants, initialValidators }); + + this.cachedEpoch = getEpochNumberAtTimestamp(BigInt(Math.floor(Date.now() / 1000)), this.l1constants); + } + + static async create(rollupAddress: EthAddress, config?: EpochCacheConfig) { + config = config ?? getEpochCacheConfigEnvVars(); + + const chain = createEthereumChain(config.l1RpcUrl, config.l1ChainId); + const publicClient = createPublicClient({ + chain: chain.chainInfo, + transport: http(chain.rpcUrl), + pollingInterval: config.viemPollingIntervalMS, + }); + + const rollup = new RollupContract(publicClient, rollupAddress.toString()); + const [l1StartBlock, l1GenesisTime, initialValidators, sampleSeed] = await Promise.all([ + rollup.getL1StartBlock(), + rollup.getL1GenesisTime(), + rollup.getCurrentEpochCommittee(), + rollup.getCurrentSampleSeed(), + ] as const); + + const l1RollupConstants: L1RollupConstants = { + l1StartBlock, + l1GenesisTime, + slotDuration: config.aztecSlotDuration, + epochDuration: config.aztecEpochDuration, + ethereumSlotDuration: config.ethereumSlotDuration, + }; + + return new EpochCache( + rollup, + initialValidators.map(v => EthAddress.fromString(v)), + sampleSeed, + l1RollupConstants, + ); + } + + getEpochAndSlotNow(): EpochAndSlot { + const now = BigInt(Math.floor(Date.now() / 1000)); + return this.getEpochAndSlotAtTimestamp(now); + } + + getEpochAndSlotInNextSlot(): EpochAndSlot { + const nextSlotTs = BigInt(Math.floor(Date.now() / 1000) + this.l1constants.slotDuration); + return this.getEpochAndSlotAtTimestamp(nextSlotTs); + } + + getEpochAndSlotAtTimestamp(ts: bigint): EpochAndSlot { + return { + epoch: getEpochNumberAtTimestamp(ts, this.l1constants), + slot: getSlotAtTimestamp(ts, this.l1constants), + ts, + }; + } + + /** + * Get the current validator set + * + * @param nextSlot - If true, get the validator set for the next slot. + * @returns The current validator set. + */ + async getCommittee(nextSlot: boolean = false): Promise { + // If the current epoch has changed, then we need to make a request to update the validator set + const { epoch: calculatedEpoch, ts } = nextSlot ? this.getEpochAndSlotInNextSlot() : this.getEpochAndSlotNow(); + + if (calculatedEpoch !== this.cachedEpoch) { + this.log.debug(`Epoch changed, updating validator set`, { calculatedEpoch, cachedEpoch: this.cachedEpoch }); + this.cachedEpoch = calculatedEpoch; + const [committeeAtTs, sampleSeedAtTs] = await Promise.all([ + this.rollup.getCommitteeAt(ts), + this.rollup.getSampleSeedAt(ts), + ]); + this.committee = committeeAtTs.map((v: `0x${string}`) => EthAddress.fromString(v)); + this.cachedSampleSeed = sampleSeedAtTs; + } + + return this.committee; + } + + /** + * Get the ABI encoding of the proposer index - see Leonidas.sol _computeProposerIndex + */ + getProposerIndexEncoding(epoch: bigint, slot: bigint, seed: bigint): `0x${string}` { + return encodeAbiParameters( + [ + { type: 'uint256', name: 'epoch' }, + { type: 'uint256', name: 'slot' }, + { type: 'uint256', name: 'seed' }, + ], + [epoch, slot, seed], + ); + } + + computeProposerIndex(slot: bigint, epoch: bigint, seed: bigint, size: bigint): bigint { + return BigInt(keccak256(this.getProposerIndexEncoding(epoch, slot, seed))) % size; + } + + /** + * Returns the current and next proposer + * + * We return the next proposer as the node will check if it is the proposer at the next ethereum block, which + * can be the next slot. If this is the case, then it will send proposals early. + * + * If we are at an epoch boundary, then we can update the cache for the next epoch, this is the last check + * we do in the validator client, so we can update the cache here. + */ + async getProposerInCurrentOrNextSlot(): Promise<[EthAddress, EthAddress]> { + // Validators are sorted by their index in the committee, and getValidatorSet will cache + const committee = await this.getCommittee(); + const { slot: currentSlot, epoch: currentEpoch } = this.getEpochAndSlotNow(); + const { slot: nextSlot, epoch: nextEpoch } = this.getEpochAndSlotInNextSlot(); + + // Compute the proposer in this and the next slot + const proposerIndex = this.computeProposerIndex( + currentSlot, + this.cachedEpoch, + this.cachedSampleSeed, + BigInt(committee.length), + ); + + // Check if the next proposer is in the next epoch + if (nextEpoch !== currentEpoch) { + await this.getCommittee(/*next slot*/ true); + } + const nextProposerIndex = this.computeProposerIndex( + nextSlot, + this.cachedEpoch, + this.cachedSampleSeed, + BigInt(committee.length), + ); + + const calculatedProposer = committee[Number(proposerIndex)]; + const nextCalculatedProposer = committee[Number(nextProposerIndex)]; + + return [calculatedProposer, nextCalculatedProposer]; + } + + /** + * Check if a validator is in the current epoch's committee + */ + async isInCommittee(validator: EthAddress): Promise { + const committee = await this.getCommittee(); + return committee.some(v => v.equals(validator)); + } +} diff --git a/yarn-project/epoch-cache/src/index.ts b/yarn-project/epoch-cache/src/index.ts new file mode 100644 index 00000000000..f6a7dba8382 --- /dev/null +++ b/yarn-project/epoch-cache/src/index.ts @@ -0,0 +1,2 @@ +export * from './epoch_cache.js'; +export * from './config.js'; diff --git a/yarn-project/epoch-cache/src/timestamp_provider.ts b/yarn-project/epoch-cache/src/timestamp_provider.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/yarn-project/epoch-cache/tsconfig.json b/yarn-project/epoch-cache/tsconfig.json new file mode 100644 index 00000000000..249d08ef855 --- /dev/null +++ b/yarn-project/epoch-cache/tsconfig.json @@ -0,0 +1,23 @@ +{ + "extends": "..", + "compilerOptions": { + "outDir": "dest", + "rootDir": "src", + "tsBuildInfoFile": ".tsbuildinfo" + }, + "include": ["src"], + "references": [ + { + "path": "../circuit-types" + }, + { + "path": "../ethereum" + }, + { + "path": "../foundation" + }, + { + "path": "../l1-artifacts" + } + ] +} diff --git a/yarn-project/ethereum/src/contracts/rollup.ts b/yarn-project/ethereum/src/contracts/rollup.ts index 98e3ac29fe2..69bc1065653 100644 --- a/yarn-project/ethereum/src/contracts/rollup.ts +++ b/yarn-project/ethereum/src/contracts/rollup.ts @@ -44,6 +44,26 @@ export class RollupContract { return this.rollup.read.getCurrentSlot(); } + getCommitteeAt(timestamp: bigint) { + return this.rollup.read.getCommitteeAt([timestamp]); + } + + getSampleSeedAt(timestamp: bigint) { + return this.rollup.read.getSampleSeedAt([timestamp]); + } + + getCurrentSampleSeed() { + return this.rollup.read.getCurrentSampleSeed(); + } + + getCurrentEpochCommittee() { + return this.rollup.read.getCurrentEpochCommittee(); + } + + getCurrentProposer() { + return this.rollup.read.getCurrentProposer(); + } + async getEpochNumber(blockNumber?: bigint) { blockNumber ??= await this.getBlockNumber(); return this.rollup.read.getEpochForBlock([BigInt(blockNumber)]); diff --git a/yarn-project/ethereum/src/deploy_l1_contracts.ts b/yarn-project/ethereum/src/deploy_l1_contracts.ts index 52a7aed1907..d6efa076de8 100644 --- a/yarn-project/ethereum/src/deploy_l1_contracts.ts +++ b/yarn-project/ethereum/src/deploy_l1_contracts.ts @@ -265,6 +265,7 @@ export function createL1Clients( const publicClient = createPublicClient({ chain, transport: http(rpcUrl), + pollingInterval: 100, }); return { walletClient, publicClient }; @@ -363,6 +364,7 @@ export const deployL1Contracts = async ( ]); logger.info(`Deployed RewardDistributor at ${rewardDistributorAddress}`); + logger.verbose(`Waiting for governance contracts to be deployed`); await govDeployer.waitForDeployments(); logger.info(`All governance contracts deployed`); diff --git a/yarn-project/ethereum/src/index.ts b/yarn-project/ethereum/src/index.ts index d6393560093..10f28f3bd0f 100644 --- a/yarn-project/ethereum/src/index.ts +++ b/yarn-project/ethereum/src/index.ts @@ -8,3 +8,4 @@ export * from './l1_reader.js'; export * from './utils.js'; export * from './config.js'; export * from './types.js'; +export * from './contracts/index.js'; diff --git a/yarn-project/ethereum/src/l1_reader.ts b/yarn-project/ethereum/src/l1_reader.ts index f2f48196824..2c6340ba025 100644 --- a/yarn-project/ethereum/src/l1_reader.ts +++ b/yarn-project/ethereum/src/l1_reader.ts @@ -1,4 +1,4 @@ -import { type ConfigMappingsType, numberConfigHelper } from '@aztec/foundation/config'; +import { type ConfigMappingsType, getConfigFromMappings, numberConfigHelper } from '@aztec/foundation/config'; import { type L1ContractAddresses, l1ContractAddressesMapping } from './l1_contract_addresses.js'; @@ -36,3 +36,7 @@ export const l1ReaderConfigMappings: ConfigMappingsType = { ...numberConfigHelper(1_000), }, }; + +export function getL1ReaderConfigFromEnv(): L1ReaderConfig { + return getConfigFromMappings(l1ReaderConfigMappings); +} diff --git a/yarn-project/kv-store/package.json b/yarn-project/kv-store/package.json index 96073106a08..f42e02ff786 100644 --- a/yarn-project/kv-store/package.json +++ b/yarn-project/kv-store/package.json @@ -79,4 +79,4 @@ "engines": { "node": ">=18" } -} \ No newline at end of file +} diff --git a/yarn-project/package.json b/yarn-project/package.json index a1547a4ccda..7d31e95a5c8 100644 --- a/yarn-project/package.json +++ b/yarn-project/package.json @@ -38,6 +38,7 @@ "docs", "end-to-end", "entrypoints", + "epoch-cache", "ethereum", "foundation", "key-store", diff --git a/yarn-project/prover-client/package.json b/yarn-project/prover-client/package.json index 08b6b59a46d..2c01d5617c0 100644 --- a/yarn-project/prover-client/package.json +++ b/yarn-project/prover-client/package.json @@ -104,4 +104,4 @@ "engines": { "node": ">=18" } -} \ No newline at end of file +} diff --git a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts index d7e139d4dde..7be51664842 100644 --- a/yarn-project/sequencer-client/src/publisher/l1-publisher.ts +++ b/yarn-project/sequencer-client/src/publisher/l1-publisher.ts @@ -208,6 +208,10 @@ export class L1Publisher { this.l1TxUtils = new L1TxUtils(this.publicClient, this.walletClient, this.log, config); } + get publisherAddress() { + return this.account.address; + } + protected createWalletClient( account: PrivateKeyAccount, chain: EthereumChain, diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.ts index 325a2dd2d44..34c9100a4e8 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.ts @@ -362,7 +362,9 @@ export class Sequencer { throw new Error(msg); } - this.log.verbose(`Can propose block ${proposalBlockNumber} at slot ${slot}`); + this.log.verbose(`Can propose block ${proposalBlockNumber} at slot ${slot}`, { + publisherAddress: this.publisher.publisherAddress, + }); return slot; } catch (err) { const msg = prettyLogViemErrorMsg(err); @@ -691,6 +693,10 @@ export class Sequencer { this.log.info('Creating block proposal'); const proposal = await this.validatorClient.createBlockProposal(block.header, block.archive.root, txHashes); + if (!proposal) { + this.log.verbose(`Failed to create block proposal, skipping`); + return undefined; + } const slotNumber = block.header.globalVariables.slotNumber.toBigInt(); diff --git a/yarn-project/txe/package.json b/yarn-project/txe/package.json index ebae6269f3c..0fa9709f348 100644 --- a/yarn-project/txe/package.json +++ b/yarn-project/txe/package.json @@ -93,4 +93,4 @@ "engines": { "node": ">=18" } -} \ No newline at end of file +} diff --git a/yarn-project/validator-client/package.json b/yarn-project/validator-client/package.json index 7fff0272c51..23cba4d9036 100644 --- a/yarn-project/validator-client/package.json +++ b/yarn-project/validator-client/package.json @@ -61,6 +61,7 @@ "dependencies": { "@aztec/circuit-types": "workspace:^", "@aztec/circuits.js": "workspace:^", + "@aztec/epoch-cache": "workspace:^", "@aztec/ethereum": "workspace:^", "@aztec/foundation": "workspace:^", "@aztec/p2p": "workspace:^", diff --git a/yarn-project/validator-client/src/factory.ts b/yarn-project/validator-client/src/factory.ts index 56b5306969b..7ea8b09e8d6 100644 --- a/yarn-project/validator-client/src/factory.ts +++ b/yarn-project/validator-client/src/factory.ts @@ -1,3 +1,5 @@ +import { EpochCache, type EpochCacheConfig } from '@aztec/epoch-cache'; +import { type EthAddress } from '@aztec/foundation/eth-address'; import { type P2P } from '@aztec/p2p'; import { type TelemetryClient } from '@aztec/telemetry-client'; @@ -6,7 +8,12 @@ import { generatePrivateKey } from 'viem/accounts'; import { type ValidatorClientConfig } from './config.js'; import { ValidatorClient } from './validator.js'; -export function createValidatorClient(config: ValidatorClientConfig, p2pClient: P2P, telemetry: TelemetryClient) { +export async function createValidatorClient( + config: ValidatorClientConfig & EpochCacheConfig, + rollupAddress: EthAddress, + p2pClient: P2P, + telemetry: TelemetryClient, +) { if (config.disableValidator) { return undefined; } @@ -14,5 +21,8 @@ export function createValidatorClient(config: ValidatorClientConfig, p2pClient: config.validatorPrivateKey = generatePrivateKey(); } - return ValidatorClient.new(config, p2pClient, telemetry); + // Create the epoch cache + const epochCache = await EpochCache.create(rollupAddress, /*l1TimestampSource,*/ config); + + return ValidatorClient.new(config, epochCache, p2pClient, telemetry); } diff --git a/yarn-project/validator-client/src/validator.test.ts b/yarn-project/validator-client/src/validator.test.ts index d60937f2fcc..df2bee4ccbb 100644 --- a/yarn-project/validator-client/src/validator.test.ts +++ b/yarn-project/validator-client/src/validator.test.ts @@ -3,6 +3,7 @@ */ import { TxHash, mockTx } from '@aztec/circuit-types'; import { makeHeader } from '@aztec/circuits.js/testing'; +import { type EpochCache } from '@aztec/epoch-cache'; import { Secp256k1Signer } from '@aztec/foundation/crypto'; import { EthAddress } from '@aztec/foundation/eth-address'; import { Fr } from '@aztec/foundation/fields'; @@ -27,11 +28,13 @@ describe('ValidationService', () => { let config: ValidatorClientConfig; let validatorClient: ValidatorClient; let p2pClient: MockProxy; + let epochCache: MockProxy; let validatorAccount: PrivateKeyAccount; beforeEach(() => { p2pClient = mock(); p2pClient.getAttestationsForSlot.mockImplementation(() => Promise.resolve([])); + epochCache = mock(); const validatorPrivateKey = generatePrivateKey(); validatorAccount = privateKeyToAccount(validatorPrivateKey); @@ -43,12 +46,12 @@ describe('ValidationService', () => { disableValidator: false, validatorReexecute: false, }; - validatorClient = ValidatorClient.new(config, p2pClient, new NoopTelemetryClient()); + validatorClient = ValidatorClient.new(config, epochCache, p2pClient, new NoopTelemetryClient()); }); it('Should throw error if an invalid private key is provided', () => { config.validatorPrivateKey = '0x1234567890123456789'; - expect(() => ValidatorClient.new(config, p2pClient, new NoopTelemetryClient())).toThrow( + expect(() => ValidatorClient.new(config, epochCache, p2pClient, new NoopTelemetryClient())).toThrow( InvalidValidatorPrivateKeyError, ); }); @@ -56,7 +59,7 @@ describe('ValidationService', () => { it('Should throw an error if re-execution is enabled but no block builder is provided', async () => { config.validatorReexecute = true; p2pClient.getTxByHash.mockImplementation(() => Promise.resolve(mockTx())); - const val = ValidatorClient.new(config, p2pClient); + const val = ValidatorClient.new(config, epochCache, p2pClient); await expect(val.reExecuteTransactions(makeBlockProposal())).rejects.toThrow(BlockBuilderNotProvidedError); }); @@ -70,7 +73,7 @@ describe('ValidationService', () => { expect(blockProposal).toBeDefined(); const validatorAddress = EthAddress.fromString(validatorAccount.address); - expect(blockProposal.getSender()).toEqual(validatorAddress); + expect(blockProposal?.getSender()).toEqual(validatorAddress); }); it('Should a timeout if we do not collect enough attestations in time', async () => { @@ -97,8 +100,12 @@ describe('ValidationService', () => { // mock the p2pClient.getTxStatus to return undefined for all transactions p2pClient.getTxStatus.mockImplementation(() => undefined); + epochCache.getProposerInCurrentOrNextSlot.mockImplementation(() => + Promise.resolve([proposal.getSender(), proposal.getSender()]), + ); + epochCache.isInCommittee.mockImplementation(() => Promise.resolve(true)); - const val = ValidatorClient.new(config, p2pClient, new NoopTelemetryClient()); + const val = ValidatorClient.new(config, epochCache, p2pClient); val.registerBlockBuilder(() => { throw new Error('Failed to build block'); }); @@ -107,6 +114,32 @@ describe('ValidationService', () => { expect(attestation).toBeUndefined(); }); + it('Should not return an attestation if the validator is not in the committee', async () => { + const proposal = makeBlockProposal(); + + // Setup epoch cache mocks + epochCache.getProposerInCurrentOrNextSlot.mockImplementation(() => + Promise.resolve([proposal.getSender(), proposal.getSender()]), + ); + epochCache.isInCommittee.mockImplementation(() => Promise.resolve(false)); + + const attestation = await validatorClient.attestToProposal(proposal); + expect(attestation).toBeUndefined(); + }); + + it('Should not return an attestation if the proposer is not the current proposer', async () => { + const proposal = makeBlockProposal(); + + // Setup epoch cache mocks + epochCache.getProposerInCurrentOrNextSlot.mockImplementation(() => + Promise.resolve([EthAddress.random(), EthAddress.random()]), + ); + epochCache.isInCommittee.mockImplementation(() => Promise.resolve(true)); + + const attestation = await validatorClient.attestToProposal(proposal); + expect(attestation).toBeUndefined(); + }); + it('Should collect attestations for a proposal', async () => { const signer = Secp256k1Signer.random(); const attestor1 = Secp256k1Signer.random(); diff --git a/yarn-project/validator-client/src/validator.ts b/yarn-project/validator-client/src/validator.ts index 7ced2639ab1..33a78090648 100644 --- a/yarn-project/validator-client/src/validator.ts +++ b/yarn-project/validator-client/src/validator.ts @@ -7,6 +7,7 @@ import { type TxHash, } from '@aztec/circuit-types'; import { type GlobalVariables, type Header } from '@aztec/circuits.js'; +import { type EpochCache } from '@aztec/epoch-cache'; import { Buffer32 } from '@aztec/foundation/buffer'; import { type Fr } from '@aztec/foundation/fields'; import { createDebugLogger } from '@aztec/foundation/log'; @@ -47,7 +48,7 @@ export interface Validator { registerBlockBuilder(blockBuilder: BlockBuilderCallback): void; // Block validation responsiblities - createBlockProposal(header: Header, archive: Fr, txs: TxHash[]): Promise; + createBlockProposal(header: Header, archive: Fr, txs: TxHash[]): Promise; attestToProposal(proposal: BlockProposal): void; broadcastBlockProposal(proposal: BlockProposal): void; @@ -61,11 +62,15 @@ export class ValidatorClient extends WithTracer implements Validator { private validationService: ValidationService; private metrics: ValidatorMetrics; + // Used to check if we are sending the same proposal twice + private previousProposal?: BlockProposal; + // Callback registered to: sequencer.buildBlock private blockBuilder?: BlockBuilderCallback = undefined; constructor( - keyStore: ValidatorKeyStore, + private keyStore: ValidatorKeyStore, + private epochCache: EpochCache, private p2pClient: P2P, private config: ValidatorClientConfig, telemetry: TelemetryClient = new NoopTelemetryClient(), @@ -75,12 +80,16 @@ export class ValidatorClient extends WithTracer implements Validator { super(telemetry, 'Validator'); this.metrics = new ValidatorMetrics(telemetry); - //TODO: We need to setup and store all of the currently active validators https://github.com/AztecProtocol/aztec-packages/issues/7962 this.validationService = new ValidationService(keyStore); this.log.verbose('Initialized validator'); } - static new(config: ValidatorClientConfig, p2pClient: P2P, telemetry: TelemetryClient = new NoopTelemetryClient()) { + static new( + config: ValidatorClientConfig, + epochCache: EpochCache, + p2pClient: P2P, + telemetry: TelemetryClient = new NoopTelemetryClient(), + ) { if (!config.validatorPrivateKey) { throw new InvalidValidatorPrivateKeyError(); } @@ -88,7 +97,7 @@ export class ValidatorClient extends WithTracer implements Validator { const privateKey = validatePrivateKey(config.validatorPrivateKey); const localKeyStore = new LocalKeyStore(privateKey); - const validator = new ValidatorClient(localKeyStore, p2pClient, config, telemetry); + const validator = new ValidatorClient(localKeyStore, epochCache, p2pClient, config, telemetry); validator.registerBlockProposalHandler(); return validator; } @@ -118,6 +127,19 @@ export class ValidatorClient extends WithTracer implements Validator { } async attestToProposal(proposal: BlockProposal): Promise { + // Check that I am in the committee + if (!(await this.epochCache.isInCommittee(this.keyStore.getAddress()))) { + this.log.verbose(`Not in the committee, skipping attestation`); + return undefined; + } + + // Check that the proposal is from the current proposer, or the next proposer. + const [currentProposer, nextSlotProposer] = await this.epochCache.getProposerInCurrentOrNextSlot(); + if (!proposal.getSender().equals(currentProposer) && !proposal.getSender().equals(nextSlotProposer)) { + this.log.verbose(`Not the current or next proposer, skipping attestation`); + return undefined; + } + // Check that all of the tranasctions in the proposal are available in the tx pool before attesting this.log.verbose(`request to attest`, { archive: proposal.payload.archive.toString(), @@ -131,14 +153,18 @@ export class ValidatorClient extends WithTracer implements Validator { await this.reExecuteTransactions(proposal); } } catch (error: any) { + // If the transactions are not available, then we should not attempt to attest if (error instanceof TransactionsNotAvailableError) { this.log.error(`Transactions not available, skipping attestation ${error.message}`); } else { + // This branch most commonly be hit if the transactions are available, but the re-execution fails // Catch all error handler this.log.error(`Failed to attest to proposal: ${error.message}`); } return undefined; } + + // Provided all of the above checks pass, we can attest to the proposal this.log.verbose( `Transactions available, attesting to proposal with ${proposal.payload.txHashes.length} transactions`, ); @@ -210,8 +236,15 @@ export class ValidatorClient extends WithTracer implements Validator { } } - createBlockProposal(header: Header, archive: Fr, txs: TxHash[]): Promise { - return this.validationService.createBlockProposal(header, archive, txs); + async createBlockProposal(header: Header, archive: Fr, txs: TxHash[]): Promise { + if (this.previousProposal?.slotNumber.equals(header.globalVariables.slotNumber)) { + this.log.verbose(`Already made a proposal for the same slot, skipping proposal`); + return Promise.resolve(undefined); + } + + const newProposal = await this.validationService.createBlockProposal(header, archive, txs); + this.previousProposal = newProposal; + return newProposal; } broadcastBlockProposal(proposal: BlockProposal): void { diff --git a/yarn-project/validator-client/tsconfig.json b/yarn-project/validator-client/tsconfig.json index 17533523097..d2409d81fdb 100644 --- a/yarn-project/validator-client/tsconfig.json +++ b/yarn-project/validator-client/tsconfig.json @@ -12,6 +12,9 @@ { "path": "../circuits.js" }, + { + "path": "../epoch-cache" + }, { "path": "../ethereum" }, diff --git a/yarn-project/yarn.lock b/yarn-project/yarn.lock index c6c51d2d87d..6a843300ccc 100644 --- a/yarn-project/yarn.lock +++ b/yarn-project/yarn.lock @@ -373,7 +373,7 @@ __metadata: languageName: unknown linkType: soft -"@aztec/circuit-types@workspace:^, @aztec/circuit-types@workspace:circuit-types": +"@aztec/circuit-types@workspace:*, @aztec/circuit-types@workspace:^, @aztec/circuit-types@workspace:circuit-types": version: 0.0.0-use.local resolution: "@aztec/circuit-types@workspace:circuit-types" dependencies: @@ -545,6 +545,7 @@ __metadata: "@aztec/world-state": "workspace:^" "@jest/globals": ^29.5.0 "@noble/curves": ^1.0.0 + "@sinonjs/fake-timers": ^13.0.5 "@swc/core": ^1.4.11 "@swc/jest": ^0.2.36 "@types/fs-extra": ^11.0.2 @@ -611,7 +612,31 @@ __metadata: languageName: unknown linkType: soft -"@aztec/ethereum@workspace:^, @aztec/ethereum@workspace:ethereum": +"@aztec/epoch-cache@workspace:^, @aztec/epoch-cache@workspace:epoch-cache": + version: 0.0.0-use.local + resolution: "@aztec/epoch-cache@workspace:epoch-cache" + dependencies: + "@aztec/circuit-types": "workspace:*" + "@aztec/ethereum": "workspace:*" + "@aztec/foundation": "workspace:^" + "@aztec/l1-artifacts": "workspace:^" + "@jest/globals": ^29.5.0 + "@types/jest": ^29.5.0 + "@types/node": ^18.14.6 + "@viem/anvil": ^0.0.10 + dotenv: ^16.0.3 + get-port: ^7.1.0 + jest: ^29.5.0 + jest-mock-extended: ^3.0.7 + ts-node: ^10.9.1 + tslib: ^2.4.0 + typescript: ^5.0.4 + viem: ^2.7.15 + zod: ^3.23.8 + languageName: unknown + linkType: soft + +"@aztec/ethereum@workspace:*, @aztec/ethereum@workspace:^, @aztec/ethereum@workspace:ethereum": version: 0.0.0-use.local resolution: "@aztec/ethereum@workspace:ethereum" dependencies: @@ -1283,6 +1308,7 @@ __metadata: dependencies: "@aztec/circuit-types": "workspace:^" "@aztec/circuits.js": "workspace:^" + "@aztec/epoch-cache": "workspace:^" "@aztec/ethereum": "workspace:^" "@aztec/foundation": "workspace:^" "@aztec/p2p": "workspace:^" @@ -4116,7 +4142,7 @@ __metadata: languageName: node linkType: hard -"@sinonjs/commons@npm:^3.0.0": +"@sinonjs/commons@npm:^3.0.0, @sinonjs/commons@npm:^3.0.1": version: 3.0.1 resolution: "@sinonjs/commons@npm:3.0.1" dependencies: @@ -4134,6 +4160,15 @@ __metadata: languageName: node linkType: hard +"@sinonjs/fake-timers@npm:^13.0.5": + version: 13.0.5 + resolution: "@sinonjs/fake-timers@npm:13.0.5" + dependencies: + "@sinonjs/commons": ^3.0.1 + checksum: b1c6ba87fadb7666d3aa126c9e8b4ac32b2d9e84c9e5fd074aa24cab3c8342fd655459de014b08e603be1e6c24c9f9716d76d6d2a36c50f59bb0091be61601dd + languageName: node + linkType: hard + "@swc/core-darwin-arm64@npm:1.5.5": version: 1.5.5 resolution: "@swc/core-darwin-arm64@npm:1.5.5"