From 0fb16ab952c0538f3f0be2b5e0f0057eb5b7e463 Mon Sep 17 00:00:00 2001 From: Ramana Kumar Date: Wed, 10 Apr 2024 07:35:32 +0100 Subject: [PATCH] Add initial draft of fee recipient rules RPIP --- RPIPs/rpip-fee_recipient_rules.md | 39 ++++ .../fee-recipient-spec.md | 172 ++++++++++++++++++ .../penalty-system.md | 94 ++++++++++ 3 files changed, 305 insertions(+) create mode 100644 RPIPs/rpip-fee_recipient_rules.md create mode 100644 assets/rpip-fee_recipient_rules/fee-recipient-spec.md create mode 100644 assets/rpip-fee_recipient_rules/penalty-system.md diff --git a/RPIPs/rpip-fee_recipient_rules.md b/RPIPs/rpip-fee_recipient_rules.md new file mode 100644 index 00000000..01617626 --- /dev/null +++ b/RPIPs/rpip-fee_recipient_rules.md @@ -0,0 +1,39 @@ +--- +rpip: # +title: Current Fee Recipient Rules +description: Specifies the rules for setting the fee recipient on proposed blocks and the penalties for breaking them +author: Ramana Kumar (@xrchz), Joe Clapis (@jcrtp) +discussions-to: https://github.com/rocket-pool/rocketpool-research/issues +status: Draft +type: Informational +created: 2024-04-14 +--- + +## Abstract +This RPIP describes the current state of the specification of Rocket Pool's fee recipient rules and proposed penalty system. +Node operators are supposed to follow these rules when setting the fee recipient (or providing a fee recipient to a MEV relay) on any blocks they propose. +A penalty system to make it costly to break the rules has been proposed but is not currently implemented. + +## Motivation +Rocket Pool's expectations of node operators needs a clear specification so that node operators know what they ought to do and what to expect if they do not follow the rules. + +The rules regarding fee recipients and the proposed penalty system have been developed in the external [Rocket Pool Research](https://github.com/rocket-pool/rocketpool-research/tree/master/Merkle%20Rewards%20System) repository. However, as with [RPIP-51](./RPIP-51.md), these rules are a core protocol specification whose proper home is in an RPIP where they can be easily found and referred to with a clear status as an official definition of (part of) the Rocket Pool protocol. This RPIP is an informational document describing the status quo, with the intention of following up with a future pDAO-ratified RPIP (which may or may not include changes). + +## Specification +There are three parts to the fee recipient rules and penalty system, only two of which at present have extant (draft) specifications. +1. The fee recipient a Rocket Pool node operator SHOULD use when they propose a block. +2. The fee recipient a Rocket Pool node operator did use when they proposed a block. +3. The penalties applied when the actual fee recipient differs from the expected fee recipient. + +Relevant drafts from the external [research repository](https://github.com/rocket-pool/rocketpool-research) are attached to this RPIP as assets: +1. [Expected Fee Recipient Calculation](../assets/rpip-fee_recipient_rules/fee-recipient-spec.md) +2. No specification for this section of the rules currently exists. +3. [Penalty System (draft)](../assets/rpip-fee_recipient_rules/penalty-system.md) + +## Rationale +This specification describes the material available to Rocket Pool node operators as of April 2024. +Since the penalty system is not being used, it is purely informational. +The intention here is to capture the status quo and highlight the need for further work to fill out and implement the penalty system. + +## Copyright +Copyright and related rights waived via [CC0](https://creativecommons.org/publicdomain/zero/1.0/). diff --git a/assets/rpip-fee_recipient_rules/fee-recipient-spec.md b/assets/rpip-fee_recipient_rules/fee-recipient-spec.md new file mode 100644 index 00000000..5d838534 --- /dev/null +++ b/assets/rpip-fee_recipient_rules/fee-recipient-spec.md @@ -0,0 +1,172 @@ +# Determining a Node Operator's Fee Recipient via On-Chain Data + +The expected fee recipient for any Rocket Pool validator (a "minipool") can be completely derived via on-chain data. +This is the specification for determining what a node operator's (and by extension, their minipools') fee recipient should be based on the current state of the Beacon and Execution chains. +Block builder relays can use this to detect if a validator corresponds to a Rocket Pool minipool and derive its proper fee recipient address instead of using whatever fee recipient is provided during registration. + +This has two primary benefits: +- It prevents penalties on the node operator due to accidental misconfiguration +- It prevents intentional misconfiguration and theft of rewards ("cheating") + +Essentially, the process for fee recipient derivation is broken into the following steps: +- Determine if the validator corresponds to a Rocket Pool minipool +- If so, retrieve the node address that owns the minipool +- Check if the node is opted into the Smoothing Pool + - If so, use the Smoothing Pool contract address + - If not, check if the node left the Smoothing Pool less than 2 epochs ago + - If so, use the Smoothing Pool contract address (to prevent front-running exploits) + - If not, retrieve and use the node's fee distributor contract address + +Each of these steps is described in more detail below. + + +## Prerequisites + +To perform this check, you will need: +- A synced Execution Client with RPC access +- A synced Consensus Client (Beacon Node) with standard Beacon API access + + +## Acquiring the Relevant Contract Addresses and ABIs + +### RocketStorage + +This process begins by retrieving the `RocketStorage` contract for the relevant network. +`RocketStorage` is effectively the "base" contract which contains addresses and variables for the rest of the protocol; everything can be retrieved from it. + +The `RocketStorage` addresses for various networks are listed below: +- Mainnet: `0x1d8f8f00cfa6758d7bE78336684788Fb0ee0Fa46` ([Etherscan source + ABI](https://etherscan.io/address/0x1d8f8f00cfa6758d7bE78336684788Fb0ee0Fa46#code)) +- Goerli-Prater: `0xd8Cd47263414aFEca62d6e2a3917d6600abDceB3` ([Etherscan source + ABI](https://goerli.etherscan.io/address/0xd8Cd47263414aFEca62d6e2a3917d6600abDceB3#code)) + + +### Network Contract Addresses + +You can acquire the **address** of a network contract as follows: +``` +address := rocketStorage.getAddress(Keccak256("contract.address" + contractName)) +``` + +For example, the `rocketMinipoolManager` contract address would be retrieved via: +``` +rocketMinipoolManagerAddress := rocketStorage.getAddress(Keccak256("contract.addressrocketMinipoolManager")) +``` + + +### Network Contract ABIs + +You can acquire the **ABI** of a network contract as follows: +``` +abiString := rocketStorage.getString(Keccak256("contract.abi" + contractName)) +``` + +For example, the `rocketMinipoolManager` contract ABI would be retrieved via: +``` +rocketMinipoolManagerAbiString := rocketStorage.getString(Keccak256("contract.abirocketMinipoolManager")) +``` + +The resulting string is **zlib compressed** and **encoded via base64**. +You must decode it in order to retreive the original JSON describing the contract's ABI. + + + +## Required Contracts + +You will need to acquire the following contracts (both addresses and ABIs unless otherwise specified): + +- `rocketMinipoolManager` +- `rocketMinipool` (ABI only) +- `rocketNodeManager` +- `rocketSmoothingPool` +- `rocketNodeDistributorFactory` + + +## Determining if a Validator is a Rocket Pool Minipool + +To check if a validator is part of the Rocket Pool network, use the `rocketMinipoolManager.getMinipoolByPubkey(pubkey)` function by providing the validator's pubkey as the only argument. + +For example, to check validator [`0x9904d13c62f33b9b2c17ed0991e56c8e0337f632bbac1b789d282c228b20b93016d3a43d0c86788e8982c7d8c96b6a3f`](https://beaconcha.in/validator/0x9904d13c62f33b9b2c17ed0991e56c8e0337f632bbac1b789d282c228b20b93016d3a43d0c86788e8982c7d8c96b6a3f): + +``` +minipoolAddress := rocketMinipoolManager.getMinipoolByPubkey(0x9904d13c62f33b9b2c17ed0991e56c8e0337f632bbac1b789d282c228b20b93016d3a43d0c86788e8982c7d8c96b6a3f) +``` + +*Note that this function expects a byte array as the argument, not a string.* + +If this returns `0x0000000000000000000000000000000000000000`, then the validator is **not** a Rocket Pool minipool. + +If it returns any other valid address, it **is** a Rocket Pool minipool. +The address provided is the contract address of the minipool that corresponds to the provided validator. + + +## Retrieving the Node Address of a Minipool's Owner + +Acquire the ABI of the `rocketMinipool` contract, and construct a new contract binding at the `minipoolAddress` retrieved in the previous step. + +Call this function to retrieve the address of the owning node: + +``` +nodeAddress := rocketMinipool.getNodeAddress() +``` + + +## Checking if a Node Operator is in the Smoothing Pool + +Call the following function to check a node's Smoothing Pool status: + +``` +isOptedIn := rocketNodeManager.getSmoothingPoolRegistrationState(nodeAddress) +``` + +This will be `true` if they are opted in, or `false` if they are not. + +If this returns `true`, then **the fee recipient should be the address of the `rocketSmoothingPool` contract**. + + +## Checking the Opt-Out Time to Prevent Front-Running + +If the above returns `false`, you must verify that the user **didn't opt-out immediately upon seeing an upcoming proposal in the current or next epoch** - this is considered to be a front-running attack against the protocol. +Rocket Pool's Smartnode software is designed to prevent users from doing this unless they intentionally exploit it, in which case your relay can enforce the correct fee recipient and circumvent the attack. + +This is done by checking the **opt-out time**, deriving the **Epoch** on the Beacon chain at the time of the opt-out, and retaining the Smoothing Pool contract address as the fee recipient until **the Epoch after the opt-out Epoch has passed**. + +Check the opt-out time as follows: + +``` +optOutTime := rocketNodeManager.getSmoothingPoolRegistrationChanged(nodeAddress) +``` + +This value will be a Unix timestamp, in seconds. + +If the value is `0`, the node never opted into the Smoothing Pool. +**The fee recipient should be the address of the node's fee distributor contract** (see below for details). + +If the value is not `0`, the node opted into, and then opted out of, the Smoothing Pool. +It must be checked for this attack. + +Derive the Epoch number at the opt-out time by using the standard Beacon API for [the Genesis information](https://ethereum.github.io/beacon-APIs/#/Beacon/getGenesis) and [the Beacon config](https://ethereum.github.io/beacon-APIs/#/Config/getSpec): + +``` +secondsSinceGenesis := optOutTime - genesis.data.genesis_time +optOutEpoch := secondsSinceGenesis / (beaconConfig.data.SECONDS_PER_SLOT * beaconConfig.data.SLOTS_PER_EPOCH) +``` + +Get the current Epoch from [the standard Beacon API](https://ethereum.github.io/beacon-APIs/#/Beacon/getBlockHeader) for the `head` state: + +``` +currentEpoch := header.data.header.message.slot / beaconConfig.data.SLOTS_PER_EPOCH +``` + +If `currentEpoch` is greater than `optOutEpoch + 1`, then **the fee recipient should be the address of the node's fee distributor contract** (see below for details). + +If `currentEpoch` is less than or equal to `optOutEpoch + 1`, then **the fee recipient should be the address of the `rocketSmoothingPool` contract**. + + +## Getting the Node's Fee Distributor Contract Address + +If the node has never opted into the Smoothing Pool, or has opted out and has passed the front-running cooldown window, then their fee recipient should be the node's unique fee distributor contract. + +The fee distributor contract address can be retrieved as follows: + +``` +feeDistributorAddress := rocketNodeDistributorFactory.getProxyAddress(nodeAddress) +``` diff --git a/assets/rpip-fee_recipient_rules/penalty-system.md b/assets/rpip-fee_recipient_rules/penalty-system.md new file mode 100644 index 00000000..fc694493 --- /dev/null +++ b/assets/rpip-fee_recipient_rules/penalty-system.md @@ -0,0 +1,94 @@ +# [Draft] Specification for the Penalty System + +**This document is still a DRAFT and is subject to change until the release of Rocket Pool Redstone on Mainnet.** + +## Background + +Rocket Pool is a trustless, permissionless protocol. +Anyone can join as a node operator as long as they put their fair share of collateral up for each validator. + +When the protocol was originally conceived, the understanding was that all of a validator's rewards would be provided to its address on the Beacon Chain. +All of those rewards would ultimately be transferred to the validator's withdrawal credentials (in our case, the minipool contract on the Execution layer). +As the minipool contract enforces the fair reception and distribution of those rewards, there was no special action necessary to assess said distribution. + +However, during the middle of 2021, it became clear that this was not going to be the case. + +The advent of the "Quick Merge" indicated that a validator's rewards would actually be split into two categories, and live on two separate chains: + +- Attestation rewards are provided on the Beacon Chain +- Block Proposal rewards are provided on the Beacon Chain +- Sync committee rewards are provided on the Beacon Chain + +- Priority fees (transaction tips given during block proposals) are provided **to an address of the Node Operator's choosing on the Execution Layer** +- MEV extraction is provided **to an address of the Node Operator's choosing on the Execution Layer** + +The complication here is that priority fees and MEV are both sent to an arbitrary Execution layer address that is completely up to the discretion of the Node Operator. +Neither the Rocket Pool protocol, nor the general Ethereum core protocol, have a way to enforce its assignment while permitting trustless, permissionless node operation. + +As documented extensively in [our attempt to (partially) resolve this issue](https://github.com/ethereum/consensus-specs/pull/2454), the issue here is that a malicious Node Operator would be able to adjust this address to one that they control, thus pocketing the priority fees and MEV without needing to share them amongst the staking pool. + +It became clear during this research period that the core protocol was evolving in such a way that it would not be able to enforce the fair sharing of these rewards, and thus Rocket Pool required an additional layer on top of the core protocol that could satisfy this need. +From this requirement, the **Penalty System** was born. + + +## The Penalty System + +The penalty system is a function built into the current minipool contract for all of Rocket Pool's minipools. +The rules it follows are simple: + +- Each minipool has its own **penalty counter**. This is a `uint256` value stored in the `RocketStorage` contract that tracks the number of penalties applied to that minipool. + - The value can be retrieved via this contract function: + ```go + penaltyCount := RocketStorage.getUint(keccak256("network.penalties.penalty", minipoolAddress)) + ``` +- When a malicious condition is detected by the Oracle DAO, the Oracle DAO can vote to issue that minipool a **penalty**. This will increase the penalty counter on that minipool by 1. + - A voting quorum of 51% of the Oracle DAO members is required. + - The malicious conditions mentioned are discussed in a subsequent section. +- The **first two penalties** are known as **strikes**. These apply as "warnings", indicating that a malicious condition was detected but no actual penalty has been applied to the minipool. The intent is to be lenient on node operators who accidentally misconfigured their system, and prompt them to correct it without any action being taken. +- The **third penalty and onward** are known as **infractions**. Each infraction will result in **10% of the node operator's share of the Beacon chain balance** distributed by the Ethereum protocol (either partially, during skimming, or fully, during a validator exit) to be rerouted to the staking pool instead. + - This applies to both the rewards **and the initial 16 ETH deposit**. + - The maximum penalty that can be levied on a minipool is **80%**, which still leaves some incentive to exit the validator. + +For example: + - If a node operator exits with one infraction, they will receive 90% of their expected ETH from the Beacon chain. + - If a node operator exits with five infractions, they will receive 50% of their expected ETH. + - If a node operator exits with eight or more infractions, they will receive 20% of their expected ETH. + + +## Penalizable Conditions + +The following is a list of conditions the Oracle DAO can detect to issue a penalty on a minipool. + +**NOTE: for all of the below conditions, the address `0x000...000` is *not* a legal address, because the proposal resulted in the loss of ETH for the pool stakers. Proposals with a fee recipient of this address will be assigned a penalty.** + + +### Users that Never Joined the Smoothing Pool + +If the user *never* joined the Smoothing Pool, they can have the following **legal** fee recipients: +- The rETH contract address +- The Smoothing Pool contract address +- The node's **fee distributor** contract address + +If the minipool proposes a block with a fee recipient other than *any one of these three addresses*, the minipool will be issued a penalty. + + +### Users that are Actively Enrolled the Smoothing Pool + +If the user is **opted into** the Smoothing Pool at the time of the block proposal, they can have the following **legal** fee recipients: +- The Smoothing Pool contract address + +If the minipool proposes a block with any fee recipient *other than the Smoothing Pool address*, the minipool will be issued a penalty. + + +### Users that Recently Unenrolled from the Smoothing Pool + +If a user was previously opted into the Smoothing Pool and opts out, their fee recipient **must remain as the Smoothing Pool address** until the Epoch *after* they opted out has been **finalized**. + +For example: + +- Bob opts into the Smoothing Pool on slot 30 (Epoch 0). +- Some time later, Bob opts out of the Smoothing Pool on slot 32005 (Epoch 1000). +- Bob queries his Beacon node, and receives confirmation that Epoch **1001** (1 after the Epoch he opted out in) was finalized by slot 32064 (Epoch 1002). After slot 32064, Bob can now safely change his fee recipient to his node's fee distributor contract address. + +The rationale here is to prevent people from noticing they have an upcoming proposal assignment and intentionally front-running it with an opt-out transaction so they collect the priority fees and MEV for themselves. +As proposal assignments are only fully decided the Epoch **before** the one in which the proposal is due, this "cooldown" period lasts until the Epoch after the opt-out transaction has been finalized to ensure the user could not have taken advantage of this lookahead functionality. \ No newline at end of file