prysm-pulse/client/attester/service.go

175 lines
5.3 KiB
Go

// Package attester defines all relevant functionality for a Attester actor
// within a sharded Ethereum blockchain.
package attester
import (
"bytes"
"context"
"io"
"github.com/gogo/protobuf/proto"
"github.com/golang/protobuf/ptypes/empty"
"github.com/prysmaticlabs/prysm/client/types"
pb "github.com/prysmaticlabs/prysm/proto/beacon/rpc/v1"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/blake2b"
)
var log = logrus.WithField("prefix", "attester")
// Attester holds functionality required to run a collation attester
// in a sharded system. Must satisfy the Service interface defined in
// sharding/service.go.
type Attester struct {
ctx context.Context
cancel context.CancelFunc
clientService types.RPCClient
validatorIndex int
assignedHeight uint64
isHeightAssigned bool
}
// NewAttester creates a new attester instance.
func NewAttester(ctx context.Context, clientService types.RPCClient) *Attester {
ctx, cancel := context.WithCancel(ctx)
return &Attester{
ctx: ctx,
cancel: cancel,
clientService: clientService,
}
}
// Start the main routine for a attester.
func (at *Attester) Start() {
log.Info("Starting service")
rpcClient := at.clientService.BeaconServiceClient()
go at.fetchBeaconBlocks(rpcClient)
go at.fetchCrystallizedState(rpcClient)
}
// Stop the main loop for notarizing collations.
func (at *Attester) Stop() error {
defer at.cancel()
log.Info("Stopping service")
return nil
}
func (at *Attester) fetchBeaconBlocks(client pb.BeaconServiceClient) {
stream, err := client.LatestBeaconBlock(at.ctx, &empty.Empty{})
if err != nil {
log.Errorf("Could not setup beacon chain block streaming client: %v", err)
return
}
for {
block, err := stream.Recv()
// If the stream is closed, we stop the loop.
if err == io.EOF {
break
}
if err != nil {
log.Errorf("Could not receive latest beacon block from stream: %v", err)
continue
}
log.WithField("slotNumber", block.GetSlotNumber()).Info("Latest beacon block slot number")
// Based on the height determined from the latest crystallized state, check if
// it matches the latest received beacon height. If so, the attester has to perform
// its responsibilities.
if at.isHeightAssigned && block.GetSlotNumber() == at.assignedHeight {
log.Info("Assigned attestation height reached, performing attestation responsibility")
// Reset is height assigned.
at.isHeightAssigned = false
// TODO: perform attestation responsibility.
}
}
}
func (at *Attester) fetchCrystallizedState(client pb.BeaconServiceClient) {
stream, err := client.LatestCrystallizedState(at.ctx, &empty.Empty{})
if err != nil {
log.Errorf("Could not setup crystallized beacon state streaming client: %v", err)
return
}
for {
crystallizedState, err := stream.Recv()
// If the stream is closed, we stop the loop.
if err == io.EOF {
break
}
if err != nil {
log.Errorf("Could not receive latest crystallized beacon state from stream: %v", err)
continue
}
// After receiving the crystallized state, get its hash, and
// this attester's index in the list.
stateData, err := proto.Marshal(crystallizedState)
if err != nil {
log.Errorf("Could not marshal crystallized state proto: %v", err)
continue
}
crystallizedStateHash := blake2b.Sum256(stateData)
activeValidators := crystallizedState.GetActiveValidators()
isValidatorIndexSet := false
for i, val := range activeValidators {
// TODO: Check the public key instead of withdrawal address. This will
// use BLS.
if isZeroAddress(val.GetWithdrawalAddress()) {
at.validatorIndex = i
isValidatorIndexSet = true
break
}
}
// If validator was not found in the validator set was not set, keep listening for
// crystallized states.
if !isValidatorIndexSet {
log.Debug("Validator index not found in latest crystallized state's active validator list")
continue
}
req := &pb.ShuffleRequest{
CrystallizedStateHash: crystallizedStateHash[:],
}
res, err := client.FetchShuffledValidatorIndices(at.ctx, req)
if err != nil {
log.Errorf("Could not fetch shuffled validator indices: %v", err)
continue
}
// Based on the cutoff and assigned heights, determine the beacon block
// height at which attester has to perform its responsibility.
currentAssignedHeights := res.GetAssignedAttestationHeights()
currentCutoffs := res.GetCutoffIndices()
// The algorithm functions as follows:
// Given a list of heights: [0 19 38 57 12 31 50] and
// A list of cutoff indices: [0 142 285 428 571 714 857 1000]
// if the validator index is between 0-142, it can attest at height 0, if it is
// between 142-285, that validator can attest at height 19, etc.
heightIndex := 0
for i := 0; i < len(currentCutoffs)-1; i++ {
lowCutoff := currentCutoffs[i]
highCutoff := currentCutoffs[i+1]
if (uint64(at.validatorIndex) >= lowCutoff) && (uint64(at.validatorIndex) <= highCutoff) {
break
}
heightIndex++
}
at.isHeightAssigned = true
at.assignedHeight = currentAssignedHeights[heightIndex]
log.Debug("Attestation height responsibility assigned based on cutoff indices")
}
}
// isZeroAddress compares a withdrawal address to an empty byte array.
func isZeroAddress(withdrawalAddress []byte) bool {
return bytes.Equal(withdrawalAddress, []byte{})
}