diff --git a/beacon-chain/blockchain/pow_block.go b/beacon-chain/blockchain/pow_block.go index aed9842bbc..1b20babe04 100644 --- a/beacon-chain/blockchain/pow_block.go +++ b/beacon-chain/blockchain/pow_block.go @@ -15,6 +15,7 @@ import ( "github.com/prysmaticlabs/prysm/v3/consensus-types/interfaces" types "github.com/prysmaticlabs/prysm/v3/consensus-types/primitives" "github.com/prysmaticlabs/prysm/v3/encoding/bytesutil" + "github.com/prysmaticlabs/prysm/v3/runtime/version" "github.com/prysmaticlabs/prysm/v3/time/slots" "github.com/sirupsen/logrus" ) @@ -92,6 +93,7 @@ func (s *Service) getBlkParentHashAndTD(ctx context.Context, blkHash []byte) ([] if blk == nil { return nil, nil, errors.New("pow block is nil") } + blk.Version = version.Bellatrix blkTDBig, err := hexutil.DecodeBig(blk.TotalDifficulty) if err != nil { return nil, nil, errors.Wrap(err, "could not decode merge block total difficulty") diff --git a/beacon-chain/execution/BUILD.bazel b/beacon-chain/execution/BUILD.bazel index 8b363460a6..35e1ff8f2e 100644 --- a/beacon-chain/execution/BUILD.bazel +++ b/beacon-chain/execution/BUILD.bazel @@ -121,6 +121,7 @@ go_test( "//network/authorization:go_default_library", "//proto/engine/v1:go_default_library", "//proto/prysm/v1alpha1:go_default_library", + "//runtime/version:go_default_library", "//testing/assert:go_default_library", "//testing/require:go_default_library", "//testing/util:go_default_library", diff --git a/beacon-chain/execution/engine_client.go b/beacon-chain/execution/engine_client.go index 1d00d2619e..8760e757e4 100644 --- a/beacon-chain/execution/engine_client.go +++ b/beacon-chain/execution/engine_client.go @@ -57,7 +57,7 @@ type ForkchoiceUpdatedResponse struct { // block with an execution payload from a signed beacon block and a connection // to an execution client's engine API. type ExecutionPayloadReconstructor interface { - ReconstructFullBellatrixBlock( + ReconstructFullBlock( ctx context.Context, blindedBlock interfaces.SignedBeaconBlock, ) (interfaces.SignedBeaconBlock, error) ReconstructFullBellatrixBlockBatch( @@ -403,9 +403,9 @@ func (s *Service) HeaderByNumber(ctx context.Context, number *big.Int) (*types.H return hdr, err } -// ReconstructFullBellatrixBlock takes in a blinded beacon block and reconstructs +// ReconstructFullBlock takes in a blinded beacon block and reconstructs // a beacon block with a full execution payload via the engine API. -func (s *Service) ReconstructFullBellatrixBlock( +func (s *Service) ReconstructFullBlock( ctx context.Context, blindedBlock interfaces.SignedBeaconBlock, ) (interfaces.SignedBeaconBlock, error) { if err := blocks.BeaconBlockIsNil(blindedBlock); err != nil { @@ -437,11 +437,12 @@ func (s *Service) ReconstructFullBellatrixBlock( if executionBlock == nil { return nil, fmt.Errorf("received nil execution block for request by hash %#x", executionBlockHash) } + executionBlock.Version = blindedBlock.Version() payload, err := fullPayloadFromExecutionBlock(header, executionBlock) if err != nil { return nil, err } - fullBlock, err := blocks.BuildSignedBeaconBlockFromExecutionPayload(blindedBlock, payload) + fullBlock, err := blocks.BuildSignedBeaconBlockFromExecutionPayload(blindedBlock, payload.Proto()) if err != nil { return nil, err } @@ -504,7 +505,7 @@ func (s *Service) ReconstructFullBellatrixBlockBatch( if err != nil { return nil, err } - fullBlock, err := blocks.BuildSignedBeaconBlockFromExecutionPayload(blindedBlocks[realIdx], payload) + fullBlock, err := blocks.BuildSignedBeaconBlockFromExecutionPayload(blindedBlocks[realIdx], payload.Proto()) if err != nil { return nil, err } @@ -526,26 +527,47 @@ func (s *Service) ReconstructFullBellatrixBlockBatch( func fullPayloadFromExecutionBlock( header interfaces.ExecutionData, block *pb.ExecutionBlock, -) (*pb.ExecutionPayload, error) { +) (interfaces.ExecutionData, error) { if header.IsNil() || block == nil { return nil, errors.New("execution block and header cannot be nil") } - if !bytes.Equal(header.BlockHash(), block.Hash[:]) { + blockHash := block.Hash + if !bytes.Equal(header.BlockHash(), blockHash[:]) { return nil, fmt.Errorf( "block hash field in execution header %#x does not match execution block hash %#x", header.BlockHash(), - block.Hash, + blockHash, ) } - txs := make([][]byte, len(block.Transactions)) - for i, tx := range block.Transactions { + blockTransactions := block.Transactions + txs := make([][]byte, len(blockTransactions)) + for i, tx := range blockTransactions { txBin, err := tx.MarshalBinary() if err != nil { return nil, err } txs[i] = txBin } - return &pb.ExecutionPayload{ + + if block.Version == version.Bellatrix { + return blocks.WrappedExecutionPayload(&pb.ExecutionPayload{ + ParentHash: header.ParentHash(), + FeeRecipient: header.FeeRecipient(), + StateRoot: header.StateRoot(), + ReceiptsRoot: header.ReceiptsRoot(), + LogsBloom: header.LogsBloom(), + PrevRandao: header.PrevRandao(), + BlockNumber: header.BlockNumber(), + GasLimit: header.GasLimit(), + GasUsed: header.GasUsed(), + Timestamp: header.Timestamp(), + ExtraData: header.ExtraData(), + BaseFeePerGas: header.BaseFeePerGas(), + BlockHash: blockHash[:], + Transactions: txs, + }) + } + return blocks.WrappedExecutionPayloadCapella(&pb.ExecutionPayloadCapella{ ParentHash: header.ParentHash(), FeeRecipient: header.FeeRecipient(), StateRoot: header.StateRoot(), @@ -558,9 +580,10 @@ func fullPayloadFromExecutionBlock( Timestamp: header.Timestamp(), ExtraData: header.ExtraData(), BaseFeePerGas: header.BaseFeePerGas(), - BlockHash: block.Hash[:], + BlockHash: blockHash[:], Transactions: txs, - }, nil + Withdrawals: block.Withdrawals, + }) } // Handles errors received from the RPC server according to the specification. diff --git a/beacon-chain/execution/engine_client_test.go b/beacon-chain/execution/engine_client_test.go index efb1b7e822..eedf06182d 100644 --- a/beacon-chain/execution/engine_client_test.go +++ b/beacon-chain/execution/engine_client_test.go @@ -27,6 +27,7 @@ import ( payloadattribute "github.com/prysmaticlabs/prysm/v3/consensus-types/payload-attribute" "github.com/prysmaticlabs/prysm/v3/encoding/bytesutil" pb "github.com/prysmaticlabs/prysm/v3/proto/engine/v1" + "github.com/prysmaticlabs/prysm/v3/runtime/version" "github.com/prysmaticlabs/prysm/v3/testing/assert" "github.com/prysmaticlabs/prysm/v3/testing/require" "github.com/prysmaticlabs/prysm/v3/testing/util" @@ -493,7 +494,7 @@ func TestReconstructFullBellatrixBlock(t *testing.T) { t.Run("nil block", func(t *testing.T) { service := &Service{} - _, err := service.ReconstructFullBellatrixBlock(ctx, nil) + _, err := service.ReconstructFullBlock(ctx, nil) require.ErrorContains(t, "nil data", err) }) t.Run("only blinded block", func(t *testing.T) { @@ -502,7 +503,7 @@ func TestReconstructFullBellatrixBlock(t *testing.T) { bellatrixBlock := util.NewBeaconBlockBellatrix() wrapped, err := blocks.NewSignedBeaconBlock(bellatrixBlock) require.NoError(t, err) - _, err = service.ReconstructFullBellatrixBlock(ctx, wrapped) + _, err = service.ReconstructFullBlock(ctx, wrapped) require.ErrorContains(t, want, err) }) t.Run("pre-merge execution payload", func(t *testing.T) { @@ -517,7 +518,7 @@ func TestReconstructFullBellatrixBlock(t *testing.T) { require.NoError(t, err) wantedWrapped, err := blocks.NewSignedBeaconBlock(wanted) require.NoError(t, err) - reconstructed, err := service.ReconstructFullBellatrixBlock(ctx, wrapped) + reconstructed, err := service.ReconstructFullBlock(ctx, wrapped) require.NoError(t, err) require.DeepEqual(t, wantedWrapped, reconstructed) }) @@ -590,7 +591,7 @@ func TestReconstructFullBellatrixBlock(t *testing.T) { blindedBlock.Block.Body.ExecutionPayloadHeader = header wrapped, err := blocks.NewSignedBeaconBlock(blindedBlock) require.NoError(t, err) - reconstructed, err := service.ReconstructFullBellatrixBlock(ctx, wrapped) + reconstructed, err := service.ReconstructFullBlock(ctx, wrapped) require.NoError(t, err) got, err := reconstructed.Block().Body().Execution() @@ -1149,6 +1150,7 @@ func fixtures() map[string]interface{} { receiptsRoot := bytesutil.PadTo([]byte("receiptsRoot"), fieldparams.RootLength) logsBloom := bytesutil.PadTo([]byte("logs"), fieldparams.LogsBloomLength) executionBlock := &pb.ExecutionBlock{ + Version: version.Bellatrix, Header: gethtypes.Header{ ParentHash: common.BytesToHash(parent), UncleHash: common.BytesToHash(sha3Uncles), @@ -1257,7 +1259,7 @@ func Test_fullPayloadFromExecutionBlock(t *testing.T) { tests := []struct { name string args args - want *pb.ExecutionPayload + want func() interfaces.ExecutionData err string }{ { @@ -1267,7 +1269,8 @@ func Test_fullPayloadFromExecutionBlock(t *testing.T) { BlockHash: []byte("foo"), }, block: &pb.ExecutionBlock{ - Hash: common.BytesToHash([]byte("bar")), + Version: version.Bellatrix, + Hash: common.BytesToHash([]byte("bar")), }, }, err: "does not match execution block hash", @@ -1279,22 +1282,30 @@ func Test_fullPayloadFromExecutionBlock(t *testing.T) { BlockHash: wantedHash[:], }, block: &pb.ExecutionBlock{ - Hash: wantedHash, + Version: version.Bellatrix, + Hash: wantedHash, }, }, - want: &pb.ExecutionPayload{ - BlockHash: wantedHash[:], + want: func() interfaces.ExecutionData { + p, err := blocks.WrappedExecutionPayload(&pb.ExecutionPayload{ + BlockHash: wantedHash[:], + Transactions: [][]byte{}, + }) + require.NoError(t, err) + return p }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { wrapped, err := blocks.WrappedExecutionPayloadHeader(tt.args.header) + require.NoError(t, err) got, err := fullPayloadFromExecutionBlock(wrapped, tt.args.block) - if (err != nil) && !strings.Contains(err.Error(), tt.err) { - t.Fatalf("Wanted err %s got %v", tt.err, err) + if err != nil { + assert.ErrorContains(t, tt.err, err) + } else { + assert.DeepEqual(t, tt.want(), got) } - require.DeepEqual(t, tt.want, got) }) } } diff --git a/beacon-chain/execution/testing/mock_engine_client.go b/beacon-chain/execution/testing/mock_engine_client.go index b857ffc281..9ac9c876f7 100644 --- a/beacon-chain/execution/testing/mock_engine_client.go +++ b/beacon-chain/execution/testing/mock_engine_client.go @@ -76,7 +76,8 @@ func (e *EngineClient) ExecutionBlockByHash(_ context.Context, h common.Hash, _ return b, e.ErrExecBlockByHash } -func (e *EngineClient) ReconstructFullBellatrixBlock( +// ReconstructFullBlock -- +func (e *EngineClient) ReconstructFullBlock( _ context.Context, blindedBlock interfaces.SignedBeaconBlock, ) (interfaces.SignedBeaconBlock, error) { if !blindedBlock.Block().IsBlinded() { @@ -94,12 +95,13 @@ func (e *EngineClient) ReconstructFullBellatrixBlock( return blocks.BuildSignedBeaconBlockFromExecutionPayload(blindedBlock, payload) } +// ReconstructFullBellatrixBlockBatch -- func (e *EngineClient) ReconstructFullBellatrixBlockBatch( ctx context.Context, blindedBlocks []interfaces.SignedBeaconBlock, ) ([]interfaces.SignedBeaconBlock, error) { fullBlocks := make([]interfaces.SignedBeaconBlock, 0, len(blindedBlocks)) for _, b := range blindedBlocks { - newBlock, err := e.ReconstructFullBellatrixBlock(ctx, b) + newBlock, err := e.ReconstructFullBlock(ctx, b) if err != nil { return nil, err } diff --git a/beacon-chain/rpc/eth/beacon/blocks.go b/beacon-chain/rpc/eth/beacon/blocks.go index 33056a968c..a6108b1492 100644 --- a/beacon-chain/rpc/eth/beacon/blocks.go +++ b/beacon-chain/rpc/eth/beacon/blocks.go @@ -506,7 +506,7 @@ func (bs *Server) GetBlockV2(ctx context.Context, req *ethpbv2.BlockRequestV2) ( if blindedBellatrixBlk == nil { return nil, status.Error(codes.Internal, "Nil block") } - signedFullBlock, err := bs.ExecutionPayloadReconstructor.ReconstructFullBellatrixBlock(ctx, blk) + signedFullBlock, err := bs.ExecutionPayloadReconstructor.ReconstructFullBlock(ctx, blk) if err != nil { return nil, status.Errorf( codes.Internal, @@ -641,7 +641,7 @@ func (bs *Server) GetBlockSSZV2(ctx context.Context, req *ethpbv2.BlockRequestV2 if blindedBellatrixBlk == nil { return nil, status.Error(codes.Internal, "Nil block") } - signedFullBlock, err := bs.ExecutionPayloadReconstructor.ReconstructFullBellatrixBlock(ctx, blk) + signedFullBlock, err := bs.ExecutionPayloadReconstructor.ReconstructFullBlock(ctx, blk) if err != nil { return nil, status.Errorf( codes.Internal, diff --git a/beacon-chain/sync/rpc_beacon_blocks_by_root.go b/beacon-chain/sync/rpc_beacon_blocks_by_root.go index cb884692ce..46ea536428 100644 --- a/beacon-chain/sync/rpc_beacon_blocks_by_root.go +++ b/beacon-chain/sync/rpc_beacon_blocks_by_root.go @@ -75,7 +75,7 @@ func (s *Service) beaconBlocksRootRPCHandler(ctx context.Context, msg interface{ } if blk.Block().IsBlinded() { - blk, err = s.cfg.executionPayloadReconstructor.ReconstructFullBellatrixBlock(ctx, blk) + blk, err = s.cfg.executionPayloadReconstructor.ReconstructFullBlock(ctx, blk) if err != nil { log.WithError(err).Error("Could not get reconstruct full bellatrix block from blinded body") s.writeErrorResponseToStream(responseCodeServerError, types.ErrGeneric.Error(), stream) diff --git a/encoding/bytesutil/bytes.go b/encoding/bytesutil/bytes.go index 323f9d083e..8ee328dff8 100644 --- a/encoding/bytesutil/bytes.go +++ b/encoding/bytesutil/bytes.go @@ -100,6 +100,15 @@ func ToBytes4(x []byte) [4]byte { return y } +// ToBytes20 is a convenience method for converting a byte slice to a fix +// sized 20 byte array. This method will truncate the input if it is larger +// than 20 bytes. +func ToBytes20(x []byte) [20]byte { + var y [20]byte + copy(y[:], x) + return y +} + // ToBytes32 is a convenience method for converting a byte slice to a fix // sized 32 byte array. This method will truncate the input if it is larger // than 32 bytes. diff --git a/encoding/bytesutil/bytes_test.go b/encoding/bytesutil/bytes_test.go index 459fef7bc4..61a57cc574 100644 --- a/encoding/bytesutil/bytes_test.go +++ b/encoding/bytesutil/bytes_test.go @@ -637,6 +637,24 @@ func TestToBytes48Array(t *testing.T) { } } +func TestToBytes20(t *testing.T) { + tests := []struct { + a []byte + b [20]byte + }{ + {nil, [20]byte{}}, + {[]byte{}, [20]byte{}}, + {[]byte{1}, [20]byte{1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}}, + {[]byte{1, 2, 3}, [20]byte{1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}}, + {[]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, [20]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}}, + {[]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21}, [20]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}}, + } + for _, tt := range tests { + b := bytesutil.ToBytes20(tt.a) + assert.DeepEqual(t, tt.b, b) + } +} + func TestLittleEndianBytesToBigInt(t *testing.T) { bytes := make([]byte, 8) binary.LittleEndian.PutUint64(bytes, 1234567890) diff --git a/proto/engine/v1/BUILD.bazel b/proto/engine/v1/BUILD.bazel index f248c4ef13..a4547d5379 100644 --- a/proto/engine/v1/BUILD.bazel +++ b/proto/engine/v1/BUILD.bazel @@ -76,14 +76,17 @@ go_library( deps = [ "//config/fieldparams:go_default_library", "//config/params:go_default_library", + "//consensus-types/primitives:go_default_library", "//encoding/bytesutil:go_default_library", "//proto/eth/ext:go_default_library", + "//runtime/version:go_default_library", "@com_github_ethereum_go_ethereum//common:go_default_library", "@com_github_ethereum_go_ethereum//common/hexutil:go_default_library", "@com_github_ethereum_go_ethereum//core/types:go_default_library", "@com_github_golang_protobuf//proto:go_default_library", "@com_github_pkg_errors//:go_default_library", "@com_github_prysmaticlabs_fastssz//:go_default_library", + "@com_github_sirupsen_logrus//:go_default_library", "@go_googleapis//google/api:annotations_go_proto", "@io_bazel_rules_go//proto/wkt:descriptor_go_proto", "@io_bazel_rules_go//proto/wkt:timestamp_go_proto", @@ -114,6 +117,7 @@ go_test( ":go_default_library", "//config/fieldparams:go_default_library", "//config/params:go_default_library", + "//consensus-types/primitives:go_default_library", "//encoding/bytesutil:go_default_library", "//testing/require:go_default_library", "@com_github_ethereum_go_ethereum//common:go_default_library", diff --git a/proto/engine/v1/json_marshal_unmarshal.go b/proto/engine/v1/json_marshal_unmarshal.go index 630c52f162..dda72bc836 100644 --- a/proto/engine/v1/json_marshal_unmarshal.go +++ b/proto/engine/v1/json_marshal_unmarshal.go @@ -11,7 +11,9 @@ import ( gethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/pkg/errors" fieldparams "github.com/prysmaticlabs/prysm/v3/config/fieldparams" + types "github.com/prysmaticlabs/prysm/v3/consensus-types/primitives" "github.com/prysmaticlabs/prysm/v3/encoding/bytesutil" + "github.com/prysmaticlabs/prysm/v3/runtime/version" ) // PayloadIDBytes defines a custom type for Payload IDs used by the engine API @@ -26,10 +28,12 @@ func (b PayloadIDBytes) MarshalJSON() ([]byte, error) { // ExecutionBlock is the response kind received by the eth_getBlockByHash and // eth_getBlockByNumber endpoints via JSON-RPC. type ExecutionBlock struct { + Version int gethtypes.Header Hash common.Hash `json:"hash"` Transactions []*gethtypes.Transaction `json:"transactions"` TotalDifficulty string `json:"totalDifficulty"` + Withdrawals []*Withdrawal `json:"withdrawals"` } func (e *ExecutionBlock) MarshalJSON() ([]byte, error) { @@ -44,13 +48,22 @@ func (e *ExecutionBlock) MarshalJSON() ([]byte, error) { decoded["hash"] = e.Hash.String() decoded["transactions"] = e.Transactions decoded["totalDifficulty"] = e.TotalDifficulty + + if e.Version == version.Capella { + decoded["withdrawals"] = e.Withdrawals + } + return json.Marshal(decoded) } func (e *ExecutionBlock) UnmarshalJSON(enc []byte) error { - type transactionJson struct { + type transactionsJson struct { Transactions []*gethtypes.Transaction `json:"transactions"` } + type withdrawalsJson struct { + Withdrawals []*withdrawalJSON `json:"withdrawals"` + } + if err := e.Header.UnmarshalJSON(enc); err != nil { return err } @@ -71,6 +84,26 @@ func (e *ExecutionBlock) UnmarshalJSON(enc []byte) error { if !ok { return errors.New("expected `totalDifficulty` field in JSON response") } + + rawWithdrawals, ok := decoded["withdrawals"] + if !ok || rawWithdrawals == nil { + e.Version = version.Bellatrix + } else { + e.Version = version.Capella + j := &withdrawalsJson{} + if err := json.Unmarshal(enc, j); err != nil { + return err + } + ws := make([]*Withdrawal, len(j.Withdrawals)) + for i, wj := range j.Withdrawals { + ws[i], err = wj.ToWithdrawal() + if err != nil { + return err + } + } + e.Withdrawals = ws + } + rawTxList, ok := decoded["transactions"] if !ok || rawTxList == nil { // Exit early if there are no transactions stored in the json payload. @@ -80,8 +113,6 @@ func (e *ExecutionBlock) UnmarshalJSON(enc []byte) error { if !ok { return errors.Errorf("expected transaction list to be of a slice interface type.") } - - // for _, tx := range txsList { // If the transaction is just a hex string, do not attempt to // unmarshal into a full transaction object. @@ -91,7 +122,7 @@ func (e *ExecutionBlock) UnmarshalJSON(enc []byte) error { } // If the block contains a list of transactions, we JSON unmarshal // them into a list of geth transaction objects. - txJson := &transactionJson{} + txJson := &transactionsJson{} if err := json.Unmarshal(enc, txJson); err != nil { return err } @@ -109,6 +140,70 @@ func (b *PayloadIDBytes) UnmarshalJSON(enc []byte) error { return nil } +type withdrawalJSON struct { + Index *hexutil.Uint64 `json:"index"` + Validator *hexutil.Uint64 `json:"validatorIndex"` + Address *common.Address `json:"address"` + Amount string `json:"amount"` +} + +func (j *withdrawalJSON) ToWithdrawal() (*Withdrawal, error) { + w := &Withdrawal{} + b, err := json.Marshal(j) + if err != nil { + return nil, err + } + if err := w.UnmarshalJSON(b); err != nil { + return nil, err + } + return w, nil +} + +func (w *Withdrawal) MarshalJSON() ([]byte, error) { + index := hexutil.Uint64(w.WithdrawalIndex) + validatorIndex := hexutil.Uint64(w.ValidatorIndex) + address := common.BytesToAddress(w.ExecutionAddress) + wei := new(big.Int).SetUint64(1000000000) + amountWei := new(big.Int).Mul(new(big.Int).SetUint64(w.Amount), wei) + return json.Marshal(withdrawalJSON{ + Index: &index, + Validator: &validatorIndex, + Address: &address, + Amount: hexutil.EncodeBig(amountWei), + }) +} + +func (w *Withdrawal) UnmarshalJSON(enc []byte) error { + dec := withdrawalJSON{} + if err := json.Unmarshal(enc, &dec); err != nil { + return err + } + if dec.Index == nil { + return errors.New("missing withdrawal index") + } + if dec.Validator == nil { + return errors.New("missing validator index") + } + if dec.Address == nil { + return errors.New("missing execution address") + } + *w = Withdrawal{} + w.WithdrawalIndex = uint64(*dec.Index) + w.ValidatorIndex = types.ValidatorIndex(*dec.Validator) + w.ExecutionAddress = dec.Address.Bytes() + wei := new(big.Int).SetUint64(1000000000) + amountWei, err := hexutil.DecodeBig(dec.Amount) + if err != nil { + return err + } + amount := new(big.Int).Div(amountWei, wei) + if !amount.IsUint64() { + return errors.New("withdrawal amount overflow") + } + w.Amount = amount.Uint64() + return nil +} + type executionPayloadJSON struct { ParentHash *common.Hash `json:"parentHash"` FeeRecipient *common.Address `json:"feeRecipient"` diff --git a/proto/engine/v1/json_marshal_unmarshal_test.go b/proto/engine/v1/json_marshal_unmarshal_test.go index 22d2a29c63..615d899fa0 100644 --- a/proto/engine/v1/json_marshal_unmarshal_test.go +++ b/proto/engine/v1/json_marshal_unmarshal_test.go @@ -6,15 +6,24 @@ import ( "testing" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" gethtypes "github.com/ethereum/go-ethereum/core/types" "github.com/holiman/uint256" fieldparams "github.com/prysmaticlabs/prysm/v3/config/fieldparams" "github.com/prysmaticlabs/prysm/v3/config/params" + types "github.com/prysmaticlabs/prysm/v3/consensus-types/primitives" "github.com/prysmaticlabs/prysm/v3/encoding/bytesutil" enginev1 "github.com/prysmaticlabs/prysm/v3/proto/engine/v1" "github.com/prysmaticlabs/prysm/v3/testing/require" ) +type withdrawalJSON struct { + Index *hexutil.Uint64 `json:"index"` + Validator *hexutil.Uint64 `json:"validatorIndex"` + Address *common.Address `json:"address"` + Amount string `json:"amount"` +} + func TestJsonMarshalUnmarshal(t *testing.T) { t.Run("payload attributes", func(t *testing.T) { random := bytesutil.PadTo([]byte("random"), fieldparams.RootLength) @@ -312,6 +321,91 @@ func TestJsonMarshalUnmarshal(t *testing.T) { require.Equal(t, 1, len(payloadPb.Transactions)) require.DeepEqual(t, txs[0].Hash(), payloadPb.Transactions[0].Hash()) }) + t.Run("execution block with withdrawals", func(t *testing.T) { + baseFeePerGas := big.NewInt(1770307273) + want := &gethtypes.Header{ + Number: big.NewInt(1), + ParentHash: common.BytesToHash([]byte("parent")), + UncleHash: common.BytesToHash([]byte("uncle")), + Coinbase: common.BytesToAddress([]byte("coinbase")), + Root: common.BytesToHash([]byte("uncle")), + TxHash: common.BytesToHash([]byte("txHash")), + ReceiptHash: common.BytesToHash([]byte("receiptHash")), + Bloom: gethtypes.BytesToBloom([]byte("bloom")), + Difficulty: big.NewInt(2), + GasLimit: 3, + GasUsed: 4, + Time: 5, + BaseFee: baseFeePerGas, + Extra: []byte("extraData"), + MixDigest: common.BytesToHash([]byte("mix")), + Nonce: gethtypes.EncodeNonce(6), + } + enc, err := json.Marshal(want) + require.NoError(t, err) + + payloadItems := make(map[string]interface{}) + require.NoError(t, json.Unmarshal(enc, &payloadItems)) + + blockHash := want.Hash() + payloadItems["hash"] = blockHash.String() + payloadItems["totalDifficulty"] = "0x393a2e53de197c" + + withdrawalIndex1 := hexutil.Uint64(1) + withdrawalIndex2 := hexutil.Uint64(2) + withdrawalValidator1 := hexutil.Uint64(1) + withdrawalValidator2 := hexutil.Uint64(2) + address1 := common.Address(bytesutil.ToBytes20([]byte("address1"))) + address2 := common.Address(bytesutil.ToBytes20([]byte("address2"))) + payloadItems["withdrawals"] = []*withdrawalJSON{ + { + Index: &withdrawalIndex1, + Validator: &withdrawalValidator1, + Address: &address1, + Amount: "0x3b9aca00", + }, + { + Index: &withdrawalIndex2, + Validator: &withdrawalValidator2, + Address: &address2, + Amount: "0x77359400", + }, + } + + encodedPayloadItems, err := json.Marshal(payloadItems) + require.NoError(t, err) + + payloadPb := &enginev1.ExecutionBlock{} + require.NoError(t, json.Unmarshal(encodedPayloadItems, payloadPb)) + + require.DeepEqual(t, blockHash, payloadPb.Hash) + require.DeepEqual(t, want.Number, payloadPb.Number) + require.DeepEqual(t, want.ParentHash, payloadPb.ParentHash) + require.DeepEqual(t, want.UncleHash, payloadPb.UncleHash) + require.DeepEqual(t, want.Coinbase, payloadPb.Coinbase) + require.DeepEqual(t, want.Root, payloadPb.Root) + require.DeepEqual(t, want.TxHash, payloadPb.TxHash) + require.DeepEqual(t, want.ReceiptHash, payloadPb.ReceiptHash) + require.DeepEqual(t, want.Bloom, payloadPb.Bloom) + require.DeepEqual(t, want.Difficulty, payloadPb.Difficulty) + require.DeepEqual(t, payloadItems["totalDifficulty"], payloadPb.TotalDifficulty) + require.DeepEqual(t, want.GasUsed, payloadPb.GasUsed) + require.DeepEqual(t, want.GasLimit, payloadPb.GasLimit) + require.DeepEqual(t, want.Time, payloadPb.Time) + require.DeepEqual(t, want.BaseFee, payloadPb.BaseFee) + require.DeepEqual(t, want.Extra, payloadPb.Extra) + require.DeepEqual(t, want.MixDigest, payloadPb.MixDigest) + require.DeepEqual(t, want.Nonce, payloadPb.Nonce) + require.Equal(t, 2, len(payloadPb.Withdrawals)) + require.Equal(t, uint64(1), payloadPb.Withdrawals[0].WithdrawalIndex) + require.Equal(t, types.ValidatorIndex(1), payloadPb.Withdrawals[0].ValidatorIndex) + require.DeepEqual(t, bytesutil.PadTo([]byte("address1"), 20), payloadPb.Withdrawals[0].ExecutionAddress) + require.Equal(t, uint64(1), payloadPb.Withdrawals[0].Amount) + require.Equal(t, uint64(2), payloadPb.Withdrawals[1].WithdrawalIndex) + require.Equal(t, types.ValidatorIndex(2), payloadPb.Withdrawals[1].ValidatorIndex) + require.DeepEqual(t, bytesutil.PadTo([]byte("address2"), 20), payloadPb.Withdrawals[1].ExecutionAddress) + require.Equal(t, uint64(2), payloadPb.Withdrawals[1].Amount) + }) } func TestPayloadIDBytes_MarshalUnmarshalJSON(t *testing.T) {