From becb0b19e26233fc1040d37395e0b658372e5ff8 Mon Sep 17 00:00:00 2001 From: rkapka Date: Tue, 28 Jul 2020 22:29:12 +0200 Subject: [PATCH] Voting With The Majority (#6644) * eth1DataMajorityVote draft * Merge branch 'origin-master' into eth1-majority-voting * small test change * move minimal config's HeadOfVotingPeriodLength closer to sqroot(32) * remove todo * remove head period and simplify equality check * extract private functions from eth1DataMajorityVote * feature flag * Merge branch 'origin-master' into eth1-majority-voting * add bytes import to tests * Merge branch 'master' into eth1-majority-voting * re-run build * Merge branch 'master' into eth1-majority-voting * Merge branch 'master' into eth1-majority-voting * Merge branch 'master' into eth1-majority-voting --- beacon-chain/powchain/testing/mock.go | 16 +- beacon-chain/rpc/validator/proposer.go | 166 +++++++- beacon-chain/rpc/validator/proposer_test.go | 424 +++++++++++++++++++- shared/featureconfig/config.go | 5 + shared/featureconfig/flags.go | 8 +- shared/params/minimal_config.go | 1 + 6 files changed, 603 insertions(+), 17 deletions(-) diff --git a/beacon-chain/powchain/testing/mock.go b/beacon-chain/powchain/testing/mock.go index 4a5022a6715..8fc4c9fa37c 100644 --- a/beacon-chain/powchain/testing/mock.go +++ b/beacon-chain/powchain/testing/mock.go @@ -22,13 +22,13 @@ import ( // POWChain defines a properly functioning mock for the powchain service. type POWChain struct { - ChainFeed *event.Feed - LatestBlockNumber *big.Int - HashesByHeight map[int][]byte - TimesByHeight map[int]uint64 - BlockNumberByHeight map[uint64]*big.Int - Eth1Data *ethpb.Eth1Data - GenesisEth1Block *big.Int + ChainFeed *event.Feed + LatestBlockNumber *big.Int + HashesByHeight map[int][]byte + TimesByHeight map[int]uint64 + BlockNumberByTime map[uint64]*big.Int + Eth1Data *ethpb.Eth1Data + GenesisEth1Block *big.Int } // Eth2GenesisPowchainInfo -- @@ -78,7 +78,7 @@ func (m *POWChain) BlockTimeByHeight(_ context.Context, height *big.Int) (uint64 // BlockNumberByTimestamp -- func (m *POWChain) BlockNumberByTimestamp(_ context.Context, time uint64) (*big.Int, error) { - return m.BlockNumberByHeight[time], nil + return m.BlockNumberByTime[time], nil } // DepositRoot -- diff --git a/beacon-chain/rpc/validator/proposer.go b/beacon-chain/rpc/validator/proposer.go index 4e8cbcbdc65..ce1fd4635b4 100644 --- a/beacon-chain/rpc/validator/proposer.go +++ b/beacon-chain/rpc/validator/proposer.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "fmt" "math/big" + "reflect" "time" fastssz "github.com/ferranbt/fastssz" @@ -38,6 +39,16 @@ var eth1DataNotification bool const eth1dataTimeout = 2 * time.Second +type eth1DataSingleVote struct { + eth1Data ethpb.Eth1Data + blockHeight *big.Int +} + +type eth1DataAggregatedVote struct { + data eth1DataSingleVote + votes int +} + // GetBlock is called by a proposer during its assigned slot to request a block to sign // by passing in the slot and the signed randao reveal of the slot. func (vs *Server) GetBlock(ctx context.Context, req *ethpb.BlockRequest) (*ethpb.BeaconBlock, error) { @@ -54,7 +65,13 @@ func (vs *Server) GetBlock(ctx context.Context, req *ethpb.BlockRequest) (*ethpb if err != nil { return nil, status.Errorf(codes.Internal, "Could not retrieve head root: %v", err) } - eth1Data, err := vs.eth1Data(ctx, req.Slot) + + var eth1Data *ethpb.Eth1Data + if featureconfig.Get().EnableEth1DataMajorityVote { + eth1Data, err = vs.eth1DataMajorityVote(ctx, req.Slot) + } else { + eth1Data, err = vs.eth1Data(ctx, req.Slot) + } if err != nil { return nil, status.Errorf(codes.Internal, "Could not get ETH1 data: %v", err) } @@ -170,14 +187,12 @@ func (vs *Server) eth1Data(ctx context.Context, slot uint64) (*ethpb.Eth1Data, e if vs.MockEth1Votes { return vs.mockETH1DataVote(ctx, slot) } - if !vs.Eth1InfoFetcher.IsConnectedToETH1() { return vs.randomETH1DataVote(ctx) } eth1DataNotification = false - eth1VotingPeriodStartTime, _ := vs.Eth1InfoFetcher.Eth2GenesisPowchainInfo() - eth1VotingPeriodStartTime += (slot - (slot % (params.BeaconConfig().EpochsPerEth1VotingPeriod * params.BeaconConfig().SlotsPerEpoch))) * params.BeaconConfig().SecondsPerSlot + eth1VotingPeriodStartTime := vs.slotStartTime(slot) // Look up most recent block up to timestamp blockNumber, err := vs.Eth1BlockFetcher.BlockNumberByTimestamp(ctx, eth1VotingPeriodStartTime) @@ -194,6 +209,143 @@ func (vs *Server) eth1Data(ctx context.Context, slot uint64) (*ethpb.Eth1Data, e return eth1Data, nil } +// eth1DataMajorityVote determines the appropriate eth1data for a block proposal using an extended +// simple voting algorithm - voting with the majority. The algorithm for this method is as follows: +// - Determine the timestamp for the start slot for the current eth1 voting period. +// - Determine the timestamp for the start slot for the previous eth1 voting period. +// - Determine the most recent eth1 block before each timestamp. +// - Subtract the current period's eth1block.number by ETH1_FOLLOW_DISTANCE to determine the voting upper bound. +// - Subtract the previous period's eth1block.number by ETH1_FOLLOW_DISTANCE to determine the voting lower bound. +// - Filter out votes on unknown blocks and blocks which are outside of the range determined by the lower and upper bounds. +// - If no blocks are left after filtering, use the current period's most recent eth1 block for proposal. +// - Determine the vote with the highest count. Prefer the vote with the highest eth1 block height in the event of a tie. +// - This vote's block is the eth1block to use for the block proposal. +func (vs *Server) eth1DataMajorityVote(ctx context.Context, slot uint64) (*ethpb.Eth1Data, error) { + ctx, cancel := context.WithTimeout(ctx, eth1dataTimeout) + defer cancel() + + if vs.MockEth1Votes { + return vs.mockETH1DataVote(ctx, slot) + } + if !vs.Eth1InfoFetcher.IsConnectedToETH1() { + return vs.randomETH1DataVote(ctx) + } + eth1DataNotification = false + + slotsPerVotingPeriod := params.BeaconConfig().EpochsPerEth1VotingPeriod * params.BeaconConfig().SlotsPerEpoch + currentPeriodVotingStartTime := vs.slotStartTime(slot) + // Can't use slotStartTime function because slot would be negative in the initial voting period. + previousPeriodVotingStartTime := currentPeriodVotingStartTime - + slotsPerVotingPeriod*params.BeaconConfig().SecondsPerSlot + + currentPeriodBlockNumber, err := vs.Eth1BlockFetcher.BlockNumberByTimestamp(ctx, currentPeriodVotingStartTime) + if err != nil { + log.WithError(err).Error("Failed to get block number for current voting period") + return vs.randomETH1DataVote(ctx) + } + previousPeriodBlockNumber, err := vs.Eth1BlockFetcher.BlockNumberByTimestamp(ctx, previousPeriodVotingStartTime) + if err != nil { + log.WithError(err).Error("Failed to get block number for previous voting period") + return vs.randomETH1DataVote(ctx) + } + + headState, err := vs.HeadFetcher.HeadState(ctx) + if err != nil { + return nil, status.Error(codes.Internal, "Could not get head state") + } + if len(headState.Eth1DataVotes()) == 0 { + eth1Data, err := vs.defaultEth1DataResponse(ctx, currentPeriodBlockNumber) + if err != nil { + log.WithError(err).Error("Failed to get eth1 data from current period block number") + return vs.randomETH1DataVote(ctx) + } + return eth1Data, nil + } + + inRangeVotes, err := vs.inRangeVotes(ctx, currentPeriodBlockNumber, previousPeriodBlockNumber) + if err != nil { + return nil, err + } + if len(inRangeVotes) == 0 { + eth1Data, err := vs.defaultEth1DataResponse(ctx, currentPeriodBlockNumber) + if err != nil { + log.WithError(err).Error("Failed to get eth1 data from current period block number") + return vs.randomETH1DataVote(ctx) + } + return eth1Data, nil + } + + chosenVote := chosenEth1DataMajorityVote(inRangeVotes) + return &chosenVote.data.eth1Data, nil +} + +func (vs *Server) slotStartTime(slot uint64) uint64 { + startTime, _ := vs.Eth1InfoFetcher.Eth2GenesisPowchainInfo() + startTime += + (slot - (slot % (params.BeaconConfig().EpochsPerEth1VotingPeriod * params.BeaconConfig().SlotsPerEpoch))) * + params.BeaconConfig().SecondsPerSlot + return startTime +} + +func (vs *Server) inRangeVotes( + ctx context.Context, + currentPeriodBlockNumber *big.Int, + previousPeriodBlockNumber *big.Int) ([]eth1DataSingleVote, error) { + + eth1FollowDistance := int64(params.BeaconConfig().Eth1FollowDistance) + lastValidBlockNumber := big.NewInt(0).Sub(currentPeriodBlockNumber, big.NewInt(eth1FollowDistance)) + firstValidBlockNumber := big.NewInt(0).Sub(previousPeriodBlockNumber, big.NewInt(eth1FollowDistance)) + + headState, err := vs.HeadFetcher.HeadState(ctx) + if err != nil { + return nil, status.Error(codes.Internal, "Could not get head state") + } + + var inRangeVotes []eth1DataSingleVote + for _, eth1Data := range headState.Eth1DataVotes() { + ok, height, err := vs.BlockFetcher.BlockExists(ctx, bytesutil.ToBytes32(eth1Data.BlockHash)) + if err != nil { + log.WithError(err).Warning("Could not fetch eth1data height for received eth1data vote") + } + if ok && firstValidBlockNumber.Cmp(height) < 1 && lastValidBlockNumber.Cmp(height) > -1 { + inRangeVotes = append(inRangeVotes, eth1DataSingleVote{eth1Data: *eth1Data, blockHeight: height}) + } + } + + return inRangeVotes, nil +} + +func chosenEth1DataMajorityVote(votes []eth1DataSingleVote) eth1DataAggregatedVote { + var voteCount []eth1DataAggregatedVote + for _, singleVote := range votes { + newVote := true + for i, aggregatedVote := range voteCount { + aggregatedData := aggregatedVote.data + if reflect.DeepEqual(singleVote.eth1Data, aggregatedData.eth1Data) { + voteCount[i].votes++ + newVote = false + break + } + } + + if newVote { + voteCount = append(voteCount, eth1DataAggregatedVote{data: singleVote, votes: 1}) + } + } + + currentVote := voteCount[0] + for _, aggregatedVote := range voteCount[1:] { + // Choose new eth1data if it has more votes or the same number of votes with a bigger block height. + if aggregatedVote.votes > currentVote.votes || + (aggregatedVote.votes == currentVote.votes && + aggregatedVote.data.blockHeight.Cmp(currentVote.data.blockHeight) == 1) { + currentVote = aggregatedVote + } + } + + return currentVote +} + func (vs *Server) mockETH1DataVote(ctx context.Context, slot uint64) (*ethpb.Eth1Data, error) { if !eth1DataNotification { log.Warn("Beacon Node is no longer connected to an ETH1 chain, so ETH1 data votes are now mocked.") @@ -329,7 +481,11 @@ func (vs *Server) deposits(ctx context.Context, currentVote *ethpb.Eth1Data) ([] } // canonicalEth1Data determines the canonical eth1data and eth1 block height to use for determining deposits. -func (vs *Server) canonicalEth1Data(ctx context.Context, beaconState *stateTrie.BeaconState, currentVote *ethpb.Eth1Data) (*ethpb.Eth1Data, *big.Int, error) { +func (vs *Server) canonicalEth1Data( + ctx context.Context, + beaconState *stateTrie.BeaconState, + currentVote *ethpb.Eth1Data) (*ethpb.Eth1Data, *big.Int, error) { + var eth1BlockHash [32]byte // Add in current vote, to get accurate vote tally diff --git a/beacon-chain/rpc/validator/proposer_test.go b/beacon-chain/rpc/validator/proposer_test.go index 6fd45ed02e0..82fa08d36f1 100644 --- a/beacon-chain/rpc/validator/proposer_test.go +++ b/beacon-chain/rpc/validator/proposer_test.go @@ -1,6 +1,7 @@ package validator import ( + "bytes" "context" "math/big" "testing" @@ -1118,7 +1119,7 @@ func TestDefaultEth1Data_NoBlockExists(t *testing.T) { require.NoError(t, err) if !proto.Equal(result, defEth1Data) { - t.Errorf("Did not receive default eth1data. Wanted %v but Got %v", defEth1Data, result) + t.Errorf("Did not receive default eth1data. Wanted %v but got %v", defEth1Data, result) } } @@ -1126,7 +1127,7 @@ func TestEth1Data(t *testing.T) { slot := uint64(20000) p := &mockPOW.POWChain{ - BlockNumberByHeight: map[uint64]*big.Int{ + BlockNumberByTime: map[uint64]*big.Int{ slot * params.BeaconConfig().SecondsPerSlot: big.NewInt(8196), }, HashesByHeight: map[int][]byte{ @@ -1191,7 +1192,7 @@ func TestEth1Data_SmallerDepositCount(t *testing.T) { } p := &mockPOW.POWChain{ - BlockNumberByHeight: map[uint64]*big.Int{ + BlockNumberByTime: map[uint64]*big.Int{ slot * params.BeaconConfig().SecondsPerSlot: big.NewInt(4096), }, HashesByHeight: map[int][]byte{ @@ -1260,6 +1261,423 @@ func TestEth1Data_MockEnabled(t *testing.T) { } } +func TestEth1DataMajorityVote_ChooseHighestCount(t *testing.T) { + slot := uint64(64) + + p := &mockPOW.POWChain{ + BlockNumberByTime: map[uint64]*big.Int{ + 32 * params.BeaconConfig().SecondsPerSlot: big.NewInt(50), + 64 * params.BeaconConfig().SecondsPerSlot: big.NewInt(100), + }, + HashesByHeight: map[int][]byte{ + int(100 - params.BeaconConfig().Eth1FollowDistance - 1): []byte("first"), + int(100 - params.BeaconConfig().Eth1FollowDistance): []byte("second"), + }, + } + + dc := dbpb.DepositContainer{ + Index: 0, + Eth1BlockHeight: 0, + Deposit: ðpb.Deposit{ + Data: ðpb.Deposit_Data{ + PublicKey: []byte("a"), + Signature: make([]byte, 96), + WithdrawalCredentials: make([]byte, 32), + }}, + } + depositTrie, err := trieutil.NewTrie(int(params.BeaconConfig().DepositContractTreeDepth)) + require.NoError(t, err) + depositCache, err := depositcache.NewDepositCache() + require.NoError(t, err) + depositCache.InsertDeposit(context.Background(), dc.Deposit, dc.Eth1BlockHeight, dc.Index, depositTrie.Root()) + + beaconState, err := beaconstate.InitializeFromProto(&pbp2p.BeaconState{ + Eth1DataVotes: []*ethpb.Eth1Data{ + {BlockHash: []byte("first")}, + {BlockHash: []byte("first")}, + {BlockHash: []byte("second")}, + }, + }) + require.NoError(t, err) + + ps := &Server{ + ChainStartFetcher: p, + Eth1InfoFetcher: p, + Eth1BlockFetcher: p, + BlockFetcher: p, + DepositFetcher: depositCache, + HeadFetcher: &mock.ChainService{State: beaconState, ETH1Data: ðpb.Eth1Data{}}, + } + + ctx := context.Background() + majorityVoteEth1Data, err := ps.eth1DataMajorityVote(ctx, slot) + require.NoError(t, err) + + hash := majorityVoteEth1Data.BlockHash + + expectedHash := []byte("first") + if bytes.Compare(hash, expectedHash) != 0 { + t.Errorf("Chosen eth1data for block hash %v vs expected %v", hash, expectedHash) + } +} + +func TestEth1DataMajorityVote_HighestCountBeforeRange_ChooseHighestCountWithinRange(t *testing.T) { + slot := uint64(64) + + p := &mockPOW.POWChain{ + BlockNumberByTime: map[uint64]*big.Int{ + 32 * params.BeaconConfig().SecondsPerSlot: big.NewInt(50), + 64 * params.BeaconConfig().SecondsPerSlot: big.NewInt(100), + }, + HashesByHeight: map[int][]byte{ + int(50 - params.BeaconConfig().Eth1FollowDistance - 1): []byte("before_range"), + int(100 - params.BeaconConfig().Eth1FollowDistance - 1): []byte("first"), + int(100 - params.BeaconConfig().Eth1FollowDistance): []byte("second"), + }, + } + + dc := dbpb.DepositContainer{ + Index: 0, + Eth1BlockHeight: 0, + Deposit: ðpb.Deposit{ + Data: ðpb.Deposit_Data{ + PublicKey: []byte("a"), + Signature: make([]byte, 96), + WithdrawalCredentials: make([]byte, 32), + }}, + } + depositTrie, err := trieutil.NewTrie(int(params.BeaconConfig().DepositContractTreeDepth)) + require.NoError(t, err) + depositCache, err := depositcache.NewDepositCache() + require.NoError(t, err) + depositCache.InsertDeposit(context.Background(), dc.Deposit, dc.Eth1BlockHeight, dc.Index, depositTrie.Root()) + + beaconState, err := beaconstate.InitializeFromProto(&pbp2p.BeaconState{ + Eth1DataVotes: []*ethpb.Eth1Data{ + {BlockHash: []byte("before_range")}, + {BlockHash: []byte("before_range")}, + {BlockHash: []byte("first")}, + }, + }) + require.NoError(t, err) + + ps := &Server{ + ChainStartFetcher: p, + Eth1InfoFetcher: p, + Eth1BlockFetcher: p, + BlockFetcher: p, + DepositFetcher: depositCache, + HeadFetcher: &mock.ChainService{State: beaconState, ETH1Data: ðpb.Eth1Data{}}, + } + + ctx := context.Background() + majorityVoteEth1Data, err := ps.eth1DataMajorityVote(ctx, slot) + require.NoError(t, err) + + hash := majorityVoteEth1Data.BlockHash + + expectedHash := []byte("first") + if bytes.Compare(hash, expectedHash) != 0 { + t.Errorf("Chosen eth1data for block hash %v vs expected %v", hash, expectedHash) + } +} + +func TestEth1DataMajorityVote_HighestCountAfterRange_ChooseHighestCountWithinRange(t *testing.T) { + slot := uint64(64) + + p := &mockPOW.POWChain{ + BlockNumberByTime: map[uint64]*big.Int{ + 32 * params.BeaconConfig().SecondsPerSlot: big.NewInt(50), + 64 * params.BeaconConfig().SecondsPerSlot: big.NewInt(100), + }, + HashesByHeight: map[int][]byte{ + int(100 - params.BeaconConfig().Eth1FollowDistance): []byte("first"), + int(100 - params.BeaconConfig().Eth1FollowDistance + 1): []byte("after_range"), + }, + } + + dc := dbpb.DepositContainer{ + Index: 0, + Eth1BlockHeight: 0, + Deposit: ðpb.Deposit{ + Data: ðpb.Deposit_Data{ + PublicKey: []byte("a"), + Signature: make([]byte, 96), + WithdrawalCredentials: make([]byte, 32), + }}, + } + depositTrie, err := trieutil.NewTrie(int(params.BeaconConfig().DepositContractTreeDepth)) + require.NoError(t, err) + depositCache, err := depositcache.NewDepositCache() + require.NoError(t, err) + depositCache.InsertDeposit(context.Background(), dc.Deposit, dc.Eth1BlockHeight, dc.Index, depositTrie.Root()) + + beaconState, err := beaconstate.InitializeFromProto(&pbp2p.BeaconState{ + Eth1DataVotes: []*ethpb.Eth1Data{ + {BlockHash: []byte("first")}, + {BlockHash: []byte("after_range")}, + {BlockHash: []byte("after_range")}, + }, + }) + require.NoError(t, err) + + ps := &Server{ + ChainStartFetcher: p, + Eth1InfoFetcher: p, + Eth1BlockFetcher: p, + BlockFetcher: p, + DepositFetcher: depositCache, + HeadFetcher: &mock.ChainService{State: beaconState, ETH1Data: ðpb.Eth1Data{}}, + } + + ctx := context.Background() + majorityVoteEth1Data, err := ps.eth1DataMajorityVote(ctx, slot) + require.NoError(t, err) + + hash := majorityVoteEth1Data.BlockHash + + expectedHash := []byte("first") + if bytes.Compare(hash, expectedHash) != 0 { + t.Errorf("Chosen eth1data for block hash %v vs expected %v", hash, expectedHash) + } +} + +func TestEth1DataMajorityVote_HighestCountOnUnknownBlock_ChooseKnownBlockWithHighestCount(t *testing.T) { + slot := uint64(64) + + p := &mockPOW.POWChain{ + BlockNumberByTime: map[uint64]*big.Int{ + 32 * params.BeaconConfig().SecondsPerSlot: big.NewInt(50), + 64 * params.BeaconConfig().SecondsPerSlot: big.NewInt(100), + }, + HashesByHeight: map[int][]byte{ + int(100 - params.BeaconConfig().Eth1FollowDistance - 1): []byte("first"), + int(100 - params.BeaconConfig().Eth1FollowDistance): []byte("second"), + }, + } + + dc := dbpb.DepositContainer{ + Index: 0, + Eth1BlockHeight: 0, + Deposit: ðpb.Deposit{ + Data: ðpb.Deposit_Data{ + PublicKey: []byte("a"), + Signature: make([]byte, 96), + WithdrawalCredentials: make([]byte, 32), + }}, + } + depositTrie, err := trieutil.NewTrie(int(params.BeaconConfig().DepositContractTreeDepth)) + require.NoError(t, err) + depositCache, err := depositcache.NewDepositCache() + require.NoError(t, err) + depositCache.InsertDeposit(context.Background(), dc.Deposit, dc.Eth1BlockHeight, dc.Index, depositTrie.Root()) + + beaconState, err := beaconstate.InitializeFromProto(&pbp2p.BeaconState{ + Eth1DataVotes: []*ethpb.Eth1Data{ + {BlockHash: []byte("unknown")}, + {BlockHash: []byte("unknown")}, + {BlockHash: []byte("first")}, + }, + }) + require.NoError(t, err) + + ps := &Server{ + ChainStartFetcher: p, + Eth1InfoFetcher: p, + Eth1BlockFetcher: p, + BlockFetcher: p, + DepositFetcher: depositCache, + HeadFetcher: &mock.ChainService{State: beaconState, ETH1Data: ðpb.Eth1Data{}}, + } + + ctx := context.Background() + majorityVoteEth1Data, err := ps.eth1DataMajorityVote(ctx, slot) + require.NoError(t, err) + + hash := majorityVoteEth1Data.BlockHash + + expectedHash := []byte("first") + if bytes.Compare(hash, expectedHash) != 0 { + t.Errorf("Chosen eth1data for block hash %v vs expected %v", hash, expectedHash) + } +} + +func TestEth1DataMajorityVote_NoVotesInRange_ChooseDefault(t *testing.T) { + slot := uint64(64) + + p := &mockPOW.POWChain{ + BlockNumberByTime: map[uint64]*big.Int{ + 32 * params.BeaconConfig().SecondsPerSlot: big.NewInt(50), + 64 * params.BeaconConfig().SecondsPerSlot: big.NewInt(100), + }, + HashesByHeight: map[int][]byte{ + int(50 - params.BeaconConfig().Eth1FollowDistance - 1): []byte("before_range"), + int(100 - params.BeaconConfig().Eth1FollowDistance - 1): []byte("first"), + int(100 - params.BeaconConfig().Eth1FollowDistance): []byte("second"), + int(100 - params.BeaconConfig().Eth1FollowDistance + 1): []byte("after_range"), + }, + } + + dc := dbpb.DepositContainer{ + Index: 0, + Eth1BlockHeight: 0, + Deposit: ðpb.Deposit{ + Data: ðpb.Deposit_Data{ + PublicKey: []byte("a"), + Signature: make([]byte, 96), + WithdrawalCredentials: make([]byte, 32), + }}, + } + depositTrie, err := trieutil.NewTrie(int(params.BeaconConfig().DepositContractTreeDepth)) + require.NoError(t, err) + depositCache, err := depositcache.NewDepositCache() + require.NoError(t, err) + depositCache.InsertDeposit(context.Background(), dc.Deposit, dc.Eth1BlockHeight, dc.Index, depositTrie.Root()) + + beaconState, err := beaconstate.InitializeFromProto(&pbp2p.BeaconState{ + Eth1DataVotes: []*ethpb.Eth1Data{ + {BlockHash: []byte("before_range")}, + {BlockHash: []byte("after_range")}, + }, + }) + require.NoError(t, err) + + ps := &Server{ + ChainStartFetcher: p, + Eth1InfoFetcher: p, + Eth1BlockFetcher: p, + BlockFetcher: p, + DepositFetcher: depositCache, + HeadFetcher: &mock.ChainService{State: beaconState, ETH1Data: ðpb.Eth1Data{}}, + } + + ctx := context.Background() + majorityVoteEth1Data, err := ps.eth1DataMajorityVote(ctx, slot) + require.NoError(t, err) + + hash := majorityVoteEth1Data.BlockHash + + expectedHash := make([]byte, 32) + copy(expectedHash, "second") + if bytes.Compare(hash, expectedHash) != 0 { + t.Errorf("Chosen eth1data for block hash %v vs expected %v", hash, expectedHash) + } +} + +func TestEth1DataMajorityVote_NoVotes_ChooseDefault(t *testing.T) { + slot := uint64(64) + + p := &mockPOW.POWChain{ + BlockNumberByTime: map[uint64]*big.Int{ + 32 * params.BeaconConfig().SecondsPerSlot: big.NewInt(50), + 64 * params.BeaconConfig().SecondsPerSlot: big.NewInt(100), + }, + HashesByHeight: map[int][]byte{ + int(100 - params.BeaconConfig().Eth1FollowDistance - 1): []byte("first"), + int(100 - params.BeaconConfig().Eth1FollowDistance): []byte("second"), + }, + } + + dc := dbpb.DepositContainer{ + Index: 0, + Eth1BlockHeight: 0, + Deposit: ðpb.Deposit{ + Data: ðpb.Deposit_Data{ + PublicKey: []byte("a"), + Signature: make([]byte, 96), + WithdrawalCredentials: make([]byte, 32), + }}, + } + depositTrie, err := trieutil.NewTrie(int(params.BeaconConfig().DepositContractTreeDepth)) + require.NoError(t, err) + depositCache, err := depositcache.NewDepositCache() + require.NoError(t, err) + depositCache.InsertDeposit(context.Background(), dc.Deposit, dc.Eth1BlockHeight, dc.Index, depositTrie.Root()) + + beaconState, err := beaconstate.InitializeFromProto(&pbp2p.BeaconState{Eth1DataVotes: []*ethpb.Eth1Data{}}) + require.NoError(t, err) + + ps := &Server{ + ChainStartFetcher: p, + Eth1InfoFetcher: p, + Eth1BlockFetcher: p, + BlockFetcher: p, + DepositFetcher: depositCache, + HeadFetcher: &mock.ChainService{State: beaconState, ETH1Data: ðpb.Eth1Data{}}, + } + + ctx := context.Background() + majorityVoteEth1Data, err := ps.eth1DataMajorityVote(ctx, slot) + require.NoError(t, err) + + hash := majorityVoteEth1Data.BlockHash + + expectedHash := make([]byte, 32) + copy(expectedHash, "second") + if bytes.Compare(hash, expectedHash) != 0 { + t.Errorf("Chosen eth1data for block hash %v vs expected %v", hash, expectedHash) + } +} + +func TestEth1DataMajorityVote_SameCount_ChooseMoreRecentBlock(t *testing.T) { + slot := uint64(64) + + p := &mockPOW.POWChain{ + BlockNumberByTime: map[uint64]*big.Int{ + 32 * params.BeaconConfig().SecondsPerSlot: big.NewInt(50), + 64 * params.BeaconConfig().SecondsPerSlot: big.NewInt(100), + }, + HashesByHeight: map[int][]byte{ + int(100 - params.BeaconConfig().Eth1FollowDistance - 1): []byte("first"), + int(100 - params.BeaconConfig().Eth1FollowDistance): []byte("second"), + }, + } + + dc := dbpb.DepositContainer{ + Index: 0, + Eth1BlockHeight: 0, + Deposit: ðpb.Deposit{ + Data: ðpb.Deposit_Data{ + PublicKey: []byte("a"), + Signature: make([]byte, 96), + WithdrawalCredentials: make([]byte, 32), + }}, + } + depositTrie, err := trieutil.NewTrie(int(params.BeaconConfig().DepositContractTreeDepth)) + require.NoError(t, err) + depositCache, err := depositcache.NewDepositCache() + require.NoError(t, err) + depositCache.InsertDeposit(context.Background(), dc.Deposit, dc.Eth1BlockHeight, dc.Index, depositTrie.Root()) + + beaconState, err := beaconstate.InitializeFromProto(&pbp2p.BeaconState{ + Eth1DataVotes: []*ethpb.Eth1Data{ + {BlockHash: []byte("first")}, + {BlockHash: []byte("second")}, + }, + }) + require.NoError(t, err) + + ps := &Server{ + ChainStartFetcher: p, + Eth1InfoFetcher: p, + Eth1BlockFetcher: p, + BlockFetcher: p, + DepositFetcher: depositCache, + HeadFetcher: &mock.ChainService{State: beaconState, ETH1Data: ðpb.Eth1Data{}}, + } + + ctx := context.Background() + majorityVoteEth1Data, err := ps.eth1DataMajorityVote(ctx, slot) + require.NoError(t, err) + + hash := majorityVoteEth1Data.BlockHash + + expectedHash := []byte("second") + if bytes.Compare(hash, expectedHash) != 0 { + t.Errorf("Chosen eth1data for block hash %v vs expected %v", hash, expectedHash) + } +} + func TestFilterAttestation_OK(t *testing.T) { db, _ := dbutil.SetupDB(t) ctx := context.Background() diff --git a/shared/featureconfig/config.go b/shared/featureconfig/config.go index b4a290d1d71..d5c6fc6468d 100644 --- a/shared/featureconfig/config.go +++ b/shared/featureconfig/config.go @@ -59,6 +59,7 @@ type Flags struct { BatchBlockVerify bool // BatchBlockVerify performs batched verification of block batches that we receive when syncing. InitSyncVerbose bool // InitSyncVerbose logs every processed block during initial syncing. EnableFinalizedDepositsCache bool // EnableFinalizedDepositsCache enables utilization of cached finalized deposits. + EnableEth1DataMajorityVote bool // EnableEth1DataMajorityVote uses the Voting With The Majority algorithm to vote for eth1data. // DisableForkChoice disables using LMD-GHOST fork choice to update // the head of the chain based on attestations and instead accepts any valid received block @@ -244,6 +245,10 @@ func ConfigureBeaconChain(ctx *cli.Context) { log.Warn("Enabling finalized deposits cache") cfg.EnableFinalizedDepositsCache = true } + if ctx.Bool(enableEth1DataMajorityVote.Name) { + log.Warn("Enabling eth1data majority vote") + cfg.EnableEth1DataMajorityVote = true + } Init(cfg) } diff --git a/shared/featureconfig/flags.go b/shared/featureconfig/flags.go index 11d9952b689..5a0feabf5a4 100644 --- a/shared/featureconfig/flags.go +++ b/shared/featureconfig/flags.go @@ -156,12 +156,16 @@ var ( } initSyncVerbose = &cli.BoolFlag{ Name: "init-sync-verbose", - Usage: "Enable logging every processed block during initial syncing. ", + Usage: "Enable logging every processed block during initial syncing.", } enableFinalizedDepositsCache = &cli.BoolFlag{ Name: "enable-finalized-deposits-cache", Usage: "Enables utilization of cached finalized deposits", } + enableEth1DataMajorityVote = &cli.BoolFlag{ + Name: "enable-eth1-data-majority-vote", + Usage: "When enabled, voting on eth1 data will use the Voting With The Majority algorithm.", + } ) // devModeFlags holds list of flags that are set when development mode is on. @@ -619,6 +623,7 @@ var BeaconChainFlags = append(deprecatedFlags, []cli.Flag{ batchBlockVerify, initSyncVerbose, enableFinalizedDepositsCache, + enableEth1DataMajorityVote, }...) // E2EBeaconChainFlags contains a list of the beacon chain feature flags to be tested in E2E. @@ -629,4 +634,5 @@ var E2EBeaconChainFlags = []string{ "--attestation-aggregation-strategy=max_cover", "--dev", "--enable-finalized-deposits-cache", + "--enable-eth1-data-majority-vote", } diff --git a/shared/params/minimal_config.go b/shared/params/minimal_config.go index 6d2971ee682..76723456580 100644 --- a/shared/params/minimal_config.go +++ b/shared/params/minimal_config.go @@ -76,5 +76,6 @@ func MinimalSpecConfig() *BeaconChainConfig { minimalConfig.DepositContractTreeDepth = 32 minimalConfig.FarFutureEpoch = 1<<64 - 1 + return minimalConfig }