From 06fa0f97c4fbbc646211514022c8199e29f0f845 Mon Sep 17 00:00:00 2001 From: Kasey Kirkham Date: Fri, 26 Apr 2024 18:09:16 -0500 Subject: [PATCH] fork-specific interface for electra --- consensus-types/blocks/execution.go | 116 ++++++--------------- consensus-types/blocks/factory_test.go | 24 +++++ consensus-types/blocks/getters.go | 14 +-- consensus-types/blocks/getters_test.go | 6 ++ consensus-types/blocks/types.go | 3 + consensus-types/interfaces/BUILD.bazel | 12 ++- consensus-types/interfaces/beacon_block.go | 17 ++- consensus-types/interfaces/cast.go | 15 +++ consensus-types/interfaces/error.go | 27 +++++ consensus-types/interfaces/error_test.go | 14 +++ consensus-types/mock/block.go | 3 +- 11 files changed, 152 insertions(+), 99 deletions(-) create mode 100644 consensus-types/interfaces/cast.go create mode 100644 consensus-types/interfaces/error.go create mode 100644 consensus-types/interfaces/error_test.go diff --git a/consensus-types/blocks/execution.go b/consensus-types/blocks/execution.go index d4006244efed..50ff152c80a3 100644 --- a/consensus-types/blocks/execution.go +++ b/consensus-types/blocks/execution.go @@ -23,6 +23,8 @@ type executionPayload struct { p *enginev1.ExecutionPayload } +var _ interfaces.ExecutionData = &executionPayload{} + // WrappedExecutionPayload is a constructor which wraps a protobuf execution payload into an interface. func WrappedExecutionPayload(p *enginev1.ExecutionPayload) (interfaces.ExecutionData, error) { w := executionPayload{p: p} @@ -202,16 +204,6 @@ func (executionPayload) ValueInGwei() (uint64, error) { return 0, consensus_types.ErrUnsupportedField } -// DepositReceipts -- -func (e executionPayload) DepositReceipts() ([]*enginev1.DepositReceipt, error) { - return nil, consensus_types.ErrUnsupportedField -} - -// WithdrawalRequests -- -func (e executionPayload) WithdrawalRequests() ([]*enginev1.ExecutionLayerWithdrawalRequest, error) { - return nil, consensus_types.ErrUnsupportedField -} - // executionPayloadHeader is a convenience wrapper around a blinded beacon block body's execution header data structure // This wrapper allows us to conform to a common interface so that beacon // blocks for future forks can also be applied across Prysm without issues. @@ -219,6 +211,8 @@ type executionPayloadHeader struct { p *enginev1.ExecutionPayloadHeader } +var _ interfaces.ExecutionData = &executionPayloadHeader{} + // WrappedExecutionPayloadHeader is a constructor which wraps a protobuf execution header into an interface. func WrappedExecutionPayloadHeader(p *enginev1.ExecutionPayloadHeader) (interfaces.ExecutionData, error) { w := executionPayloadHeader{p: p} @@ -398,16 +392,6 @@ func (executionPayloadHeader) ValueInGwei() (uint64, error) { return 0, consensus_types.ErrUnsupportedField } -// DepositReceipts -- -func (e executionPayloadHeader) DepositReceipts() ([]*enginev1.DepositReceipt, error) { - return nil, consensus_types.ErrUnsupportedField -} - -// WithdrawalRequests -- -func (e executionPayloadHeader) WithdrawalRequests() ([]*enginev1.ExecutionLayerWithdrawalRequest, error) { - return nil, consensus_types.ErrUnsupportedField -} - // PayloadToHeader converts `payload` into execution payload header format. func PayloadToHeader(payload interfaces.ExecutionData) (*enginev1.ExecutionPayloadHeader, error) { txs, err := payload.Transactions() @@ -445,6 +429,8 @@ type executionPayloadCapella struct { gweiValue uint64 } +var _ interfaces.ExecutionData = &executionPayloadCapella{} + // WrappedExecutionPayloadCapella is a constructor which wraps a protobuf execution payload into an interface. func WrappedExecutionPayloadCapella(p *enginev1.ExecutionPayloadCapella, value math.Wei) (interfaces.ExecutionData, error) { w := executionPayloadCapella{p: p, weiValue: value, gweiValue: uint64(math.WeiToGwei(value))} @@ -624,16 +610,6 @@ func (e executionPayloadCapella) ValueInGwei() (uint64, error) { return e.gweiValue, nil } -// DepositReceipts -- -func (e executionPayloadCapella) DepositReceipts() ([]*enginev1.DepositReceipt, error) { - return nil, consensus_types.ErrUnsupportedField -} - -// WithdrawalRequests -- -func (e executionPayloadCapella) WithdrawalRequests() ([]*enginev1.ExecutionLayerWithdrawalRequest, error) { - return nil, consensus_types.ErrUnsupportedField -} - // executionPayloadHeaderCapella is a convenience wrapper around a blinded beacon block body's execution header data structure // This wrapper allows us to conform to a common interface so that beacon // blocks for future forks can also be applied across Prysm without issues. @@ -643,6 +619,8 @@ type executionPayloadHeaderCapella struct { gweiValue uint64 } +var _ interfaces.ExecutionData = &executionPayloadHeaderCapella{} + // WrappedExecutionPayloadHeaderCapella is a constructor which wraps a protobuf execution header into an interface. func WrappedExecutionPayloadHeaderCapella(p *enginev1.ExecutionPayloadHeaderCapella, value math.Wei) (interfaces.ExecutionData, error) { w := executionPayloadHeaderCapella{p: p, weiValue: value, gweiValue: uint64(math.WeiToGwei(value))} @@ -822,16 +800,6 @@ func (e executionPayloadHeaderCapella) ValueInGwei() (uint64, error) { return e.gweiValue, nil } -// DepositReceipts -- -func (e executionPayloadHeaderCapella) DepositReceipts() ([]*enginev1.DepositReceipt, error) { - return nil, consensus_types.ErrUnsupportedField -} - -// WithdrawalRequests -- -func (e executionPayloadHeaderCapella) WithdrawalRequests() ([]*enginev1.ExecutionLayerWithdrawalRequest, error) { - return nil, consensus_types.ErrUnsupportedField -} - // PayloadToHeaderCapella converts `payload` into execution payload header format. func PayloadToHeaderCapella(payload interfaces.ExecutionData) (*enginev1.ExecutionPayloadHeaderCapella, error) { txs, err := payload.Transactions() @@ -919,7 +887,7 @@ func PayloadToHeaderDeneb(payload interfaces.ExecutionData) (*enginev1.Execution } // PayloadToHeaderElectra converts `payload` into execution payload header format. -func PayloadToHeaderElectra(payload interfaces.ExecutionData) (*enginev1.ExecutionPayloadHeaderElectra, error) { +func PayloadToHeaderElectra(payload interfaces.ExecutionDataElectra) (*enginev1.ExecutionPayloadHeaderElectra, error) { txs, err := payload.Transactions() if err != nil { return nil, err @@ -945,18 +913,13 @@ func PayloadToHeaderElectra(payload interfaces.ExecutionData) (*enginev1.Executi return nil, err } - depositReceipts, err := payload.DepositReceipts() - if err != nil { - return nil, err - } + depositReceipts := payload.DepositReceipts() depositReceiptsRoot, err := ssz.DepositReceiptSliceRoot(depositReceipts, fieldparams.MaxDepositReceiptsPerPayload) if err != nil { return nil, err } - withdrawalRequests, err := payload.WithdrawalRequests() - if err != nil { - return nil, err - } + + withdrawalRequests := payload.WithdrawalRequests() withdrawalRequestsRoot, err := ssz.WithdrawalRequestSliceRoot(withdrawalRequests, fieldparams.MaxWithdrawalRequestsPerPayload) if err != nil { return nil, err @@ -1043,23 +1006,14 @@ func IsEmptyExecutionData(data interfaces.ExecutionData) (bool, error) { return false, nil } - drs, err := data.DepositReceipts() - switch { - case errors.Is(err, consensus_types.ErrUnsupportedField): - case err != nil: - return false, err - default: + epe, postElectra := data.(interfaces.ExecutionDataElectra) + if postElectra { + drs := epe.DepositReceipts() if len(drs) != 0 { return false, nil } - } - wrs, err := data.WithdrawalRequests() - switch { - case errors.Is(err, consensus_types.ErrUnsupportedField): - case err != nil: - return false, err - default: + wrs := epe.WithdrawalRequests() if len(wrs) != 0 { return false, nil } @@ -1077,6 +1031,8 @@ type executionPayloadHeaderDeneb struct { gweiValue uint64 } +var _ interfaces.ExecutionData = &executionPayloadHeaderDeneb{} + // WrappedExecutionPayloadHeaderDeneb is a constructor which wraps a protobuf execution header into an interface. func WrappedExecutionPayloadHeaderDeneb(p *enginev1.ExecutionPayloadHeaderDeneb, value math.Wei) (interfaces.ExecutionData, error) { w := executionPayloadHeaderDeneb{p: p, weiValue: value, gweiValue: uint64(math.WeiToGwei(value))} @@ -1251,16 +1207,6 @@ func (e executionPayloadHeaderDeneb) ValueInGwei() (uint64, error) { return e.gweiValue, nil } -// DepositReceipts -- -func (e executionPayloadHeaderDeneb) DepositReceipts() ([]*enginev1.DepositReceipt, error) { - return nil, consensus_types.ErrUnsupportedField -} - -// WithdrawalRequests -- -func (e executionPayloadHeaderDeneb) WithdrawalRequests() ([]*enginev1.ExecutionLayerWithdrawalRequest, error) { - return nil, consensus_types.ErrUnsupportedField -} - // IsBlinded returns true if the underlying data is blinded. func (e executionPayloadHeaderDeneb) IsBlinded() bool { return true @@ -1275,6 +1221,8 @@ type executionPayloadDeneb struct { gweiValue uint64 } +var _ interfaces.ExecutionData = &executionPayloadDeneb{} + // WrappedExecutionPayloadDeneb is a constructor which wraps a protobuf execution payload into an interface. func WrappedExecutionPayloadDeneb(p *enginev1.ExecutionPayloadDeneb, value math.Wei) (interfaces.ExecutionData, error) { w := executionPayloadDeneb{p: p, weiValue: value, gweiValue: uint64(math.WeiToGwei(value))} @@ -1447,16 +1395,6 @@ func (e executionPayloadDeneb) ValueInGwei() (uint64, error) { return e.gweiValue, nil } -// DepositReceipts -- -func (e executionPayloadDeneb) DepositReceipts() ([]*enginev1.DepositReceipt, error) { - return nil, consensus_types.ErrUnsupportedField -} - -// WithdrawalRequests -- -func (e executionPayloadDeneb) WithdrawalRequests() ([]*enginev1.ExecutionLayerWithdrawalRequest, error) { - return nil, consensus_types.ErrUnsupportedField -} - // IsBlinded returns true if the underlying data is blinded. func (e executionPayloadDeneb) IsBlinded() bool { return false @@ -1471,6 +1409,9 @@ type executionPayloadHeaderElectra struct { gweiValue uint64 } +var _ interfaces.ExecutionData = &executionPayloadElectra{} +var _ interfaces.ExecutionDataElectra = &executionPayloadElectra{} + // WrappedExecutionPayloadHeaderElectra is a constructor which wraps a protobuf execution header into an interface. func WrappedExecutionPayloadHeaderElectra(p *enginev1.ExecutionPayloadHeaderElectra, value math.Wei) (interfaces.ExecutionData, error) { w := executionPayloadHeaderElectra{p: p, weiValue: value, gweiValue: uint64(math.WeiToGwei(value))} @@ -1678,6 +1619,9 @@ func WrappedExecutionPayloadElectra(p *enginev1.ExecutionPayloadElectra, value m return w, nil } +var _ interfaces.ExecutionData = &executionPayloadElectra{} +var _ interfaces.ExecutionDataElectra = &executionPayloadElectra{} + // IsNil checks if the underlying data is nil. func (e executionPayloadElectra) IsNil() bool { return e.p == nil @@ -1842,13 +1786,13 @@ func (e executionPayloadElectra) ValueInGwei() (uint64, error) { } // DepositReceipts -- -func (e executionPayloadElectra) DepositReceipts() ([]*enginev1.DepositReceipt, error) { - return e.p.DepositReceipts, nil +func (e executionPayloadElectra) DepositReceipts() []*enginev1.DepositReceipt { + return e.p.DepositReceipts } // WithdrawalRequests -- -func (e executionPayloadElectra) WithdrawalRequests() ([]*enginev1.ExecutionLayerWithdrawalRequest, error) { - return e.p.WithdrawalRequests, nil +func (e executionPayloadElectra) WithdrawalRequests() []*enginev1.ExecutionLayerWithdrawalRequest { + return e.p.WithdrawalRequests } // IsBlinded returns true if the underlying data is blinded. diff --git a/consensus-types/blocks/factory_test.go b/consensus-types/blocks/factory_test.go index 2d9dc92c22ea..80a4a4f4f187 100644 --- a/consensus-types/blocks/factory_test.go +++ b/consensus-types/blocks/factory_test.go @@ -7,6 +7,7 @@ import ( "testing" fieldparams "github.com/prysmaticlabs/prysm/v5/config/fieldparams" + "github.com/prysmaticlabs/prysm/v5/consensus-types/interfaces" "github.com/prysmaticlabs/prysm/v5/encoding/bytesutil" enginev1 "github.com/prysmaticlabs/prysm/v5/proto/engine/v1" eth "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1" @@ -537,3 +538,26 @@ func TestBuildSignedBeaconBlockFromExecutionPayload(t *testing.T) { require.DeepEqual(t, uint64(321), payload.BlobGasUsed) }) } + +func TestElectraBlockBodyCast(t *testing.T) { + t.Run("deneb cast fails", func(t *testing.T) { + pb := ð.BeaconBlockBodyDeneb{} + i, err := NewBeaconBlockBody(pb) + require.NoError(t, err) + b, ok := i.(*BeaconBlockBody) + require.Equal(t, true, ok) + assert.Equal(t, version.Deneb, b.version) + _, err = interfaces.AsROBlockBodyElectra(b) + require.ErrorIs(t, err, interfaces.ErrInvalidCast) + }) + t.Run("electra cast succeeds", func(t *testing.T) { + pb := ð.BeaconBlockBodyElectra{} + i, err := NewBeaconBlockBody(pb) + require.NoError(t, err) + b, ok := i.(*BeaconBlockBody) + require.Equal(t, true, ok) + assert.Equal(t, version.Electra, b.version) + _, err = interfaces.AsROBlockBodyElectra(b) + require.NoError(t, err) + }) +} diff --git a/consensus-types/blocks/getters.go b/consensus-types/blocks/getters.go index acc80158bf73..d6578327c8e3 100644 --- a/consensus-types/blocks/getters.go +++ b/consensus-types/blocks/getters.go @@ -348,7 +348,11 @@ func (b *SignedBeaconBlock) ToBlinded() (interfaces.ReadOnlySignedBeaconBlock, e Signature: b.signature[:], }) case *enginev1.ExecutionPayloadElectra: - header, err := PayloadToHeaderElectra(payload) + pe, ok := payload.(interfaces.ExecutionDataElectra) + if !ok { + return nil, interfaces.ErrIncompatibleFork + } + header, err := PayloadToHeaderElectra(pe) if err != nil { return nil, err } @@ -377,7 +381,6 @@ func (b *SignedBeaconBlock) ToBlinded() (interfaces.ReadOnlySignedBeaconBlock, e }, Signature: b.signature[:], }) - default: return nil, fmt.Errorf("%T is not an execution payload header", p) } @@ -1230,11 +1233,8 @@ func (b *BeaconBlockBody) BlobKzgCommitments() ([][]byte, error) { } } -func (b *BeaconBlockBody) Consolidations() ([]*eth.SignedConsolidation, error) { - if b.version < version.Electra { - return nil, consensus_types.ErrNotSupported("Consolidations", b.version) - } - return b.signedConsolidations, nil +func (b *BeaconBlockBody) Consolidations() []*eth.SignedConsolidation { + return b.signedConsolidations } // Version returns the version of the beacon block body diff --git a/consensus-types/blocks/getters_test.go b/consensus-types/blocks/getters_test.go index b18a2caa7746..f5a7f250fe61 100644 --- a/consensus-types/blocks/getters_test.go +++ b/consensus-types/blocks/getters_test.go @@ -490,3 +490,9 @@ func hydrateBeaconBlockBody() *eth.BeaconBlockBody { }, } } + +func TestPreElectraFailsInterfaceAssertion(t *testing.T) { + var epd interfaces.ExecutionData = &executionPayloadDeneb{} + _, ok := epd.(interfaces.ExecutionDataElectra) + require.Equal(t, false, ok) +} diff --git a/consensus-types/blocks/types.go b/consensus-types/blocks/types.go index c6216b1d593c..b450ede83332 100644 --- a/consensus-types/blocks/types.go +++ b/consensus-types/blocks/types.go @@ -57,6 +57,9 @@ type BeaconBlockBody struct { signedConsolidations []*eth.SignedConsolidation } +var _ interfaces.ReadOnlyBeaconBlockBody = &BeaconBlockBody{} +var _ interfaces.ROBlockBodyElectra = &BeaconBlockBody{} + // BeaconBlock is the main beacon block structure. It can represent any block type. type BeaconBlock struct { version int diff --git a/consensus-types/interfaces/BUILD.bazel b/consensus-types/interfaces/BUILD.bazel index a6fbdd5df957..54ae434e8ed7 100644 --- a/consensus-types/interfaces/BUILD.bazel +++ b/consensus-types/interfaces/BUILD.bazel @@ -4,6 +4,8 @@ go_library( name = "go_default_library", srcs = [ "beacon_block.go", + "cast.go", + "error.go", "utils.go", ], importpath = "github.com/prysmaticlabs/prysm/v5/consensus-types/interfaces", @@ -15,6 +17,7 @@ go_library( "//proto/engine/v1:go_default_library", "//proto/prysm/v1alpha1:go_default_library", "//proto/prysm/v1alpha1/validator-client:go_default_library", + "//runtime/version:go_default_library", "@com_github_pkg_errors//:go_default_library", "@com_github_prysmaticlabs_fastssz//:go_default_library", "@org_golang_google_protobuf//proto:go_default_library", @@ -23,14 +26,19 @@ go_library( go_test( name = "go_default_test", - srcs = ["utils_test.go"], + srcs = [ + "error_test.go", + "utils_test.go", + ], + embed = [":go_default_library"], deps = [ - ":go_default_library", "//config/fieldparams:go_default_library", "//consensus-types/blocks:go_default_library", "//encoding/bytesutil:go_default_library", "//proto/prysm/v1alpha1:go_default_library", + "//runtime/version:go_default_library", "//testing/assert:go_default_library", "//testing/require:go_default_library", + "@com_github_pkg_errors//:go_default_library", ], ) diff --git a/consensus-types/interfaces/beacon_block.go b/consensus-types/interfaces/beacon_block.go index 7fb049f70a2c..ee79b3ec1067 100644 --- a/consensus-types/interfaces/beacon_block.go +++ b/consensus-types/interfaces/beacon_block.go @@ -1,6 +1,7 @@ package interfaces import ( + "github.com/pkg/errors" ssz "github.com/prysmaticlabs/fastssz" field_params "github.com/prysmaticlabs/prysm/v5/config/fieldparams" "github.com/prysmaticlabs/prysm/v5/consensus-types/primitives" @@ -11,6 +12,8 @@ import ( "google.golang.org/protobuf/proto" ) +var ErrIncompatibleFork = errors.New("can't convert to fork-specific interface") + // ReadOnlySignedBeaconBlock is an interface describing the method set of // a signed beacon block. type ReadOnlySignedBeaconBlock interface { @@ -77,7 +80,11 @@ type ReadOnlyBeaconBlockBody interface { Execution() (ExecutionData, error) BLSToExecutionChanges() ([]*ethpb.SignedBLSToExecutionChange, error) BlobKzgCommitments() ([][]byte, error) - Consolidations() ([]*ethpb.SignedConsolidation, error) +} + +type ROBlockBodyElectra interface { + ReadOnlyBeaconBlockBody + Consolidations() []*ethpb.SignedConsolidation } type SignedBeaconBlock interface { @@ -136,6 +143,10 @@ type ExecutionData interface { PbElectra() (*enginev1.ExecutionPayloadElectra, error) ValueInWei() (math.Wei, error) ValueInGwei() (uint64, error) - DepositReceipts() ([]*enginev1.DepositReceipt, error) - WithdrawalRequests() ([]*enginev1.ExecutionLayerWithdrawalRequest, error) +} + +type ExecutionDataElectra interface { + ExecutionData + DepositReceipts() []*enginev1.DepositReceipt + WithdrawalRequests() []*enginev1.ExecutionLayerWithdrawalRequest } diff --git a/consensus-types/interfaces/cast.go b/consensus-types/interfaces/cast.go new file mode 100644 index 000000000000..29f0c7a83082 --- /dev/null +++ b/consensus-types/interfaces/cast.go @@ -0,0 +1,15 @@ +package interfaces + +import "github.com/prysmaticlabs/prysm/v5/runtime/version" + +// AsROBlockBodyElectra safely asserts the ReadOnlyBeaconBlockBody to a ROBlockBodyElectra. +// This allows the caller to access methods on the block body which are only available on values after +// the Electra hard fork. If the value is for an earlier fork (based on comparing its Version() to the electra version) +// an error will be returned. Callers that want to conditionally process electra data can check for this condition +// and safely ignore it like `if err != nil && errors.Is(interfaces.ErrInvalidCast) {` +func AsROBlockBodyElectra(in ReadOnlyBeaconBlockBody) (ROBlockBodyElectra, error) { + if in.Version() >= version.Electra { + return in.(ROBlockBodyElectra), nil + } + return nil, NewInvalidCastError(in.Version(), version.Electra) +} diff --git a/consensus-types/interfaces/error.go b/consensus-types/interfaces/error.go new file mode 100644 index 000000000000..cb3c5aa6ed69 --- /dev/null +++ b/consensus-types/interfaces/error.go @@ -0,0 +1,27 @@ +package interfaces + +import ( + "github.com/pkg/errors" + "github.com/prysmaticlabs/prysm/v5/runtime/version" +) + +var ErrInvalidCast = errors.New("unable to cast between types") + +type InvalidCastError struct { + from int + to int +} + +func (e *InvalidCastError) Error() string { + return errors.Wrapf(ErrInvalidCast, + "from=%s(%d), to=%s(%d)", version.String(e.from), e.from, version.String(e.to), e.to). + Error() +} + +func (e *InvalidCastError) Is(err error) bool { + return errors.Is(err, ErrInvalidCast) +} + +func NewInvalidCastError(from, to int) *InvalidCastError { + return &InvalidCastError{from: from, to: to} +} diff --git a/consensus-types/interfaces/error_test.go b/consensus-types/interfaces/error_test.go new file mode 100644 index 000000000000..4bac169a255b --- /dev/null +++ b/consensus-types/interfaces/error_test.go @@ -0,0 +1,14 @@ +package interfaces + +import ( + "testing" + + "github.com/pkg/errors" + "github.com/prysmaticlabs/prysm/v5/runtime/version" + "github.com/prysmaticlabs/prysm/v5/testing/require" +) + +func TestNewInvalidCastError(t *testing.T) { + err := NewInvalidCastError(version.Phase0, version.Electra) + require.Equal(t, true, errors.Is(err, ErrInvalidCast)) +} diff --git a/consensus-types/mock/block.go b/consensus-types/mock/block.go index 4ba8e678aa81..c97496e23be4 100644 --- a/consensus-types/mock/block.go +++ b/consensus-types/mock/block.go @@ -313,7 +313,7 @@ func (b *BeaconBlockBody) BlobKzgCommitments() ([][]byte, error) { panic("implement me") } -func (b *BeaconBlockBody) Consolidations() ([]*eth.SignedConsolidation, error) { +func (b *BeaconBlockBody) Consolidations() []*eth.SignedConsolidation { panic("implement me") } @@ -328,3 +328,4 @@ func (b *BeaconBlockBody) Version() int { var _ interfaces.ReadOnlySignedBeaconBlock = &SignedBeaconBlock{} var _ interfaces.ReadOnlyBeaconBlock = &BeaconBlock{} var _ interfaces.ReadOnlyBeaconBlockBody = &BeaconBlockBody{} +var _ interfaces.ROBlockBodyElectra = &BeaconBlockBody{}