diff --git a/beacon-chain/powchain/engine-api-client/v1/mocks/BUILD.bazel b/beacon-chain/powchain/engine-api-client/v1/mocks/BUILD.bazel index 709bc5ca6..e9efc7ead 100644 --- a/beacon-chain/powchain/engine-api-client/v1/mocks/BUILD.bazel +++ b/beacon-chain/powchain/engine-api-client/v1/mocks/BUILD.bazel @@ -8,5 +8,6 @@ go_library( deps = [ "//proto/engine/v1:go_default_library", "@com_github_ethereum_go_ethereum//common:go_default_library", + "@com_github_pkg_errors//:go_default_library", ], ) diff --git a/beacon-chain/powchain/engine-api-client/v1/mocks/client.go b/beacon-chain/powchain/engine-api-client/v1/mocks/client.go index f3c25bc65..bc5c0b4ee 100644 --- a/beacon-chain/powchain/engine-api-client/v1/mocks/client.go +++ b/beacon-chain/powchain/engine-api-client/v1/mocks/client.go @@ -4,6 +4,7 @@ import ( "context" "github.com/ethereum/go-ethereum/common" + "github.com/pkg/errors" pb "github.com/prysmaticlabs/prysm/proto/engine/v1" ) @@ -15,6 +16,9 @@ type EngineClient struct { ExecutionPayload *pb.ExecutionPayload Err error ExecutionBlock *pb.ExecutionBlock + ErrLatestExecBlock error + ErrExecBlockByHash error + BlockByHashMap map[[32]byte]*pb.ExecutionBlock } // NewPayload -- @@ -41,10 +45,14 @@ func (e *EngineClient) ExchangeTransitionConfiguration(_ context.Context, _ *pb. // LatestExecutionBlock -- func (e *EngineClient) LatestExecutionBlock(_ context.Context) (*pb.ExecutionBlock, error) { - return e.ExecutionBlock, nil + return e.ExecutionBlock, e.ErrLatestExecBlock } // ExecutionBlockByHash -- -func (e *EngineClient) ExecutionBlockByHash(_ context.Context, _ common.Hash) (*pb.ExecutionBlock, error) { - return e.ExecutionBlock, nil +func (e *EngineClient) ExecutionBlockByHash(_ context.Context, h common.Hash) (*pb.ExecutionBlock, error) { + b, ok := e.BlockByHashMap[h] + if !ok { + return nil, errors.New("block not found") + } + return b, e.ErrExecBlockByHash } diff --git a/beacon-chain/rpc/prysm/v1alpha1/validator/BUILD.bazel b/beacon-chain/rpc/prysm/v1alpha1/validator/BUILD.bazel index 2da5222e1..a58796b41 100644 --- a/beacon-chain/rpc/prysm/v1alpha1/validator/BUILD.bazel +++ b/beacon-chain/rpc/prysm/v1alpha1/validator/BUILD.bazel @@ -15,6 +15,7 @@ go_library( "proposer_bellatrix.go", "proposer_deposits.go", "proposer_eth1data.go", + "proposer_execution_payload.go", "proposer_phase0.go", "proposer_sync_aggregate.go", "server.go", @@ -46,6 +47,7 @@ go_library( "//beacon-chain/operations/voluntaryexits:go_default_library", "//beacon-chain/p2p:go_default_library", "//beacon-chain/powchain:go_default_library", + "//beacon-chain/powchain/engine-api-client/v1:go_default_library", "//beacon-chain/state:go_default_library", "//beacon-chain/state/stategen:go_default_library", "//beacon-chain/sync:go_default_library", @@ -71,7 +73,9 @@ go_library( "//runtime/version:go_default_library", "//time:go_default_library", "//time/slots:go_default_library", + "@com_github_ethereum_go_ethereum//common/hexutil:go_default_library", "@com_github_ferranbt_fastssz//:go_default_library", + "@com_github_holiman_uint256//:go_default_library", "@com_github_pkg_errors//:go_default_library", "@com_github_prysmaticlabs_eth2_types//:go_default_library", "@com_github_prysmaticlabs_go_bitfield//:go_default_library", @@ -95,6 +99,7 @@ go_test( "blocks_test.go", "exit_test.go", "proposer_attestations_test.go", + "proposer_execution_payload_test.go", "proposer_sync_aggregate_test.go", "proposer_test.go", "server_test.go", @@ -124,6 +129,7 @@ go_test( "//beacon-chain/operations/synccommittee:go_default_library", "//beacon-chain/operations/voluntaryexits:go_default_library", "//beacon-chain/p2p/testing:go_default_library", + "//beacon-chain/powchain/engine-api-client/v1/mocks:go_default_library", "//beacon-chain/powchain/testing:go_default_library", "//beacon-chain/state:go_default_library", "//beacon-chain/state/stategen:go_default_library", @@ -149,7 +155,9 @@ go_test( "//time:go_default_library", "//time/slots:go_default_library", "@com_github_d4l3k_messagediff//:go_default_library", + "@com_github_ethereum_go_ethereum//common:go_default_library", "@com_github_golang_mock//gomock:go_default_library", + "@com_github_holiman_uint256//:go_default_library", "@com_github_prysmaticlabs_eth2_types//:go_default_library", "@com_github_prysmaticlabs_go_bitfield//:go_default_library", "@com_github_sirupsen_logrus//:go_default_library", diff --git a/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_execution_payload.go b/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_execution_payload.go new file mode 100644 index 000000000..0ff1009ca --- /dev/null +++ b/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_execution_payload.go @@ -0,0 +1,126 @@ +package validator + +import ( + "context" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/holiman/uint256" + "github.com/pkg/errors" + "github.com/prysmaticlabs/prysm/config/params" + "github.com/prysmaticlabs/prysm/encoding/bytesutil" + "github.com/sirupsen/logrus" +) + +// This returns the valid terminal block hash with an existence bool value. +// +// Spec code: +// def get_terminal_pow_block(pow_chain: Dict[Hash32, PowBlock]) -> Optional[PowBlock]: +// if TERMINAL_BLOCK_HASH != Hash32(): +// # Terminal block hash override takes precedence over terminal total difficulty +// if TERMINAL_BLOCK_HASH in pow_chain: +// return pow_chain[TERMINAL_BLOCK_HASH] +// else: +// return None +// +// return get_pow_block_at_terminal_total_difficulty(pow_chain) +func (vs *Server) getTerminalBlockHashIfExists(ctx context.Context) ([]byte, bool, error) { + terminalBlockHash := params.BeaconConfig().TerminalBlockHash + // Terminal block hash override takes precedence over terminal total difficulty. + if params.BeaconConfig().TerminalBlockHash != params.BeaconConfig().ZeroHash { + exists, _, err := vs.Eth1BlockFetcher.BlockExists(ctx, terminalBlockHash) + if err != nil { + return nil, false, err + } + if !exists { + return nil, false, nil + } + + return terminalBlockHash.Bytes(), true, nil + } + + return vs.getPowBlockHashAtTerminalTotalDifficulty(ctx) +} + +// This returns the valid terminal block hash based on total difficulty. +// +// Spec code: +// def get_pow_block_at_terminal_total_difficulty(pow_chain: Dict[Hash32, PowBlock]) -> Optional[PowBlock]: +// # `pow_chain` abstractly represents all blocks in the PoW chain +// for block in pow_chain: +// parent = pow_chain[block.parent_hash] +// block_reached_ttd = block.total_difficulty >= TERMINAL_TOTAL_DIFFICULTY +// parent_reached_ttd = parent.total_difficulty >= TERMINAL_TOTAL_DIFFICULTY +// if block_reached_ttd and not parent_reached_ttd: +// return block +// +// return None +func (vs *Server) getPowBlockHashAtTerminalTotalDifficulty(ctx context.Context) ([]byte, bool, error) { + ttd := new(big.Int) + ttd.SetString(params.BeaconConfig().TerminalTotalDifficulty, 10) + terminalTotalDifficulty, overflows := uint256.FromBig(ttd) + if overflows { + return nil, false, errors.New("could not convert terminal total difficulty to uint256") + } + blk, err := vs.ExecutionEngineCaller.LatestExecutionBlock(ctx) + if err != nil { + return nil, false, errors.Wrap(err, "could not get latest execution block") + } + if blk == nil { + return nil, false, errors.New("latest execution block is nil") + } + + for { + if ctx.Err() != nil { + return nil, false, ctx.Err() + } + currentTotalDifficulty, err := tDStringToUint256(blk.TotalDifficulty) + if err != nil { + return nil, false, errors.Wrap(err, "could not convert total difficulty to uint256") + } + blockReachedTTD := currentTotalDifficulty.Cmp(terminalTotalDifficulty) >= 0 + + parentHash := bytesutil.ToBytes32(blk.ParentHash) + if len(blk.ParentHash) == 0 || parentHash == params.BeaconConfig().ZeroHash { + return nil, false, nil + } + parentBlk, err := vs.ExecutionEngineCaller.ExecutionBlockByHash(ctx, parentHash) + if err != nil { + return nil, false, errors.Wrap(err, "could not get parent execution block") + } + if parentBlk == nil { + return nil, false, errors.New("parent execution block is nil") + } + if blockReachedTTD { + parentTotalDifficulty, err := tDStringToUint256(parentBlk.TotalDifficulty) + if err != nil { + return nil, false, errors.Wrap(err, "could not convert total difficulty to uint256") + } + parentReachedTTD := parentTotalDifficulty.Cmp(terminalTotalDifficulty) >= 0 + if !parentReachedTTD { + log.WithFields(logrus.Fields{ + "number": blk.Number, + "hash": fmt.Sprintf("%#x", bytesutil.Trunc(blk.Hash)), + "td": blk.TotalDifficulty, + "parentTd": parentBlk.TotalDifficulty, + "ttd": terminalTotalDifficulty, + }).Info("Retrieved terminal block hash") + return blk.Hash, true, nil + } + } + blk = parentBlk + } +} + +func tDStringToUint256(td string) (*uint256.Int, error) { + b, err := hexutil.DecodeBig(td) + if err != nil { + return nil, err + } + i, overflows := uint256.FromBig(b) + if overflows { + return nil, errors.New("total difficulty overflowed") + } + return i, nil +} diff --git a/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_execution_payload_test.go b/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_execution_payload_test.go new file mode 100644 index 000000000..78fed4894 --- /dev/null +++ b/beacon-chain/rpc/prysm/v1alpha1/validator/proposer_execution_payload_test.go @@ -0,0 +1,228 @@ +package validator + +import ( + "context" + "errors" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/holiman/uint256" + "github.com/prysmaticlabs/prysm/beacon-chain/powchain/engine-api-client/v1/mocks" + powtesting "github.com/prysmaticlabs/prysm/beacon-chain/powchain/testing" + "github.com/prysmaticlabs/prysm/config/params" + "github.com/prysmaticlabs/prysm/encoding/bytesutil" + pb "github.com/prysmaticlabs/prysm/proto/engine/v1" + "github.com/prysmaticlabs/prysm/testing/require" +) + +func Test_tDStringToUint256(t *testing.T) { + i, err := tDStringToUint256("0x0") + require.NoError(t, err) + require.DeepEqual(t, uint256.NewInt(0), i) + + i, err = tDStringToUint256("0x10000") + require.NoError(t, err) + require.DeepEqual(t, uint256.NewInt(65536), i) + + _, err = tDStringToUint256("100") + require.ErrorContains(t, "hex string without 0x prefix", err) + + _, err = tDStringToUint256("0xzzzzzz") + require.ErrorContains(t, "invalid hex string", err) + + _, err = tDStringToUint256("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" + + "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF") + require.ErrorContains(t, "hex number > 256 bits", err) +} + +func TestServer_getPowBlockHashAtTerminalTotalDifficulty(t *testing.T) { + tests := []struct { + name string + paramsTd string + currentPowBlock *pb.ExecutionBlock + parentPowBlock *pb.ExecutionBlock + errLatestExecutionBlk error + wantTerminalBlockHash []byte + wantExists bool + errString string + }{ + { + name: "config td overflows", + paramsTd: "1115792089237316195423570985008687907853269984665640564039457584007913129638912", + errString: "could not convert terminal total difficulty to uint256", + }, + { + name: "could not get latest execution block", + paramsTd: "1", + errLatestExecutionBlk: errors.New("blah"), + errString: "could not get latest execution block", + }, + { + name: "nil latest execution block", + paramsTd: "1", + errString: "latest execution block is nil", + }, + { + name: "current execution block invalid TD", + paramsTd: "1", + currentPowBlock: &pb.ExecutionBlock{ + Hash: []byte{'a'}, + TotalDifficulty: "1115792089237316195423570985008687907853269984665640564039457584007913129638912", + }, + errString: "could not convert total difficulty to uint256", + }, + { + name: "current execution block has zero hash parent", + paramsTd: "2", + currentPowBlock: &pb.ExecutionBlock{ + Hash: []byte{'a'}, + ParentHash: params.BeaconConfig().ZeroHash[:], + TotalDifficulty: "0x3", + }, + }, + { + name: "could not get parent block", + paramsTd: "2", + currentPowBlock: &pb.ExecutionBlock{ + Hash: []byte{'a'}, + ParentHash: []byte{'b'}, + TotalDifficulty: "0x3", + }, + errString: "could not get parent execution block", + }, + { + name: "parent execution block invalid TD", + paramsTd: "2", + currentPowBlock: &pb.ExecutionBlock{ + Hash: []byte{'a'}, + ParentHash: []byte{'b'}, + TotalDifficulty: "0x3", + }, + parentPowBlock: &pb.ExecutionBlock{ + Hash: []byte{'b'}, + ParentHash: []byte{'c'}, + TotalDifficulty: "1", + }, + errString: "could not convert total difficulty to uint256", + }, + { + name: "happy case", + paramsTd: "2", + currentPowBlock: &pb.ExecutionBlock{ + Hash: []byte{'a'}, + ParentHash: []byte{'b'}, + TotalDifficulty: "0x3", + }, + parentPowBlock: &pb.ExecutionBlock{ + Hash: []byte{'b'}, + ParentHash: []byte{'c'}, + TotalDifficulty: "0x1", + }, + wantExists: true, + wantTerminalBlockHash: []byte{'a'}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := params.BeaconConfig() + cfg.TerminalTotalDifficulty = tt.paramsTd + params.OverrideBeaconConfig(cfg) + var m map[[32]byte]*pb.ExecutionBlock + if tt.parentPowBlock != nil { + m = map[[32]byte]*pb.ExecutionBlock{ + bytesutil.ToBytes32(tt.parentPowBlock.Hash): tt.parentPowBlock, + } + } + vs := &Server{ + ExecutionEngineCaller: &mocks.EngineClient{ + ErrLatestExecBlock: tt.errLatestExecutionBlk, + ExecutionBlock: tt.currentPowBlock, + BlockByHashMap: m, + }, + } + b, e, err := vs.getPowBlockHashAtTerminalTotalDifficulty(context.Background()) + if tt.errString != "" { + require.ErrorContains(t, tt.errString, err) + } else { + require.NoError(t, err) + require.DeepEqual(t, tt.wantExists, e) + require.DeepEqual(t, tt.wantTerminalBlockHash, b) + } + }) + } +} + +func TestServer_getTerminalBlockHashIfExists(t *testing.T) { + tests := []struct { + name string + paramsTerminalHash []byte + paramsTd string + currentPowBlock *pb.ExecutionBlock + parentPowBlock *pb.ExecutionBlock + wantTerminalBlockHash []byte + wantExists bool + errString string + }{ + { + name: "use terminal block hash, doesn't exist", + paramsTerminalHash: []byte{'a'}, + errString: "could not fetch height for hash", + }, + { + name: "use terminal block hash, exists", + paramsTerminalHash: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + wantExists: true, + wantTerminalBlockHash: []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, + }, + { + name: "use terminal total difficulty", + paramsTd: "2", + currentPowBlock: &pb.ExecutionBlock{ + Hash: []byte{'a'}, + ParentHash: []byte{'b'}, + TotalDifficulty: "0x3", + }, + parentPowBlock: &pb.ExecutionBlock{ + Hash: []byte{'b'}, + ParentHash: []byte{'c'}, + TotalDifficulty: "0x1", + }, + wantExists: true, + wantTerminalBlockHash: []byte{'a'}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := params.BeaconConfig() + cfg.TerminalTotalDifficulty = tt.paramsTd + cfg.TerminalBlockHash = common.BytesToHash(tt.paramsTerminalHash) + params.OverrideBeaconConfig(cfg) + var m map[[32]byte]*pb.ExecutionBlock + if tt.parentPowBlock != nil { + m = map[[32]byte]*pb.ExecutionBlock{ + bytesutil.ToBytes32(tt.parentPowBlock.Hash): tt.parentPowBlock, + } + } + c := powtesting.NewPOWChain() + c.HashesByHeight[0] = tt.wantTerminalBlockHash + vs := &Server{ + Eth1BlockFetcher: c, + ExecutionEngineCaller: &mocks.EngineClient{ + ExecutionBlock: tt.currentPowBlock, + BlockByHashMap: m, + }, + } + b, e, err := vs.getTerminalBlockHashIfExists(context.Background()) + if tt.errString != "" { + require.ErrorContains(t, tt.errString, err) + require.DeepEqual(t, tt.wantExists, e) + } else { + require.NoError(t, err) + require.DeepEqual(t, tt.wantExists, e) + require.DeepEqual(t, tt.wantTerminalBlockHash, b) + } + }) + } +} diff --git a/beacon-chain/rpc/prysm/v1alpha1/validator/server.go b/beacon-chain/rpc/prysm/v1alpha1/validator/server.go index 0a4911bca..0209a0298 100644 --- a/beacon-chain/rpc/prysm/v1alpha1/validator/server.go +++ b/beacon-chain/rpc/prysm/v1alpha1/validator/server.go @@ -22,6 +22,7 @@ import ( "github.com/prysmaticlabs/prysm/beacon-chain/operations/voluntaryexits" "github.com/prysmaticlabs/prysm/beacon-chain/p2p" "github.com/prysmaticlabs/prysm/beacon-chain/powchain" + enginev1 "github.com/prysmaticlabs/prysm/beacon-chain/powchain/engine-api-client/v1" "github.com/prysmaticlabs/prysm/beacon-chain/state/stategen" "github.com/prysmaticlabs/prysm/beacon-chain/sync" "github.com/prysmaticlabs/prysm/config/params" @@ -62,6 +63,7 @@ type Server struct { PendingDepositsFetcher depositcache.PendingDepositsFetcher OperationNotifier opfeed.Notifier StateGen stategen.StateManager + ExecutionEngineCaller enginev1.Caller } // WaitForActivation checks if a validator public key exists in the active validator registry of the current