diff --git a/beacon-chain/powchain/service.go b/beacon-chain/powchain/service.go index f22f6afa631c..920df8414e07 100644 --- a/beacon-chain/powchain/service.go +++ b/beacon-chain/powchain/service.go @@ -83,7 +83,7 @@ type Web3Service struct { reader Reader logger bind.ContractFilterer blockFetcher POWBlockFetcher - blockNumber *big.Int // the latest ETH1.0 chain blockNumber. + blockHeight *big.Int // the latest ETH1.0 chain blockHeight. blockHash common.Hash // the latest ETH1.0 chain blockHash. depositContractCaller *contracts.DepositContractCaller depositRoot []byte @@ -135,7 +135,7 @@ func NewWeb3Service(ctx context.Context, config *Web3ServiceConfig) (*Web3Servic cancel: cancel, headerChan: make(chan *gethTypes.Header), endpoint: config.Endpoint, - blockNumber: nil, + blockHeight: nil, blockHash: common.BytesToHash([]byte{}), depositContractAddress: config.DepositContract, chainStartFeed: new(event.Feed), @@ -190,9 +190,15 @@ func (w *Web3Service) Status() error { return nil } -// LatestBlockNumber in the ETH1.0 chain. -func (w *Web3Service) LatestBlockNumber() *big.Int { - return w.blockNumber +// DepositRoot returns the Merkle root of the latest deposit trie +// from the ETH1.0 deposit contract. +func (w *Web3Service) DepositRoot() [32]byte { + return w.depositTrie.Root() +} + +// LatestBlockHeight in the ETH1.0 chain. +func (w *Web3Service) LatestBlockHeight() *big.Int { + return w.blockHeight } // LatestBlockHash in the ETH1.0 chain. @@ -200,6 +206,18 @@ func (w *Web3Service) LatestBlockHash() common.Hash { return w.blockHash } +// BlockExists -- +// TODO(#1657): Unimplemented, Work in Progress. +func (w *Web3Service) BlockExists(hash common.Hash) (bool, *big.Int, error) { + return false, big.NewInt(0), nil +} + +// BlockHashByHeight -- +// TODO(#1657): Unimplemented, Work in Progress. +func (w *Web3Service) BlockHashByHeight(height *big.Int) (common.Hash, error) { + return [32]byte{}, nil +} + // Client for interacting with the ETH1.0 chain. func (w *Web3Service) Client() Client { return w.client @@ -355,7 +373,7 @@ func (w *Web3Service) run(done <-chan struct{}) { return } - w.blockNumber = header.Number + w.blockHeight = header.Number w.blockHash = header.Hash() // Only process logs if the chain start delay flag is not enabled. @@ -380,14 +398,14 @@ func (w *Web3Service) run(done <-chan struct{}) { return case header := <-w.headerChan: blockNumberGauge.Set(float64(header.Number.Int64())) - w.blockNumber = header.Number + w.blockHeight = header.Number w.blockHash = header.Hash() log.WithFields(logrus.Fields{ - "blockNumber": w.blockNumber, + "blockNumber": w.blockHeight, "blockHash": w.blockHash.Hex(), }).Debug("Latest web3 chain event") case <-ticker.C: - if w.lastRequestedBlock.Cmp(w.blockNumber) == 0 { + if w.lastRequestedBlock.Cmp(w.blockHeight) == 0 { continue } if err := w.requestBatchedLogs(); err != nil { @@ -436,7 +454,7 @@ func (w *Web3Service) processPastLogs() error { for _, log := range logs { w.ProcessLog(log) } - w.lastRequestedBlock.Set(w.blockNumber) + w.lastRequestedBlock.Set(w.blockHeight) return nil } @@ -446,7 +464,7 @@ func (w *Web3Service) requestBatchedLogs() error { // We request for the nth block behind the current head, in order to have // stabilised logs when we retrieve it from the 1.0 chain. - requestedBlock := big.NewInt(0).Sub(w.blockNumber, big.NewInt(params.BeaconConfig().LogBlockDelay)) + requestedBlock := big.NewInt(0).Sub(w.blockHeight, big.NewInt(params.BeaconConfig().LogBlockDelay)) query := ethereum.FilterQuery{ Addresses: []common.Address{ w.depositContractAddress, diff --git a/beacon-chain/powchain/service_test.go b/beacon-chain/powchain/service_test.go index 4fcb339077f3..49f4bf618a98 100644 --- a/beacon-chain/powchain/service_test.go +++ b/beacon-chain/powchain/service_test.go @@ -383,8 +383,8 @@ func TestLatestMainchainInfo(t *testing.T) { web3Service.cancel() exitRoutine <- true - if web3Service.blockNumber.Cmp(header.Number) != 0 { - t.Errorf("block number not set, expected %v, got %v", header.Number, web3Service.blockNumber) + if web3Service.blockHeight.Cmp(header.Number) != 0 { + t.Errorf("block number not set, expected %v, got %v", header.Number, web3Service.blockHeight) } if web3Service.blockHash.Hex() != header.Hash().Hex() { diff --git a/beacon-chain/rpc/BUILD.bazel b/beacon-chain/rpc/BUILD.bazel index aaeccb9056fe..23dbc4afd4a5 100644 --- a/beacon-chain/rpc/BUILD.bazel +++ b/beacon-chain/rpc/BUILD.bazel @@ -24,6 +24,7 @@ go_library( "//shared/hashutil:go_default_library", "//shared/params:go_default_library", "//shared/ssz:go_default_library", + "@com_github_ethereum_go_ethereum//common:go_default_library", "@com_github_gogo_protobuf//types:go_default_library", "@com_github_sirupsen_logrus//:go_default_library", "@org_golang_google_grpc//:go_default_library", @@ -50,10 +51,12 @@ go_test( "//beacon-chain/internal:go_default_library", "//proto/beacon/p2p/v1:go_default_library", "//proto/beacon/rpc/v1:go_default_library", + "//shared/bytesutil:go_default_library", "//shared/event:go_default_library", "//shared/params:go_default_library", "//shared/ssz:go_default_library", "//shared/testutil:go_default_library", + "@com_github_ethereum_go_ethereum//common:go_default_library", "@com_github_gogo_protobuf//proto:go_default_library", "@com_github_gogo_protobuf//types:go_default_library", "@com_github_golang_mock//gomock:go_default_library", diff --git a/beacon-chain/rpc/beacon_server.go b/beacon-chain/rpc/beacon_server.go index 12d6641ce134..12a953e813e4 100644 --- a/beacon-chain/rpc/beacon_server.go +++ b/beacon-chain/rpc/beacon_server.go @@ -7,6 +7,8 @@ import ( "math/big" "time" + "github.com/prysmaticlabs/prysm/shared/bytesutil" + ptypes "github.com/gogo/protobuf/types" "github.com/prysmaticlabs/prysm/beacon-chain/db" pbp2p "github.com/prysmaticlabs/prysm/proto/beacon/p2p/v1" @@ -97,24 +99,122 @@ func (bs *BeaconServer) LatestAttestation(req *ptypes.Empty, stream pb.BeaconSer } } -// Eth1Data fetches the current ETH 1 data which should be used when voting via -// block proposal. -// TODO(1463): Implement this. +// Eth1Data is a mechanism used by block proposers vote on a recent Ethereum 1.0 block hash and an +// associated deposit root found in the Ethereum 1.0 deposit contract. When consensus is formed, +// state.latest_eth1_data is updated, and validator deposits up to this root can be processed. +// The deposit root can be calculated by calling the get_deposit_root() function of +// the deposit contract using the post-state of the block hash. func (bs *BeaconServer) Eth1Data(ctx context.Context, _ *ptypes.Empty) (*pb.Eth1DataResponse, error) { - return &pb.Eth1DataResponse{}, nil + beaconState, err := bs.beaconDB.State() + if err != nil { + return nil, fmt.Errorf("could not fetch beacon state: %v", err) + } + dataVotes := []*pbp2p.Eth1DataVote{} + eth1FollowDistance := int64(params.BeaconConfig().Eth1FollowDistance) + for _, vote := range beaconState.Eth1DataVotes { + eth1Hash := bytesutil.ToBytes32(vote.Eth1Data.BlockHash32) + // Verify the block from the vote's block hash exists in the eth1.0 chain and fetch its height. + blockExists, blockHeight, err := bs.powChainService.BlockExists(eth1Hash) + if err != nil { + log.Errorf("Could not verify block with hash exists in Eth1 chain: %#x: %v", eth1Hash, err) + continue + } + if !blockExists { + continue + } + // Fetch the current canonical chain height from the eth1.0 chain. + currentHeight := bs.powChainService.LatestBlockHeight() + // Fetch the height of the block pointed to by the beacon state's latest_eth1_data.block_hash + // in the canonical, eth1.0 chain. + stateLatestEth1Hash := bytesutil.ToBytes32(beaconState.LatestEth1Data.BlockHash32) + _, stateLatestEth1Height, err := bs.powChainService.BlockExists(stateLatestEth1Hash) + if err != nil { + log.Errorf("Could not verify block with hash exists in Eth1 chain: %#x: %v", eth1Hash, err) + continue + } + // Let dataVotes be the set of Eth1DataVote objects vote in state.eth1_data_votes where: + // vote.eth1_data.block_hash is the hash of an eth1.0 block that is: + // (i) part of the canonical chain + // (ii) >= ETH1_FOLLOW_DISTANCE blocks behind the head + // (iii) newer than state.latest_eth1_data.block_data. + // vote.eth1_data.deposit_root is the deposit root of the eth1.0 deposit contract + // at the block defined by vote.eth1_data.block_hash. + isBehindFollowDistance := blockHeight.Add(blockHeight, big.NewInt(eth1FollowDistance)).Cmp(currentHeight) >= -1 + isAheadStateLatestEth1Data := blockHeight.Cmp(stateLatestEth1Height) == 1 + if blockExists && isBehindFollowDistance && isAheadStateLatestEth1Data { + dataVotes = append(dataVotes, vote) + } + } + + // Now we handle the following two scenarios: + // If dataVotes is empty: + // Let block_hash be the block hash of the ETH1_FOLLOW_DISTANCE'th ancestor of the head of + // the canonical eth1.0 chain. + // Let deposit_root be the deposit root of the eth1.0 deposit contract in the + // post-state of the block referenced by block_hash. + if len(dataVotes) == 0 { + // Fetch the current canonical chain height from the eth1.0 chain. + currentHeight := bs.powChainService.LatestBlockHeight() + ancestorHeight := currentHeight.Sub(currentHeight, big.NewInt(eth1FollowDistance)) + blockHash, err := bs.powChainService.BlockHashByHeight(ancestorHeight) + if err != nil { + return nil, fmt.Errorf("could not fetch ETH1_FOLLOW_DISTANCE ancestor: %v", err) + } + // TODO(#1656): Fetch the deposit root of the post-state deposit contract of the block + // references by the block hash of the ancestor instead. + depositRoot := bs.powChainService.DepositRoot() + return &pb.Eth1DataResponse{ + Eth1Data: &pbp2p.Eth1Data{ + DepositRootHash32: depositRoot[:], + BlockHash32: blockHash[:], + }, + }, nil + } + + // If dataVotes is non-empty: + // Let best_vote be the member of D that has the highest vote.eth1_data.vote_count, + // breaking ties by favoring block hashes with higher associated block height. + // Let block_hash = best_vote.eth1_data.block_hash. + // Let deposit_root = best_vote.eth1_data.deposit_root. + bestVote := dataVotes[0] + for i := 1; i < len(dataVotes); i++ { + vote := dataVotes[i] + if vote.VoteCount > bestVote.VoteCount { + bestVote = vote + } else if vote.VoteCount == bestVote.VoteCount { + bestVoteHash := bytesutil.ToBytes32(bestVote.Eth1Data.BlockHash32) + voteHash := bytesutil.ToBytes32(vote.Eth1Data.BlockHash32) + _, bestVoteHeight, err := bs.powChainService.BlockExists(bestVoteHash) + if err != nil { + log.Errorf("Could not fetch block height: %v", err) + continue + } + _, voteHeight, err := bs.powChainService.BlockExists(voteHash) + if err != nil { + log.Errorf("Could not fetch block height: %v", err) + continue + } + if voteHeight.Cmp(bestVoteHeight) == 1 { + bestVote = vote + } + } + } + return &pb.Eth1DataResponse{ + Eth1Data: &pbp2p.Eth1Data{ + BlockHash32: bestVote.Eth1Data.BlockHash32, + DepositRootHash32: bestVote.Eth1Data.DepositRootHash32, + }, + }, nil } // PendingDeposits returns a list of pending deposits that are ready for // inclusion in the next beacon block. func (bs *BeaconServer) PendingDeposits(ctx context.Context, _ *ptypes.Empty) (*pb.PendingDepositsResponse, error) { - bNum := bs.powChainService.LatestBlockNumber() - + bNum := bs.powChainService.LatestBlockHeight() if bNum == nil { return nil, errors.New("latest PoW block number is unknown") } - // Only request deposits that have passed the ETH1 follow distance window. bNum = bNum.Sub(bNum, big.NewInt(int64(params.BeaconConfig().Eth1FollowDistance))) - return &pb.PendingDepositsResponse{PendingDeposits: bs.beaconDB.PendingDeposits(ctx, bNum)}, nil } diff --git a/beacon-chain/rpc/beacon_server_test.go b/beacon-chain/rpc/beacon_server_test.go index b61445d29538..5b2119cb08d6 100644 --- a/beacon-chain/rpc/beacon_server_test.go +++ b/beacon-chain/rpc/beacon_server_test.go @@ -1,13 +1,19 @@ package rpc import ( + "bytes" "context" "errors" + "fmt" "math/big" "reflect" + "strings" "testing" "time" + "github.com/ethereum/go-ethereum/common" + "github.com/prysmaticlabs/prysm/shared/bytesutil" + "github.com/prysmaticlabs/prysm/shared/event" ptypes "github.com/gogo/protobuf/types" @@ -31,13 +37,26 @@ func (f *faultyPOWChainService) HasChainStartLogOccurred() (bool, uint64, error) func (f *faultyPOWChainService) ChainStartFeed() *event.Feed { return f.chainStartFeed } -func (f *faultyPOWChainService) LatestBlockNumber() *big.Int { +func (f *faultyPOWChainService) LatestBlockHeight() *big.Int { return big.NewInt(0) } +func (f *faultyPOWChainService) BlockExists(hash common.Hash) (bool, *big.Int, error) { + return false, big.NewInt(1), errors.New("failed") +} + +func (f *faultyPOWChainService) BlockHashByHeight(height *big.Int) (common.Hash, error) { + return [32]byte{}, errors.New("failed") +} + +func (f *faultyPOWChainService) DepositRoot() [32]byte { + return [32]byte{} +} + type mockPOWChainService struct { chainStartFeed *event.Feed latestBlockNumber *big.Int + hashesByHeight map[int][]byte } func (m *mockPOWChainService) HasChainStartLogOccurred() (bool, uint64, error) { @@ -46,10 +65,38 @@ func (m *mockPOWChainService) HasChainStartLogOccurred() (bool, uint64, error) { func (m *mockPOWChainService) ChainStartFeed() *event.Feed { return m.chainStartFeed } -func (m *mockPOWChainService) LatestBlockNumber() *big.Int { +func (m *mockPOWChainService) LatestBlockHeight() *big.Int { return m.latestBlockNumber } +func (m *mockPOWChainService) BlockExists(hash common.Hash) (bool, *big.Int, error) { + // Reverse the map of heights by hash. + heightsByHash := make(map[[32]byte]int) + for k, v := range m.hashesByHeight { + h := bytesutil.ToBytes32(v) + heightsByHash[h] = k + } + val, ok := heightsByHash[hash] + if !ok { + return false, nil, fmt.Errorf("could not fetch height for hash: %#x", hash) + } + return true, big.NewInt(int64(val)), nil +} + +func (m *mockPOWChainService) BlockHashByHeight(height *big.Int) (common.Hash, error) { + k := int(height.Int64()) + val, ok := m.hashesByHeight[k] + if !ok { + return [32]byte{}, fmt.Errorf("could not fetch hash for height: %v", height) + } + return bytesutil.ToBytes32(val), nil +} + +func (m *mockPOWChainService) DepositRoot() [32]byte { + root := []byte("depositroot") + return bytesutil.ToBytes32(root) +} + func TestWaitForChainStart_ContextClosed(t *testing.T) { hook := logTest.NewGlobal() ctx, cancel := context.WithCancel(context.Background()) @@ -282,3 +329,136 @@ func TestPendingDeposits_ReturnsDepositsOutsideEth1FollowWindow(t *testing.T) { ) } } + +func TestEth1Data_EmptyVotesFetchBlockHashFailure(t *testing.T) { + db := internal.SetupDB(t) + defer internal.TeardownDB(t, db) + beaconServer := &BeaconServer{ + beaconDB: db, + powChainService: &faultyPOWChainService{}, + } + beaconState := &pbp2p.BeaconState{} + if err := beaconServer.beaconDB.SaveState(beaconState); err != nil { + t.Fatal(err) + } + want := "could not fetch ETH1_FOLLOW_DISTANCE ancestor" + if _, err := beaconServer.Eth1Data(context.Background(), nil); !strings.Contains(err.Error(), want) { + t.Errorf("Expected error %v, received %v", want, err) + } +} + +func TestEth1Data_EmptyVotesOk(t *testing.T) { + db := internal.SetupDB(t) + defer internal.TeardownDB(t, db) + powChainService := &mockPOWChainService{ + latestBlockNumber: big.NewInt(int64(params.BeaconConfig().Eth1FollowDistance)), + hashesByHeight: map[int][]byte{ + 0: []byte("hash0"), + }, + } + beaconServer := &BeaconServer{ + beaconDB: db, + powChainService: powChainService, + } + beaconState := &pbp2p.BeaconState{} + if err := beaconServer.beaconDB.SaveState(beaconState); err != nil { + t.Fatal(err) + } + result, err := beaconServer.Eth1Data(context.Background(), nil) + if err != nil { + t.Fatal(err) + } + // If the data vote objects are empty, the deposit root should be the one corresponding + // to the deposit contract in the powchain service, fetched using powChainService.DepositRoot() + depositRoot := beaconServer.powChainService.DepositRoot() + if !bytes.Equal(result.Eth1Data.DepositRootHash32, depositRoot[:]) { + t.Errorf( + "Expected deposit roots to match, received %#x == %#x", + result.Eth1Data.DepositRootHash32, + depositRoot, + ) + } +} + +func TestEth1Data_NonEmptyVotesSelectsBestVote(t *testing.T) { + db := internal.SetupDB(t) + defer internal.TeardownDB(t, db) + eth1DataVotes := []*pbp2p.Eth1DataVote{ + { + VoteCount: 1, + Eth1Data: &pbp2p.Eth1Data{ + BlockHash32: []byte("block0"), + DepositRootHash32: []byte("deposit0"), + }, + }, + { + VoteCount: 2, + Eth1Data: &pbp2p.Eth1Data{ + BlockHash32: []byte("block1"), + DepositRootHash32: []byte("deposit1"), + }, + }, + // We include the case in which the vote counts might match and in that + // case we break ties by checking which block hash has the greatest + // block height in the eth1.0 chain, accordingly. + { + VoteCount: 3, + Eth1Data: &pbp2p.Eth1Data{ + BlockHash32: []byte("block2"), + DepositRootHash32: []byte("deposit2"), + }, + }, + { + VoteCount: 3, + Eth1Data: &pbp2p.Eth1Data{ + BlockHash32: []byte("block4"), + DepositRootHash32: []byte("deposit3"), + }, + }, + } + beaconState := &pbp2p.BeaconState{ + Eth1DataVotes: eth1DataVotes, + LatestEth1Data: &pbp2p.Eth1Data{ + BlockHash32: []byte("stub"), + }, + } + if err := db.SaveState(beaconState); err != nil { + t.Fatal(err) + } + currentHeight := params.BeaconConfig().Eth1FollowDistance + 5 + beaconServer := &BeaconServer{ + beaconDB: db, + powChainService: &mockPOWChainService{ + latestBlockNumber: big.NewInt(int64(currentHeight)), + hashesByHeight: map[int][]byte{ + 0: beaconState.LatestEth1Data.BlockHash32, + 1: beaconState.Eth1DataVotes[0].Eth1Data.BlockHash32, + 2: beaconState.Eth1DataVotes[1].Eth1Data.BlockHash32, + 3: beaconState.Eth1DataVotes[3].Eth1Data.BlockHash32, + // We will give the hash at index 2 in the beacon state's latest eth1 votes + // priority in being selected as the best vote by giving it the highest block number. + 4: beaconState.Eth1DataVotes[2].Eth1Data.BlockHash32, + }, + }, + } + result, err := beaconServer.Eth1Data(context.Background(), nil) + if err != nil { + t.Fatal(err) + } + // Vote at index 2 should have won the best vote selection mechanism as it had the highest block number + // despite being tied at vote count with the vote at index 3. + if !bytes.Equal(result.Eth1Data.BlockHash32, beaconState.Eth1DataVotes[2].Eth1Data.BlockHash32) { + t.Errorf( + "Expected block hashes to match, received %#x == %#x", + result.Eth1Data.BlockHash32, + beaconState.Eth1DataVotes[2].Eth1Data.BlockHash32, + ) + } + if !bytes.Equal(result.Eth1Data.DepositRootHash32, beaconState.Eth1DataVotes[2].Eth1Data.DepositRootHash32) { + t.Errorf( + "Expected deposit roots to match, received %#x == %#x", + result.Eth1Data.DepositRootHash32, + beaconState.Eth1DataVotes[2].Eth1Data.DepositRootHash32, + ) + } +} diff --git a/beacon-chain/rpc/service.go b/beacon-chain/rpc/service.go index 65527fca8b6c..a61ac9a525c5 100644 --- a/beacon-chain/rpc/service.go +++ b/beacon-chain/rpc/service.go @@ -8,6 +8,8 @@ import ( "net" "time" + "github.com/ethereum/go-ethereum/common" + "github.com/prysmaticlabs/prysm/beacon-chain/db" pbp2p "github.com/prysmaticlabs/prysm/proto/beacon/p2p/v1" pb "github.com/prysmaticlabs/prysm/proto/beacon/rpc/v1" @@ -44,7 +46,10 @@ type operationService interface { type powChainService interface { HasChainStartLogOccurred() (bool, uint64, error) ChainStartFeed() *event.Feed - LatestBlockNumber() *big.Int + LatestBlockHeight() *big.Int + BlockExists(hash common.Hash) (bool, *big.Int, error) + BlockHashByHeight(height *big.Int) (common.Hash, error) + DepositRoot() [32]byte } // Service defining an RPC server for a beacon node.