Ethereum 2.0 Prysm Beacon Node

The beacon node shipped with Prysm is a key component of the Ethereum 2.0 protocol which powers the network. It implements in full the official Ethereum 2.0 Serenity Specification for Phase 0. The main goal of a beacon node is to run a full proof of stake blockchain known as a beacon chain which uses distributed consensus to agree on blocks produces and voted on by a set of actors known as validators. Validators have two responsibilities: propose blocks known as beacon blocks which contain consensus information about shards across the network, or vote on beacon blocks. A vote on Ethereum 2.0 is known as an attestation.

Validators lock up a 32 ETH deposit to join and are rewarded for correctly proposing (producing) or attesting to blocks in the beacon chain. If validators act against the protocol, their locked up deposit will be cut in a process known as slashing. Validators that are intermittently offline or do not have reliable uptime will gradually lose their deposit, eventually leaking enough until they can be kicked out of the network for being idle. The Prysm beacon node handles and implements the functionality above through a series of services, which are common runtime features that perform unique responsibilities and can communicate between each other for information and resources.

Nodes communicate their processed blocks to their peers via a peer-to-peer network and handle the lifecycle of connected validator clients. Our beacon node contains all of the following services during its runtime:

What Are the Different Parts of a Prysm Beacon Node?

  • An ETH 1.0 service that listens to latest events and logs from the validator deposit contract and the ETH 1.0 blockchain

  • A public RPC server for anyone to request information about the beacon chain's state, latest block, validator information, and more

  • A sync service which queries nodes across the network to ensure it is synced with the latest canonical head and state and which processes incoming new block announcements from peers

  • A p2p server which handles the lifecycle of connections to peers and provides the ability to broadcast information across a network

  • A blockchain service which processes incoming blocks from the network, advances the beacon chain's state, and applies a fork choice rule to select the best head block

  • An operations service which handles information contained in beacon blocks received from peers such as block deposits, attestations, voluntary validator exits, and prepares them for inclusion into blocks by validators

  • A full test suite for running simulation on Ethereum 2.0 state transitions, benchmarks, and conformity tests across clients

  • A core package containing a full implementation of the Ethereum 2.0 official specification core functions, utilities, and state transitions required for conformity with the protocol

We isolate each of these services into its own package, and each service is responsible for its own lifecycle, logging, and dependency management. A Prysm service implements the following interface, proving a helpful way to start, stop, and verify its status at any time:

type Service interface {
// Start spawns any goroutines required by the service.
Start()
// Stop terminates all goroutines belonging to the service,
// blocking until they are all terminated.
Stop() error
// Returns error if the service is not considered healthy.
Status() error
}

We register services in order of importance and in order of fewer dependencies. That is, if a service depends on the RPC service, it should be registered and started after the RPC service is.

The ETH 1.0 Service

Our Eth1 service uses the go-ethereum ethclient to connect to a running Ethereum 1.0 node and listen for incoming logs on the Validator Deposit Contract. Our eth1 service kicks off a goroutine which listens to all deposit logs and stores them in a persistent key-value store. Validators include deposit objects inside of their proposed blocks, and the beacon chain state transition function then activates any pending validators from these deposits. We always ensure we are synchronized with the eth1 chain:

// ProcessLog is the main method which handles the processing of all
// logs from the deposit contract on the ETH1.0 chain.
func (w *Web3Service) ProcessLog(depositLog gethTypes.Log) {
// Process logs according to their event signature.
if depositLog.Topics[0] == hashutil.Hash(depositEventSignature) {
w.ProcessDepositLog(depositLog)
return
}
if depositLog.Topics[0] == hashutil.Hash(chainStartEventSignature) && !w.chainStarted {
w.ProcessChainStartLog(depositLog)
return
}
log.Debugf("Log is not of a valid event signature %#x", depositLog.Topics[0])
}

Our eth1 service also includes the ability to cache received logs and blocks from the Ethereum 1.0 chain, as the rest our beacon node will be frequently accessing information and we do not want to rely on the Ethereum 1.0 node we connect to to have perfectly reliable uptime.

