1 of 33

Prysm and the Altair Hard Fork

Prysmatic Labs

Terence Tsao & Raul Jordan

2 of 33

Outline

  • What’s coming in Prysm’s Altair support
  • Challenges of supporting hard forks in Golang
  • Major code changes
  • End-to-end testing the fork transition
  • How to contribute
  • Hiring

3 of 33

What’s coming with Prysm’s Altair support

4 of 33

Major Revamp: Prysm V2

  • Full support for the Altair hard fork transition
  • Revamped slasher, optimized for lower resource requirements
  • Much better documentation and onboarding in preparation for the merge
  • Full support for the Ethereum consensus standard API

5 of 33

Unique Challenges of Hard Forks in Go

6 of 33

Challenges

  • Hard forks require using generic functions on data types as block data structures can be different before and after the hard fork
  • Go does not yet support generics
  • Having if/else conditionals across the codebase is gross! How can we make this more maintainable for multiple forks?

7 of 33

Compared to other programming languages, Golang does NOT support generic types

// Requires some code duplication :(

func processPhase0Block(blk *types.Phase0BeaconBlock) error

func processPhase0Block(blk *types.AltairBeaconBlock) error

// Cannot do this yet in Go!

func processBlock<T>(blk) error

8 of 33

Interfaces help us as an alternative, but they aren’t really β€œgenerics”

// Interfaces help us define behavior as alternatives

// to generics in Go, but they aren't perfect.

type BeaconBlock interface {

Slot() uint64

ParentRoot() [32]byte

SyncContribution() *types.SyncCommitteeContribution

...

}

9 of 33

type BeaconBlock interface {

Slot() uint64

ParentRoot() [32]byte

SyncContribution() *types.SyncCommitteeContribution

...

}

type Phase0BeaconBlock struct {

...

}

func (b *Phase0BeaconBlock) SyncContribution() *types.SyncContribution {

panic("not supported for phase0")

}

type AltairBeaconBlock struct {

...

}

func (b *AltairBeaconBlock) SyncContribution() *types.SyncContribution {

return b.syncContribution

}

10 of 33

Golang code generation is our friend for dealing with lack of generics!

// In the type definition file

//go:generate ssz-bindings

type Phase0BeaconBlock struct {

Slot uint64

ParentRoot [32]byte

Deposits []*types.Deposit

...

}

//go:generate ssz-bindings

type AltairBeaconBlock struct {

Slot uint64

ParentRoot [32]byte

Deposits []*types.Deposit

...

}

// Generated output!

func (s *Phase0BeaconBlock) MarshalSSZTo(buf []byte) (dst []byte, err error) {

dst = buf

offset := int(100)

// Offset (0) 'Block'

dst = ssz.WriteOffset(dst, offset)

if s == nil {

s = new(Phase0BeaconBlock)

}

offset += s.SizeSSZ()

// Field (1) 'Signature'

if len(s.Signature) != 96 {

err = ssz.ErrBytesLength

return

}

dst = append(dst, s.Signature...)

...

}

...

11 of 33

How prysm is handling fork

12 of 33

Potential solutions #1

  • Copy all functions manually for Altair

func ExecuteStateTransition(

state *stateTrie.BeaconState,

block *ethpb.SignedBeaconBlock)

(*stateTrie.BeaconState, error) {

// Execute input `state` with `block` and return a new state

}

func ExecuteAltairStateTransition(

state *stateTrie.BeaconState,

block *ethpb.SignedBeaconBlockAltair)

(*stateTrie.BeaconState, error) {

// Execute input `state` with altair version `block` and return a new state

}

13 of 33

Potential solutions #2

  • Abstract extended beacon chain object with interface

func ExecuteStateTransition(

state *stateTrie.BeaconState,

block *block.SignedBeaconBlock)

(*stateTrie.BeaconState, error) {

// Execute input `state` with interface SignedBeaconBlock and return a new state

}

14 of 33

type SignedBeaconBlock interface {

Block() BeaconBlock

Signature() []byte

...

}

type BeaconBlock interface {

Slot() types.Slot

Body() BeaconBlockBody

...

}

type BeaconBlockBody interface {

Attestations() []*ethpb.Attestation

Deposits() []*ethpb.Deposit

...

}

15 of 33

type altairSignedBeaconBlock struct {

b *eth.SignedBeaconBlockAltair

}

func (w altairSignedBeaconBlock) Signature() []byte {

return w.b.Signature

}

func (w altairSignedBeaconBlock) Block() block.BeaconBlock {

return altairBeaconBlock{b: w.b.Block}

}

16 of 33

func WrappedAltairSignedBeaconBlock(b *eth.SignedBeaconBlockAltair) (block.SignedBeaconBlock, error) {

w := altairSignedBeaconBlock{b: b}

if w.IsNil() {

return nil, ErrNilObjectWrapped

}

return w, nil

}

17 of 33

Improving our code quality is key

18 of 33

SOLID Software Principles

  • Single responsibility principle: your functions should only do as they say!
  • Open/closed: you should be able to extend behavior without modifying the underlying code
  • Liskov substitution: you should leverage interfaces
  • Interface segregation: make functions only depend on what they need
  • Dependency inversion: depend on abstractions, not on implementation details

https://hackmd.io/@prysmaticlabs/design-principles

19 of 33

// Clean p2p validation pipelines!

