An agent-based model of Ethereum's Proof-of-Stake consensus layer.
Notebooks using the current beaconrunner
library.
05. Altair incentives + source
Altair is a planned upgrade to the PoS consensus layer. We check that Altair incentives provide the expected rewards per validator by simulating the protocol for a few epochs.
The beacon runner was built iteratively over several notebooks. Early notebooks use early iterations of the Beacon Runner codebase and will not function with the current code contained in this package. These early notebooks however provide background to PoS Ethereum concepts and to the general approach of our simulations. The code necessary to run them is provided in the source links.
This notebook introduces basic Ethereum PoS concepts and provides a "centralised client" implementation. We introduce the main duties of validators in PoS: producing blocks and attesting. In this implementation, the centralised client is the only one adding blocks to the beacon chain and attesting, thus it also has perfect view of the chain. This allows us to focus on the interplay between state (the state of the beacon chain) and policies (the duties performed by the centralised client).
The centralised client of the previous notebook was the only agent producing blocks and attestations. In this notebook, we introduce validators distributed over a peer-to-peer network, who exchange the blocks and attestations they produce. We assume the network is split in half, such that neither half is able to finalise the state of the beacon chain, focusing on the cryptoeconomic mechanism that allows finalisation to resume. Our implementation is still somewhat centralised, in the sense that all validators in the same half of the network have the same view of the chain (albeit a different view from the other half's).
We fully decentralise the model of the previous notebook by allowing each validator to have its own view of the chain. Additionally, we provide an interface to model the behaviour of validators, using a simple API. In this notebook, we implement honest validation and observe the progress of the chain.
We show that honest, protocol-following agents sometimes perform worse than agents who behave more prudently. This is the case when latency is bad enough that agents hedge their bets before taking action. Agents are modelled with the beacon runner validator API and simulated.
You can simply run the following commands in a terminal.
git clone https://github.com/ethereum/beaconrunner.git
cd beaconrunner
###
# Optional: use a virtual environment
# Python 3.8 is required
python3.8 -m venv venv
# Unix
source venv/bin/activate
# Windows
.\venv\Scripts\activate
###
pip install -r requirements.txt
Once you enter the shell, you can type in
jupyter lab
The architecture is layered to make it simpler to extend individual layers. The tl;dr is that validatorlib
is an "intelligent" wrap of the specs
, network
instantiates validators from validatorlib
to place them on its network and the simulator
helps move the simulation along, by specifying simulation transitions.
We take the eth2.0 specs as our ground truth. We can export the specs to a python file using custom presets and configuration (mostly turning down the size of some of the data structures).
Although our principle is to take the code as is and only "wrap" it around in an execution environment, we have brought the following changes in specs.py
:
-
Turn off crypto operations for performance improvement:
- Set
bls.bls_active = False
- Turn off
is_valid_indexed_attestation
everywhere - Turn off
is_valid_merkle_branch
inprocess_deposit
- Set
-
Modify
on_block
to use store:- Add
state: BeaconState = None
to the arguments. - Add the following code block in the body:
in place of
# Check the block is valid and compute the post-state if state is None: state = state_transition(pre_state, signed_block, True) else: process_block(state, signed_block.message)
# Check the block is valid and compute the post-state state = state_transition(pre_state, signed_block, True)
- Add
Validator interface validatorlib
The specs obtained are mostly responsible for three tasks:
- Defining the state transition (
beacon-chain.md
). This is where rewards and penalties are applied and where blocks are processed to apply the state transition. - Defining the honest validator behaviour (
validator.md
). In particular, this holds useful functions such asget_committee_assignment
, which we can use in our interface to feed data to the validator. - Defining the fork choice rule (
fork-choice.md
). Validators have knowledge of existing blocks and attestations, some of which are already included in the beacon chain, some of which only broadcasted on the p2p network. Based on this knowledge, validators decide which is the correct head of the chain. We use theStore
object defined in thefork-choice.md
file to hold this knowledge.
In the beaconrunner
library, the validatorlib.py
file is the interface between our validator behaviours (held in beaconrunner/validators
) and the environment.
All validators inherit from BRValidator
, defined in validatorlib.py
. This class has a store
attribute, from which validators obtain the current head of the beacon chain, as well as a data
attribute, which we use to store current validation information, such as their assignment to committees.
The main role of this class is to provide up-to-date data to validator. For instance, in update_data
, the validator reacts to a new event:
- Either a new block or a new attestation was received
- Or a new slot is reached
In both cases, this warrants an update of the validator's data. When a new block is received, either that block builds on the current head, or it is a fork of the existing chain. In the latter case, this sometimes means that the validator assignment to committees has changed. We check whether these conditions are satisfied in update_data
and call for a refresh of the get_committee_assignment
.
BRValidator
also logs the history of play of validators, so that validators can refer back to it when performing their duties ("have I already attested in this slot? history says yes...")
We also define honest_attest
and honest_propose
, which can be used by validators to build their behaviours (see Thunderdome for an example of how they are used).
Validator behaviours beaconrunner/validators
Validator behaviours (e.g., ASAPValidator) follow a simple API.
def <duty>(self, known_items)
By calling self.data
, a validator has access to its "environment knowledge": did they perform the duty already? how far into the slot are they? which is the current epoch? are they supposed to perform the duty?
We follow the principle that a validator should not introspect anything else than this data
attribute, in particular, not its store
. Whenever a validator needs access to additional info, a "translation" should be made in BRValidator
to have this info accessible in self.data
(for instance, we don't want validator behaviours to call get_committee_assignment
, they should be able to access up-to-date info in their data
attribute instead). This makes for a cleaner separation as well as better performance, as we can cache results of expensive operations.
known_items
is fed from the network
, these are the blocks and attestations on the p2p network that the validator knows about. It is used in honest_propose
for instance, where a validator takes latest attestations and packages them in a block.
The network object is populated with subclasses of BRValidator
which define the behaviour of various types of validators (ASAP
and Prudent
behaviours are given in the current library). The network is responsible for propagating data over a simulated p2p network.
Whenever a validator produces an item (a block, an attestation), it is sent on the network and disseminated over time to the other validators.
This class is responsible for implementing the simulation proper. It defines the steps in the simulate
method:
tick
: move the environment "clock" by one step. We can the frequency is a parameter, with higher frequency => smaller time steps => more simulation steps.attest_policy
: responsible for "pinging" the validators for their attestations.update_attestations
: send the obtained attestations over the network.propose_policy
: responsible for "pinging" the validators for their block.update_blocks
: send the obtained blocks over the network.
Most of the time, validators do not have anything to attest or propose, so the simulation proceeds.
Some documentation is available here.