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}`);