Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial draft of fee recipient rules RPIP #173

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions RPIPs/rpip-fee_recipient_rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
rpip: #<to be assigned>
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/).
172 changes: 172 additions & 0 deletions assets/rpip-fee_recipient_rules/fee-recipient-spec.md
Original file line number Diff line number Diff line change
@@ -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)
```
94 changes: 94 additions & 0 deletions assets/rpip-fee_recipient_rules/penalty-system.md
Original file line number Diff line number Diff line change
@@ -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.