From 90101c5d15dbc0db3460efe00f83437344e76c1d Mon Sep 17 00:00:00 2001 From: Mitchell Tracy Date: Wed, 7 May 2025 15:14:36 -0400 Subject: [PATCH 01/12] feat: custom slashing design doc --- docs/custom-slashing/dd.template.md | 83 +++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 docs/custom-slashing/dd.template.md diff --git a/docs/custom-slashing/dd.template.md b/docs/custom-slashing/dd.template.md new file mode 100644 index 0000000..9aa23bf --- /dev/null +++ b/docs/custom-slashing/dd.template.md @@ -0,0 +1,83 @@ +# Custom Slashing Design Document + +- Owner: @just-mitch +- Approvers: + - @LHerskind + - @Maddiaa0 +- Target DD Approval Date: 2025-05-09 +- Target Project Delivery Date: 2025-05-16 + +## Executive Summary + +The L1 contracts currently only "allow" slashing all validators in an epoch if the epoch is never proven. + +We want to provide a mechanism for slashing specific validators for not participating in consensus. + +## Requirements + +The requirements with filled checkboxes are met by the design below. + +- [x] There MUST be ready-made L1 contract(s) that can be used to slash specific validators for not participating in consensus. +- [x] The Aztec Labs node client software ("the node") MUST automatically slash validators for not participating in consensus. +- [x] It SHOULD be possible to slash more than one validator at a time. +- [x] Coordinating the slash SHOULD NOT require any coordination between the validators beyond the existing voting/signalling mechanism; each validator SHOULD be able to inspect L1 and its state to determine: + - If it agrees with the slash + - How/where to vote/signal on L1 +- [x] Node operators SHOULD be able to configure their node to specify thresholds for what they consider "not participating". +- [x] The "offence" that triggers the slash MAY be specified on L1. +- [ ] The amount to be slashed MAY be configurable without deploying a new contract. +- [ ] The threshold of number of validators (M/N) that need to signal/vote for the CustomSlashFactory payload MAY be configurable without deploying a new contract or a governance action. + +## Overview + +Rename the existing `SlashFactory` contract to `EpochSlashFactory` to denote that it slashes all validators in an epoch. Make a new `CustomSlashFactory` contract which creates a payload to slash a provided list of validators for an explicit offence. The amounts to be slashed for each offence will be provided in the constructor of the `CustomSlashFactory` contract. There will only be one explicit offence right now: "missing attestations". Creating the payload via the `CustomSlashFactory` will emit an event with the payload address, and the validator addresses. + +Aztec nodes will listen for these events, and then check if the validator is bad committed the alleged offence. If so, they will vote/signal for the payload on L1. + +## Details + +The SlasherClient will remain the interface that the SequencerPublisher uses to adjust the transaction it sends to the forwarder contract. + +Internally, though, the SlasherClient will monitor two conditions: + +- an epoch was not proven, so slash all validators via the `EpochSlashFactory` contract (i.e. the current state, rename the L1 contract from `SlashFactory`) +- a validator has missed X% (e.g. 90%) of attestations according to the Sentinel, so slash that validator via the `CustomSlashFactory` contract (this is new) + +Validators will need to have a way to order the various slashing events they observe. + +Their first priority is to slash payloads from the `EpochSlashFactory`, sorted oldest to newest. + +Their second priority is to slash payloads from the `CustomSlashFactory` for the "missing attestations" offence, sorted oldest to newest, but folding together payloads until one is found that they disagree with. + +They will vote to slash any validator that has missed Y% (e.g. 50%) of attestations according to the Sentinel; there are two percentages to be configured: + +- the percentage of attestations missed required to create a payload +- the percentage of attestations missed required to signal/vote for the payload + +Node operators will configure their node to listen to the specific CustomSlashFactory contract and specify the percentage of attestations missed required to create a payload. + +This will be done via CLI arguments or environment variables. + +## Notes + +The amount to slash should be high for testnet (e.g. the minimum stake amount). We can use a different amount for mainnet. + +The threshold of number of validators (M/N) that need to signal/vote for the CustomSlashFactory payload will be the same as the number of validators that need to signal/vote for the EpochSlashFactory payload. + +This requires that M/N validators are listening to the same CustomSlashFactory contract, and participating in the protocol. + +If we do not have M/N validators participating, we will need to make a "propose with lock" on the governance to deploy a new rollup instance. If that is not palatable, we could update the staking lib to allow the governance (not just the slasher) to slash validators; then we could "propose with lock" to slash whatever validators governance decides. + +## Timeline + +Outline the timeline for the project. E.g. + +- L1 contracts : 1 day +- Refactor SlasherClient : 1 day +- Review/Polish : 1-2 days + +Total: 3-4 days + +## Disclaimer + +The information set out herein is for discussion purposes only and does not represent any binding indication or commitment by Aztec Labs and its employees to take any action whatsoever, including relating to the structure and/or any potential operation of the Aztec protocol or the protocol roadmap. In particular: (i) nothing in these projects, requests, or comments is intended to create any contractual or other form of legal relationship with Aztec Labs or third parties who engage with this AztecProtocol GitHub account (including, without limitation, by responding to a conversation or submitting comments) (ii) by engaging with any conversation or request, the relevant persons are consenting to Aztec Labs’ use and publication of such engagement and related information on an open-source basis (and agree that Aztec Labs will not treat such engagement and related information as confidential), and (iii) Aztec Labs is not under any duty to consider any or all engagements, and that consideration of such engagements and any decision to award grants or other rewards for any such engagement is entirely at Aztec Labs’ sole discretion. Please do not rely on any information on this account for any purpose - the development, release, and timing of any products, features, or functionality remains subject to change and is currently entirely hypothetical. Nothing on this account should be treated as an offer to sell any security or any other asset by Aztec Labs or its affiliates, and you should not rely on any content or comments for advice of any kind, including legal, investment, financial, tax, or other professional advice. From 41f1e4d1e155229110ce91c075aa1dfb61042fec Mon Sep 17 00:00:00 2001 From: Mitchell Tracy Date: Thu, 8 May 2025 11:31:22 -0400 Subject: [PATCH 02/12] feat: expand slashing to allow variable amounts chore: refactor to have a single slash factory chore: update summary, more detail in implementation --- docs/custom-slashing/dd.template.md | 97 ++++++++++++++++++++++++----- 1 file changed, 81 insertions(+), 16 deletions(-) diff --git a/docs/custom-slashing/dd.template.md b/docs/custom-slashing/dd.template.md index 9aa23bf..f32bacf 100644 --- a/docs/custom-slashing/dd.template.md +++ b/docs/custom-slashing/dd.template.md @@ -9,9 +9,23 @@ ## Executive Summary -The L1 contracts currently only "allow" slashing all validators in an epoch if the epoch is never proven. +The `StakingLib` designates a "slasher" address which is able to slash arbitrary validators for arbitrary amounts. -We want to provide a mechanism for slashing specific validators for not participating in consensus. +The contract used as the slasher is currently `Slasher`, which takes directives from a `SlashingProposer`. + +The `SlashingProposer` is an instance of `EmpireBase`, which operates in "rounds" of `N` L2 slots, during which at least `M` proposers must vote for a specific contract address "payload" to be executed. + +The payload just calls "slash", with the list of validators to slash, and the amount to slash each. + +So the L1 contracts currently allow arbitrary slashing as long as the motion has support from `M/N` validators. + +In practice, however, there are only mechanisms built to create and vote for payloads to slash all validators in an epoch if the epoch is never proven, namely an out-of-protocol `SlashFactory` contract, and corresponding logic on the node to utilize it. + +We want to expand this `SlashFactory` to allow for "custom slashing", which would allow the node to programmatically create or vote for payloads to slash specific validators for specific amounts for specific offences. + +In addition, we will add corresponding logic to the node to utilize the new `SlashFactory` to slash validators for inactivity. + +Last, we will add an override, which may be set by the node operator, which will configure the node to vote for a particular payload no matter what. ## Requirements @@ -25,48 +39,99 @@ The requirements with filled checkboxes are met by the design below. - How/where to vote/signal on L1 - [x] Node operators SHOULD be able to configure their node to specify thresholds for what they consider "not participating". - [x] The "offence" that triggers the slash MAY be specified on L1. -- [ ] The amount to be slashed MAY be configurable without deploying a new contract. +- [x] The amount to be slashed MAY be configurable without deploying a new factory contract. - [ ] The threshold of number of validators (M/N) that need to signal/vote for the CustomSlashFactory payload MAY be configurable without deploying a new contract or a governance action. -## Overview +## L1 Changes + +We make no changes to the `Slasher` contract, or any other "in-protocol" contracts. + +Refactor the `SlashFactory` to accept an array of validator addresses, amounts, and offences. I.e. + +```solidity +interface ISlashFactory { + enum Offense { + Unknown, + EpochPruned, + Inactivity + } -Rename the existing `SlashFactory` contract to `EpochSlashFactory` to denote that it slashes all validators in an epoch. Make a new `CustomSlashFactory` contract which creates a payload to slash a provided list of validators for an explicit offence. The amounts to be slashed for each offence will be provided in the constructor of the `CustomSlashFactory` contract. There will only be one explicit offence right now: "missing attestations". Creating the payload via the `CustomSlashFactory` will emit an event with the payload address, and the validator addresses. + event SlashPayloadCreated( + address payloadAddress, address[] validators, uint256[] amounts, Offense[] offences + ); -Aztec nodes will listen for these events, and then check if the validator is bad committed the alleged offence. If so, they will vote/signal for the payload on L1. + function createSlashPayload( + address[] memory _validators, + uint256[] memory _amounts, + Offense[] memory _offences + ) external returns (IPayload); +} +``` -## Details +Creating the payload via the `SlashFactory` will emit an event with the payload address, and the validator addresses/amounts/offences. + +Aztec nodes will listen for these events, and then check if the validator committed the alleged offence. If so, they will vote/signal for the payload on L1. + +## Node Changes The SlasherClient will remain the interface that the SequencerPublisher uses to adjust the transaction it sends to the forwarder contract. Internally, though, the SlasherClient will monitor two conditions: -- an epoch was not proven, so slash all validators via the `EpochSlashFactory` contract (i.e. the current state, rename the L1 contract from `SlashFactory`) -- a validator has missed X% (e.g. 90%) of attestations according to the Sentinel, so slash that validator via the `CustomSlashFactory` contract (this is new) +- an epoch was not proven, so slash all validators (this exists today) +- a validator has missed X% (e.g. 90%) of attestations according to the Sentinel, so slash that validator (this is new) +- an override payload is set Validators will need to have a way to order the various slashing events they observe. -Their first priority is to slash payloads from the `EpochSlashFactory`, sorted oldest to newest. +The Aztec client will use the following heuristics to determine which payload to signal/vote for: + +1. If there is an override payload, signal/vote for it. +2. Check if the payload is older than a configurable TTL. If so, discard it. +3. Sum the amounts of remaining slash proposals, so we have a `totalSlashAmount` for each payload. +4. Sort the payloads by `totalSlashAmount`, largest to smallest. +5. Pick the top payload that the validator agrees with (i.e. all the named validators committed the named offences), and signal/vote for it. + +This process should be done each time: + +- a new payload is created, as determined by watching the `SlashFactory` contract events +- the proposer has an opportunity to signal/vote -Their second priority is to slash payloads from the `CustomSlashFactory` for the "missing attestations" offence, sorted oldest to newest, but folding together payloads until one is found that they disagree with. +### Slashing for epoch pruning -They will vote to slash any validator that has missed Y% (e.g. 50%) of attestations according to the Sentinel; there are two percentages to be configured: +We will need to modify the logic around slashing for epoch pruning. Instead of specifying a particular epoch, we just specify all the validators within the epoch. + +### Slashing for inactivity + +Regarding the slash for "inactivity", nodes will vote to slash any validator that has missed Y% (e.g. 50%) of attestations according to the Sentinel; there are two percentages to be configured: - the percentage of attestations missed required to create a payload - the percentage of attestations missed required to signal/vote for the payload -Node operators will configure their node to listen to the specific CustomSlashFactory contract and specify the percentage of attestations missed required to create a payload. +Node operators will configure their node to listen to a specific SlashFactory contract (as they do today) and specify the percentage of attestations missed required to create a payload. This will be done via CLI arguments or environment variables. +### New configuration + +- `SLASH_OVERRIDE_PAYLOAD`: the address of a payload to signal/vote for no matter what +- `SLASH_PAYLOAD_TTL`: the maximum age of a payload to signal/vote for +- `SLASH_PRUNE_CREATE`: whether to create a payload for epoch pruning +- `SLASH_PRUNE_PENALTY`: the amount to slash each validator that was in an epoch that was pruned +- `SLASH_PRUNE_SIGNAL`: whether to signal/vote for a payload for epoch pruning +- `SLASH_INACTIVITY_CREATE_TARGET`: the percentage of attestations missed required to create a payload +- `SLASH_INACTIVITY_CREATE_PENALTY`: the amount to slash each validator that is inactive +- `SLASH_INACTIVITY_SIGNAL_TARGET`: the percentage of attestations missed required to signal/vote for the payload + ## Notes The amount to slash should be high for testnet (e.g. the minimum stake amount). We can use a different amount for mainnet. -The threshold of number of validators (M/N) that need to signal/vote for the CustomSlashFactory payload will be the same as the number of validators that need to signal/vote for the EpochSlashFactory payload. +The threshold of number of validators (M/N) that need to signal/vote for the SlashFactory payload will be the same as the number of validators that need to signal/vote for any other slashing payload, i.e. this ratio is fixed at set per rollup. -This requires that M/N validators are listening to the same CustomSlashFactory contract, and participating in the protocol. +This requires that M/N validators are listening to the same SlashFactory contract, and participating in the protocol. -If we do not have M/N validators participating, we will need to make a "propose with lock" on the governance to deploy a new rollup instance. If that is not palatable, we could update the staking lib to allow the governance (not just the slasher) to slash validators; then we could "propose with lock" to slash whatever validators governance decides. +If we do not have M/N validators participating, someone will need to make a "propose with lock" on the governance to deploy a new rollup instance. If that is not palatable, we could update the staking lib to allow the governance (not just the slasher) to slash validators; then someone could "propose with lock" to slash whatever validators governance decides. ## Timeline From a734dbb0dffcb2c480c4aeecbb69cf2482ba9263 Mon Sep 17 00:00:00 2001 From: Mitchell Tracy Date: Fri, 9 May 2025 07:56:17 -0400 Subject: [PATCH 03/12] feat: use uint256 rather than enum for offences chore: include note about updating slasher chore: rename file --- .../custom-slashing/{dd.template.md => dd.md} | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) rename docs/custom-slashing/{dd.template.md => dd.md} (92%) diff --git a/docs/custom-slashing/dd.template.md b/docs/custom-slashing/dd.md similarity index 92% rename from docs/custom-slashing/dd.template.md rename to docs/custom-slashing/dd.md index f32bacf..c7cfa97 100644 --- a/docs/custom-slashing/dd.template.md +++ b/docs/custom-slashing/dd.md @@ -50,24 +50,30 @@ Refactor the `SlashFactory` to accept an array of validator addresses, amounts, ```solidity interface ISlashFactory { - enum Offense { - Unknown, - EpochPruned, - Inactivity - } + event SlashPayloadCreated( - address payloadAddress, address[] validators, uint256[] amounts, Offense[] offences + address payloadAddress, address[] validators, uint64[] amounts, uint256[] offences ); function createSlashPayload( address[] memory _validators, - uint256[] memory _amounts, - Offense[] memory _offences + uint64[] memory _amounts, + uint256[] memory _offences ) external returns (IPayload); } ``` +This implies a max slash of 2^64-1 for each validator. + +For now, the `offences` field will effectively be an enum, with the following possible values: + +- 0: unknown +- 1: epoch pruned +- 2: inactivity + +The use of `uint256` for offences rather than an explicit enum allows for future flexibility, e.g. adding more offences and interpreting them off-chain, or, by using `uint256` rather than `uint8`, using a hash/commitment to some external data/proof. + Creating the payload via the `SlashFactory` will emit an event with the payload address, and the validator addresses/amounts/offences. Aztec nodes will listen for these events, and then check if the validator committed the alleged offence. If so, they will vote/signal for the payload on L1. @@ -133,6 +139,8 @@ This requires that M/N validators are listening to the same SlashFactory contrac If we do not have M/N validators participating, someone will need to make a "propose with lock" on the governance to deploy a new rollup instance. If that is not palatable, we could update the staking lib to allow the governance (not just the slasher) to slash validators; then someone could "propose with lock" to slash whatever validators governance decides. +In the future, we could also allow the slasher itself or governance to change who the slasher is. + ## Timeline Outline the timeline for the project. E.g. From 0e10601f48920eee1c4c3b25af93d037df04a303 Mon Sep 17 00:00:00 2001 From: Mitchell Tracy Date: Fri, 9 May 2025 08:00:22 -0400 Subject: [PATCH 04/12] chore: just use uint256 for slash amount --- docs/custom-slashing/dd.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/custom-slashing/dd.md b/docs/custom-slashing/dd.md index c7cfa97..336a1b1 100644 --- a/docs/custom-slashing/dd.md +++ b/docs/custom-slashing/dd.md @@ -53,19 +53,17 @@ interface ISlashFactory { event SlashPayloadCreated( - address payloadAddress, address[] validators, uint64[] amounts, uint256[] offences + address payloadAddress, address[] validators, uint256[] amounts, uint256[] offences ); function createSlashPayload( address[] memory _validators, - uint64[] memory _amounts, + uint256[] memory _amounts, uint256[] memory _offences ) external returns (IPayload); } ``` -This implies a max slash of 2^64-1 for each validator. - For now, the `offences` field will effectively be an enum, with the following possible values: - 0: unknown From 76c1a3b54d99feb59376a2b9f5f1a53b1adeed50 Mon Sep 17 00:00:00 2001 From: Mitchell Tracy Date: Fri, 9 May 2025 08:04:28 -0400 Subject: [PATCH 05/12] chore: add description of create payload --- docs/custom-slashing/dd.md | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/docs/custom-slashing/dd.md b/docs/custom-slashing/dd.md index 336a1b1..d3a5dd6 100644 --- a/docs/custom-slashing/dd.md +++ b/docs/custom-slashing/dd.md @@ -64,6 +64,44 @@ interface ISlashFactory { } ``` +The core function in the `SlashFactory` will look like: + +```solidity + function createSlashPayload( + address[] memory _validators, + uint256[] memory _amounts, + uint256[] memory _offences + ) external override(ISlashFactory) returns (IPayload) { + require( + _validators.length == _amounts.length, + ISlashFactory.SlashPayloadAmountsLengthMismatch(_validators.length, _amounts.length) + ); + require( + _validators.length == _offences.length, + ISlashFactory.SlashPayloadOffencesLengthMismatch(_validators.length, _offences.length) + ); + + uint256 currentHour = block.timestamp / 3600; + (address predictedAddress, bool isDeployed) = + getAddressAndIsDeployed(_validators, _amounts, _offences, currentHour); + + if (isDeployed) { + return IPayload(predictedAddress); + } + + // Use a salt so that validators don't create many payloads for the same slash. + // Include the current hour in the salt to allow repeat slashing. + // This implies you cannot create a new payload for the same slash until the next hour. + bytes32 salt = keccak256(abi.encodePacked(_validators, _amounts, _offences, currentHour)); + + // Don't need to pass _offences as they are not used in the payload. + SlashPayload payload = new SlashPayload{salt: salt}(_validators, VALIDATOR_SELECTION, _amounts); + + emit SlashPayloadCreated(address(payload), _validators, _amounts, _offences); + return IPayload(address(payload)); + } +``` + For now, the `offences` field will effectively be an enum, with the following possible values: - 0: unknown From e3100759070132e095ad14eab361b362ee54ca75 Mon Sep 17 00:00:00 2001 From: Mitchell Tracy Date: Fri, 9 May 2025 09:58:26 -0400 Subject: [PATCH 06/12] chore: remove time from payload salt --- docs/custom-slashing/dd.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/docs/custom-slashing/dd.md b/docs/custom-slashing/dd.md index d3a5dd6..db2551a 100644 --- a/docs/custom-slashing/dd.md +++ b/docs/custom-slashing/dd.md @@ -81,18 +81,15 @@ The core function in the `SlashFactory` will look like: ISlashFactory.SlashPayloadOffencesLengthMismatch(_validators.length, _offences.length) ); - uint256 currentHour = block.timestamp / 3600; (address predictedAddress, bool isDeployed) = - getAddressAndIsDeployed(_validators, _amounts, _offences, currentHour); + getAddressAndIsDeployed(_validators, _amounts, _offences); if (isDeployed) { return IPayload(predictedAddress); } // Use a salt so that validators don't create many payloads for the same slash. - // Include the current hour in the salt to allow repeat slashing. - // This implies you cannot create a new payload for the same slash until the next hour. - bytes32 salt = keccak256(abi.encodePacked(_validators, _amounts, _offences, currentHour)); + bytes32 salt = keccak256(abi.encodePacked(_validators, _amounts, _offences)); // Don't need to pass _offences as they are not used in the payload. SlashPayload payload = new SlashPayload{salt: salt}(_validators, VALIDATOR_SELECTION, _amounts); From fff53580895af5a4af3d2ab10e3ca1daaa381ff1 Mon Sep 17 00:00:00 2001 From: Mitchell Tracy Date: Tue, 13 May 2025 19:31:44 +0100 Subject: [PATCH 07/12] feat: add the slashing conditions we actually want. --- docs/custom-slashing/dd.md | 107 +++++++++++++++++++++++++------------ 1 file changed, 73 insertions(+), 34 deletions(-) diff --git a/docs/custom-slashing/dd.md b/docs/custom-slashing/dd.md index db2551a..eebde2e 100644 --- a/docs/custom-slashing/dd.md +++ b/docs/custom-slashing/dd.md @@ -21,11 +21,15 @@ So the L1 contracts currently allow arbitrary slashing as long as the motion has In practice, however, there are only mechanisms built to create and vote for payloads to slash all validators in an epoch if the epoch is never proven, namely an out-of-protocol `SlashFactory` contract, and corresponding logic on the node to utilize it. -We want to expand this `SlashFactory` to allow for "custom slashing", which would allow the node to programmatically create or vote for payloads to slash specific validators for specific amounts for specific offences. +We want to expand this `SlashFactory` to allow nodes to programmatically create and vote for payloads to slash specific validators for specific amounts for specific "verifiable offences". -In addition, we will add corresponding logic to the node to utilize the new `SlashFactory` to slash validators for inactivity. +Specifically, we will automatically slash in the following cases: -Last, we will add an override, which may be set by the node operator, which will configure the node to vote for a particular payload no matter what. +1. A block was proven, so slash all validators that did not attest to it. +2. An epoch was valid but was not proven, so slash each proposer. +3. A validator proposed a block that was invalid, so slash the validator. + +Last, we will add an override, which may be set by the node operator, which will configure the node to vote for a particular payload no matter what; this affords offline coordination to effect a slash. ## Requirements @@ -40,6 +44,7 @@ The requirements with filled checkboxes are met by the design below. - [x] Node operators SHOULD be able to configure their node to specify thresholds for what they consider "not participating". - [x] The "offence" that triggers the slash MAY be specified on L1. - [x] The amount to be slashed MAY be configurable without deploying a new factory contract. +- [x] The node MUST NOT trigger a slash unless it is certain that the validator was "faulty" (in its opinion). - [ ] The threshold of number of validators (M/N) that need to signal/vote for the CustomSlashFactory payload MAY be configurable without deploying a new contract or a governance action. ## L1 Changes @@ -102,8 +107,9 @@ The core function in the `SlashFactory` will look like: For now, the `offences` field will effectively be an enum, with the following possible values: - 0: unknown -- 1: epoch pruned -- 2: inactivity +- 1: proven block not attested to +- 2: unproven valid epoch +- 3: invalid block proposed The use of `uint256` for offences rather than an explicit enum allows for future flexibility, e.g. adding more offences and interpreting them off-chain, or, by using `uint256` rather than `uint8`, using a hash/commitment to some external data/proof. @@ -113,54 +119,83 @@ Aztec nodes will listen for these events, and then check if the validator commit ## Node Changes -The SlasherClient will remain the interface that the SequencerPublisher uses to adjust the transaction it sends to the forwarder contract. +Most of the work is in the node. + +### SlasherClient + +The SlasherClient will remain the interface that the SequencerPublisher uses to adjust the transaction it sends to the forwarder contract. That is, the SequencerPublisher will continue to call `SlasherClient.getSlashPayload` to get the address of the payload to signal/vote for. + +Its internal operations will be different, though. + +It will instantiate "Watchers", which will have the following responsibilities: -Internally, though, the SlasherClient will monitor two conditions: +- emit WANT_TO_SLASH events with the arguments to `createSlashPayload` +- expose a function which takes a validator address, amount, and offence and returns whether it agrees with the slash -- an epoch was not proven, so slash all validators (this exists today) -- a validator has missed X% (e.g. 90%) of attestations according to the Sentinel, so slash that validator (this is new) -- an override payload is set +The SlasherClient has the following responsibilities: + +- listen for WANT_TO_SLASH events and create a payload from the arguments +- listen for the payload to be created and insert it into a priority queue +- return the payload with the highest priority when `getSlashPayload` is called + +### Payload priority Validators will need to have a way to order the various slashing events they observe. -The Aztec client will use the following heuristics to determine which payload to signal/vote for: +Each time a new payload is observed on L1, the node will: -1. If there is an override payload, signal/vote for it. -2. Check if the payload is older than a configurable TTL. If so, discard it. -3. Sum the amounts of remaining slash proposals, so we have a `totalSlashAmount` for each payload. +1. Sum the amounts of new slash proposals, so we have a `totalSlashAmount` for each payload. +2. Filter the payloads to only include those that the Watchers agree with. +3. Insert the payload with metadata into a priority queue 4. Sort the payloads by `totalSlashAmount`, largest to smallest. -5. Pick the top payload that the validator agrees with (i.e. all the named validators committed the named offences), and signal/vote for it. -This process should be done each time: +Whenever `getSlashPayload` is called, the node will: + +1. Check if there is an override payload. If so, signal/vote for it. +2. Filter out payloads from the queue that are older than a configurable TTL. +3. Return the first payload in the queue. + +### Proven block not attested to + +The first slashing event to be implemented will be for the case where a validator did not attest to a proven block. -- a new payload is created, as determined by watching the `SlashFactory` contract events -- the proposer has an opportunity to signal/vote +This will be done by an `InactivityWatcher`, which will: -### Slashing for epoch pruning +- listen for L2 blocks `chain-proven` events emitted from the `L2BlockStream` +- for each slot, call `Sentinel.processSlot` to get a map of validators and whether they voted +- emit a `WANT_TO_SLASH` event for each validator that missed more than `SLASH_INACTIVITY_CREATE_TARGET` slots, slashing them for the amount specified in `SLASH_INACTIVITY_CREATE_PENALTY` -We will need to modify the logic around slashing for epoch pruning. Instead of specifying a particular epoch, we just specify all the validators within the epoch. +When asked, it will agree to slash any validator that missed more than `SLASH_INACTIVITY_SIGNAL_TARGET` slots. -### Slashing for inactivity +### A validator proposed an invalid block -Regarding the slash for "inactivity", nodes will vote to slash any validator that has missed Y% (e.g. 50%) of attestations according to the Sentinel; there are two percentages to be configured: +This requires that full nodes have the ability to re-execute blocks. -- the percentage of attestations missed required to create a payload -- the percentage of attestations missed required to signal/vote for the payload +Further, when executing a block, we will store invalid blocks in a cache, and emit an `invalid-block` event. -Node operators will configure their node to listen to a specific SlashFactory contract (as they do today) and specify the percentage of attestations missed required to create a payload. +A `InvalidBlockWatcher` will take an executor as an argument, subscribe to the `invalid-block` event, and then emit a `WANT_TO_SLASH` event naming the proposer of the invalid block, slashing them for the amount specified in `SLASH_INVALID_BLOCK_PENALTY`. -This will be done via CLI arguments or environment variables. +When asked, it will agree to slash any validator that proposed an invalid block which it sees in its cache of invalid blocks. + +### A valid epoch was not proven + +This requires that full nodes have the ability to re-execute blocks. + +A `ValidEpochUnprovenWatcher` will listen to `chain-pruned` events emitted by the `L2BlockStream`, and emit a `WANT_TO_SLASH` event for all validators that were in the epoch that was pruned IF there were no `invalid-block` events emitted for that epoch, slashing them for the amount specified in `SLASH_PRUNE_PENALTY`. + +When asked, it will agree to slash any validator that was in an epoch that was pruned and there were no `invalid-block` events emitted for that epoch. ### New configuration -- `SLASH_OVERRIDE_PAYLOAD`: the address of a payload to signal/vote for no matter what - `SLASH_PAYLOAD_TTL`: the maximum age of a payload to signal/vote for -- `SLASH_PRUNE_CREATE`: whether to create a payload for epoch pruning +- `SLASH_OVERRIDE_PAYLOAD`: the address of a payload to signal/vote for no matter what +- `SLASH_PRUNE_ENABLED`: whether to create a payload for epoch pruning - `SLASH_PRUNE_PENALTY`: the amount to slash each validator that was in an epoch that was pruned -- `SLASH_PRUNE_SIGNAL`: whether to signal/vote for a payload for epoch pruning - `SLASH_INACTIVITY_CREATE_TARGET`: the percentage of attestations missed required to create a payload -- `SLASH_INACTIVITY_CREATE_PENALTY`: the amount to slash each validator that is inactive - `SLASH_INACTIVITY_SIGNAL_TARGET`: the percentage of attestations missed required to signal/vote for the payload +- `SLASH_INACTIVITY_CREATE_PENALTY`: the amount to slash each validator that is inactive +- `SLASH_INVALID_BLOCK_ENABLED`: whether to signal/vote for a payload for invalid blocks +- `SLASH_INVALID_BLOCK_PENALTY`: the amount to slash each validator that proposed an invalid block ## Notes @@ -179,10 +214,14 @@ In the future, we could also allow the slasher itself or governance to change wh Outline the timeline for the project. E.g. - L1 contracts : 1 day -- Refactor SlasherClient : 1 day -- Review/Polish : 1-2 days - -Total: 3-4 days +- Refactor SlasherClient : 2 days +- Implement ProvenBlockNotAttestedWatcher : 1-2 days +- Implement Rexecute on Full Node : 2 days +- Implement InvalidBlockWatcher : 1-2 days +- Implement ValidEpochUnprovenWatcher : 1-2 days +- Review/Polish : 2 days + +Total: 10-12 days ## Disclaimer From bdf2bbb619b3b0dde1c2942dd5851879c7838373 Mon Sep 17 00:00:00 2001 From: Mitchell Tracy Date: Wed, 14 May 2025 08:28:31 +0100 Subject: [PATCH 08/12] chore: update slashing conditions --- docs/custom-slashing/dd.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/custom-slashing/dd.md b/docs/custom-slashing/dd.md index eebde2e..fe14240 100644 --- a/docs/custom-slashing/dd.md +++ b/docs/custom-slashing/dd.md @@ -25,9 +25,9 @@ We want to expand this `SlashFactory` to allow nodes to programmatically create Specifically, we will automatically slash in the following cases: -1. A block was proven, so slash all validators that did not attest to it. -2. An epoch was valid but was not proven, so slash each proposer. -3. A validator proposed a block that was invalid, so slash the validator. +1. (liveness) A block was proven, so slash all validators that did not attest to it. +2. (data availability and finality) An epoch was not proven and either i. the data is unavailable, or ii. the data is available and the epoch was valid, so slash each validator that was in the epoch's committee. +3. (safety) A validator proposed a block that was invalid, so slash the validator. Last, we will add an override, which may be set by the node operator, which will configure the node to vote for a particular payload no matter what; this affords offline coordination to effect a slash. From 74ad504394d1fc510f44af7a9644c71fa907c777 Mon Sep 17 00:00:00 2001 From: Mitchell Tracy Date: Wed, 14 May 2025 11:31:38 +0100 Subject: [PATCH 09/12] feat: more notes on full node re-execution --- docs/custom-slashing/dd.md | 73 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/docs/custom-slashing/dd.md b/docs/custom-slashing/dd.md index fe14240..3f9ec71 100644 --- a/docs/custom-slashing/dd.md +++ b/docs/custom-slashing/dd.md @@ -197,6 +197,79 @@ When asked, it will agree to slash any validator that was in an epoch that was p - `SLASH_INVALID_BLOCK_ENABLED`: whether to signal/vote for a payload for invalid blocks - `SLASH_INVALID_BLOCK_PENALTY`: the amount to slash each validator that proposed an invalid block +## Full Node Re-execution + +There are two primary points in time where a full node would want to re-execute blocks: + +1. When a block is proposed on the p2p network and is gathering attestations. +2. When a block been proposed to the L1 and is part of the pending chain. + +To slash all malicious validators, we only need to support the first case; we need to adjust the validator client to re-execute, even if the node is not in the committee for the block, and retain a cache of invalid blocks. + +To avoid slashing honest validators who built on a bad block which they blindly accepted/synced from the previous committee/L1, we need to support the second case. + +### A generic `BlockBuilder` + +We will build a new `BlockBuilder` class which is a component of the `AztecNode`. + +Its interface will be: + +```typescript +interface GlobalContext { + chainId: Fr; + version: Fr; + blockNumber: Fr; + slotNumber: Fr; + timestamp: Fr; + coinbase: EthAddress; + feeRecipient: AztecAddress; + gasFees: GasFees; +} + +interface BuiltBlockResult { + block: L2Block; + publicGas: Gas; + publicProcessorDuration: number; + numMsgs: number; + numTxs: number; + numFailedTxs: number; + blockBuildingTimer: Timer; + usedTxs: Tx[]; +} + +interface ExecutionOptions {} + +interface BlockBuilder { + gatherTransactions(txHashes: TxHash[]): Promise; + executeTransactions( + txs: Tx[], + globals: GlobalContext, + options: ExecutionOptions + ): Promise; +} +``` + +`executeTransactions` will execute transactions against the current world state and archiver. + +The caller then may compare the results against whatever they expect (e.g. the state roots a peer sent, or that they downloaded from L1). + +We will then update the archiver to optionally take a `BlockBuilder` as an argument, and use this to validate blocks coming in on L1. + +Further, the Sequencer and Validator clients will accept a `BlockBuilder` as an argument, which they will use to build/re-execute blocks. + +Thus we have: + +1. Block comes in on p2p + 1. validator is on committee + 1. block is good, broadcast attestation + 2. block is bad, add to invalid block cache + 2. validator is not on committee + 1. block is good, do nothing + 2. block is bad, add to invalid block cache +2. Block comes in on L1 + 1. block is good, add to state + 2. block is bad, add to invalid block cache + ## Notes The amount to slash should be high for testnet (e.g. the minimum stake amount). We can use a different amount for mainnet. From 271a278d29e847e30c58cdd9265da33838c832ed Mon Sep 17 00:00:00 2001 From: Mitchell Tracy Date: Wed, 14 May 2025 11:44:18 +0100 Subject: [PATCH 10/12] chore: a note on implementation order --- docs/custom-slashing/dd.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/custom-slashing/dd.md b/docs/custom-slashing/dd.md index 3f9ec71..f2734f4 100644 --- a/docs/custom-slashing/dd.md +++ b/docs/custom-slashing/dd.md @@ -296,6 +296,8 @@ Outline the timeline for the project. E.g. Total: 10-12 days +The intent is to first merge functionality to slash inactive validators, then do the broader refactor needed for the later two cases. + ## Disclaimer The information set out herein is for discussion purposes only and does not represent any binding indication or commitment by Aztec Labs and its employees to take any action whatsoever, including relating to the structure and/or any potential operation of the Aztec protocol or the protocol roadmap. In particular: (i) nothing in these projects, requests, or comments is intended to create any contractual or other form of legal relationship with Aztec Labs or third parties who engage with this AztecProtocol GitHub account (including, without limitation, by responding to a conversation or submitting comments) (ii) by engaging with any conversation or request, the relevant persons are consenting to Aztec Labs’ use and publication of such engagement and related information on an open-source basis (and agree that Aztec Labs will not treat such engagement and related information as confidential), and (iii) Aztec Labs is not under any duty to consider any or all engagements, and that consideration of such engagements and any decision to award grants or other rewards for any such engagement is entirely at Aztec Labs’ sole discretion. Please do not rely on any information on this account for any purpose - the development, release, and timing of any products, features, or functionality remains subject to change and is currently entirely hypothetical. Nothing on this account should be treated as an offer to sell any security or any other asset by Aztec Labs or its affiliates, and you should not rely on any content or comments for advice of any kind, including legal, investment, financial, tax, or other professional advice. From a95b05963ffa4f93bb7f26c34548c1901e18a480 Mon Sep 17 00:00:00 2001 From: Mitchell Tracy Date: Mon, 19 May 2025 19:39:13 -0400 Subject: [PATCH 11/12] updates based on comments --- docs/custom-slashing/dd.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/custom-slashing/dd.md b/docs/custom-slashing/dd.md index f2734f4..0c9659b 100644 --- a/docs/custom-slashing/dd.md +++ b/docs/custom-slashing/dd.md @@ -4,6 +4,7 @@ - Approvers: - @LHerskind - @Maddiaa0 + - @aminsammara - Target DD Approval Date: 2025-05-09 - Target Project Delivery Date: 2025-05-16 @@ -13,11 +14,11 @@ The `StakingLib` designates a "slasher" address which is able to slash arbitrary The contract used as the slasher is currently `Slasher`, which takes directives from a `SlashingProposer`. -The `SlashingProposer` is an instance of `EmpireBase`, which operates in "rounds" of `N` L2 slots, during which at least `M` proposers must vote for a specific contract address "payload" to be executed. +The `SlashingProposer` is an instance of `EmpireBase`, which operates in "rounds" of `M` L2 slots, during which at least `N` proposers must vote for a specific contract address "payload" to be executed. The payload just calls "slash", with the list of validators to slash, and the amount to slash each. -So the L1 contracts currently allow arbitrary slashing as long as the motion has support from `M/N` validators. +So the L1 contracts currently allow arbitrary slashing as long as the motion has support from `N/M` validators. In practice, however, there are only mechanisms built to create and vote for payloads to slash all validators in an epoch if the epoch is never proven, namely an out-of-protocol `SlashFactory` contract, and corresponding logic on the node to utilize it. @@ -45,7 +46,7 @@ The requirements with filled checkboxes are met by the design below. - [x] The "offence" that triggers the slash MAY be specified on L1. - [x] The amount to be slashed MAY be configurable without deploying a new factory contract. - [x] The node MUST NOT trigger a slash unless it is certain that the validator was "faulty" (in its opinion). -- [ ] The threshold of number of validators (M/N) that need to signal/vote for the CustomSlashFactory payload MAY be configurable without deploying a new contract or a governance action. +- [ ] The threshold of number of validators (N/M) that need to signal/vote for the CustomSlashFactory payload MAY be configurable without deploying a new contract or a governance action. ## L1 Changes From 1cab68678b300e4584228b342ecd94feffd2166b Mon Sep 17 00:00:00 2001 From: Mitchell Tracy Date: Mon, 19 May 2025 19:41:29 -0400 Subject: [PATCH 12/12] updates based on comments --- docs/custom-slashing/dd.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/custom-slashing/dd.md b/docs/custom-slashing/dd.md index 0c9659b..aac09d8 100644 --- a/docs/custom-slashing/dd.md +++ b/docs/custom-slashing/dd.md @@ -166,7 +166,7 @@ This will be done by an `InactivityWatcher`, which will: - for each slot, call `Sentinel.processSlot` to get a map of validators and whether they voted - emit a `WANT_TO_SLASH` event for each validator that missed more than `SLASH_INACTIVITY_CREATE_TARGET` slots, slashing them for the amount specified in `SLASH_INACTIVITY_CREATE_PENALTY` -When asked, it will agree to slash any validator that missed more than `SLASH_INACTIVITY_SIGNAL_TARGET` slots. +When asked, it will agree to slash any validator that missed more than `SLASH_INACTIVITY_SIGNAL_TARGET` slots, so long as the amount is less than `SLASH_INACTIVITY_MAX_PENALTY`. ### A validator proposed an invalid block @@ -176,7 +176,7 @@ Further, when executing a block, we will store invalid blocks in a cache, and em A `InvalidBlockWatcher` will take an executor as an argument, subscribe to the `invalid-block` event, and then emit a `WANT_TO_SLASH` event naming the proposer of the invalid block, slashing them for the amount specified in `SLASH_INVALID_BLOCK_PENALTY`. -When asked, it will agree to slash any validator that proposed an invalid block which it sees in its cache of invalid blocks. +When asked, it will agree to slash any validator that proposed an invalid block which it sees in its cache of invalid blocks, as long as the amount is less than `SLASH_INVALID_BLOCK_MAX_PENALTY`. ### A valid epoch was not proven @@ -184,7 +184,7 @@ This requires that full nodes have the ability to re-execute blocks. A `ValidEpochUnprovenWatcher` will listen to `chain-pruned` events emitted by the `L2BlockStream`, and emit a `WANT_TO_SLASH` event for all validators that were in the epoch that was pruned IF there were no `invalid-block` events emitted for that epoch, slashing them for the amount specified in `SLASH_PRUNE_PENALTY`. -When asked, it will agree to slash any validator that was in an epoch that was pruned and there were no `invalid-block` events emitted for that epoch. +When asked, it will agree to slash any validator that was in an epoch that was pruned and there were no `invalid-block` events emitted for that epoch, as long as the amount is less than `SLASH_PRUNE_MAX_PENALTY`. ### New configuration @@ -192,11 +192,14 @@ When asked, it will agree to slash any validator that was in an epoch that was p - `SLASH_OVERRIDE_PAYLOAD`: the address of a payload to signal/vote for no matter what - `SLASH_PRUNE_ENABLED`: whether to create a payload for epoch pruning - `SLASH_PRUNE_PENALTY`: the amount to slash each validator that was in an epoch that was pruned +- `SLASH_PRUNE_MAX_PENALTY`: the maximum amount to slash each validator that was in an epoch that was pruned +- `SLASH_INACTIVITY_ENABLED`: whether to signal/vote for a payload for inactivity - `SLASH_INACTIVITY_CREATE_TARGET`: the percentage of attestations missed required to create a payload - `SLASH_INACTIVITY_SIGNAL_TARGET`: the percentage of attestations missed required to signal/vote for the payload - `SLASH_INACTIVITY_CREATE_PENALTY`: the amount to slash each validator that is inactive - `SLASH_INVALID_BLOCK_ENABLED`: whether to signal/vote for a payload for invalid blocks - `SLASH_INVALID_BLOCK_PENALTY`: the amount to slash each validator that proposed an invalid block +- `SLASH_INVALID_BLOCK_MAX_PENALTY`: the maximum amount to slash each validator that proposed an invalid block ## Full Node Re-execution