From 6d31ad236b678376227b1ca408b0f0169e05fc83 Mon Sep 17 00:00:00 2001 From: Lasse Herskind <16536249+LHerskind@users.noreply.github.com> Date: Tue, 27 Aug 2024 17:02:40 +0100 Subject: [PATCH] feat: initial validator set (#8133) Fixes #8102. Supports having an initial validator set in the rollup. For the DevNet, this should make it slightly simpler to deploy as well as the sequencer can just be set at deployment and don't need the extra call. @PhilWindle this might be useful for you. --- l1-contracts/src/core/Rollup.sol | 9 ++- .../core/sequencer_selection/ILeonidas.sol | 1 + .../src/core/sequencer_selection/Leonidas.sol | 19 ++++- l1-contracts/test/Rollup.t.sol | 3 +- l1-contracts/test/portals/TokenPortal.t.sol | 7 +- l1-contracts/test/portals/UniswapPortal.t.sol | 7 +- l1-contracts/test/sparta/DevNet.t.sol | 11 ++- l1-contracts/test/sparta/Sparta.t.sol | 7 +- .../end-to-end/src/e2e_p2p_network.test.ts | 71 +++++-------------- yarn-project/end-to-end/src/fixtures/utils.ts | 14 +++- .../ethereum/src/deploy_l1_contracts.ts | 9 ++- 11 files changed, 93 insertions(+), 65 deletions(-) diff --git a/l1-contracts/src/core/Rollup.sol b/l1-contracts/src/core/Rollup.sol index 2d32c4858c8..946eee20138 100644 --- a/l1-contracts/src/core/Rollup.sol +++ b/l1-contracts/src/core/Rollup.sol @@ -77,7 +77,8 @@ contract Rollup is Leonidas, IRollup, ITestRollup { IAvailabilityOracle _availabilityOracle, IFeeJuicePortal _fpcJuicePortal, bytes32 _vkTreeRoot, - address _ares + address _ares, + address[] memory _validators ) Leonidas(_ares) { verifier = new MockVerifier(); REGISTRY = _registry; @@ -97,6 +98,10 @@ contract Rollup is Leonidas, IRollup, ITestRollup { }); pendingBlockCount = 1; provenBlockCount = 1; + + for (uint256 i = 0; i < _validators.length; i++) { + _addValidator(_validators[i]); + } } /** @@ -524,7 +529,7 @@ contract Rollup is Leonidas, IRollup, ITestRollup { } if (!isValidator(msg.sender)) { - revert Errors.Leonidas__InvalidProposer(address(0), msg.sender); + revert Errors.Leonidas__InvalidProposer(getValidatorAt(0), msg.sender); } return; } diff --git a/l1-contracts/src/core/sequencer_selection/ILeonidas.sol b/l1-contracts/src/core/sequencer_selection/ILeonidas.sol index a7de4ac1b3c..a9542975205 100644 --- a/l1-contracts/src/core/sequencer_selection/ILeonidas.sol +++ b/l1-contracts/src/core/sequencer_selection/ILeonidas.sol @@ -17,6 +17,7 @@ interface ILeonidas { function getCurrentSlot() external view returns (uint256); function isValidator(address _validator) external view returns (bool); function getValidatorCount() external view returns (uint256); + function getValidatorAt(uint256 _index) external view returns (address); // Consider removing below this point function getTimestampForSlot(uint256 _slotNumber) external view returns (uint256); diff --git a/l1-contracts/src/core/sequencer_selection/Leonidas.sol b/l1-contracts/src/core/sequencer_selection/Leonidas.sol index 511b49eb622..33826e6f563 100644 --- a/l1-contracts/src/core/sequencer_selection/Leonidas.sol +++ b/l1-contracts/src/core/sequencer_selection/Leonidas.sol @@ -96,7 +96,7 @@ contract Leonidas is Ownable, ILeonidas { */ function addValidator(address _validator) external override(ILeonidas) onlyOwner { setupEpoch(); - validatorSet.add(_validator); + _addValidator(_validator); } /** @@ -189,6 +189,15 @@ contract Leonidas is Ownable, ILeonidas { return validatorSet.length(); } + /** + * @notice Get the number of validators in the validator set + * + * @return The number of validators in the validator set + */ + function getValidatorAt(uint256 _index) public view override(ILeonidas) returns (address) { + return validatorSet.at(_index); + } + /** * @notice Checks if an address is in the validator set * @@ -320,6 +329,14 @@ contract Leonidas is Ownable, ILeonidas { return committee[_computeProposerIndex(epochNumber, slot, sampleSeed, committee.length)]; } + /** + * @notice Adds a validator to the set WITHOUT setting up the epoch + * @param _validator - The validator to add + */ + function _addValidator(address _validator) internal { + validatorSet.add(_validator); + } + /** * @notice Process a pending block from the point-of-view of sequencer selection. Will: * - Setup the epoch if needed (if epoch committee is empty skips the rest) diff --git a/l1-contracts/test/Rollup.t.sol b/l1-contracts/test/Rollup.t.sol index 7c1c9739b01..05e33af8f14 100644 --- a/l1-contracts/test/Rollup.t.sol +++ b/l1-contracts/test/Rollup.t.sol @@ -65,7 +65,8 @@ contract RollupTest is DecoderBase { availabilityOracle, IFeeJuicePortal(address(feeJuicePortal)), bytes32(0), - address(this) + address(this), + new address[](0) ); inbox = Inbox(address(rollup.INBOX())); outbox = Outbox(address(rollup.OUTBOX())); diff --git a/l1-contracts/test/portals/TokenPortal.t.sol b/l1-contracts/test/portals/TokenPortal.t.sol index 411579d3e91..6241c57880e 100644 --- a/l1-contracts/test/portals/TokenPortal.t.sol +++ b/l1-contracts/test/portals/TokenPortal.t.sol @@ -62,7 +62,12 @@ contract TokenPortalTest is Test { registry = new Registry(address(this)); portalERC20 = new PortalERC20(); rollup = new Rollup( - registry, new AvailabilityOracle(), IFeeJuicePortal(address(0)), bytes32(0), address(this) + registry, + new AvailabilityOracle(), + IFeeJuicePortal(address(0)), + bytes32(0), + address(this), + new address[](0) ); inbox = rollup.INBOX(); outbox = rollup.OUTBOX(); diff --git a/l1-contracts/test/portals/UniswapPortal.t.sol b/l1-contracts/test/portals/UniswapPortal.t.sol index be0144dd0ae..06269177402 100644 --- a/l1-contracts/test/portals/UniswapPortal.t.sol +++ b/l1-contracts/test/portals/UniswapPortal.t.sol @@ -54,7 +54,12 @@ contract UniswapPortalTest is Test { registry = new Registry(address(this)); rollup = new Rollup( - registry, new AvailabilityOracle(), IFeeJuicePortal(address(0)), bytes32(0), address(this) + registry, + new AvailabilityOracle(), + IFeeJuicePortal(address(0)), + bytes32(0), + address(this), + new address[](0) ); registry.upgrade(address(rollup)); diff --git a/l1-contracts/test/sparta/DevNet.t.sol b/l1-contracts/test/sparta/DevNet.t.sol index ff0907ee642..d37defe7ba8 100644 --- a/l1-contracts/test/sparta/DevNet.t.sol +++ b/l1-contracts/test/sparta/DevNet.t.sol @@ -57,7 +57,12 @@ contract DevNetTest is DecoderBase { registry = new Registry(address(this)); availabilityOracle = new AvailabilityOracle(); rollup = new Rollup( - registry, availabilityOracle, IFeeJuicePortal(address(0)), bytes32(0), address(this) + registry, + availabilityOracle, + IFeeJuicePortal(address(0)), + bytes32(0), + address(this), + new address[](0) ); inbox = Inbox(address(rollup.INBOX())); outbox = Outbox(address(rollup.OUTBOX())); @@ -160,7 +165,9 @@ contract DevNetTest is DecoderBase { ree.proposer = address(uint160(uint256(keccak256(abi.encode("invalid", ree.proposer))))); // Why don't we end up here? vm.expectRevert( - abi.encodeWithSelector(Errors.Leonidas__InvalidProposer.selector, address(0), ree.proposer) + abi.encodeWithSelector( + Errors.Leonidas__InvalidProposer.selector, rollup.getValidatorAt(0), ree.proposer + ) ); ree.shouldRevert = true; } diff --git a/l1-contracts/test/sparta/Sparta.t.sol b/l1-contracts/test/sparta/Sparta.t.sol index dad40cec374..028533839d2 100644 --- a/l1-contracts/test/sparta/Sparta.t.sol +++ b/l1-contracts/test/sparta/Sparta.t.sol @@ -63,7 +63,12 @@ contract SpartaTest is DecoderBase { availabilityOracle = new AvailabilityOracle(); portalERC20 = new PortalERC20(); rollup = new Rollup( - registry, availabilityOracle, IFeeJuicePortal(address(0)), bytes32(0), address(this) + registry, + availabilityOracle, + IFeeJuicePortal(address(0)), + bytes32(0), + address(this), + new address[](0) ); inbox = Inbox(address(rollup.INBOX())); outbox = Outbox(address(rollup.OUTBOX())); diff --git a/yarn-project/end-to-end/src/e2e_p2p_network.test.ts b/yarn-project/end-to-end/src/e2e_p2p_network.test.ts index c3ec8d93737..f297b084b47 100644 --- a/yarn-project/end-to-end/src/e2e_p2p_network.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p_network.test.ts @@ -1,27 +1,14 @@ import { getSchnorrAccount } from '@aztec/accounts/schnorr'; import { type AztecNodeConfig, type AztecNodeService } from '@aztec/aztec-node'; -import { - CompleteAddress, - type DebugLogger, - type DeployL1Contracts, - EthCheatCodes, - Fr, - GrumpkinScalar, - type SentTx, - TxStatus, - sleep, -} from '@aztec/aztec.js'; -import { IS_DEV_NET } from '@aztec/circuits.js'; -import { RollupAbi } from '@aztec/l1-artifacts'; +import { CompleteAddress, type DebugLogger, Fr, GrumpkinScalar, type SentTx, TxStatus, sleep } from '@aztec/aztec.js'; +import { EthAddress, IS_DEV_NET } from '@aztec/circuits.js'; import { type BootstrapNode } from '@aztec/p2p'; import { type PXEService, createPXEService, getPXEServiceConfig as getRpcConfig } from '@aztec/pxe'; import { jest } from '@jest/globals'; import fs from 'fs'; -import { getContract } from 'viem'; -import { mnemonicToAccount, privateKeyToAccount } from 'viem/accounts'; +import { privateKeyToAccount } from 'viem/accounts'; -import { MNEMONIC } from './fixtures/fixtures.js'; import { type NodeContext, createBootstrapNode, @@ -29,7 +16,7 @@ import { createNodes, generatePeerIdPrivateKeys, } from './fixtures/setup_p2p_test.js'; -import { setup } from './fixtures/utils.js'; +import { getPrivateKeyFromIndex, setup } from './fixtures/utils.js'; // Don't set this to a higher value than 9 because each node will use a different L1 publisher account and anvil seeds const NUM_NODES = 4; @@ -45,7 +32,6 @@ describe('e2e_p2p_network', () => { let teardown: () => Promise; let bootstrapNode: BootstrapNode; let bootstrapNodeEnr: string; - let deployL1ContractsValues: DeployL1Contracts; beforeEach(async () => { // If we want to test with interval mining, we can use the local host and start `anvil --block-time 12` @@ -54,43 +40,22 @@ describe('e2e_p2p_network', () => { jest.setTimeout(300_000); } const options = useLocalHost ? { l1RpcUrl: 'http://127.0.0.1:8545' } : {}; - ({ teardown, config, logger, deployL1ContractsValues } = await setup(0, options)); - // It would likely be useful if we had the sequencers in such that they don't spam each other. - // However, even if they do, it should still work. Not sure what caused the failure - // Would be easier if I could see the errors from anvil as well, but those seem to be hidden. - - const rollup = getContract({ - address: deployL1ContractsValues.l1ContractAddresses.rollupAddress.toString(), - abi: RollupAbi, - client: deployL1ContractsValues.walletClient, - }); - - if (IS_DEV_NET) { - // Add just ONE of the peers as sequencer, he will be the proposer all blocks. - const hdAccount = mnemonicToAccount(MNEMONIC, { addressIndex: 1 }); - const publisherPrivKey = Buffer.from(hdAccount.getHdKey().privateKey!); - const account = privateKeyToAccount(`0x${publisherPrivKey!.toString('hex')}`); - await rollup.write.addValidator([account.address], { gas: 1_000_000n }); - logger.info(`Adding sequencer ${account.address}`); - } else { - // Add all nodes as validators - they will all sign attestations of each other's proposals - for (let i = 0; i < NUM_NODES; i++) { - const hdAccount = mnemonicToAccount(MNEMONIC, { addressIndex: i + 1 }); - const publisherPrivKey = Buffer.from(hdAccount.getHdKey().privateKey!); - const account = privateKeyToAccount(`0x${publisherPrivKey!.toString('hex')}`); - await rollup.write.addValidator([account.address], { gas: 1_000_000n }); - logger.info(`Adding sequencer ${account.address}`); - } - } - //@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]); + // We need the very first node to be the sequencer for this is the one doing everything throughout the setup. + // Without it we will wait forever. + const account = privateKeyToAccount(`0x${getPrivateKeyFromIndex(0)!.toString('hex')}`); + + const initialValidators = [EthAddress.fromString(account.address)]; + + // Add 1 extra validator if in devnet or NUM_NODES if not. + // Each of these will become a validator and sign attestations. + const limit = IS_DEV_NET ? 1 : NUM_NODES; + for (let i = 0; i < limit; i++) { + const account = privateKeyToAccount(`0x${getPrivateKeyFromIndex(i + 1)!.toString('hex')}`); + initialValidators.push(EthAddress.fromString(account.address)); + } - const cheatCodes = new EthCheatCodes(config.l1RpcUrl); - await cheatCodes.warp(Number(timestamp)); + ({ teardown, config, logger } = await setup(0, { initialValidators, ...options })); bootstrapNode = await createBootstrapNode(BOOT_NODE_UDP_PORT); bootstrapNodeEnr = bootstrapNode.getENR().encodeTxt(); diff --git a/yarn-project/end-to-end/src/fixtures/utils.ts b/yarn-project/end-to-end/src/fixtures/utils.ts index a065b2037b1..a34e6a918ec 100644 --- a/yarn-project/end-to-end/src/fixtures/utils.ts +++ b/yarn-project/end-to-end/src/fixtures/utils.ts @@ -32,6 +32,7 @@ import { type BBNativePrivateKernelProver } from '@aztec/bb-prover'; import { CANONICAL_AUTH_REGISTRY_ADDRESS, CANONICAL_KEY_REGISTRY_ADDRESS, + type EthAddress, GasSettings, MAX_PACKED_PUBLIC_BYTECODE_SIZE_IN_FIELDS, computeContractAddressFromInstance, @@ -113,7 +114,7 @@ export const setupL1Contracts = async ( l1RpcUrl: string, account: HDAccount | PrivateKeyAccount, logger: DebugLogger, - args: { salt?: number } = {}, + args: { salt?: number; initialValidators?: EthAddress[] } = {}, chain: Chain = foundry, ) => { const l1Artifacts: L1ContractArtifactsForDeployment = { @@ -151,6 +152,7 @@ export const setupL1Contracts = async ( l2FeeJuiceAddress: FeeJuiceAddress, vkTreeRoot: getVKTreeRoot(), salt: args.salt, + initialValidators: args.initialValidators, }); return l1Data; @@ -295,6 +297,8 @@ type SetupOptions = { skipProtocolContracts?: boolean; /** Salt to use in L1 contract deployment */ salt?: number; + /** An initial set of validators */ + initialValidators?: EthAddress[]; } & Partial; /** Context for an end-to-end test as returned by the `setup` function */ @@ -388,7 +392,13 @@ export async function setup( const deployL1ContractsValues = opts.deployL1ContractsValues ?? - (await setupL1Contracts(config.l1RpcUrl, publisherHdAccount!, logger, { salt: opts.salt }, chain)); + (await setupL1Contracts( + config.l1RpcUrl, + publisherHdAccount!, + logger, + { salt: opts.salt, initialValidators: opts.initialValidators }, + chain, + )); config.l1Contracts = deployL1ContractsValues.l1ContractAddresses; diff --git a/yarn-project/ethereum/src/deploy_l1_contracts.ts b/yarn-project/ethereum/src/deploy_l1_contracts.ts index 9eb75129880..360252453fe 100644 --- a/yarn-project/ethereum/src/deploy_l1_contracts.ts +++ b/yarn-project/ethereum/src/deploy_l1_contracts.ts @@ -147,7 +147,13 @@ export const deployL1Contracts = async ( chain: Chain, logger: DebugLogger, contractsToDeploy: L1ContractArtifactsForDeployment, - args: { l2FeeJuiceAddress: AztecAddress; vkTreeRoot: Fr; assumeProvenUntil?: number; salt: number | undefined }, + args: { + l2FeeJuiceAddress: AztecAddress; + vkTreeRoot: Fr; + assumeProvenUntil?: number; + salt: number | undefined; + initialValidators?: EthAddress[]; + }, ): Promise => { // We are assuming that you are running this on a local anvil node which have 1s block times // To align better with actual deployment, we update the block interval to 12s @@ -234,6 +240,7 @@ export const deployL1Contracts = async ( getAddress(feeJuicePortalAddress.toString()), args.vkTreeRoot.toString(), account.address.toString(), + args.initialValidators?.map(v => v.toString()) ?? [], ]); logger.info(`Deployed Rollup at ${rollupAddress}`);