From 48b7ac4dd823533843018e177e0cf2bd00d1bdba Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Tue, 28 Jan 2025 11:04:13 -0300 Subject: [PATCH] feat: Validate L1 config against L1 on startup (#11540) Validates all L1 contract addresses and config (eg aztec slot duration) using the L1 rpc url and governance contract address. Once we clean up how we load config in the startup commands, it should be easy to change this so we load those settings instead of just validating them. --- .../aztec/src/cli/cmds/start_archiver.ts | 11 ++- yarn-project/aztec/src/cli/cmds/start_node.ts | 10 ++- .../aztec/src/cli/cmds/start_prover_node.ts | 6 ++ yarn-project/aztec/src/cli/validation.test.ts | 47 +++++++++++ yarn-project/aztec/src/cli/validation.ts | 38 +++++++++ yarn-project/aztec/src/sandbox.ts | 52 +++--------- .../src/{ethereum_chain.ts => chain.ts} | 0 yarn-project/ethereum/src/client.ts | 56 +++++++++++++ .../ethereum/src/contracts/governance.ts | 50 +++++++++++ .../src/contracts/governance_proposer.ts | 41 +++++++++ yarn-project/ethereum/src/contracts/index.ts | 3 + yarn-project/ethereum/src/contracts/rollup.ts | 84 ++++++++++++++++++- .../src/contracts/slashing_proposer.ts | 31 +++++++ .../ethereum/src/deploy_l1_contracts.ts | 2 +- yarn-project/ethereum/src/index.ts | 4 +- yarn-project/ethereum/src/l1_reader.ts | 3 +- yarn-project/ethereum/src/queries.ts | 77 +++++++++++++++++ yarn-project/ethereum/src/types.ts | 4 + yarn-project/foundation/src/config/index.ts | 22 +++-- yarn-project/kv-store/src/config.ts | 2 +- 20 files changed, 481 insertions(+), 62 deletions(-) create mode 100644 yarn-project/aztec/src/cli/validation.test.ts create mode 100644 yarn-project/aztec/src/cli/validation.ts rename yarn-project/ethereum/src/{ethereum_chain.ts => chain.ts} (100%) create mode 100644 yarn-project/ethereum/src/client.ts create mode 100644 yarn-project/ethereum/src/contracts/governance.ts create mode 100644 yarn-project/ethereum/src/contracts/governance_proposer.ts create mode 100644 yarn-project/ethereum/src/contracts/slashing_proposer.ts create mode 100644 yarn-project/ethereum/src/queries.ts diff --git a/yarn-project/aztec/src/cli/cmds/start_archiver.ts b/yarn-project/aztec/src/cli/cmds/start_archiver.ts index a6d9fd8e994..82c5cee86b5 100644 --- a/yarn-project/aztec/src/cli/cmds/start_archiver.ts +++ b/yarn-project/aztec/src/cli/cmds/start_archiver.ts @@ -1,4 +1,10 @@ -import { Archiver, type ArchiverConfig, KVArchiverDataStore, archiverConfigMappings } from '@aztec/archiver'; +import { + Archiver, + type ArchiverConfig, + KVArchiverDataStore, + archiverConfigMappings, + getArchiverConfigFromEnv, +} from '@aztec/archiver'; import { createLogger } from '@aztec/aztec.js'; import { createBlobSinkClient } from '@aztec/blob-sink/client'; import { ArchiverApiSchema } from '@aztec/circuit-types'; @@ -8,6 +14,7 @@ import { createStore } from '@aztec/kv-store/lmdb'; import { getConfigEnvVars as getTelemetryClientConfig, initTelemetryClient } from '@aztec/telemetry-client'; import { extractRelevantOptions } from '../util.js'; +import { validateL1Config } from '../validation.js'; /** Starts a standalone archiver. */ export async function startArchiver( @@ -24,6 +31,8 @@ export async function startArchiver( 'archiver', ); + await validateL1Config({ ...getArchiverConfigFromEnv(), ...archiverConfig }); + const storeLog = createLogger('archiver:lmdb'); const store = await createStore('archiver', archiverConfig, storeLog); const archiverStore = new KVArchiverDataStore(store, archiverConfig.maxLogs); diff --git a/yarn-project/aztec/src/cli/cmds/start_node.ts b/yarn-project/aztec/src/cli/cmds/start_node.ts index a68b6a5b6e0..ddb35e2ab33 100644 --- a/yarn-project/aztec/src/cli/cmds/start_node.ts +++ b/yarn-project/aztec/src/cli/cmds/start_node.ts @@ -1,4 +1,4 @@ -import { aztecNodeConfigMappings } from '@aztec/aztec-node'; +import { aztecNodeConfigMappings, getConfigEnvVars as getNodeConfigEnvVars } from '@aztec/aztec-node'; import { AztecNodeApiSchema, P2PApiSchema, type PXE } from '@aztec/circuit-types'; import { NULL_KEY } from '@aztec/ethereum'; import { type NamespacedApiHandlers } from '@aztec/foundation/json-rpc/server'; @@ -13,6 +13,7 @@ import { mnemonicToAccount, privateKeyToAccount } from 'viem/accounts'; import { createAztecNode, deployContractsToL1 } from '../../sandbox.js'; import { extractNamespacedOptions, extractRelevantOptions } from '../util.js'; +import { validateL1Config } from '../validation.js'; export async function startNode( options: any, @@ -42,11 +43,18 @@ export async function startNode( } else { throw new Error('--node.publisherPrivateKey or --l1-mnemonic is required to deploy L1 contracts'); } + // REFACTOR: We should not be calling a method from sandbox on the prod start flow await deployContractsToL1(nodeConfig, account!, undefined, { assumeProvenThroughBlockNumber: nodeSpecificOptions.assumeProvenThroughBlockNumber, salt: nodeSpecificOptions.deployAztecContractsSalt, }); } + // If not deploying, validate that the addresses and config provided are correct. + // Eventually, we should be able to dynamically load this just by having the L1 governance address, + // instead of only validating the config the user has entered. + else { + await validateL1Config({ ...getNodeConfigEnvVars(), ...nodeConfig }); + } // if no publisher private key, then use l1Mnemonic if (!options.archiver) { diff --git a/yarn-project/aztec/src/cli/cmds/start_prover_node.ts b/yarn-project/aztec/src/cli/cmds/start_prover_node.ts index 275be3eeea3..6264a46ac5f 100644 --- a/yarn-project/aztec/src/cli/cmds/start_prover_node.ts +++ b/yarn-project/aztec/src/cli/cmds/start_prover_node.ts @@ -14,6 +14,7 @@ import { initTelemetryClient, telemetryClientConfigMappings } from '@aztec/telem import { mnemonicToAccount } from 'viem/accounts'; import { extractRelevantOptions } from '../util.js'; +import { validateL1Config } from '../validation.js'; import { startProverBroker } from './start_prover_broker.js'; export async function startProverNode( @@ -58,6 +59,11 @@ export async function startProverNode( proverConfig.l1Contracts = await createAztecNodeClient(nodeUrl).getL1ContractAddresses(); } + // If we create an archiver here, validate the L1 config + if (options.archiver) { + await validateL1Config(proverConfig); + } + const telemetry = initTelemetryClient(extractRelevantOptions(options, telemetryClientConfigMappings, 'tel')); let broker: ProvingJobBroker; diff --git a/yarn-project/aztec/src/cli/validation.test.ts b/yarn-project/aztec/src/cli/validation.test.ts new file mode 100644 index 00000000000..a2913b6bf47 --- /dev/null +++ b/yarn-project/aztec/src/cli/validation.test.ts @@ -0,0 +1,47 @@ +import { type AztecNodeConfig, aztecNodeConfigMappings } from '@aztec/aztec-node'; +import { EthAddress } from '@aztec/circuits.js'; +import { startAnvil } from '@aztec/ethereum/test'; +import { getDefaultConfig } from '@aztec/foundation/config'; + +import { type Anvil } from '@viem/anvil'; +import { mnemonicToAccount } from 'viem/accounts'; + +import { DefaultMnemonic } from '../mnemonic.js'; +import { deployContractsToL1 } from '../sandbox.js'; +import { validateL1Config } from './validation.js'; + +describe('validation', () => { + describe('L1 config', () => { + let anvil: Anvil; + let l1RpcUrl: string; + let nodeConfig: AztecNodeConfig; + + beforeAll(async () => { + ({ anvil, rpcUrl: l1RpcUrl } = await startAnvil()); + + nodeConfig = { ...getDefaultConfig(aztecNodeConfigMappings), l1RpcUrl }; + nodeConfig.aztecSlotDuration = 72; // Tweak config so we don't have just defaults + const account = mnemonicToAccount(DefaultMnemonic); + const deployed = await deployContractsToL1(nodeConfig, account, undefined, { salt: 1 }); + nodeConfig.l1Contracts = deployed; + }); + + afterAll(async () => { + await anvil.stop(); + }); + + it('validates correct config', async () => { + await validateL1Config(nodeConfig); + }); + + it('throws on invalid l1 settings', async () => { + await expect(validateL1Config({ ...nodeConfig, aztecSlotDuration: 96 })).rejects.toThrow(/aztecSlotDuration/); + }); + + it('throws on mismatching l1 addresses', async () => { + const wrongL1Contracts = { ...nodeConfig.l1Contracts, feeJuicePortalAddress: EthAddress.random() }; + const wrongConfig = { ...nodeConfig, l1Contracts: wrongL1Contracts }; + await expect(validateL1Config(wrongConfig)).rejects.toThrow(/feeJuicePortalAddress/); + }); + }); +}); diff --git a/yarn-project/aztec/src/cli/validation.ts b/yarn-project/aztec/src/cli/validation.ts new file mode 100644 index 00000000000..c400ecab0cd --- /dev/null +++ b/yarn-project/aztec/src/cli/validation.ts @@ -0,0 +1,38 @@ +import { + type L1ContractAddresses, + type L1ContractsConfig, + getL1ContractsAddresses, + getL1ContractsConfig, + getPublicClient, +} from '@aztec/ethereum'; + +/** + * Connects to L1 using the provided L1 RPC URL and reads all addresses and settings from the governance + * contract. For each key, compares it against the provided config (if it is not empty) and throws on mismatches. + */ +export async function validateL1Config( + config: L1ContractsConfig & { l1Contracts: L1ContractAddresses } & { l1ChainId: number; l1RpcUrl: string }, +) { + const publicClient = getPublicClient(config); + const actualAddresses = await getL1ContractsAddresses(publicClient, config.l1Contracts.governanceAddress); + + for (const keyStr in actualAddresses) { + const key = keyStr as keyof Awaited>; + const actual = actualAddresses[key]; + const expected = config.l1Contracts[key]; + + if (expected !== undefined && !expected.isZero() && !actual.equals(expected)) { + throw new Error(`Expected L1 contract address ${key} to be ${expected} but found ${actual}`); + } + } + + const actualConfig = await getL1ContractsConfig(publicClient, actualAddresses); + for (const keyStr in actualConfig) { + const key = keyStr as keyof Awaited> & keyof L1ContractsConfig; + const actual = actualConfig[key]; + const expected = config[key]; + if (expected !== undefined && actual !== expected) { + throw new Error(`Expected L1 setting ${key} to be ${expected} but found ${actual}`); + } + } +} diff --git a/yarn-project/aztec/src/sandbox.ts b/yarn-project/aztec/src/sandbox.ts index c6720fda7ed..65ee1391374 100644 --- a/yarn-project/aztec/src/sandbox.ts +++ b/yarn-project/aztec/src/sandbox.ts @@ -1,16 +1,16 @@ #!/usr/bin/env -S node --no-warnings import { type AztecNodeConfig, AztecNodeService, getConfigEnvVars } from '@aztec/aztec-node'; -import { AnvilTestWatcher, EthCheatCodes, SignerlessWallet, retryUntil } from '@aztec/aztec.js'; +import { AnvilTestWatcher, EthCheatCodes, SignerlessWallet } from '@aztec/aztec.js'; import { DefaultMultiCallEntrypoint } from '@aztec/aztec.js/entrypoint'; import { type BlobSinkClientInterface, createBlobSinkClient } from '@aztec/blob-sink/client'; import { type AztecNode } from '@aztec/circuit-types'; import { setupCanonicalL2FeeJuice } from '@aztec/cli/setup-contracts'; import { - type DeployL1Contracts, NULL_KEY, createEthereumChain, deployL1Contracts, getL1ContractsConfigEnvVars, + waitForPublicClient, } from '@aztec/ethereum'; import { createLogger } from '@aztec/foundation/log'; import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types/vks'; @@ -32,39 +32,6 @@ const logger = createLogger('sandbox'); const localAnvil = foundry; -/** - * Helper function that waits for the Ethereum RPC server to respond before deploying L1 contracts. - */ -async function waitThenDeploy(config: AztecNodeConfig, deployFunction: () => Promise) { - const chain = createEthereumChain(config.l1RpcUrl, config.l1ChainId); - // wait for ETH RPC to respond to a request. - const publicClient = createPublicClient({ - chain: chain.chainInfo, - transport: httpViemTransport(chain.rpcUrl), - }); - const l1ChainID = await retryUntil( - async () => { - let chainId = 0; - try { - chainId = await publicClient.getChainId(); - } catch (err) { - logger.warn(`Failed to connect to Ethereum node at ${chain.rpcUrl}. Retrying...`); - } - return chainId; - }, - 'isEthRpcReady', - 600, - 1, - ); - - if (!l1ChainID) { - throw Error(`Ethereum node unresponsive at ${chain.rpcUrl}.`); - } - - // Deploy L1 contracts - return await deployFunction(); -} - /** * Function to deploy our L1 contracts to the sandbox L1 * @param aztecNodeConfig - The Aztec Node Config @@ -80,15 +47,22 @@ export async function deployContractsToL1( ? createEthereumChain(aztecNodeConfig.l1RpcUrl, aztecNodeConfig.l1ChainId) : { chainInfo: localAnvil }; - const l1Contracts = await waitThenDeploy(aztecNodeConfig, async () => - deployL1Contracts(aztecNodeConfig.l1RpcUrl, hdAccount, chain.chainInfo, contractDeployLogger, { + await waitForPublicClient(aztecNodeConfig); + + const l1Contracts = await deployL1Contracts( + aztecNodeConfig.l1RpcUrl, + hdAccount, + chain.chainInfo, + contractDeployLogger, + { + ...getL1ContractsConfigEnvVars(), // TODO: We should not need to be loading config from env again, caller should handle this + ...aztecNodeConfig, l2FeeJuiceAddress: ProtocolContractAddress.FeeJuice, vkTreeRoot: await getVKTreeRoot(), protocolContractTreeRoot, assumeProvenThrough: opts.assumeProvenThroughBlockNumber, salt: opts.salt, - ...getL1ContractsConfigEnvVars(), - }), + }, ); aztecNodeConfig.l1Contracts = l1Contracts.l1ContractAddresses; diff --git a/yarn-project/ethereum/src/ethereum_chain.ts b/yarn-project/ethereum/src/chain.ts similarity index 100% rename from yarn-project/ethereum/src/ethereum_chain.ts rename to yarn-project/ethereum/src/chain.ts diff --git a/yarn-project/ethereum/src/client.ts b/yarn-project/ethereum/src/client.ts new file mode 100644 index 00000000000..47565201b07 --- /dev/null +++ b/yarn-project/ethereum/src/client.ts @@ -0,0 +1,56 @@ +import { type Logger } from '@aztec/foundation/log'; +import { retryUntil } from '@aztec/foundation/retry'; + +import { createPublicClient, http } from 'viem'; + +import { createEthereumChain } from './chain.js'; +import { type ViemPublicClient } from './types.js'; + +type Config = { + /** The RPC Url of the ethereum host. */ + l1RpcUrl: string; + /** The chain ID of the ethereum host. */ + l1ChainId: number; + /** The polling interval viem uses in ms */ + viemPollingIntervalMS?: number; +}; + +// TODO: Use these methods to abstract the creation of viem clients. + +/** Returns a viem public client given the L1 config. */ +export function getPublicClient(config: Config): ViemPublicClient { + const chain = createEthereumChain(config.l1RpcUrl, config.l1ChainId); + return createPublicClient({ + chain: chain.chainInfo, + transport: http(chain.rpcUrl), + pollingInterval: config.viemPollingIntervalMS, + }); +} + +/** Returns a viem public client after waiting for the L1 RPC node to become available. */ +export async function waitForPublicClient(config: Config, logger?: Logger): Promise { + const client = getPublicClient(config); + await waitForRpc(client, config, logger); + return client; +} + +async function waitForRpc(client: ViemPublicClient, config: Config, logger?: Logger) { + const l1ChainId = await retryUntil( + async () => { + let chainId = 0; + try { + chainId = await client.getChainId(); + } catch (err) { + logger?.warn(`Failed to connect to Ethereum node at ${config.l1RpcUrl}. Retrying...`); + } + return chainId; + }, + `L1 RPC url at ${config.l1RpcUrl}`, + 600, + 1, + ); + + if (l1ChainId !== config.l1ChainId) { + throw new Error(`Ethereum node at ${config.l1RpcUrl} has chain ID ${l1ChainId} but expected ${config.l1ChainId}`); + } +} diff --git a/yarn-project/ethereum/src/contracts/governance.ts b/yarn-project/ethereum/src/contracts/governance.ts new file mode 100644 index 00000000000..1cbc513b283 --- /dev/null +++ b/yarn-project/ethereum/src/contracts/governance.ts @@ -0,0 +1,50 @@ +import { EthAddress } from '@aztec/foundation/eth-address'; +import { GovernanceAbi } from '@aztec/l1-artifacts'; + +import { + type Chain, + type GetContractReturnType, + type Hex, + type HttpTransport, + type PublicClient, + getContract, +} from 'viem'; + +import { type L1ContractAddresses } from '../l1_contract_addresses.js'; +import { GovernanceProposerContract } from './governance_proposer.js'; + +export type L1GovernanceContractAddresses = Pick< + L1ContractAddresses, + 'governanceAddress' | 'rollupAddress' | 'registryAddress' | 'governanceProposerAddress' +>; + +export class GovernanceContract { + private readonly governance: GetContractReturnType>; + + constructor(public readonly client: PublicClient, address: Hex) { + this.governance = getContract({ address, abi: GovernanceAbi, client }); + } + + public get address() { + return EthAddress.fromString(this.governance.address); + } + + public async getProposer() { + const governanceProposerAddress = EthAddress.fromString(await this.governance.read.governanceProposer()); + return new GovernanceProposerContract(this.client, governanceProposerAddress.toString()); + } + + public async getGovernanceAddresses(): Promise { + const governanceProposer = await this.getProposer(); + const [rollupAddress, registryAddress] = await Promise.all([ + governanceProposer.getRollupAddress(), + governanceProposer.getRegistryAddress(), + ]); + return { + governanceAddress: this.address, + rollupAddress, + registryAddress, + governanceProposerAddress: governanceProposer.address, + }; + } +} diff --git a/yarn-project/ethereum/src/contracts/governance_proposer.ts b/yarn-project/ethereum/src/contracts/governance_proposer.ts new file mode 100644 index 00000000000..5f802709e27 --- /dev/null +++ b/yarn-project/ethereum/src/contracts/governance_proposer.ts @@ -0,0 +1,41 @@ +import { memoize } from '@aztec/foundation/decorators'; +import { EthAddress } from '@aztec/foundation/eth-address'; +import { GovernanceProposerAbi } from '@aztec/l1-artifacts'; + +import { + type Chain, + type GetContractReturnType, + type Hex, + type HttpTransport, + type PublicClient, + getContract, +} from 'viem'; + +export class GovernanceProposerContract { + private readonly proposer: GetContractReturnType>; + + constructor(public readonly client: PublicClient, address: Hex) { + this.proposer = getContract({ address, abi: GovernanceProposerAbi, client }); + } + + public get address() { + return EthAddress.fromString(this.proposer.address); + } + + public async getRollupAddress() { + return EthAddress.fromString(await this.proposer.read.getInstance()); + } + + @memoize + public async getRegistryAddress() { + return EthAddress.fromString(await this.proposer.read.REGISTRY()); + } + + public getQuorumSize() { + return this.proposer.read.N(); + } + + public getRoundSize() { + return this.proposer.read.M(); + } +} diff --git a/yarn-project/ethereum/src/contracts/index.ts b/yarn-project/ethereum/src/contracts/index.ts index f35e118a5a1..6156b922083 100644 --- a/yarn-project/ethereum/src/contracts/index.ts +++ b/yarn-project/ethereum/src/contracts/index.ts @@ -1 +1,4 @@ export * from './rollup.js'; +export * from './governance.js'; +export * from './governance_proposer.js'; +export * from './slashing_proposer.js'; diff --git a/yarn-project/ethereum/src/contracts/rollup.ts b/yarn-project/ethereum/src/contracts/rollup.ts index d839b58543b..35dce8ebfbc 100644 --- a/yarn-project/ethereum/src/contracts/rollup.ts +++ b/yarn-project/ethereum/src/contracts/rollup.ts @@ -1,6 +1,6 @@ -import { AztecAddress } from '@aztec/foundation/aztec-address'; import { memoize } from '@aztec/foundation/decorators'; -import { RollupAbi } from '@aztec/l1-artifacts'; +import { EthAddress } from '@aztec/foundation/eth-address'; +import { RollupAbi, SlasherAbi } from '@aztec/l1-artifacts'; import { type Chain, @@ -13,9 +13,22 @@ import { http, } from 'viem'; +import { createEthereumChain } from '../chain.js'; import { type DeployL1Contracts } from '../deploy_l1_contracts.js'; -import { createEthereumChain } from '../ethereum_chain.js'; +import { type L1ContractAddresses } from '../l1_contract_addresses.js'; import { type L1ReaderConfig } from '../l1_reader.js'; +import { SlashingProposerContract } from './slashing_proposer.js'; + +export type L1RollupContractAddresses = Pick< + L1ContractAddresses, + | 'rollupAddress' + | 'inboxAddress' + | 'outboxAddress' + | 'feeJuicePortalAddress' + | 'feeJuiceAddress' + | 'stakingAssetAddress' + | 'rewardDistributorAddress' +>; export class RollupContract { private readonly rollup: GetContractReturnType>; @@ -25,7 +38,15 @@ export class RollupContract { } public get address() { - return AztecAddress.fromString(this.rollup.address); + return EthAddress.fromString(this.rollup.address); + } + + @memoize + public async getSlashingProposer() { + const slasherAddress = await this.rollup.read.SLASHER(); + const slasher = getContract({ address: slasherAddress, abi: SlasherAbi, client: this.client }); + const proposerAddress = await slasher.read.PROPOSER(); + return new SlashingProposerContract(this.client, proposerAddress); } @memoize @@ -38,6 +59,31 @@ export class RollupContract { return this.rollup.read.GENESIS_TIME(); } + @memoize + getClaimDurationInL2Slots() { + return this.rollup.read.CLAIM_DURATION_IN_L2_SLOTS(); + } + + @memoize + getEpochDuration() { + return this.rollup.read.EPOCH_DURATION(); + } + + @memoize + getSlotDuration() { + return this.rollup.read.SLOT_DURATION(); + } + + @memoize + getTargetCommitteeSize() { + return this.rollup.read.TARGET_COMMITTEE_SIZE(); + } + + @memoize + getMinimumStake() { + return this.rollup.read.MINIMUM_STAKE(); + } + getBlockNumber() { return this.rollup.read.getPendingBlockNumber(); } @@ -75,6 +121,36 @@ export class RollupContract { return this.rollup.read.getEpochForBlock([BigInt(blockNumber)]); } + async getRollupAddresses(): Promise { + const [ + inboxAddress, + outboxAddress, + feeJuicePortalAddress, + rewardDistributorAddress, + feeJuiceAddress, + stakingAssetAddress, + ] = ( + await Promise.all([ + this.rollup.read.INBOX(), + this.rollup.read.OUTBOX(), + this.rollup.read.FEE_JUICE_PORTAL(), + this.rollup.read.REWARD_DISTRIBUTOR(), + this.rollup.read.ASSET(), + this.rollup.read.STAKING_ASSET(), + ] as const) + ).map(EthAddress.fromString); + + return { + rollupAddress: this.address, + inboxAddress, + outboxAddress, + feeJuicePortalAddress, + feeJuiceAddress, + stakingAssetAddress, + rewardDistributorAddress, + }; + } + static getFromL1ContractsValues(deployL1ContractsValues: DeployL1Contracts) { const { publicClient, diff --git a/yarn-project/ethereum/src/contracts/slashing_proposer.ts b/yarn-project/ethereum/src/contracts/slashing_proposer.ts new file mode 100644 index 00000000000..a765622f341 --- /dev/null +++ b/yarn-project/ethereum/src/contracts/slashing_proposer.ts @@ -0,0 +1,31 @@ +import { EthAddress } from '@aztec/foundation/eth-address'; +import { SlashingProposerAbi } from '@aztec/l1-artifacts'; + +import { + type Chain, + type GetContractReturnType, + type Hex, + type HttpTransport, + type PublicClient, + getContract, +} from 'viem'; + +export class SlashingProposerContract { + private readonly proposer: GetContractReturnType>; + + constructor(public readonly client: PublicClient, address: Hex) { + this.proposer = getContract({ address, abi: SlashingProposerAbi, client }); + } + + public get address() { + return EthAddress.fromString(this.proposer.address); + } + + public getQuorumSize() { + return this.proposer.read.N(); + } + + public getRoundSize() { + return this.proposer.read.M(); + } +} diff --git a/yarn-project/ethereum/src/deploy_l1_contracts.ts b/yarn-project/ethereum/src/deploy_l1_contracts.ts index e9aedd283cd..cd68e300c9c 100644 --- a/yarn-project/ethereum/src/deploy_l1_contracts.ts +++ b/yarn-project/ethereum/src/deploy_l1_contracts.ts @@ -54,8 +54,8 @@ import { import { type HDAccount, type PrivateKeyAccount, mnemonicToAccount, privateKeyToAccount } from 'viem/accounts'; import { foundry } from 'viem/chains'; +import { isAnvilTestChain } from './chain.js'; import { type L1ContractsConfig } from './config.js'; -import { isAnvilTestChain } from './ethereum_chain.js'; import { type L1ContractAddresses } from './l1_contract_addresses.js'; import { L1TxUtils } from './l1_tx_utils.js'; diff --git a/yarn-project/ethereum/src/index.ts b/yarn-project/ethereum/src/index.ts index 10f28f3bd0f..f0e1e486e71 100644 --- a/yarn-project/ethereum/src/index.ts +++ b/yarn-project/ethereum/src/index.ts @@ -1,6 +1,6 @@ export * from './constants.js'; export * from './deploy_l1_contracts.js'; -export * from './ethereum_chain.js'; +export * from './chain.js'; export * from './eth_cheat_codes.js'; export * from './l1_tx_utils.js'; export * from './l1_contract_addresses.js'; @@ -9,3 +9,5 @@ export * from './utils.js'; export * from './config.js'; export * from './types.js'; export * from './contracts/index.js'; +export * from './queries.js'; +export * from './client.js'; diff --git a/yarn-project/ethereum/src/l1_reader.ts b/yarn-project/ethereum/src/l1_reader.ts index 2c6340ba025..b9b9f75438c 100644 --- a/yarn-project/ethereum/src/l1_reader.ts +++ b/yarn-project/ethereum/src/l1_reader.ts @@ -25,10 +25,9 @@ export const l1ReaderConfigMappings: ConfigMappingsType = { defaultValue: 31337, description: 'The chain ID of the ethereum host.', }, - // NOTE: Special case for l1Contracts l1Contracts: { description: 'The deployed L1 contract addresses', - defaultValue: l1ContractAddressesMapping, + nested: l1ContractAddressesMapping, }, viemPollingIntervalMS: { env: 'L1_READER_VIEM_POLLING_INTERVAL_MS', diff --git a/yarn-project/ethereum/src/queries.ts b/yarn-project/ethereum/src/queries.ts new file mode 100644 index 00000000000..0b2ff75239a --- /dev/null +++ b/yarn-project/ethereum/src/queries.ts @@ -0,0 +1,77 @@ +import { type EthAddress } from '@aztec/foundation/eth-address'; + +import { type Chain, type HttpTransport, type PublicClient } from 'viem'; + +import { type L1ContractsConfig } from './config.js'; +import { GovernanceContract } from './contracts/governance.js'; +import { RollupContract } from './contracts/rollup.js'; +import { type L1ContractAddresses } from './l1_contract_addresses.js'; + +/** Given the Governance contract address, reads the addresses from all other contracts from L1. */ +export async function getL1ContractsAddresses( + publicClient: PublicClient, + governanceAddress: EthAddress, +): Promise> { + const governance = new GovernanceContract(publicClient, governanceAddress.toString()); + const governanceAddresses = await governance.getGovernanceAddresses(); + + const rollup = new RollupContract(publicClient, governanceAddresses.rollupAddress.toString()); + const rollupAddresses = await rollup.getRollupAddresses(); + + return { + ...governanceAddresses, + ...rollupAddresses, + }; +} + +/** Reads the L1ContractsConfig from L1 contracts. */ +export async function getL1ContractsConfig( + publicClient: PublicClient, + addresses: { governanceAddress: EthAddress; rollupAddress?: EthAddress }, +): Promise & { l1StartBlock: bigint; l1GenesisTime: bigint }> { + const governance = new GovernanceContract(publicClient, addresses.governanceAddress.toString()); + const governanceProposer = await governance.getProposer(); + const rollupAddress = addresses.rollupAddress ?? (await governance.getGovernanceAddresses()).rollupAddress; + const rollup = new RollupContract(publicClient, rollupAddress.toString()); + const slasherProposer = await rollup.getSlashingProposer(); + + const [ + l1StartBlock, + l1GenesisTime, + aztecEpochDuration, + aztecEpochProofClaimWindowInL2Slots, + aztecSlotDuration, + aztecTargetCommitteeSize, + minimumStake, + governanceProposerQuorum, + governanceProposerRoundSize, + slashingQuorum, + slashingRoundSize, + ] = await Promise.all([ + rollup.getL1StartBlock(), + rollup.getL1GenesisTime(), + rollup.getEpochDuration(), + rollup.getClaimDurationInL2Slots(), + rollup.getSlotDuration(), + rollup.getTargetCommitteeSize(), + rollup.getMinimumStake(), + governanceProposer.getQuorumSize(), + governanceProposer.getRoundSize(), + slasherProposer.getQuorumSize(), + slasherProposer.getRoundSize(), + ] as const); + + return { + l1StartBlock, + l1GenesisTime, + aztecEpochDuration: Number(aztecEpochDuration), + aztecEpochProofClaimWindowInL2Slots: Number(aztecEpochProofClaimWindowInL2Slots), + aztecSlotDuration: Number(aztecSlotDuration), + aztecTargetCommitteeSize: Number(aztecTargetCommitteeSize), + governanceProposerQuorum: Number(governanceProposerQuorum), + governanceProposerRoundSize: Number(governanceProposerRoundSize), + minimumStake, + slashingQuorum: Number(slashingQuorum), + slashingRoundSize: Number(slashingRoundSize), + }; +} diff --git a/yarn-project/ethereum/src/types.ts b/yarn-project/ethereum/src/types.ts index 2dfb9dc591c..1638685841c 100644 --- a/yarn-project/ethereum/src/types.ts +++ b/yarn-project/ethereum/src/types.ts @@ -4,6 +4,7 @@ import { type HttpTransport, type PrivateKeyAccount, type PublicActions, + type PublicClient, type PublicRpcSchema, type WalletActions, type WalletRpcSchema, @@ -20,3 +21,6 @@ export type ViemClient = Client< [...PublicRpcSchema, ...WalletRpcSchema], PublicActions & WalletActions >; + +/** Type for a viem public client */ +export type ViemPublicClient = PublicClient; diff --git a/yarn-project/foundation/src/config/index.ts b/yarn-project/foundation/src/config/index.ts index b49fbb776aa..297c357ec9e 100644 --- a/yarn-project/foundation/src/config/index.ts +++ b/yarn-project/foundation/src/config/index.ts @@ -9,6 +9,7 @@ export interface ConfigMapping { printDefault?: (val: any) => string; description: string; isBoolean?: boolean; + nested?: Record; } export function isBooleanConfigValue(obj: T, key: keyof T): boolean { @@ -21,18 +22,15 @@ export function getConfigFromMappings(configMappings: ConfigMappingsType): const config = {} as T; for (const key in configMappings) { - if (configMappings[key]) { - const { env, parseEnv, defaultValue: def } = configMappings[key]; - // Special case for L1 contract addresses which is an object of config values - if (key === 'l1Contracts' && def) { - (config as any)[key] = getConfigFromMappings(def); - } else { - const val = env ? process.env[env] : undefined; - if (val !== undefined) { - (config as any)[key] = parseEnv ? parseEnv(val) : val; - } else if (def !== undefined) { - (config as any)[key] = def; - } + const { env, parseEnv, defaultValue: def, nested } = configMappings[key]; + if (nested) { + (config as any)[key] = getConfigFromMappings(nested); + } else { + const val = env ? process.env[env] : undefined; + if (val !== undefined) { + (config as any)[key] = parseEnv ? parseEnv(val) : val; + } else if (def !== undefined) { + (config as any)[key] = def; } } } diff --git a/yarn-project/kv-store/src/config.ts b/yarn-project/kv-store/src/config.ts index f1f9ed44de6..ca807e6bb8c 100644 --- a/yarn-project/kv-store/src/config.ts +++ b/yarn-project/kv-store/src/config.ts @@ -20,7 +20,7 @@ export const dataConfigMappings: ConfigMappingsType = { }, l1Contracts: { description: 'The deployed L1 contract addresses', - defaultValue: l1ContractAddressesMapping, + nested: l1ContractAddressesMapping, }, };