-
Notifications
You must be signed in to change notification settings - Fork 310
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
1 parent
f77b11e
commit 48b7ac4
Showing
20 changed files
with
481 additions
and
62 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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/); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<ReturnType<typeof getL1ContractsAddresses>>; | ||
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<ReturnType<typeof getL1ContractsConfig>> & 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}`); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<ViemPublicClient> { | ||
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}`); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<typeof GovernanceAbi, PublicClient<HttpTransport, Chain>>; | ||
|
||
constructor(public readonly client: PublicClient<HttpTransport, Chain>, 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<L1GovernanceContractAddresses> { | ||
const governanceProposer = await this.getProposer(); | ||
const [rollupAddress, registryAddress] = await Promise.all([ | ||
governanceProposer.getRollupAddress(), | ||
governanceProposer.getRegistryAddress(), | ||
]); | ||
return { | ||
governanceAddress: this.address, | ||
rollupAddress, | ||
registryAddress, | ||
governanceProposerAddress: governanceProposer.address, | ||
}; | ||
} | ||
} |
Oops, something went wrong.