The Public Beacon Node RPC Service

A very important part of our beacon node is its public RPC server. It implements a variety of methods that validators connected to the node can query for to obtain assignments, propose beacon blocks, attest to blocks, and more. We use the popular gRPC framework by Google to allow us to create easy API servers and clients with robust tests and definitions. We simply define our APIs in a protobuf format file as follows:

service BeaconService {
rpc WaitForChainStart(google.protobuf.Empty) returns (stream ChainStartResponse);
rpc CanonicalHead(google.protobuf.Empty) returns (ethereum.beacon.p2p.v1.BeaconBlock);
rpc PendingDeposits(google.protobuf.Empty) returns (PendingDepositsResponse);
rpc Eth1Data(google.protobuf.Empty) returns (Eth1DataResponse);
rpc ForkData(google.protobuf.Empty) returns (ethereum.beacon.p2p.v1.Fork);
}

This definition makes it easy for us to implement the methods outlined above as:

// CanonicalHead of the current beacon chain. This method is requested on-demand
// by a validator when it is their time to propose or attest.
func (bs *BeaconServer) CanonicalHead(ctx context.Context, req *ptypes.Empty) (*pbp2p.BeaconBlock, error) {
block, err := bs.beaconDB.ChainHead()
if err != nil {
return nil, fmt.Errorf("could not get canonical head block: %v", err)
}
return block, nil
}

Any client that implements the client side of these methods can connect via gRPC to our beacon node and begin requesting data from its public endpoints. We also envision this information could be useful for third parties for metrics gathering and other interesting applications once Ethereum 2.0 is on mainnet.

Our Core Package

Our core package implements the Ethereum 2.0 state transition function and all core helpers and utilities of the official specification:

func ExecuteStateTransition(
ctx context.Context,
state *pb.BeaconState,
block *pb.BeaconBlock,
headRoot [32]byte,
config *TransitionConfig,
) (*pb.BeaconState, error) {
var err error
// Execute per slot transition.
state = ProcessSlot(ctx, state, headRoot)
// Execute per block transition.
if block != nil {
state, err = ProcessBlock(ctx, state, block, config)
if err != nil {
return nil, fmt.Errorf("could not process block: %v", err)
}
}
// Execute per epoch transition.
if e.CanProcessEpoch(state) {
state, err = ProcessEpoch(ctx, state, config)
}
if err != nil {
return nil, fmt.Errorf("could not process epoch: %v", err)
}
return state, nil
}

Every function dealing with block processing, epoch processing, validator shuffling, finality, and more are all defined within our core package. We aim to keep this package as free of outside code as possible, and it is comprised of mostly pure functions which do not require access to the other services across Prysm to function. It is designed to be a near-identical translation of the official specification.

The Blockchain Service

Our blockchain service, also known as the ChainService within Prysm, serves to handle the lifecycle of received blocks and applies a fork choice rule along with a state transition function to advance the beacon chain. It is arguably the most important part of the Prysm project, as it allows the network to reach consensus on the state of the protocol itself. The beacon chain, however, will not start until a valid deposit threshold is reached in the Validator Deposit Contract on Eth 1.0. Instead, our chain service just listens for this ChainStart event to occur, initializes a genesis state and block, and begins listening for incoming blocks into the node from validators. When a block is received and passes basic integrity checking, the chain service runs a state transition and applies the Ethereum 2.0 fork-choice rule, known as Latest-Message-Driven GHOST (Greediest, Heaviest, Observed Sub-Tree)