func (s *Service) validateBlockPubSub(req *rpc.Request) {

...

blk := readFromRequest(req, &ethpb.SignedBeaconBlock{})

valid, err := validationPipeline(

blk,

validStructure,

validSignature(proposerPubKey),

isCanonical,

notYetSeen,

fromValidPeer(s.p2p.PeerSet()),

)

...

}

func validationPipeline(

ctx context.Context, fns ...validationFn,

) pubsub.ValidationResult {

for _, fn := range fns {

if result := fn(ctx); result != pubsub.ValidationAccept {

return result

}

}

return pubsub.ValidationAccept

}

20 of 33

Where does new code live?

21 of 33

Core transition

/ethereum/consensus-specs/blob/dev/specs/altair/beacon-chain.md

/prysm/beacon-chain/core

πŸ‘‡ implements

22 of 33

Network

ethereum/consensus-specs/blob/dev/specs/altair/p2p-interface.md

/prysm/beacon-chain/sync

/prysm/beacon-chain/p2p

πŸ‘‡ implements

23 of 33

Validator

ethereum/consensus-specs/blob/dev/specs/altair/validator.md

/prysm/beacon-chain/rpc/prysm/validator

/prysm/beacon-chain/rpc/eth/beacon

/prysm/validator/client

πŸ‘‡ implements

24 of 33

Optimizations

/prysm/beacon-chain/cache/sync_committee

/prysm/beacon-chain/cache/active_balance

25 of 33

New beacon state structure

/prysm/beacon-chain/state/v2

  • State trie caching
  • Setters
  • Getters

26 of 33

New spec tests

ethereum/consensus-spec-tests/tree/master/tests

/spectest/mainnet

/spectest/minimal

πŸ‘‡ implements

27 of 33

Testing the fork transition

28 of 33

Fork transition

// If state.slot % SLOTS_PER_EPOCH == 0 and compute_epoch_at_slot(state.slot) == ALTAIR_FORK_EPOCH, an // irregular state change is made to upgrade to Altair.

if CanUpgradeToAltair(state.Slot()) {

state, err = altair.UpgradeToAltair(ctx, state)

if err != nil {

tracing.AnnotateError(span, err)

return nil, err

}

}

29 of 33

End-to-end testing is critical

  • We run a full e2e suite of beacon nodes, validators, and geth nodes on every pull request
  • Our test harness makes it easy to add β€œevaluators” at certain epochs to ensure behavior is as expected in the test chain
  • We test the Altair fork transition went smoothly!

evals := []types.Evaluator{

ev.PeersConnect{Epoch: 0},

ev.HealthzCheck{Epoch: 0},

ev.MetricsCheck{Epoch: 0},

ev.ValidatorsAreActive{Epoch: 0},

ev.ValidatorsParticipating{Epoch: 0},

ev.FinalizationOccurs{Epoch: 4},

ev.ProcessesDepositsInBlocks{Epoch: 4},

ev.VerifyBlockGraffiti{Epoch: 4},

ev.ActivatesDepositedValidators{Epoch: 6},

ev.DepositedValidatorsAreActive{Epoch: 6},

ev.ProposeVoluntaryExit{Epoch: 6},

ev.ValidatorHasExited{Epoch: 8},

ev.ValidatorsVoteWithTheMajority{Epoch: 8},

ev.ColdStateCheckpoint{Epoch: 8},

ev.ForkTransition{Epoch: 8},

ev.APIGatewayV1VerifyIntegrity{Epoch: 0},

ev.APIGatewayV1Alpha1VerifyIntegrity{Epoch: 0},

}

newTestRunner(t, evals).run()

30 of 33

// ForkTransition ensures that the hard fork has occurred successfully.

var ForkTransition = types.Evaluator{

Name: "fork_transition_%d",

Policy: policies.OnEpoch(params.AltairE2EForkEpoch),

Evaluation: forkOccurs,

}

func forkOccurs(conns ...*grpc.ClientConn) error {

conn := conns[0]

client := ethpb.NewBeaconNodeValidatorClient(conn)

stream := client.StreamBlocksAltair(ctx, &ethpb.StreamBlocksRequest{})

forkSlot := core.StartSlot(params.AltairE2EForkEpoch)

res := stream.Recv()

if res.GetPhase0Block() == nil && res.GetAltairBlock() == nil {

return errors.New("nil block returned by beacon node")

}

if res.GetPhase0Block() != nil {

return errors.New("phase 0 block returned after altair fork has occurred")

}

blk := wrapperv2.WrappedAltairSignedBeaconBlock(res.GetAltairBlock())

if blk.Block().Slot() < fSlot {

return errors.Errorf(

"wanted a block >= %d but received %d", fSlot, blk.Block().Slot(),

)

}

return nil

}

31 of 33

How to contribute

  • Help us review code by cross-checking against the specification!
  • Find gaps in testing, run tests with coverage using `go test -cov`
  • Find ways our code is confusing and bring it up to our team
  • Contribute to open issues on Github

32 of 33

We are hiring!

  • Our team is currently hiring for DevOps and Security Engineering positions, come join us!
  • We are on the brink of the largest accomplishment in Ethereum’s history: shipping the merge of Proof of Stake to the whole network

33 of 33

Contact Us!

  • Twitter
    • @prylabs
  • Website
    • prysmaticlabs.com
  • Github
    • github.com/prysmaticlabs
  • Discord
    • https://discord.gg/PrfGvU4
  • Our Eth2 Client
    • github.com/prysmaticlabs/prysm