diff --git a/l1-contracts/src/core/libraries/Errors.sol b/l1-contracts/src/core/libraries/Errors.sol index 8f385ff1eae4..8496265b326f 100644 --- a/l1-contracts/src/core/libraries/Errors.sol +++ b/l1-contracts/src/core/libraries/Errors.sol @@ -198,6 +198,7 @@ library Errors { error TallySlashingProposer__SlashOffsetMustBeGreaterThanZero(uint256 slashOffset); error TallySlashingProposer__InvalidEpochIndex(uint256 epochIndex, uint256 roundSizeInEpochs); error TallySlashingProposer__VoteSizeTooBig(uint256 voteSize, uint256 maxSize); + error TallySlashingProposer__VotesMustBeMultipleOf4(uint256 votes); // SlashPayloadLib error SlashPayload_ArraySizeMismatch(uint256 expected, uint256 actual); diff --git a/l1-contracts/src/core/slashing/TallySlashingProposer.sol b/l1-contracts/src/core/slashing/TallySlashingProposer.sol index 6f0a7d98b7bf..01e59de9a38e 100644 --- a/l1-contracts/src/core/slashing/TallySlashingProposer.sol +++ b/l1-contracts/src/core/slashing/TallySlashingProposer.sol @@ -355,6 +355,11 @@ contract TallySlashingProposer is EIP712 { // We have allocated 4 bytes32 slots = 128 bytes maximum uint256 voteSize = COMMITTEE_SIZE * ROUND_SIZE_IN_EPOCHS / 4; require(voteSize <= 128, Errors.TallySlashingProposer__VoteSizeTooBig(voteSize, 128)); + + require( + COMMITTEE_SIZE * ROUND_SIZE_IN_EPOCHS % 4 == 0, + Errors.TallySlashingProposer__VotesMustBeMultipleOf4(COMMITTEE_SIZE * ROUND_SIZE_IN_EPOCHS) + ); } /** diff --git a/yarn-project/aztec/src/cli/chain_l2_config.ts b/yarn-project/aztec/src/cli/chain_l2_config.ts index bddba40c9725..aac48b15c32f 100644 --- a/yarn-project/aztec/src/cli/chain_l2_config.ts +++ b/yarn-project/aztec/src/cli/chain_l2_config.ts @@ -123,11 +123,7 @@ export const alphaTestnetL2ChainConfig: L2ChainConfig = { /** The minimum stake for a validator. */ ejectionThreshold: DefaultL1ContractsConfig.ejectionThreshold, /** The slashing round size */ - slashingRoundSize: 32 * 6, // 6 epochs - /** The slashing quorum */ - slashingQuorum: (32 * 6) / 2 + 1, // 6 epochs, majority of validators - /** Governance proposing quorum */ - governanceProposerQuorum: 151, + slashingRoundSizeInEpochs: 4, /** Governance proposing round size */ governanceProposerRoundSize: 300, /** The mana target for the rollup */ @@ -219,9 +215,9 @@ export async function getL2ChainConfig( return undefined; } -function enrichVar(envVar: EnvVar, value: string) { +function enrichVar(envVar: EnvVar, value: string | undefined) { // Don't override - if (process.env[envVar]) { + if (process.env[envVar] || value === undefined) { return; } process.env[envVar] = value; @@ -306,9 +302,9 @@ export async function enrichEnvironmentWithChainConfig(networkName: NetworkNames enrichVar('AZTEC_PROOF_SUBMISSION_EPOCHS', config.aztecProofSubmissionEpochs.toString()); enrichVar('AZTEC_ACTIVATION_THRESHOLD', config.activationThreshold.toString()); enrichVar('AZTEC_EJECTION_THRESHOLD', config.ejectionThreshold.toString()); - enrichVar('AZTEC_SLASHING_QUORUM', config.slashingQuorum.toString()); - enrichVar('AZTEC_SLASHING_ROUND_SIZE', config.slashingRoundSize.toString()); - enrichVar('AZTEC_GOVERNANCE_PROPOSER_QUORUM', config.governanceProposerQuorum.toString()); + enrichVar('AZTEC_SLASHING_QUORUM', config.slashingQuorum?.toString()); + enrichVar('AZTEC_SLASHING_ROUND_SIZE_IN_EPOCHS', config.slashingRoundSizeInEpochs.toString()); + enrichVar('AZTEC_GOVERNANCE_PROPOSER_QUORUM', config.governanceProposerQuorum?.toString()); enrichVar('AZTEC_GOVERNANCE_PROPOSER_ROUND_SIZE', config.governanceProposerRoundSize.toString()); enrichVar('AZTEC_MANA_TARGET', config.manaTarget.toString()); enrichVar('AZTEC_PROVING_COST_PER_MANA', config.provingCostPerMana.toString()); diff --git a/yarn-project/aztec/src/sandbox/sandbox.ts b/yarn-project/aztec/src/sandbox/sandbox.ts index 4ca7c04f3b96..1200c401b219 100644 --- a/yarn-project/aztec/src/sandbox/sandbox.ts +++ b/yarn-project/aztec/src/sandbox/sandbox.ts @@ -82,6 +82,7 @@ export async function deployContractsToL1( salt: opts.salt, feeJuicePortalInitialBalance: opts.feeJuicePortalInitialBalance, aztecTargetCommitteeSize: 0, // no committee in sandbox + slasherFlavor: 'none', // no slashing in sandbox realVerifier: false, }, ); diff --git a/yarn-project/end-to-end/src/e2e_epochs/epochs_invalidate_block.test.ts b/yarn-project/end-to-end/src/e2e_epochs/epochs_invalidate_block.test.ts index e829aa783c46..693dc2f7b110 100644 --- a/yarn-project/end-to-end/src/e2e_epochs/epochs_invalidate_block.test.ts +++ b/yarn-project/end-to-end/src/e2e_epochs/epochs_invalidate_block.test.ts @@ -52,6 +52,8 @@ describe('e2e_epochs/epochs_invalidate_block', () => { archiverPollingIntervalMS: 200, anvilAccounts: 20, anvilPort: ++anvilPort, + slashingRoundSizeInEpochs: 4, + slashingOffsetInRounds: 256, slasherFlavor: 'tally', }); diff --git a/yarn-project/end-to-end/src/e2e_p2p/add_rollup.test.ts b/yarn-project/end-to-end/src/e2e_p2p/add_rollup.test.ts index f3cc146fea84..5a27571ae678 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/add_rollup.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/add_rollup.test.ts @@ -75,7 +75,6 @@ describe('e2e_p2p_add_rollup', () => { initialConfig: { ...SHORTENED_BLOCK_TIME_CONFIG_NO_PRUNES, listenAddress: '127.0.0.1', - governanceProposerQuorum: 6, governanceProposerRoundSize: 10, }, }); @@ -158,7 +157,7 @@ describe('e2e_p2p_add_rollup', () => { aztecTargetCommitteeSize: t.ctx.aztecNodeConfig.aztecTargetCommitteeSize, aztecProofSubmissionEpochs: t.ctx.aztecNodeConfig.aztecProofSubmissionEpochs, slashingQuorum: t.ctx.aztecNodeConfig.slashingQuorum, - slashingRoundSize: t.ctx.aztecNodeConfig.slashingRoundSize, + slashingRoundSizeInEpochs: t.ctx.aztecNodeConfig.slashingRoundSizeInEpochs, slashingLifetimeInRounds: t.ctx.aztecNodeConfig.slashingLifetimeInRounds, slashingExecutionDelayInRounds: t.ctx.aztecNodeConfig.slashingExecutionDelayInRounds, slashingVetoer: t.ctx.aztecNodeConfig.slashingVetoer, diff --git a/yarn-project/end-to-end/src/e2e_p2p/data_withholding_slash.test.ts b/yarn-project/end-to-end/src/e2e_p2p/data_withholding_slash.test.ts index e6e521a452ba..e99ccd4e012f 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/data_withholding_slash.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/data_withholding_slash.test.ts @@ -58,7 +58,7 @@ describe('e2e_p2p_data_withholding_slash', () => { aztecSlotDuration, aztecProofSubmissionEpochs: 0, // effectively forces instant reorgs slashingQuorum, - slashingRoundSize, + slashingRoundSizeInEpochs: slashingRoundSize / 2, slashAmountSmall: slashingUnit, slashAmountMedium: slashingUnit * 2n, slashAmountLarge: slashingUnit * 3n, 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 e8c34a75ad64..1ca696ff3acf 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 @@ -59,7 +59,7 @@ describe('e2e_p2p_network', () => { initialConfig: { ...SHORTENED_BLOCK_TIME_CONFIG_NO_PRUNES, aztecEpochDuration: 4, - slashingRoundSize: 8, + slashingRoundSizeInEpochs: 2, slashingQuorum: 5, listenAddress: '127.0.0.1', }, diff --git a/yarn-project/end-to-end/src/e2e_p2p/inactivity_slash.test.ts b/yarn-project/end-to-end/src/e2e_p2p/inactivity_slash.test.ts index bcad2157a6e1..2133a5028a6c 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/inactivity_slash.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/inactivity_slash.test.ts @@ -41,7 +41,7 @@ describe('e2e_p2p_inactivity_slash', () => { basePort: BOOT_NODE_UDP_PORT, startProverNode: true, initialConfig: { - aztecTargetCommitteeSize: NUM_NODES, // ensure we can progress even after slash happens + aztecTargetCommitteeSize: NUM_VALIDATORS, aztecSlotDuration: AZTEC_SLOT_DURATION, ethereumSlotDuration: ETHEREUM_SLOT_DURATION, aztecProofSubmissionEpochs: 1024, // effectively do not reorg @@ -51,7 +51,7 @@ describe('e2e_p2p_inactivity_slash', () => { validatorReexecute: false, sentinelEnabled: true, slashingQuorum: SLASHING_QUORUM, - slashingRoundSize: SLASHING_ROUND_SIZE, + slashingRoundSizeInEpochs: SLASHING_ROUND_SIZE / EPOCH_DURATION, slashInactivityTargetPercentage: 0.5, slashAmountSmall: SLASHING_UNIT, slashAmountMedium: SLASHING_UNIT * 2n, 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 6391b7916169..b46588d6be60 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 @@ -116,7 +116,8 @@ export class P2PNetworkTest { aztecSlotDuration: initialValidatorConfig.aztecSlotDuration ?? l1ContractsConfig.aztecSlotDuration, aztecProofSubmissionEpochs: initialValidatorConfig.aztecProofSubmissionEpochs ?? l1ContractsConfig.aztecProofSubmissionEpochs, - slashingRoundSize: initialValidatorConfig.slashingRoundSize ?? l1ContractsConfig.slashingRoundSize, + slashingRoundSizeInEpochs: + initialValidatorConfig.slashingRoundSizeInEpochs ?? l1ContractsConfig.slashingRoundSizeInEpochs, slasherFlavor: initialValidatorConfig.slasherFlavor ?? 'tally', aztecTargetCommitteeSize: numberOfValidators, salt: 420, @@ -127,7 +128,8 @@ export class P2PNetworkTest { { ...initialValidatorConfig, aztecEpochDuration: initialValidatorConfig.aztecEpochDuration ?? l1ContractsConfig.aztecEpochDuration, - slashingRoundSize: initialValidatorConfig.slashingRoundSize ?? l1ContractsConfig.slashingRoundSize, + slashingRoundSizeInEpochs: + initialValidatorConfig.slashingRoundSizeInEpochs ?? l1ContractsConfig.slashingRoundSizeInEpochs, slasherFlavor: initialValidatorConfig.slasherFlavor ?? 'tally', ethereumSlotDuration: initialValidatorConfig.ethereumSlotDuration ?? l1ContractsConfig.ethereumSlotDuration, diff --git a/yarn-project/end-to-end/src/e2e_p2p/reqresp.test.ts b/yarn-project/end-to-end/src/e2e_p2p/reqresp.test.ts index 4d1a7955e5ce..8bbe5f95d762 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/reqresp.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/reqresp.test.ts @@ -36,8 +36,6 @@ describe('e2e_p2p_reqresp_tx', () => { ...SHORTENED_BLOCK_TIME_CONFIG_NO_PRUNES, listenAddress: '127.0.0.1', aztecEpochDuration: 64, // stable committee - slashingRoundSize: 128, - slashingQuorum: 65, }, }); await t.applyBaseSnapshots(); diff --git a/yarn-project/end-to-end/src/e2e_p2p/reqresp_no_handshake.test.ts b/yarn-project/end-to-end/src/e2e_p2p/reqresp_no_handshake.test.ts index 7aa979421a41..d15186aa4dc8 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/reqresp_no_handshake.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/reqresp_no_handshake.test.ts @@ -41,8 +41,6 @@ describe('e2e_p2p_reqresp_tx_no_handshake', () => { p2pDisableStatusHandshake: true, // DIFFERENCE FROM reqresp.test.ts listenAddress: '127.0.0.1', aztecEpochDuration: 64, // stable committee - slashingRoundSize: 128, - slashingQuorum: 65, }, }); await t.applyBaseSnapshots(); diff --git a/yarn-project/end-to-end/src/e2e_p2p/slash_veto_demo.test.ts b/yarn-project/end-to-end/src/e2e_p2p/slash_veto_demo.test.ts index cc18ccb73fa3..997ce45f7208 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/slash_veto_demo.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/slash_veto_demo.test.ts @@ -88,7 +88,7 @@ describe('veto slash', () => { slashAmountSmall: SLASHING_UNIT, slashAmountMedium: SLASHING_UNIT * 2n, slashAmountLarge: SLASHING_UNIT * 3n, - slashingRoundSize: SLASHING_ROUND_SIZE, + slashingRoundSizeInEpochs: SLASHING_ROUND_SIZE / EPOCH_DURATION, slashingQuorum: SLASHING_QUORUM, slashInactivityTargetPercentage: SLASH_INACTIVITY_TARGET_PERCENTAGE, }, diff --git a/yarn-project/end-to-end/src/e2e_p2p/upgrade_governance_proposer.test.ts b/yarn-project/end-to-end/src/e2e_p2p/upgrade_governance_proposer.test.ts index c23bbfc01b71..40c4ab46b75e 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/upgrade_governance_proposer.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/upgrade_governance_proposer.test.ts @@ -48,7 +48,6 @@ describe('e2e_p2p_governance_proposer', () => { initialConfig: { ...SHORTENED_BLOCK_TIME_CONFIG_NO_PRUNES, listenAddress: '127.0.0.1', - governanceProposerQuorum: 6, governanceProposerRoundSize: 10, activationThreshold: 10n ** 22n, ejectionThreshold: 5n ** 22n, diff --git a/yarn-project/end-to-end/src/e2e_p2p/valid_epoch_pruned.test.ts b/yarn-project/end-to-end/src/e2e_p2p/valid_epoch_pruned.test.ts index 0b9e2cfbe6d1..9e2e08cd59e6 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/valid_epoch_pruned.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/valid_epoch_pruned.test.ts @@ -48,7 +48,7 @@ describe('e2e_p2p_valid_epoch_pruned', () => { aztecSlotDuration, aztecProofSubmissionEpochs: 0, // reorg as soon as epoch ends slashingQuorum, - slashingRoundSize, + slashingRoundSizeInEpochs: slashingRoundSize / 2, slashSelfAllowed: true, slashAmountSmall: slashingUnit, slashAmountMedium: slashingUnit * 2n, diff --git a/yarn-project/end-to-end/src/e2e_p2p/validators_sentinel.test.ts b/yarn-project/end-to-end/src/e2e_p2p/validators_sentinel.test.ts index a595def9d68b..ca3c75ec3237 100644 --- a/yarn-project/end-to-end/src/e2e_p2p/validators_sentinel.test.ts +++ b/yarn-project/end-to-end/src/e2e_p2p/validators_sentinel.test.ts @@ -44,8 +44,7 @@ describe('e2e_p2p_validators_sentinel', () => { listenAddress: '127.0.0.1', minTxsPerBlock: 0, aztecEpochDuration: EPOCH_DURATION, - slashingRoundSize: EPOCH_DURATION * 2, - slashingQuorum: EPOCH_DURATION + 1, + slashingRoundSizeInEpochs: 2, validatorReexecute: false, sentinelEnabled: true, slashInactivityPenalty: 0n, // Set to 0 to disable 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 baef91c39bac..7d99d3ccb8ce 100644 --- a/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts +++ b/yarn-project/end-to-end/src/fixtures/snapshot_manager.ts @@ -306,9 +306,13 @@ async function setupFromFresh( const blobSinkPort = await getPort(); + // Default to no slashing + opts.slasherFlavor ??= 'none'; + deployL1ContractsArgs.slasherFlavor ??= opts.slasherFlavor; + // 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(), slasherFlavor: 'none', ...opts }; + const aztecNodeConfig: AztecNodeConfig & SetupOptions = { ...getConfigEnvVars(), ...opts }; aztecNodeConfig.peerCheckIntervalMS = TEST_PEER_CHECK_INTERVAL_MS; aztecNodeConfig.maxTxPoolSize = opts.maxTxPoolSize ?? TEST_MAX_TX_POOL_SIZE; // Only enable proving if specifically requested. diff --git a/yarn-project/end-to-end/src/fixtures/utils.ts b/yarn-project/end-to-end/src/fixtures/utils.ts index 8458cb380904..f5866420f3b3 100644 --- a/yarn-project/end-to-end/src/fixtures/utils.ts +++ b/yarn-project/end-to-end/src/fixtures/utils.ts @@ -391,8 +391,9 @@ export async function setup( let anvil: Anvil | undefined; try { opts.aztecTargetCommitteeSize ??= 0; + opts.slasherFlavor ??= 'none'; - const config: AztecNodeConfig & SetupOptions = { ...getConfigEnvVars(), slasherFlavor: 'none', ...opts }; + const config: AztecNodeConfig & SetupOptions = { ...getConfigEnvVars(), ...opts }; // use initialValidators for the node config config.validatorPrivateKeys = new SecretValue(opts.initialValidators?.map(v => v.privateKey) ?? []); diff --git a/yarn-project/end-to-end/src/spartan/upgrade_rollup_version.test.ts b/yarn-project/end-to-end/src/spartan/upgrade_rollup_version.test.ts index b671496fb31e..65320d359953 100644 --- a/yarn-project/end-to-end/src/spartan/upgrade_rollup_version.test.ts +++ b/yarn-project/end-to-end/src/spartan/upgrade_rollup_version.test.ts @@ -95,7 +95,7 @@ // aztecProofSubmissionEpochs: 1, // aztecTargetCommitteeSize: 48, // slashingQuorum: 5, -// slashingRoundSize: 8, +// slashingRoundSizeInEpochs: 2, // slashingLifetimeInRounds: 5, // slashingExecutionDelayInRounds: 0, // slashingVetoer: EthAddress.ZERO, diff --git a/yarn-project/ethereum/src/config.ts b/yarn-project/ethereum/src/config.ts index ab00b9fc93fb..cabebf4ed222 100644 --- a/yarn-project/ethereum/src/config.ts +++ b/yarn-project/ethereum/src/config.ts @@ -6,6 +6,7 @@ import { enumConfigHelper, getConfigFromMappings, numberConfigHelper, + optionalNumberConfigHelper, } from '@aztec/foundation/config'; import { EthAddress } from '@aztec/foundation/eth-address'; @@ -33,10 +34,10 @@ export type L1ContractsConfig = { activationThreshold: bigint; /** The minimum stake for a validator. */ ejectionThreshold: bigint; - /** The slashing quorum, i.e. how many slots must signal for the same payload in a round for it to be submittable to the Slasher */ - slashingQuorum: number; - /** The slashing round size, i.e. how many slots are in a round */ - slashingRoundSize: number; + /** The slashing quorum, i.e. how many slots must signal for the same payload in a round for it to be submittable to the Slasher (defaults to slashRoundSize / 2 + 1) */ + slashingQuorum?: number; + /** The slashing round size, i.e. how many epochs are in a slashing round */ + slashingRoundSizeInEpochs: number; /** The slashing lifetime in rounds. I.e., if 1, round N must be submitted before round N + 2 */ slashingLifetimeInRounds: number; /** The slashing execution delay in rounds. I.e., if 1, round N may not be submitted until round N + 2 */ @@ -53,8 +54,8 @@ export type L1ContractsConfig = { slashAmountMedium: bigint; /** Largest amount that can be slashed per round in tally slashing */ slashAmountLarge: bigint; - /** Governance proposing quorum */ - governanceProposerQuorum: number; + /** Governance proposing quorum (defaults to roundSize/2 + 1) */ + governanceProposerQuorum?: number; /** Governance proposing round size */ governanceProposerRoundSize: number; /** The mana target for the rollup */ @@ -76,12 +77,10 @@ export const DefaultL1ContractsConfig = { slashAmountSmall: BigInt(10e18), slashAmountMedium: BigInt(20e18), slashAmountLarge: BigInt(50e18), - slashingRoundSize: 32 * 6, // 6 epochs - slashingQuorum: (32 * 6) / 2 + 1, // 6 epochs, majority of validators + slashingRoundSizeInEpochs: 4, slashingLifetimeInRounds: 5, slashingExecutionDelayInRounds: 0, // round N may be submitted in round N + 1 slashingVetoer: EthAddress.ZERO, - governanceProposerQuorum: 151, governanceProposerRoundSize: 300, manaTarget: BigInt(1e10), provingCostPerMana: BigInt(100), @@ -278,12 +277,12 @@ export const l1ContractsConfigMappings: ConfigMappingsType = slashingQuorum: { env: 'AZTEC_SLASHING_QUORUM', description: 'The slashing quorum', - ...numberConfigHelper(DefaultL1ContractsConfig.slashingQuorum), + ...optionalNumberConfigHelper(), }, - slashingRoundSize: { - env: 'AZTEC_SLASHING_ROUND_SIZE', + slashingRoundSizeInEpochs: { + env: 'AZTEC_SLASHING_ROUND_SIZE_IN_EPOCHS', description: 'The slashing round size', - ...numberConfigHelper(DefaultL1ContractsConfig.slashingRoundSize), + ...numberConfigHelper(DefaultL1ContractsConfig.slashingRoundSizeInEpochs), }, slashingLifetimeInRounds: { env: 'AZTEC_SLASHING_LIFETIME_IN_ROUNDS', @@ -304,7 +303,7 @@ export const l1ContractsConfigMappings: ConfigMappingsType = governanceProposerQuorum: { env: 'AZTEC_GOVERNANCE_PROPOSER_QUORUM', description: 'The governance proposing quorum', - ...numberConfigHelper(DefaultL1ContractsConfig.governanceProposerQuorum), + ...optionalNumberConfigHelper(), }, governanceProposerRoundSize: { env: 'AZTEC_GOVERNANCE_PROPOSER_ROUND_SIZE', @@ -349,3 +348,196 @@ export function getL1ContractsConfigEnvVars(): L1ContractsConfig { export function getGenesisStateConfigEnvVars(): GenesisStateConfig { return getConfigFromMappings(genesisStateConfigMappings); } + +/** + * Validates the L1 contracts configuration to ensure all requirements enforced by L1 contracts + * during construction are satisfied before deployment. + * Accumulates all validation errors and throws an exception listing them all if any are found. + */ +export function validateConfig(config: Omit): void { + const errors: string[] = []; + + // RollupCore constructor validation: normalFlushSizeMin > 0 + // From: require(_config.stakingQueueConfig.normalFlushSizeMin > 0, Errors.Staking__InvalidStakingQueueConfig()); + const entryQueueConfig = getEntryQueueConfig('testnet'); // Get config to check normalFlushSizeMin + if (entryQueueConfig.normalFlushSizeMin <= 0n) { + errors.push('normalFlushSizeMin must be greater than 0'); + } + + // TimeLib initialization validation: aztecSlotDuration should be a multiple of ethereumSlotDuration + // While not explicitly required in constructor, this is a common validation for time-based systems + if (config.aztecSlotDuration % config.ethereumSlotDuration !== 0) { + errors.push( + `aztecSlotDuration (${config.aztecSlotDuration}) must be a multiple of ethereumSlotDuration (${config.ethereumSlotDuration})`, + ); + } + + // EmpireBase constructor validations for governance/slashing proposers + // From: require(QUORUM_SIZE > ROUND_SIZE / 2, Errors.GovernanceProposer__InvalidQuorumAndRoundSize(QUORUM_SIZE, ROUND_SIZE)); + const { governanceProposerQuorum, governanceProposerRoundSize } = config; + if ( + governanceProposerQuorum !== undefined && + governanceProposerQuorum <= Math.floor(governanceProposerRoundSize / 2) + ) { + errors.push( + `governanceProposerQuorum (${governanceProposerQuorum}) must be greater than half of governanceProposerRoundSize (${Math.floor(governanceProposerRoundSize / 2)})`, + ); + } + + // From: require(QUORUM_SIZE <= ROUND_SIZE, Errors.GovernanceProposer__QuorumCannotBeLargerThanRoundSize(QUORUM_SIZE, ROUND_SIZE)); + if (governanceProposerQuorum !== undefined && governanceProposerQuorum > governanceProposerRoundSize) { + errors.push( + `governanceProposerQuorum (${governanceProposerQuorum}) cannot be larger than governanceProposerRoundSize (${governanceProposerRoundSize})`, + ); + } + + // Slashing quorum validations (similar to governance quorum) + const slashingRoundSize = config.slashingRoundSizeInEpochs * config.aztecEpochDuration; + const { slashingQuorum } = config; + if (slashingQuorum !== undefined && slashingQuorum <= Math.floor(slashingRoundSize / 2)) { + errors.push( + `slashingQuorum (${slashingQuorum}) must be greater than half of slashingRoundSizeInEpochs (${Math.floor(slashingRoundSize / 2)})`, + ); + } + + if (slashingQuorum !== undefined && slashingQuorum > slashingRoundSize) { + errors.push( + `slashingQuorum (${slashingQuorum}) cannot be larger than slashingRoundSizeInEpochs (${slashingRoundSize})`, + ); + } + + // EmpireBase and TallySlashingProposer lifetime and execution delay validation + // From: require(LIFETIME_IN_ROUNDS > EXECUTION_DELAY_IN_ROUNDS); + if (config.slashingLifetimeInRounds <= config.slashingExecutionDelayInRounds) { + errors.push( + `slashingLifetimeInRounds (${config.slashingLifetimeInRounds}) must be greater than slashingExecutionDelayInRounds (${config.slashingExecutionDelayInRounds})`, + ); + } + + // Staking asset validation: activationThreshold > ejectionThreshold + if (config.activationThreshold < config.ejectionThreshold) { + errors.push( + `activationThreshold (${config.activationThreshold}) must be greater than ejectionThreshold (${config.ejectionThreshold})`, + ); + } + + // TallySlashingProposer constructor validations + if (config.slasherFlavor === 'tally') { + // From: require(SLASH_OFFSET_IN_ROUNDS > 0, Errors.TallySlashingProposer__SlashOffsetMustBeGreaterThanZero(...)); + if (config.slashingOffsetInRounds <= 0) { + errors.push(`slashingOffsetInRounds (${config.slashingOffsetInRounds}) must be greater than 0`); + } + + // From: require(ROUND_SIZE_IN_EPOCHS * _epochDuration == ROUND_SIZE, Errors.TallySlashingProposer__RoundSizeMustBeMultipleOfEpochDuration(...)); + const roundSizeInSlots = config.slashingRoundSizeInEpochs * config.aztecEpochDuration; + + // From: require(QUORUM > 0, Errors.TallySlashingProposer__QuorumMustBeGreaterThanZero()); + if (slashingQuorum !== undefined && slashingQuorum <= 0) { + errors.push(`slashingQuorum (${slashingQuorum}) must be greater than 0`); + } + + // From: require(ROUND_SIZE > 1, Errors.TallySlashingProposer__InvalidQuorumAndRoundSize(QUORUM, ROUND_SIZE)); + if (roundSizeInSlots <= 1) { + errors.push(`slashing round size in slots (${roundSizeInSlots}) must be greater than 1`); + } + + // From: require(_slashAmounts[0] <= _slashAmounts[1], Errors.TallySlashingProposer__InvalidSlashAmounts(_slashAmounts)); + if (config.slashAmountSmall > config.slashAmountMedium) { + errors.push( + `slashAmountSmall (${config.slashAmountSmall}) must be less than or equal to slashAmountMedium (${config.slashAmountMedium})`, + ); + } + + // From: require(_slashAmounts[1] <= _slashAmounts[2], Errors.TallySlashingProposer__InvalidSlashAmounts(_slashAmounts)); + if (config.slashAmountMedium > config.slashAmountLarge) { + errors.push( + `slashAmountMedium (${config.slashAmountMedium}) must be less than or equal to slashAmountLarge (${config.slashAmountLarge})`, + ); + } + + // From: require(LIFETIME_IN_ROUNDS < ROUNDABOUT_SIZE, Errors.TallySlashingProposer__LifetimeMustBeLessThanRoundabout(...)); + const ROUNDABOUT_SIZE = 128; // Constant from TallySlashingProposer + if (config.slashingLifetimeInRounds >= ROUNDABOUT_SIZE) { + errors.push(`slashingLifetimeInRounds (${config.slashingLifetimeInRounds}) must be less than ${ROUNDABOUT_SIZE}`); + } + + // From: require(ROUND_SIZE_IN_EPOCHS > 0, Errors.TallySlashingProposer__RoundSizeInEpochsMustBeGreaterThanZero(...)); + if (config.slashingRoundSizeInEpochs <= 0) { + errors.push(`slashingRoundSizeInEpochs (${config.slashingRoundSizeInEpochs}) must be greater than 0`); + } + + // From: require(ROUND_SIZE < MAX_ROUND_SIZE, Errors.TallySlashingProposer__RoundSizeTooLarge(ROUND_SIZE, MAX_ROUND_SIZE)); + const MAX_ROUND_SIZE = 1024; // Constant from TallySlashingProposer + if (roundSizeInSlots >= MAX_ROUND_SIZE) { + errors.push(`slashing round size in slots (${roundSizeInSlots}) must be less than ${MAX_ROUND_SIZE}`); + } + + // From: require(COMMITTEE_SIZE > 0, Errors.TallySlashingProposer__CommitteeSizeMustBeGreaterThanZero(COMMITTEE_SIZE)); + if (config.aztecTargetCommitteeSize <= 0) { + errors.push(`aztecTargetCommitteeSize (${config.aztecTargetCommitteeSize}) must be greater than 0`); + } + + // From: require(voteSize <= 128, Errors.TallySlashingProposer__VoteSizeTooBig(voteSize, 128)); + // voteSize = COMMITTEE_SIZE * ROUND_SIZE_IN_EPOCHS / 4 + const voteSize = (config.aztecTargetCommitteeSize * config.slashingRoundSizeInEpochs) / 4; + if (voteSize > 128) { + errors.push(`vote size (${voteSize}) must be <= 128 (committee size * round size in epochs / 4)`); + } + + // From: require(COMMITTEE_SIZE * ROUND_SIZE_IN_EPOCHS % 4 == 0, Errors.TallySlashingProposer__InvalidCommitteeAndRoundSize(...)); + if ((config.aztecTargetCommitteeSize * config.slashingRoundSizeInEpochs) % 4 !== 0) { + errors.push( + `aztecTargetCommitteeSize * slashingRoundSizeInEpochs (${config.aztecTargetCommitteeSize * config.slashingRoundSizeInEpochs}) must be divisible by 4`, + ); + } + + // Slashing offset validation: should be positive to allow proper slashing timing + if (config.slashingOffsetInRounds < 0) { + errors.push('slashingOffsetInRounds cannot be negative'); + } + } + + // Epoch and slot duration validations + if (config.aztecSlotDuration <= 0) { + errors.push('aztecSlotDuration must be greater than 0'); + } + + if (config.ethereumSlotDuration <= 0) { + errors.push('ethereumSlotDuration must be greater than 0'); + } + + if (config.aztecEpochDuration <= 0) { + errors.push('aztecEpochDuration must be greater than 0'); + } + + // Committee size validation + if (config.aztecTargetCommitteeSize < 0) { + errors.push('aztecTargetCommitteeSize cannot be negative'); + } + + // Proof submission epochs validation + if (config.aztecProofSubmissionEpochs < 0) { + errors.push('aztecProofSubmissionEpochs cannot be negative'); + } + + // Exit delay validation + if (config.exitDelaySeconds < 0) { + errors.push('exitDelaySeconds cannot be negative'); + } + + // Mana validation + if (config.manaTarget < 0n) { + errors.push('manaTarget cannot be negative'); + } + + if (config.provingCostPerMana < 0n) { + errors.push('provingCostPerMana cannot be negative'); + } + + // If any errors were found, throw an exception with all of them + if (errors.length > 0) { + throw new Error( + `L1 contracts configuration validation failed with ${errors.length} error(s):\n${errors.map((error, index) => `${index + 1}. ${error}`).join('\n')}`, + ); + } +} diff --git a/yarn-project/ethereum/src/contracts/tally_slashing_proposer.test.ts b/yarn-project/ethereum/src/contracts/tally_slashing_proposer.test.ts index e09e9f1002bc..30e337f62175 100644 --- a/yarn-project/ethereum/src/contracts/tally_slashing_proposer.test.ts +++ b/yarn-project/ethereum/src/contracts/tally_slashing_proposer.test.ts @@ -65,7 +65,8 @@ describe('TallySlashingProposer', () => { genesisArchiveRoot: Fr.random(), realVerifier: false, slasherFlavor: 'tally' as const, - slashingRoundSize: testSlashingRoundSize, + slashingQuorum: testSlashingRoundSize / 2 + 1, + slashingRoundSizeInEpochs: testSlashingRoundSize / DefaultL1ContractsConfig.aztecEpochDuration, aztecTargetCommitteeSize: 4, initialValidators: validatorsAddresses.map(attester => ({ attester, @@ -96,12 +97,12 @@ describe('TallySlashingProposer', () => { describe('contract constants getters', () => { it('returns correct quorum size from deployed contract', async () => { const quorumSize = await tallySlashingProposer.getQuorumSize(); - expect(quorumSize).toBe(BigInt(testConfig.slashingQuorum)); + expect(quorumSize).toBe(BigInt(testConfig.slashingQuorum!)); }); it('returns correct round size from deployed contract', async () => { const roundSize = await tallySlashingProposer.getRoundSize(); - expect(roundSize).toBe(BigInt(testConfig.slashingRoundSize)); + expect(roundSize).toBe(BigInt(testConfig.slashingRoundSizeInEpochs * testConfig.aztecEpochDuration)); }); it('returns correct committee size from deployed contract', async () => { @@ -111,7 +112,7 @@ describe('TallySlashingProposer', () => { it('returns correct round size in epochs from deployed contract', async () => { const roundSizeInEpochs = await tallySlashingProposer.getRoundSizeInEpochs(); - const expectedValue = BigInt(testConfig.slashingRoundSize / testConfig.aztecEpochDuration); + const expectedValue = BigInt(testConfig.slashingRoundSizeInEpochs); expect(roundSizeInEpochs).toBe(expectedValue); }); diff --git a/yarn-project/ethereum/src/deploy_l1_contracts.ts b/yarn-project/ethereum/src/deploy_l1_contracts.ts index 4b9f7d880366..053ed72603a4 100644 --- a/yarn-project/ethereum/src/deploy_l1_contracts.ts +++ b/yarn-project/ethereum/src/deploy_l1_contracts.ts @@ -34,6 +34,7 @@ import { getGovernanceConfiguration, getRewardBoostConfig, getRewardConfig, + validateConfig, } from './config.js'; import { GSEContract } from './contracts/gse.js'; import { deployMulticall3 } from './contracts/multicall.js'; @@ -128,7 +129,7 @@ export interface ContractArtifacts libraries?: Libraries; } -export interface DeployL1ContractsArgs extends L1ContractsConfig { +export interface DeployL1ContractsArgs extends Omit { /** The vk tree root. */ vkTreeRoot: Fr; /** The protocol contract tree root. */ @@ -195,7 +196,7 @@ export const deploySharedContracts = async ( const governanceProposerAddress = await deployer.deploy(GovernanceProposerArtifact, [ registryAddress.toString(), gseAddress.toString(), - BigInt(args.governanceProposerQuorum), + BigInt(args.governanceProposerQuorum ?? args.governanceProposerRoundSize / 2 + 1), BigInt(args.governanceProposerRoundSize), ]); logger.verbose(`Deployed GovernanceProposer at ${governanceProposerAddress}`); @@ -531,8 +532,8 @@ export const deployRollup = async ( aztecEpochDuration: BigInt(args.aztecEpochDuration), targetCommitteeSize: BigInt(args.aztecTargetCommitteeSize), aztecProofSubmissionEpochs: BigInt(args.aztecProofSubmissionEpochs), - slashingQuorum: BigInt(args.slashingQuorum), - slashingRoundSize: BigInt(args.slashingRoundSize), + slashingQuorum: BigInt(args.slashingQuorum ?? (args.slashingRoundSizeInEpochs * args.aztecEpochDuration) / 2 + 1), + slashingRoundSize: BigInt(args.slashingRoundSizeInEpochs * args.aztecEpochDuration), slashingLifetimeInRounds: BigInt(args.slashingLifetimeInRounds), slashingExecutionDelayInRounds: BigInt(args.slashingExecutionDelayInRounds), slashingVetoer: args.slashingVetoer.toString(), @@ -914,6 +915,8 @@ export const deployL1Contracts = async ( args: DeployL1ContractsArgs, txUtilsConfig: L1TxUtilsConfig = getL1TxUtilsConfigEnvVars(), ): Promise => { + validateConfig(args); + const l1Client = createExtendedL1Client(rpcUrls, account, chain); // Deploy multicall3 if it does not exist in this network diff --git a/yarn-project/ethereum/src/queries.ts b/yarn-project/ethereum/src/queries.ts index e0ebf066be90..fd84a44090d0 100644 --- a/yarn-project/ethereum/src/queries.ts +++ b/yarn-project/ethereum/src/queries.ts @@ -90,7 +90,7 @@ export async function getL1ContractsConfig( activationThreshold, ejectionThreshold, slashingQuorum: Number(slashingQuorum), - slashingRoundSize: Number(slashingRoundSize), + slashingRoundSizeInEpochs: Number(slashingRoundSize / aztecEpochDuration), slashingLifetimeInRounds: Number(slashingLifetimeInRounds), slashingExecutionDelayInRounds: Number(slashingExecutionDelayInRounds), slashingVetoer: EthAddress.fromString(slashingVetoer), diff --git a/yarn-project/foundation/src/config/env_var.ts b/yarn-project/foundation/src/config/env_var.ts index 9316e771dd6e..9f3619f11839 100644 --- a/yarn-project/foundation/src/config/env_var.ts +++ b/yarn-project/foundation/src/config/env_var.ts @@ -262,7 +262,7 @@ export type EnvVar = | 'AZTEC_MANA_TARGET' | 'AZTEC_PROVING_COST_PER_MANA' | 'AZTEC_SLASHING_QUORUM' - | 'AZTEC_SLASHING_ROUND_SIZE' + | 'AZTEC_SLASHING_ROUND_SIZE_IN_EPOCHS' | 'AZTEC_SLASHING_LIFETIME_IN_ROUNDS' | 'AZTEC_SLASHING_EXECUTION_DELAY_IN_ROUNDS' | 'AZTEC_SLASHING_VETOER' diff --git a/yarn-project/slasher/src/slash_round_monitor.ts b/yarn-project/slasher/src/slash_round_monitor.ts index ca95c0a9bc51..d5a9dfdb636a 100644 --- a/yarn-project/slasher/src/slash_round_monitor.ts +++ b/yarn-project/slasher/src/slash_round_monitor.ts @@ -1,4 +1,3 @@ -import type { L1ContractsConfig } from '@aztec/ethereum'; import { createLogger } from '@aztec/foundation/log'; import type { DateProvider } from '@aztec/foundation/timer'; import type { Prettify } from '@aztec/foundation/types'; @@ -6,8 +5,7 @@ import { type L1RollupConstants, getSlotAtTimestamp } from '@aztec/stdlib/epoch- import { getRoundForSlot } from '@aztec/stdlib/slashing'; export type SlashRoundMonitorSettings = Prettify< - Pick & - Pick + Pick & { slashingRoundSize: number } >; export class SlashRoundMonitor { @@ -16,8 +14,7 @@ export class SlashRoundMonitor { private handler: ((round: bigint) => Promise) | undefined = undefined; constructor( - private settings: Pick & - Pick, + private settings: SlashRoundMonitorSettings, private dateProvider: DateProvider, private log = createLogger('slasher:round-monitor'), ) {}