Prysm and the Altair Hard Fork
Prysmatic Labs
Terence Tsao & Raul Jordan
Outline
Whatβs coming with Prysmβs Altair support
Major Revamp: Prysm V2
Unique Challenges of Hard Forks in Go
Challenges
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
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
...
}
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
}
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...)
...
}
...
How prysm is handling fork
Potential solutions #1
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
}
Potential solutions #2
func ExecuteStateTransition(
state *stateTrie.BeaconState,
block *block.SignedBeaconBlock)
(*stateTrie.BeaconState, error) {
// Execute input `state` with interface SignedBeaconBlock and return a new state
}
type SignedBeaconBlock interface {
Block() BeaconBlock
Signature() []byte
...
}
type BeaconBlock interface {
Slot() types.Slot
Body() BeaconBlockBody
...
}
type BeaconBlockBody interface {
Attestations() []*ethpb.Attestation
Deposits() []*ethpb.Deposit
...
}
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}
}
func WrappedAltairSignedBeaconBlock(b *eth.SignedBeaconBlockAltair) (block.SignedBeaconBlock, error) {
w := altairSignedBeaconBlock{b: b}
if w.IsNil() {
return nil, ErrNilObjectWrapped
}
return w, nil
}
Improving our code quality is key
SOLID Software Principles
https://hackmd.io/@prysmaticlabs/design-principles
// Clean p2p validation pipelines!
func (s *Service) validateBlockPubSub(req *rpc.Request) {
...
blk := readFromRequest(req, ðpb.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
}
Where does new code live?
Core transition
/ethereum/consensus-specs/blob/dev/specs/altair/beacon-chain.md
/prysm/beacon-chain/core
π implements
Network
ethereum/consensus-specs/blob/dev/specs/altair/p2p-interface.md
/prysm/beacon-chain/sync
/prysm/beacon-chain/p2p
π implements
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
Optimizations
/prysm/beacon-chain/cache/sync_committee
/prysm/beacon-chain/cache/active_balance
New beacon state structure
/prysm/beacon-chain/state/v2
New spec tests
ethereum/consensus-spec-tests/tree/master/tests
/spectest/mainnet
/spectest/minimal
π implements
Testing the fork transition
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
}
}
End-to-end testing is critical
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()
// 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, ðpb.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
}
How to contribute
We are hiring!
Contact Us!