func (c *ChainService) ReceiveBlock(ctx context.Context, block *pb.BeaconBlock) (*pb.BeaconState, error) {
beaconState, err := c.beaconDB.State(ctx)
if err != nil {
return nil, fmt.Errorf("could not retrieve beacon state: %v", err)
}
blockRoot, err := hashutil.HashBeaconBlock(block)
if err != nil {
return nil, fmt.Errorf("could not tree hash incoming block: %v", err)
}
// Retrieve the latest canonical block's hash root.
headRoot, err := c.ChainHeadRoot()
if err != nil {
return nil, fmt.Errorf("could not retrieve chain head root: %v", err)
}
// Check for skipped slots.
numSkippedSlots := 0
for beaconState.Slot < block.Slot-1 {
beaconState, err = c.runStateTransition(headRoot, nil, beaconState)
if err != nil {
return nil, fmt.Errorf("could not execute state transition without block %v", err)
}
numSkippedSlots++
}
beaconState, err = c.runStateTransition(headRoot, block, beaconState)
if err != nil {
return nil, fmt.Errorf("could not execute state transition with block %v", err)
}
if err := c.applyForkChoiceRule(beaconState, block); err != nil {
return nil, fmt.Errorf("failed to update chain head and state: %v", err)
}
log.WithField("hash", fmt.Sprintf("%#x", blockRoot)).Debug("Updated chain head")
return beaconState, nil
}

In Ethereum 2.0, blocks can be proposed in intervals known as slots, which are period of seconds in which proposers are assigned to create and send blocks into the beacon node for acceptance. It is possible, however, that proposer may fail to do their job at their assigned slot. In this case, the ReceiveBlock function above processes skipped slots appropriately so the chain does not stall.

The Sync Service

Our sync service has two responsibilities: to catch our local beacon chain to the latest canonical head and state as observed by the network, and to listen to and respond to requests for new block announcements from peers. Syncing an Ethereum beacon chain is much different than syncing a regular, ETH 1.0 parity or geth node, as we do not need to download a large chain history but rather, simply sync from the last finalized block. The sync service was designed to be as independent as possible from the rest of the system and be the main point of interaction for peers between the network over p2p. Everything in our sync service runs concurrently through a single Start() function which handles all sorts of message requests and responses.

func (rs *RegularSync) Start() {
for {
select {
case <-rs.ctx.Done():
log.Debug("Exiting goroutine")
return
case msg := <-rs.announceBlockBuf:
safelyHandleMessage(rs.receiveBlockAnnounce, msg)
case msg := <-rs.attestationBuf:
safelyHandleMessage(rs.receiveAttestation, msg)
case msg := <-rs.attestationReqByHashBuf:
safelyHandleMessage(rs.handleAttestationRequestByHash, msg)
case msg := <-rs.unseenAttestationsReqBuf:
safelyHandleMessage(rs.handleUnseenAttestationsRequest, msg)
case msg := <-rs.exitBuf:
safelyHandleMessage(rs.receiveExitRequest, msg)
case msg := <-rs.blockBuf:
safelyHandleMessage(rs.receiveBlock, msg)
case msg := <-rs.blockRequestBySlot:
safelyHandleMessage(rs.handleBlockRequestBySlot, msg)
case msg := <-rs.blockRequestByHash:
safelyHandleMessage(rs.handleBlockRequestByHash, msg)
case msg := <-rs.batchedRequestBuf:
safelyHandleMessage(rs.handleBatchedBlockRequest, msg)
case msg := <-rs.stateRequestBuf:
safelyHandleMessage(rs.handleStateRequest, msg)
case msg := <-rs.chainHeadReqBuf:
safelyHandleMessage(rs.handleChainHeadRequest, msg)
case blockAnnounce := <-rs.canonicalBuf:
rs.broadcastCanonicalBlock(rs.ctx, blockAnnounce)
}
}
log.Info("Exiting regular sync run()")
}

The Operations Service

Our operations service handles important information contained within blocks in the beacon chain. It is similar to a transactions pool in ETH 1.0, except instead of storing tx's it stores voluntary validator exits, attestations, slashings, and more. These operations are received from the sync service via the network or from information the node gathers locally and are stored in a pool for easy retrieval by validators when proposing beacon blocks. Validators simply call an RPC endpoint in the beacon node that reaches into the operations service for the data they need.

func (s *Service) Start() {
log.Info("Starting service")
go s.saveOperations()
go s.removeOperations()
}