diff --git a/api/proposalopts.go b/api/proposalopts.go index 27809bce..35a555f6 100644 --- a/api/proposalopts.go +++ b/api/proposalopts.go @@ -1,4 +1,4 @@ -// Copyright © 2023 Attestant Limited. +// Copyright © 2023, 2024 Attestant Limited. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -23,9 +23,13 @@ type ProposalOpts struct { Slot phase0.Slot // RandaoReveal is the RANDAO reveal for the proposal. RandaoReveal phase0.BLSSignature - // Graffit is the graffiti to be included in the beacon block body. + // Graffiti is the graffiti to be included in the beacon block body. Graffiti [32]byte // SkipRandaoVerification is true if we do not want the server to verify our RANDAO reveal. // If this is set then the RANDAO reveal should be passed as the point at infinity (0xc0…00) SkipRandaoVerification bool + // BuilderBoostFactor is the relative weight of the builder payload versus a locally-produced + // payload, as per https://ethereum.github.io/beacon-APIs/#/Validator/produceBlockV3 + // This is optional; if not supplied it will use the default value of 100. + BuilderBoostFactor *uint64 } diff --git a/api/submitblindedproposalopts.go b/api/submitblindedproposalopts.go new file mode 100644 index 00000000..8355f47d --- /dev/null +++ b/api/submitblindedproposalopts.go @@ -0,0 +1,27 @@ +// Copyright © 2024 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import v2 "github.com/attestantio/go-eth2-client/api/v2" + +// SubmitBlindedProposalOpts are the options for submitting proposals. +type SubmitBlindedProposalOpts struct { + Common CommonOpts + + // Proposal is the proposal to submit. + Proposal *VersionedSignedBlindedProposal + + // BroadcastValidation is the validation required of the consensus node before broadcasting the proposal. + BroadcastValidation *v2.BroadcastValidation +} diff --git a/api/submitproposalopts.go b/api/submitproposalopts.go new file mode 100644 index 00000000..48884913 --- /dev/null +++ b/api/submitproposalopts.go @@ -0,0 +1,27 @@ +// Copyright © 2024 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import v2 "github.com/attestantio/go-eth2-client/api/v2" + +// SubmitProposalOpts are the options for submitting proposals. +type SubmitProposalOpts struct { + Common CommonOpts + + // Proposal is the proposal to submit. + Proposal *VersionedSignedProposal + + // BroadcastValidation is the validation required of the consensus node before broadcasting the proposal. + BroadcastValidation *v2.BroadcastValidation +} diff --git a/api/v2/broadcastvalidation.go b/api/v2/broadcastvalidation.go new file mode 100644 index 00000000..13cccaeb --- /dev/null +++ b/api/v2/broadcastvalidation.go @@ -0,0 +1,67 @@ +// Copyright © 2024 Attestant Limited. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package v2 + +import ( + "fmt" + "strings" +) + +// BroadcastValidation defines the validation to carry out prior to broadcasting proposals. +type BroadcastValidation int + +const ( + // BroadcastValidationGossip means carry out lightweight gossip checks. + BroadcastValidationGossip BroadcastValidation = iota + // BroadcastValidationConsensus means carry out full consensus checks. + BroadcastValidationConsensus + // BroadcastValidationConsensusAndEquivocation means carry out consensus and equivocation checks. + BroadcastValidationConsensusAndEquivocation +) + +var broadcastValidationStrings = [...]string{ + "gossip", + "consensus", + "consensus_and_equivocation", +} + +// MarshalJSON implements json.Marshaler. +func (b *BroadcastValidation) MarshalJSON() ([]byte, error) { + return []byte(fmt.Sprintf("%q", broadcastValidationStrings[*b])), nil +} + +// UnmarshalJSON implements json.Unmarshaler. +func (b *BroadcastValidation) UnmarshalJSON(input []byte) error { + var err error + switch strings.ToLower(string(input)) { + case `"gossip"`: + *b = BroadcastValidationGossip + case `"consensus"`: + *b = BroadcastValidationConsensus + case `"consensus_and_equivocation"`: + *b = BroadcastValidationConsensusAndEquivocation + default: + err = fmt.Errorf("unrecognised broadcast validation %s", string(input)) + } + + return err +} + +func (b BroadcastValidation) String() string { + if b < 0 || int(b) >= len(broadcastValidationStrings) { + return broadcastValidationStrings[0] // unknown + } + + return broadcastValidationStrings[b] +} diff --git a/api/versionedproposal.go b/api/versionedproposal.go index 2ead375c..5d565106 100644 --- a/api/versionedproposal.go +++ b/api/versionedproposal.go @@ -14,6 +14,10 @@ package api import ( + "math/big" + + apiv1bellatrix "github.com/attestantio/go-eth2-client/api/v1/bellatrix" + apiv1capella "github.com/attestantio/go-eth2-client/api/v1/capella" apiv1deneb "github.com/attestantio/go-eth2-client/api/v1/deneb" "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/altair" @@ -25,12 +29,18 @@ import ( // VersionedProposal contains a versioned proposal. type VersionedProposal struct { - Version spec.DataVersion - Phase0 *phase0.BeaconBlock - Altair *altair.BeaconBlock - Bellatrix *bellatrix.BeaconBlock - Capella *capella.BeaconBlock - Deneb *apiv1deneb.BlockContents + Version spec.DataVersion + Blinded bool + ConsensusValue *big.Int + ExecutionValue *big.Int + Phase0 *phase0.BeaconBlock + Altair *altair.BeaconBlock + Bellatrix *bellatrix.BeaconBlock + BellatrixBlinded *apiv1bellatrix.BlindedBeaconBlock + Capella *capella.BeaconBlock + CapellaBlinded *apiv1capella.BlindedBeaconBlock + Deneb *apiv1deneb.BlockContents + DenebBlinded *apiv1deneb.BlindedBeaconBlock } // IsEmpty returns true if there is no proposal. @@ -38,401 +48,340 @@ func (v *VersionedProposal) IsEmpty() bool { return v.Phase0 == nil && v.Altair == nil && v.Bellatrix == nil && + v.BellatrixBlinded == nil && v.Capella == nil && - v.Deneb == nil + v.CapellaBlinded == nil && + v.Deneb == nil && + v.DenebBlinded == nil } -// Slot returns the slot of the proposal. -func (v *VersionedProposal) Slot() (phase0.Slot, error) { +// BodyRoot returns the body root of the proposal. +func (v *VersionedProposal) BodyRoot() (phase0.Root, error) { + if !v.bodyPresent() { + return phase0.Root{}, ErrDataMissing + } + switch v.Version { case spec.DataVersionPhase0: - if v.Phase0 == nil { - return 0, ErrDataMissing - } - - return v.Phase0.Slot, nil + return v.Phase0.Body.HashTreeRoot() case spec.DataVersionAltair: - if v.Altair == nil { - return 0, ErrDataMissing - } - - return v.Altair.Slot, nil + return v.Altair.Body.HashTreeRoot() case spec.DataVersionBellatrix: - if v.Bellatrix == nil { - return 0, ErrDataMissing + if v.Blinded { + return v.BellatrixBlinded.Body.HashTreeRoot() } - return v.Bellatrix.Slot, nil + return v.Bellatrix.Body.HashTreeRoot() case spec.DataVersionCapella: - if v.Capella == nil { - return 0, ErrDataMissing + if v.Blinded { + return v.CapellaBlinded.Body.HashTreeRoot() } - return v.Capella.Slot, nil + return v.Capella.Body.HashTreeRoot() case spec.DataVersionDeneb: - if v.Deneb == nil || - v.Deneb.Block == nil { - return 0, ErrDataMissing + if v.Blinded { + return v.DenebBlinded.Body.HashTreeRoot() } - return v.Deneb.Block.Slot, nil + return v.Deneb.Block.Body.HashTreeRoot() default: - return 0, ErrUnsupportedVersion + return phase0.Root{}, ErrUnsupportedVersion } } -// ProposerIndex returns the proposer index of the proposal. -func (v *VersionedProposal) ProposerIndex() (phase0.ValidatorIndex, error) { +// ParentRoot returns the parent root of the proposal. +func (v *VersionedProposal) ParentRoot() (phase0.Root, error) { + if !v.proposalPresent() { + return phase0.Root{}, ErrDataMissing + } + switch v.Version { case spec.DataVersionPhase0: - if v.Phase0 == nil { - return 0, ErrDataMissing - } - - return v.Phase0.ProposerIndex, nil + return v.Phase0.ParentRoot, nil case spec.DataVersionAltair: - if v.Altair == nil { - return 0, ErrDataMissing - } - - return v.Altair.ProposerIndex, nil + return v.Altair.ParentRoot, nil case spec.DataVersionBellatrix: - if v.Bellatrix == nil { - return 0, ErrDataMissing + if v.Blinded { + return v.BellatrixBlinded.ParentRoot, nil } - return v.Bellatrix.ProposerIndex, nil + return v.Bellatrix.ParentRoot, nil case spec.DataVersionCapella: - if v.Capella == nil { - return 0, ErrDataMissing + if v.Blinded { + return v.CapellaBlinded.ParentRoot, nil } - return v.Capella.ProposerIndex, nil + return v.Capella.ParentRoot, nil case spec.DataVersionDeneb: - if v.Deneb == nil || - v.Deneb.Block == nil { - return 0, ErrDataMissing + if v.Blinded { + return v.DenebBlinded.ParentRoot, nil } - return v.Deneb.Block.ProposerIndex, nil + return v.Deneb.Block.ParentRoot, nil default: - return 0, ErrUnsupportedVersion + return phase0.Root{}, ErrUnsupportedVersion } } -// RandaoReveal returns the RANDAO reveal of the proposal. -func (v *VersionedProposal) RandaoReveal() (phase0.BLSSignature, error) { +// ProposerIndex returns the proposer index of the proposal. +func (v *VersionedProposal) ProposerIndex() (phase0.ValidatorIndex, error) { + if !v.proposalPresent() { + return 0, ErrDataMissing + } + switch v.Version { case spec.DataVersionPhase0: - if v.Phase0 == nil || - v.Phase0.Body == nil { - return phase0.BLSSignature{}, ErrDataMissing - } - - return v.Phase0.Body.RANDAOReveal, nil + return v.Phase0.ProposerIndex, nil case spec.DataVersionAltair: - if v.Altair == nil || - v.Altair.Body == nil { - return phase0.BLSSignature{}, ErrDataMissing - } - - return v.Altair.Body.RANDAOReveal, nil + return v.Altair.ProposerIndex, nil case spec.DataVersionBellatrix: - if v.Bellatrix == nil || - v.Bellatrix.Body == nil { - return phase0.BLSSignature{}, ErrDataMissing + if v.Blinded { + return v.BellatrixBlinded.ProposerIndex, nil } - return v.Bellatrix.Body.RANDAOReveal, nil + return v.Bellatrix.ProposerIndex, nil case spec.DataVersionCapella: - if v.Capella == nil || - v.Capella.Body == nil { - return phase0.BLSSignature{}, ErrDataMissing + if v.Blinded { + return v.CapellaBlinded.ProposerIndex, nil } - return v.Capella.Body.RANDAOReveal, nil + return v.Capella.ProposerIndex, nil case spec.DataVersionDeneb: - if v.Deneb == nil || - v.Deneb.Block == nil || - v.Deneb.Block.Body == nil { - return phase0.BLSSignature{}, ErrDataMissing + if v.Blinded { + return v.DenebBlinded.ProposerIndex, nil } - return v.Deneb.Block.Body.RANDAOReveal, nil + return v.Deneb.Block.ProposerIndex, nil default: - return phase0.BLSSignature{}, ErrUnsupportedVersion + return 0, ErrUnsupportedVersion } } -// Graffiti returns the graffiti of the proposal. -func (v *VersionedProposal) Graffiti() ([32]byte, error) { +// Root returns the root of the proposal. +func (v *VersionedProposal) Root() (phase0.Root, error) { + if !v.proposalPresent() { + return phase0.Root{}, ErrDataMissing + } + switch v.Version { case spec.DataVersionPhase0: - if v.Phase0 == nil || - v.Phase0.Body == nil { - return [32]byte{}, ErrDataMissing - } - - return v.Phase0.Body.Graffiti, nil + return v.Phase0.HashTreeRoot() case spec.DataVersionAltair: - if v.Altair == nil || - v.Altair.Body == nil { - return [32]byte{}, ErrDataMissing - } - - return v.Altair.Body.Graffiti, nil + return v.Altair.HashTreeRoot() case spec.DataVersionBellatrix: - if v.Bellatrix == nil || - v.Bellatrix.Body == nil { - return [32]byte{}, ErrDataMissing + if v.Blinded { + return v.BellatrixBlinded.HashTreeRoot() } - return v.Bellatrix.Body.Graffiti, nil + return v.Bellatrix.HashTreeRoot() case spec.DataVersionCapella: - if v.Capella == nil || - v.Capella.Body == nil { - return [32]byte{}, ErrDataMissing + if v.Blinded { + return v.CapellaBlinded.HashTreeRoot() } - return v.Capella.Body.Graffiti, nil + return v.Capella.HashTreeRoot() case spec.DataVersionDeneb: - if v.Deneb == nil || - v.Deneb.Block == nil || - v.Deneb.Block.Body == nil { - return [32]byte{}, ErrDataMissing + if v.Blinded { + return v.DenebBlinded.HashTreeRoot() } - return v.Deneb.Block.Body.Graffiti, nil + return v.Deneb.Block.HashTreeRoot() default: - return [32]byte{}, ErrUnsupportedVersion + return phase0.Root{}, ErrUnsupportedVersion } } -// Attestations returns the attestations of the proposal. -func (v *VersionedProposal) Attestations() ([]*phase0.Attestation, error) { +// Slot returns the slot of the proposal. +func (v *VersionedProposal) Slot() (phase0.Slot, error) { + if !v.proposalPresent() { + return 0, ErrDataMissing + } + switch v.Version { case spec.DataVersionPhase0: - if v.Phase0 == nil || - v.Phase0.Body == nil { - return nil, ErrDataMissing - } - - return v.Phase0.Body.Attestations, nil + return v.Phase0.Slot, nil case spec.DataVersionAltair: - if v.Altair == nil || - v.Altair.Body == nil { - return nil, ErrDataMissing - } - - return v.Altair.Body.Attestations, nil + return v.Altair.Slot, nil case spec.DataVersionBellatrix: - if v.Bellatrix == nil || - v.Bellatrix.Body == nil { - return nil, ErrDataMissing + if v.Blinded { + return v.BellatrixBlinded.Slot, nil } - return v.Bellatrix.Body.Attestations, nil + return v.Bellatrix.Slot, nil case spec.DataVersionCapella: - if v.Capella == nil || - v.Capella.Body == nil { - return nil, ErrDataMissing + if v.Blinded { + return v.CapellaBlinded.Slot, nil } - return v.Capella.Body.Attestations, nil + return v.Capella.Slot, nil case spec.DataVersionDeneb: - if v.Deneb == nil || - v.Deneb.Block == nil || - v.Deneb.Block.Body == nil { - return nil, ErrDataMissing + if v.Blinded { + return v.DenebBlinded.Slot, nil } - return v.Deneb.Block.Body.Attestations, nil + return v.Deneb.Block.Slot, nil default: - return nil, ErrUnsupportedVersion + return 0, ErrUnsupportedVersion } } -// Root returns the root of the proposal. -func (v *VersionedProposal) Root() (phase0.Root, error) { +// StateRoot returns the state root of the proposal. +func (v *VersionedProposal) StateRoot() (phase0.Root, error) { + if !v.proposalPresent() { + return phase0.Root{}, ErrDataMissing + } + switch v.Version { case spec.DataVersionPhase0: - if v.Phase0 == nil { - return phase0.Root{}, ErrDataMissing - } - - return v.Phase0.HashTreeRoot() + return v.Phase0.StateRoot, nil case spec.DataVersionAltair: - if v.Altair == nil { - return phase0.Root{}, ErrDataMissing - } - - return v.Altair.HashTreeRoot() + return v.Altair.StateRoot, nil case spec.DataVersionBellatrix: - if v.Bellatrix == nil { - return phase0.Root{}, ErrDataMissing + if v.Blinded { + return v.BellatrixBlinded.StateRoot, nil } - return v.Bellatrix.HashTreeRoot() + return v.Bellatrix.StateRoot, nil case spec.DataVersionCapella: - if v.Capella == nil { - return phase0.Root{}, ErrDataMissing + if v.Blinded { + return v.CapellaBlinded.StateRoot, nil } - return v.Capella.HashTreeRoot() + return v.Capella.StateRoot, nil case spec.DataVersionDeneb: - if v.Deneb == nil || - v.Deneb.Block == nil { - return phase0.Root{}, ErrDataMissing + if v.Blinded { + return v.DenebBlinded.StateRoot, nil } - return v.Deneb.Block.HashTreeRoot() + return v.Deneb.Block.StateRoot, nil default: return phase0.Root{}, ErrUnsupportedVersion } } -// BodyRoot returns the body root of the proposal. -func (v *VersionedProposal) BodyRoot() (phase0.Root, error) { +// Attestations returns the attestations of the proposal. +func (v *VersionedProposal) Attestations() ([]*phase0.Attestation, error) { + if !v.bodyPresent() { + return nil, ErrDataMissing + } + switch v.Version { case spec.DataVersionPhase0: - if v.Phase0 == nil { - return phase0.Root{}, ErrDataMissing - } - - return v.Phase0.Body.HashTreeRoot() + return v.Phase0.Body.Attestations, nil case spec.DataVersionAltair: - if v.Altair == nil { - return phase0.Root{}, ErrDataMissing - } - - return v.Altair.Body.HashTreeRoot() + return v.Altair.Body.Attestations, nil case spec.DataVersionBellatrix: - if v.Bellatrix == nil { - return phase0.Root{}, ErrDataMissing + if v.Blinded { + return v.BellatrixBlinded.Body.Attestations, nil } - return v.Bellatrix.Body.HashTreeRoot() + return v.Bellatrix.Body.Attestations, nil case spec.DataVersionCapella: - if v.Capella == nil { - return phase0.Root{}, ErrDataMissing + if v.Blinded { + return v.CapellaBlinded.Body.Attestations, nil } - return v.Capella.Body.HashTreeRoot() + return v.Capella.Body.Attestations, nil case spec.DataVersionDeneb: - if v.Deneb == nil || - v.Deneb.Block == nil || - v.Deneb.Block.Body == nil { - return phase0.Root{}, ErrDataMissing + if v.Blinded { + return v.DenebBlinded.Body.Attestations, nil } - return v.Deneb.Block.Body.HashTreeRoot() + return v.Deneb.Block.Body.Attestations, nil default: - return phase0.Root{}, ErrUnsupportedVersion + return nil, ErrUnsupportedVersion } } -// ParentRoot returns the parent root of the proposal. -func (v *VersionedProposal) ParentRoot() (phase0.Root, error) { +// Graffiti returns the graffiti of the proposal. +func (v *VersionedProposal) Graffiti() ([32]byte, error) { + if !v.bodyPresent() { + return [32]byte{}, ErrDataMissing + } + switch v.Version { case spec.DataVersionPhase0: - if v.Phase0 == nil { - return phase0.Root{}, ErrDataMissing - } - - return v.Phase0.ParentRoot, nil + return v.Phase0.Body.Graffiti, nil case spec.DataVersionAltair: - if v.Altair == nil { - return phase0.Root{}, ErrDataMissing - } - - return v.Altair.ParentRoot, nil + return v.Altair.Body.Graffiti, nil case spec.DataVersionBellatrix: - if v.Bellatrix == nil { - return phase0.Root{}, ErrDataMissing + if v.Blinded { + return v.BellatrixBlinded.Body.Graffiti, nil } - return v.Bellatrix.ParentRoot, nil + return v.Bellatrix.Body.Graffiti, nil case spec.DataVersionCapella: - if v.Capella == nil { - return phase0.Root{}, ErrDataMissing + if v.Blinded { + return v.CapellaBlinded.Body.Graffiti, nil } - return v.Capella.ParentRoot, nil + return v.Capella.Body.Graffiti, nil case spec.DataVersionDeneb: - if v.Deneb == nil || - v.Deneb.Block == nil { - return phase0.Root{}, ErrDataMissing + if v.Blinded { + return v.DenebBlinded.Body.Graffiti, nil } - return v.Deneb.Block.ParentRoot, nil + return v.Deneb.Block.Body.Graffiti, nil default: - return phase0.Root{}, ErrUnsupportedVersion + return [32]byte{}, ErrUnsupportedVersion } } -// StateRoot returns the state root of the proposal. -func (v *VersionedProposal) StateRoot() (phase0.Root, error) { +// RandaoReveal returns the RANDAO reveal of the proposal. +func (v *VersionedProposal) RandaoReveal() (phase0.BLSSignature, error) { + if !v.bodyPresent() { + return phase0.BLSSignature{}, ErrDataMissing + } + switch v.Version { case spec.DataVersionPhase0: - if v.Phase0 == nil { - return phase0.Root{}, ErrDataMissing - } - - return v.Phase0.StateRoot, nil + return v.Phase0.Body.RANDAOReveal, nil case spec.DataVersionAltair: - if v.Altair == nil { - return phase0.Root{}, ErrDataMissing - } - - return v.Altair.StateRoot, nil + return v.Altair.Body.RANDAOReveal, nil case spec.DataVersionBellatrix: - if v.Bellatrix == nil { - return phase0.Root{}, ErrDataMissing + if v.Blinded { + return v.BellatrixBlinded.Body.RANDAOReveal, nil } - return v.Bellatrix.StateRoot, nil + return v.Bellatrix.Body.RANDAOReveal, nil case spec.DataVersionCapella: - if v.Capella == nil { - return phase0.Root{}, ErrDataMissing + if v.Blinded { + return v.CapellaBlinded.Body.RANDAOReveal, nil } - return v.Capella.StateRoot, nil + return v.Capella.Body.RANDAOReveal, nil case spec.DataVersionDeneb: - if v.Deneb == nil || - v.Deneb.Block == nil { - return phase0.Root{}, ErrDataMissing + if v.Blinded { + return v.DenebBlinded.Body.RANDAOReveal, nil } - return v.Deneb.Block.StateRoot, nil + return v.Deneb.Block.Body.RANDAOReveal, nil default: - return phase0.Root{}, ErrUnsupportedVersion + return phase0.BLSSignature{}, ErrUnsupportedVersion } } // Transactions returns the transactions of the proposal. func (v *VersionedProposal) Transactions() ([]bellatrix.Transaction, error) { + if v.Version >= spec.DataVersionBellatrix && !v.payloadPresent() { + return nil, ErrDataMissing + } + switch v.Version { case spec.DataVersionBellatrix: - if v.Bellatrix == nil || - v.Bellatrix.Body == nil || - v.Bellatrix.Body.ExecutionPayload == nil { + if v.Blinded { return nil, ErrDataMissing } return v.Bellatrix.Body.ExecutionPayload.Transactions, nil case spec.DataVersionCapella: - if v.Capella == nil || - v.Capella.Body == nil || - v.Capella.Body.ExecutionPayload == nil { + if v.Blinded { return nil, ErrDataMissing } return v.Capella.Body.ExecutionPayload.Transactions, nil case spec.DataVersionDeneb: - if v.Deneb == nil || - v.Deneb.Block == nil || - v.Deneb.Block.Body == nil || - v.Deneb.Block.Body.ExecutionPayload == nil { + if v.Blinded { return nil, ErrDataMissing } @@ -444,29 +393,26 @@ func (v *VersionedProposal) Transactions() ([]bellatrix.Transaction, error) { // FeeRecipient returns the fee recipient of the proposal. func (v *VersionedProposal) FeeRecipient() (bellatrix.ExecutionAddress, error) { + if v.Version >= spec.DataVersionBellatrix && !v.payloadPresent() { + return bellatrix.ExecutionAddress{}, ErrDataMissing + } + switch v.Version { case spec.DataVersionBellatrix: - if v.Bellatrix == nil || - v.Bellatrix.Body == nil || - v.Bellatrix.Body.ExecutionPayload == nil { - return bellatrix.ExecutionAddress{}, ErrDataMissing + if v.Blinded { + return v.BellatrixBlinded.Body.ExecutionPayloadHeader.FeeRecipient, nil } return v.Bellatrix.Body.ExecutionPayload.FeeRecipient, nil case spec.DataVersionCapella: - if v.Capella == nil || - v.Capella.Body == nil || - v.Capella.Body.ExecutionPayload == nil { - return bellatrix.ExecutionAddress{}, ErrDataMissing + if v.Blinded { + return v.CapellaBlinded.Body.ExecutionPayloadHeader.FeeRecipient, nil } return v.Capella.Body.ExecutionPayload.FeeRecipient, nil case spec.DataVersionDeneb: - if v.Deneb == nil || - v.Deneb.Block == nil || - v.Deneb.Block.Body == nil || - v.Deneb.Block.Body.ExecutionPayload == nil { - return bellatrix.ExecutionAddress{}, ErrDataMissing + if v.Blinded { + return v.DenebBlinded.Body.ExecutionPayloadHeader.FeeRecipient, nil } return v.Deneb.Block.Body.ExecutionPayload.FeeRecipient, nil @@ -477,29 +423,26 @@ func (v *VersionedProposal) FeeRecipient() (bellatrix.ExecutionAddress, error) { // Timestamp returns the timestamp of the proposal. func (v *VersionedProposal) Timestamp() (uint64, error) { + if v.Version >= spec.DataVersionBellatrix && !v.payloadPresent() { + return 0, ErrDataMissing + } + switch v.Version { case spec.DataVersionBellatrix: - if v.Bellatrix == nil || - v.Bellatrix.Body == nil || - v.Bellatrix.Body.ExecutionPayload == nil { - return 0, ErrDataMissing + if v.Blinded { + return v.BellatrixBlinded.Body.ExecutionPayloadHeader.Timestamp, nil } return v.Bellatrix.Body.ExecutionPayload.Timestamp, nil case spec.DataVersionCapella: - if v.Capella == nil || - v.Capella.Body == nil || - v.Capella.Body.ExecutionPayload == nil { - return 0, ErrDataMissing + if v.Blinded { + return v.CapellaBlinded.Body.ExecutionPayloadHeader.Timestamp, nil } return v.Capella.Body.ExecutionPayload.Timestamp, nil case spec.DataVersionDeneb: - if v.Deneb == nil || - v.Deneb.Block == nil || - v.Deneb.Block.Body == nil || - v.Deneb.Block.Body.ExecutionPayload == nil { - return 0, ErrDataMissing + if v.Blinded { + return v.DenebBlinded.Body.ExecutionPayloadHeader.Timestamp, nil } return v.Deneb.Block.Body.ExecutionPayload.Timestamp, nil @@ -510,9 +453,13 @@ func (v *VersionedProposal) Timestamp() (uint64, error) { // Blobs returns the blobs of the proposal. func (v *VersionedProposal) Blobs() ([]deneb.Blob, error) { + if v.Version >= spec.DataVersionDeneb && !v.payloadPresent() { + return nil, ErrDataMissing + } + switch v.Version { case spec.DataVersionDeneb: - if v.Deneb == nil { + if v.Blinded { return nil, ErrDataMissing } @@ -524,9 +471,13 @@ func (v *VersionedProposal) Blobs() ([]deneb.Blob, error) { // KZGProofs returns the KZG proofs of the proposal. func (v *VersionedProposal) KZGProofs() ([]deneb.KZGProof, error) { + if v.Version >= spec.DataVersionDeneb && !v.payloadPresent() { + return nil, ErrDataMissing + } + switch v.Version { case spec.DataVersionDeneb: - if v.Deneb == nil { + if v.Blinded { return nil, ErrDataMissing } @@ -536,6 +487,19 @@ func (v *VersionedProposal) KZGProofs() ([]deneb.KZGProof, error) { } } +// Value returns the value of the proposal, in Wei. +func (v *VersionedProposal) Value() *big.Int { + value := big.NewInt(0) + if v.ConsensusValue != nil { + value = value.Add(value, v.ConsensusValue) + } + if v.ExecutionValue != nil { + value = value.Add(value, v.ExecutionValue) + } + + return value +} + // String returns a string version of the structure. func (v *VersionedProposal) String() string { switch v.Version { @@ -573,3 +537,90 @@ func (v *VersionedProposal) String() string { return "unknown version" } } + +func (v *VersionedProposal) proposalPresent() bool { + switch v.Version { + case spec.DataVersionPhase0: + return v.Phase0 != nil + case spec.DataVersionAltair: + return v.Altair != nil + case spec.DataVersionBellatrix: + if v.Blinded { + return v.BellatrixBlinded != nil + } + + return v.Bellatrix != nil + case spec.DataVersionCapella: + if v.Blinded { + return v.CapellaBlinded != nil + } + + return v.Capella != nil + case spec.DataVersionDeneb: + if v.Blinded { + return v.DenebBlinded != nil + } + + return v.Deneb.Block != nil + } + + return false +} + +func (v *VersionedProposal) bodyPresent() bool { + switch v.Version { + case spec.DataVersionPhase0: + return v.Phase0 != nil && v.Phase0.Body != nil + case spec.DataVersionAltair: + return v.Altair != nil && v.Altair.Body != nil + case spec.DataVersionBellatrix: + if v.Blinded { + return v.BellatrixBlinded != nil && v.BellatrixBlinded.Body != nil + } + + return v.Bellatrix != nil && v.Bellatrix.Body != nil + case spec.DataVersionCapella: + if v.Blinded { + return v.CapellaBlinded != nil && v.CapellaBlinded.Body != nil + } + + return v.Capella != nil && v.Capella.Body != nil + case spec.DataVersionDeneb: + if v.Blinded { + return v.DenebBlinded != nil && v.DenebBlinded.Body != nil + } + + return v.Deneb != nil && v.Deneb.Block != nil && v.Deneb.Block.Body != nil + } + + return false +} + +func (v *VersionedProposal) payloadPresent() bool { + switch v.Version { + case spec.DataVersionPhase0: + return false + case spec.DataVersionAltair: + return false + case spec.DataVersionBellatrix: + if v.Blinded { + return v.BellatrixBlinded != nil && v.BellatrixBlinded.Body != nil && v.BellatrixBlinded.Body.ExecutionPayloadHeader != nil + } + + return v.Bellatrix != nil && v.Bellatrix.Body != nil && v.Bellatrix.Body.ExecutionPayload != nil + case spec.DataVersionCapella: + if v.Blinded { + return v.CapellaBlinded != nil && v.CapellaBlinded.Body != nil && v.CapellaBlinded.Body.ExecutionPayloadHeader != nil + } + + return v.Capella != nil && v.Capella.Body != nil && v.Capella.Body.ExecutionPayload != nil + case spec.DataVersionDeneb: + if v.Blinded { + return v.DenebBlinded != nil && v.DenebBlinded.Body != nil && v.DenebBlinded.Body.ExecutionPayloadHeader != nil + } + + return v.Deneb != nil && v.Deneb.Block != nil && v.Deneb.Block.Body != nil && v.Deneb.Block.Body.ExecutionPayload != nil + } + + return false +} diff --git a/api/versionedsignedproposal.go b/api/versionedsignedproposal.go index 6377411e..41de741a 100644 --- a/api/versionedsignedproposal.go +++ b/api/versionedsignedproposal.go @@ -1,4 +1,4 @@ -// Copyright © 2023 Attestant Limited. +// Copyright © 2023, 2024 Attestant Limited. // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at @@ -14,6 +14,11 @@ package api import ( + "errors" + "math/big" + + apiv1bellatrix "github.com/attestantio/go-eth2-client/api/v1/bellatrix" + apiv1capella "github.com/attestantio/go-eth2-client/api/v1/capella" apiv1deneb "github.com/attestantio/go-eth2-client/api/v1/deneb" "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/altair" @@ -24,50 +29,86 @@ import ( // VersionedSignedProposal contains a versioned signed beacon node proposal. type VersionedSignedProposal struct { - Version spec.DataVersion - Phase0 *phase0.SignedBeaconBlock - Altair *altair.SignedBeaconBlock - Bellatrix *bellatrix.SignedBeaconBlock - Capella *capella.SignedBeaconBlock - Deneb *apiv1deneb.SignedBlockContents + Version spec.DataVersion + Blinded bool + ConsensusValue *big.Int + ExecutionValue *big.Int + Phase0 *phase0.SignedBeaconBlock + Altair *altair.SignedBeaconBlock + Bellatrix *bellatrix.SignedBeaconBlock + BellatrixBlinded *apiv1bellatrix.SignedBlindedBeaconBlock + Capella *capella.SignedBeaconBlock + CapellaBlinded *apiv1capella.SignedBlindedBeaconBlock + Deneb *apiv1deneb.SignedBlockContents + DenebBlinded *apiv1deneb.SignedBlindedBeaconBlock } -// Slot returns the slot of the signed proposal. -func (v *VersionedSignedProposal) Slot() (phase0.Slot, error) { +// AssertPresnet throws an error if the expected proposal +// given the version and blinded fields is not present. +func (v *VersionedSignedProposal) AssertPresent() error { switch v.Version { case spec.DataVersionPhase0: - if v.Phase0 == nil || - v.Phase0.Message == nil { - return 0, ErrDataMissing + if v.Phase0 == nil { + return errors.New("phase0 proposal not present") } - - return v.Phase0.Message.Slot, nil case spec.DataVersionAltair: - if v.Altair == nil || - v.Altair.Message == nil { - return 0, ErrDataMissing + if v.Altair == nil { + return errors.New("altair proposal not present") + } + case spec.DataVersionBellatrix: + if v.Bellatrix == nil && !v.Blinded { + return errors.New("bellatrix proposal not present") } + if v.BellatrixBlinded == nil && v.Blinded { + return errors.New("blinded bellatrix proposal not present") + } + case spec.DataVersionCapella: + if v.Capella == nil && !v.Blinded { + return errors.New("capella proposal not present") + } + if v.CapellaBlinded == nil && v.Blinded { + return errors.New("blinded capella proposal not present") + } + case spec.DataVersionDeneb: + if v.Deneb == nil && !v.Blinded { + return errors.New("deneb proposal not present") + } + if v.DenebBlinded == nil && v.Blinded { + return errors.New("blinded deneb proposal not present") + } + default: + return errors.New("unsupported version") + } + return nil +} + +// Slot returns the slot of the signed proposal. +func (v *VersionedSignedProposal) Slot() (phase0.Slot, error) { + if err := v.assertMessagePresent(); err != nil { + return 0, err + } + + switch v.Version { + case spec.DataVersionPhase0: + return v.Phase0.Message.Slot, nil + case spec.DataVersionAltair: return v.Altair.Message.Slot, nil case spec.DataVersionBellatrix: - if v.Bellatrix == nil || - v.Bellatrix.Message == nil { - return 0, ErrDataMissing + if v.Blinded { + return v.BellatrixBlinded.Message.Slot, nil } return v.Bellatrix.Message.Slot, nil case spec.DataVersionCapella: - if v.Capella == nil || - v.Capella.Message == nil { - return 0, ErrDataMissing + if v.Blinded { + return v.CapellaBlinded.Message.Slot, nil } return v.Capella.Message.Slot, nil case spec.DataVersionDeneb: - if v.Deneb == nil || - v.Deneb.SignedBlock == nil || - v.Deneb.SignedBlock.Message == nil { - return 0, ErrDataMissing + if v.Blinded { + return v.DenebBlinded.Message.Slot, nil } return v.Deneb.SignedBlock.Message.Slot, nil @@ -78,40 +119,30 @@ func (v *VersionedSignedProposal) Slot() (phase0.Slot, error) { // ProposerIndex returns the proposer index of the signed proposal. func (v *VersionedSignedProposal) ProposerIndex() (phase0.ValidatorIndex, error) { + if err := v.assertMessagePresent(); err != nil { + return 0, err + } + switch v.Version { case spec.DataVersionPhase0: - if v.Phase0 == nil || - v.Phase0.Message == nil { - return 0, ErrDataMissing - } - return v.Phase0.Message.ProposerIndex, nil case spec.DataVersionAltair: - if v.Altair == nil || - v.Altair.Message == nil { - return 0, ErrDataMissing - } - return v.Altair.Message.ProposerIndex, nil case spec.DataVersionBellatrix: - if v.Bellatrix == nil || - v.Bellatrix.Message == nil { - return 0, ErrDataMissing + if v.Blinded { + return v.BellatrixBlinded.Message.ProposerIndex, nil } return v.Bellatrix.Message.ProposerIndex, nil case spec.DataVersionCapella: - if v.Capella == nil || - v.Capella.Message == nil { - return 0, ErrDataMissing + if v.Blinded { + return v.CapellaBlinded.Message.ProposerIndex, nil } return v.Capella.Message.ProposerIndex, nil case spec.DataVersionDeneb: - if v.Deneb == nil || - v.Deneb.SignedBlock == nil || - v.Deneb.SignedBlock.Message == nil { - return 0, ErrDataMissing + if v.Blinded { + return v.DenebBlinded.Message.ProposerIndex, nil } return v.Deneb.SignedBlock.Message.ProposerIndex, nil @@ -122,32 +153,26 @@ func (v *VersionedSignedProposal) ProposerIndex() (phase0.ValidatorIndex, error) // ExecutionBlockHash returns the hash of the execution payload. func (v *VersionedSignedProposal) ExecutionBlockHash() (phase0.Hash32, error) { + if err := v.assertExecutionPayloadPresent(); err != nil { + return phase0.Hash32{}, err + } + switch v.Version { case spec.DataVersionBellatrix: - if v.Bellatrix == nil || - v.Bellatrix.Message == nil || - v.Bellatrix.Message.Body == nil || - v.Bellatrix.Message.Body.ExecutionPayload == nil { - return phase0.Hash32{}, ErrDataMissing + if v.Blinded { + return v.BellatrixBlinded.Message.Body.ExecutionPayloadHeader.BlockHash, nil } return v.Bellatrix.Message.Body.ExecutionPayload.BlockHash, nil case spec.DataVersionCapella: - if v.Capella == nil || - v.Capella.Message == nil || - v.Capella.Message.Body == nil || - v.Capella.Message.Body.ExecutionPayload == nil { - return phase0.Hash32{}, ErrDataMissing + if v.Blinded { + return v.CapellaBlinded.Message.Body.ExecutionPayloadHeader.BlockHash, nil } return v.Capella.Message.Body.ExecutionPayload.BlockHash, nil case spec.DataVersionDeneb: - if v.Deneb == nil || - v.Deneb.SignedBlock == nil || - v.Deneb.SignedBlock.Message == nil || - v.Deneb.SignedBlock.Message.Body == nil || - v.Deneb.SignedBlock.Message.Body.ExecutionPayload == nil { - return phase0.Hash32{}, ErrDataMissing + if v.Blinded { + return v.DenebBlinded.Message.Body.ExecutionPayloadHeader.BlockHash, nil } return v.Deneb.SignedBlock.Message.Body.ExecutionPayload.BlockHash, nil @@ -172,18 +197,42 @@ func (v *VersionedSignedProposal) String() string { return v.Altair.String() case spec.DataVersionBellatrix: + if v.Blinded { + if v.BellatrixBlinded == nil { + return "" + } + + return v.BellatrixBlinded.String() + } + if v.Bellatrix == nil { return "" } return v.Bellatrix.String() case spec.DataVersionCapella: + if v.Blinded { + if v.CapellaBlinded == nil { + return "" + } + + return v.CapellaBlinded.String() + } + if v.Capella == nil { return "" } return v.Capella.String() case spec.DataVersionDeneb: + if v.Blinded { + if v.DenebBlinded == nil { + return "" + } + + return v.DenebBlinded.String() + } + if v.Deneb == nil { return "" } @@ -193,3 +242,113 @@ func (v *VersionedSignedProposal) String() string { return "unsupported version" } } + +// assertMessagePresent throws an error if the expected message +// given the version and blinded fields is not present. +func (v *VersionedSignedProposal) assertMessagePresent() error { + switch v.Version { + case spec.DataVersionBellatrix: + if v.Blinded { + if v.BellatrixBlinded == nil || + v.BellatrixBlinded.Message == nil { + return ErrDataMissing + } + } else { + if v.Bellatrix == nil || + v.Bellatrix.Message == nil { + return ErrDataMissing + } + } + case spec.DataVersionCapella: + if v.Blinded { + if v.CapellaBlinded == nil || + v.CapellaBlinded.Message == nil { + return ErrDataMissing + } + } else { + if v.Capella == nil || + v.Capella.Message == nil { + return ErrDataMissing + } + } + case spec.DataVersionDeneb: + if v.Blinded { + if v.DenebBlinded == nil || + v.DenebBlinded.Message == nil { + return ErrDataMissing + } + } else { + if v.Deneb == nil || + v.Deneb.SignedBlock == nil || + v.Deneb.SignedBlock.Message == nil { + return ErrDataMissing + } + } + default: + return ErrUnsupportedVersion + } + + return nil +} + +// assertExecutionPayloadPresent throws an error if the expected execution payload or payload header +// given the version and blinded fields is not present. +// +//nolint:gocyclo +func (v *VersionedSignedProposal) assertExecutionPayloadPresent() error { + switch v.Version { + case spec.DataVersionBellatrix: + if v.Blinded { + if v.BellatrixBlinded == nil || + v.BellatrixBlinded.Message == nil || + v.BellatrixBlinded.Message.Body == nil || + v.BellatrixBlinded.Message.Body.ExecutionPayloadHeader == nil { + return ErrDataMissing + } + } else { + if v.Bellatrix == nil || + v.Bellatrix.Message == nil || + v.Bellatrix.Message.Body == nil || + v.Bellatrix.Message.Body.ExecutionPayload == nil { + return ErrDataMissing + } + } + case spec.DataVersionCapella: + if v.Blinded { + if v.CapellaBlinded == nil || + v.CapellaBlinded.Message == nil || + v.CapellaBlinded.Message.Body == nil || + v.CapellaBlinded.Message.Body.ExecutionPayloadHeader == nil { + return ErrDataMissing + } + } else { + if v.Capella == nil || + v.Capella.Message == nil || + v.Capella.Message.Body == nil || + v.Capella.Message.Body.ExecutionPayload == nil { + return ErrDataMissing + } + } + case spec.DataVersionDeneb: + if v.Blinded { + if v.DenebBlinded == nil || + v.DenebBlinded.Message == nil || + v.DenebBlinded.Message.Body == nil || + v.DenebBlinded.Message.Body.ExecutionPayloadHeader == nil { + return ErrDataMissing + } + } else { + if v.Deneb == nil || + v.Deneb.SignedBlock == nil || + v.Deneb.SignedBlock.Message == nil || + v.Deneb.SignedBlock.Message.Body == nil || + v.Deneb.SignedBlock.Message.Body.ExecutionPayload == nil { + return ErrDataMissing + } + } + default: + return ErrUnsupportedVersion + } + + return nil +} diff --git a/http/blindedproposal_test.go b/http/blindedproposal_test.go deleted file mode 100644 index 2b642776..00000000 --- a/http/blindedproposal_test.go +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright © 2023 Attestant Limited. -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package http_test - -import ( - "context" - "errors" - "os" - "testing" - "time" - - client "github.com/attestantio/go-eth2-client" - "github.com/attestantio/go-eth2-client/api" - "github.com/attestantio/go-eth2-client/http" - "github.com/attestantio/go-eth2-client/spec/phase0" - "github.com/stretchr/testify/require" -) - -func TestBlindedProposal(t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - service, err := http.New(ctx, - http.WithTimeout(timeout), - http.WithAddress(os.Getenv("HTTP_ADDRESS")), - ) - require.NoError(t, err) - - // Need to fetch current slot for proposal. - genesisResponse, err := service.(client.GenesisProvider).Genesis(ctx, &api.GenesisOpts{}) - require.NoError(t, err) - slotDuration, err := service.(client.SlotDurationProvider).SlotDuration(ctx) - require.NoError(t, err) - - tests := []struct { - name string - opts *api.BlindedProposalOpts - expected *api.VersionedBlindedProposal - err string - errCode int - }{ - { - name: "NilOpts", - err: "no options specified", - }, - { - name: "NilSlot", - opts: &api.BlindedProposalOpts{}, - err: "no slot specified", - }, - { - name: "InvalidSkipRANDAO", - opts: &api.BlindedProposalOpts{ - RandaoReveal: phase0.BLSSignature([96]byte{ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - }), - Graffiti: [32]byte{ - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, - 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, - }, - SkipRandaoVerification: true, - Slot: phase0.Slot(uint64(time.Since(genesisResponse.Data.GenesisTime).Seconds())/uint64(slotDuration.Seconds())) + 1, - }, - err: "randao reveal must be point at infinity if skip randao verification is set", - }, - { - name: "Good", - opts: &api.BlindedProposalOpts{ - RandaoReveal: phase0.BLSSignature([96]byte{ - 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - }), - Graffiti: [32]byte{ - 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, - 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, - }, - SkipRandaoVerification: true, - Slot: phase0.Slot(uint64(time.Since(genesisResponse.Data.GenesisTime).Seconds())/uint64(slotDuration.Seconds())) + 1, - }, - }, - } - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - response, err := service.(client.BlindedProposalProvider).BlindedProposal(ctx, test.opts) - switch { - case test.err != "": - require.ErrorContains(t, err, test.err) - case test.errCode != 0: - var apiErr *api.Error - if errors.As(err, &apiErr) { - require.Equal(t, test.errCode, apiErr.StatusCode) - } - default: - require.NoError(t, err) - require.NotNil(t, response) - if test.expected != nil { - require.Equal(t, test.expected, response.Data) - } - } - }) - } -} diff --git a/http/http.go b/http/http.go index 5985c629..17a6d01d 100644 --- a/http/http.go +++ b/http/http.go @@ -99,6 +99,8 @@ func (s *Service) post(ctx context.Context, endpoint string, body io.Reader) (io //nolint:unparam func (s *Service) post2(ctx context.Context, endpoint string, + query string, + _ *api.CommonOpts, body io.Reader, contentType ContentType, headers map[string]string, @@ -109,16 +111,22 @@ func (s *Service) post2(ctx context.Context, // #nosec G404 log := s.log.With().Str("id", fmt.Sprintf("%02x", rand.Int31())).Str("address", s.address).Str("endpoint", endpoint).Logger() if e := log.Trace(); e.Enabled() { - bodyBytes, err := io.ReadAll(body) - if err != nil { - return nil, errors.New("failed to read request body") + switch contentType { + case ContentTypeJSON: + bodyBytes, err := io.ReadAll(body) + if err != nil { + return nil, errors.New("failed to read request body") + } + body = bytes.NewReader(bodyBytes) + + e.Str("body", string(bodyBytes)).Msg("POST request") + default: + e.Str("content_type", contentType.String()).Msg("POST request") } - body = bytes.NewReader(bodyBytes) - - e.Str("body", string(bodyBytes)).Msg("POST request") } - url := urlForCall(s.base, endpoint, "") + url := urlForCall(s.base, endpoint, query) + log.Trace().Str("url", url.String()).Msg("URL to POST") opCtx, cancel := context.WithTimeout(ctx, s.timeout) defer cancel() @@ -200,6 +208,7 @@ func (s *Service) get(ctx context.Context, endpoint string, query string, opts * log.Trace().Msg("GET request") url := urlForCall(s.base, endpoint, query) + log.Trace().Str("url", url.String()).Msg("URL to GET") timeout := s.timeout if opts.Timeout != 0 { diff --git a/http/proposal.go b/http/proposal.go index 9cbd1faa..ca33eb80 100644 --- a/http/proposal.go +++ b/http/proposal.go @@ -18,9 +18,13 @@ import ( "context" "errors" "fmt" + "math/big" + "strings" client "github.com/attestantio/go-eth2-client" "github.com/attestantio/go-eth2-client/api" + apiv1bellatrix "github.com/attestantio/go-eth2-client/api/v1/bellatrix" + apiv1capella "github.com/attestantio/go-eth2-client/api/v1/capella" apiv1deneb "github.com/attestantio/go-eth2-client/api/v1/deneb" "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/altair" @@ -50,7 +54,7 @@ func (s *Service) Proposal(ctx context.Context, return nil, errors.Join(errors.New("no slot specified"), client.ErrInvalidOptions) } - endpoint := fmt.Sprintf("/eth/v2/validator/blocks/%d", opts.Slot) + endpoint := fmt.Sprintf("/eth/v3/validator/blocks/%d", opts.Slot) query := fmt.Sprintf("randao_reveal=%#x&graffiti=%#x", opts.RandaoReveal, opts.Graffiti) if opts.SkipRandaoVerification { @@ -60,6 +64,12 @@ func (s *Service) Proposal(ctx context.Context, query = fmt.Sprintf("%s&skip_randao_verification", query) } + if opts.BuilderBoostFactor == nil { + query += "&builder_boost_factor=100" + } else { + query = fmt.Sprintf("%s&builder_boost_factor=%d", query, *opts.BuilderBoostFactor) + } + httpResponse, err := s.get(ctx, endpoint, query, &opts.Common) if err != nil { return nil, errors.Join(errors.New("failed to request beacon block proposal"), err) @@ -113,40 +123,55 @@ func (s *Service) Proposal(ctx context.Context, func (s *Service) beaconBlockProposalFromSSZ(res *httpResponse) (*api.Response[*api.VersionedProposal], error) { response := &api.Response[*api.VersionedProposal]{ Data: &api.VersionedProposal{ - Version: res.consensusVersion, + Version: res.consensusVersion, + ConsensusValue: big.NewInt(0), + ExecutionValue: big.NewInt(0), }, Metadata: metadataFromHeaders(res.headers), } + if err := s.populateProposalDataFromHeaders(response, res.headers); err != nil { + return nil, err + } + + var err error switch res.consensusVersion { case spec.DataVersionPhase0: response.Data.Phase0 = &phase0.BeaconBlock{} - if err := response.Data.Phase0.UnmarshalSSZ(res.body); err != nil { - return nil, errors.Join(errors.New("failed to decode phase0 beacon block proposal"), err) - } + err = response.Data.Phase0.UnmarshalSSZ(res.body) case spec.DataVersionAltair: response.Data.Altair = &altair.BeaconBlock{} - if err := response.Data.Altair.UnmarshalSSZ(res.body); err != nil { - return nil, errors.Join(errors.New("failed to decode altair beacon block proposal"), err) - } + err = response.Data.Altair.UnmarshalSSZ(res.body) case spec.DataVersionBellatrix: - response.Data.Bellatrix = &bellatrix.BeaconBlock{} - if err := response.Data.Bellatrix.UnmarshalSSZ(res.body); err != nil { - return nil, errors.Join(errors.New("failed to decode bellatrix beacon block proposal"), err) + if response.Data.Blinded { + response.Data.BellatrixBlinded = &apiv1bellatrix.BlindedBeaconBlock{} + err = response.Data.BellatrixBlinded.UnmarshalSSZ(res.body) + } else { + response.Data.Bellatrix = &bellatrix.BeaconBlock{} + err = response.Data.Bellatrix.UnmarshalSSZ(res.body) } case spec.DataVersionCapella: - response.Data.Capella = &capella.BeaconBlock{} - if err := response.Data.Capella.UnmarshalSSZ(res.body); err != nil { - return nil, errors.Join(errors.New("failed to decode capella beacon block proposal"), err) + if response.Data.Blinded { + response.Data.CapellaBlinded = &apiv1capella.BlindedBeaconBlock{} + err = response.Data.CapellaBlinded.UnmarshalSSZ(res.body) + } else { + response.Data.Capella = &capella.BeaconBlock{} + err = response.Data.Capella.UnmarshalSSZ(res.body) } case spec.DataVersionDeneb: - response.Data.Deneb = &apiv1deneb.BlockContents{} - if err := response.Data.Deneb.UnmarshalSSZ(res.body); err != nil { - return nil, errors.Join(errors.New("failed to decode deneb beacon block proposal"), err) + if response.Data.Blinded { + response.Data.DenebBlinded = &apiv1deneb.BlindedBeaconBlock{} + err = response.Data.DenebBlinded.UnmarshalSSZ(res.body) + } else { + response.Data.Deneb = &apiv1deneb.BlockContents{} + err = response.Data.Deneb.UnmarshalSSZ(res.body) } default: return nil, fmt.Errorf("unhandled block proposal version %s", res.consensusVersion) } + if err != nil { + return nil, errors.Join(fmt.Errorf("failed to decode %v SSZ beacon block (blinded: %v)", res.consensusVersion, response.Data.Blinded), err) + } return response, nil } @@ -154,8 +179,15 @@ func (s *Service) beaconBlockProposalFromSSZ(res *httpResponse) (*api.Response[* func (s *Service) beaconBlockProposalFromJSON(res *httpResponse) (*api.Response[*api.VersionedProposal], error) { response := &api.Response[*api.VersionedProposal]{ Data: &api.VersionedProposal{ - Version: res.consensusVersion, + Version: res.consensusVersion, + ConsensusValue: big.NewInt(0), + ExecutionValue: big.NewInt(0), }, + Metadata: metadataFromHeaders(res.headers), + } + + if err := s.populateProposalDataFromHeaders(response, res.headers); err != nil { + return nil, err } var err error @@ -165,17 +197,54 @@ func (s *Service) beaconBlockProposalFromJSON(res *httpResponse) (*api.Response[ case spec.DataVersionAltair: response.Data.Altair, response.Metadata, err = decodeJSONResponse(bytes.NewReader(res.body), &altair.BeaconBlock{}) case spec.DataVersionBellatrix: - response.Data.Bellatrix, response.Metadata, err = decodeJSONResponse(bytes.NewReader(res.body), &bellatrix.BeaconBlock{}) + if response.Data.Blinded { + response.Data.BellatrixBlinded, response.Metadata, err = decodeJSONResponse(bytes.NewReader(res.body), &apiv1bellatrix.BlindedBeaconBlock{}) + } else { + response.Data.Bellatrix, response.Metadata, err = decodeJSONResponse(bytes.NewReader(res.body), &bellatrix.BeaconBlock{}) + } case spec.DataVersionCapella: - response.Data.Capella, response.Metadata, err = decodeJSONResponse(bytes.NewReader(res.body), &capella.BeaconBlock{}) + if response.Data.Blinded { + response.Data.CapellaBlinded, response.Metadata, err = decodeJSONResponse(bytes.NewReader(res.body), &apiv1capella.BlindedBeaconBlock{}) + } else { + response.Data.Capella, response.Metadata, err = decodeJSONResponse(bytes.NewReader(res.body), &capella.BeaconBlock{}) + } case spec.DataVersionDeneb: - response.Data.Deneb, response.Metadata, err = decodeJSONResponse(bytes.NewReader(res.body), &apiv1deneb.BlockContents{}) + if response.Data.Blinded { + response.Data.DenebBlinded, response.Metadata, err = decodeJSONResponse(bytes.NewReader(res.body), &apiv1deneb.BlindedBeaconBlock{}) + } else { + response.Data.Deneb, response.Metadata, err = decodeJSONResponse(bytes.NewReader(res.body), &apiv1deneb.BlockContents{}) + } default: err = fmt.Errorf("unsupported version %s", res.consensusVersion) } if err != nil { - return nil, err + return nil, errors.Join(fmt.Errorf("failed to decode %v JSON beacon block (blinded: %v)", res.consensusVersion, response.Data.Blinded), err) } return response, nil } + +func (s *Service) populateProposalDataFromHeaders(response *api.Response[*api.VersionedProposal], + headers map[string]string, +) error { + for k, v := range headers { + switch { + case strings.EqualFold(k, "Eth-Execution-Payload-Blinded"): + response.Data.Blinded = strings.EqualFold(v, "true") + case strings.EqualFold(k, "Eth-Execution-Payload-Value"): + var success bool + response.Data.ExecutionValue, success = new(big.Int).SetString(v, 10) + if !success { + return fmt.Errorf("proposal header Eth-Execution-Payload-Value %s not a valid integer", v) + } + case strings.EqualFold(k, "Eth-Consensus-Block-Value"): + var success bool + response.Data.ConsensusValue, success = new(big.Int).SetString(v, 10) + if !success { + return fmt.Errorf("proposal header Eth-Consensus-Block-Value %s not a valid integer", v) + } + } + } + + return nil +} diff --git a/http/submitblindedproposal.go b/http/submitblindedproposal.go index a810b1ca..6c3b5d6a 100644 --- a/http/submitblindedproposal.go +++ b/http/submitblindedproposal.go @@ -26,28 +26,33 @@ import ( ) // SubmitBlindedProposal submits a blinded proposal. -func (s *Service) SubmitBlindedProposal(ctx context.Context, proposal *api.VersionedSignedBlindedProposal) error { +func (s *Service) SubmitBlindedProposal(ctx context.Context, + opts *api.SubmitBlindedProposalOpts, +) error { if err := s.assertIsSynced(ctx); err != nil { return err } - if proposal == nil { - return errors.Join(errors.New("no blinded proposal supplied"), client.ErrInvalidOptions) + if opts == nil { + return client.ErrNoOptions + } + if opts.Proposal == nil { + return errors.Join(errors.New("no proposal supplied"), client.ErrInvalidOptions) } var specJSON []byte var err error - switch proposal.Version { + switch opts.Proposal.Version { case spec.DataVersionPhase0: err = errors.New("blinded phase0 proposals not supported") case spec.DataVersionAltair: err = errors.New("blinded altair proposals not supported") case spec.DataVersionBellatrix: - specJSON, err = json.Marshal(proposal.Bellatrix) + specJSON, err = json.Marshal(opts.Proposal.Bellatrix) case spec.DataVersionCapella: - specJSON, err = json.Marshal(proposal.Capella) + specJSON, err = json.Marshal(opts.Proposal.Capella) case spec.DataVersionDeneb: - specJSON, err = json.Marshal(proposal.Deneb) + specJSON, err = json.Marshal(opts.Proposal.Deneb) default: err = errors.New("unknown proposal version") } @@ -55,9 +60,15 @@ func (s *Service) SubmitBlindedProposal(ctx context.Context, proposal *api.Versi return errors.Join(errors.New("failed to marshal JSON"), err) } + endpoint := "/eth/v2/beacon/blocks" + query := "" + if opts.BroadcastValidation != nil { + query = "broadcast_validation=" + opts.BroadcastValidation.String() + } + headers := make(map[string]string) - headers["Eth-Consensus-Version"] = strings.ToLower(proposal.Version.String()) - _, err = s.post2(ctx, "/eth/v2/beacon/blinded_blocks", bytes.NewBuffer(specJSON), ContentTypeJSON, headers) + headers["Eth-Consensus-Version"] = strings.ToLower(opts.Proposal.Version.String()) + _, err = s.post2(ctx, endpoint, query, &opts.Common, bytes.NewBuffer(specJSON), ContentTypeJSON, headers) if err != nil { return errors.Join(errors.New("failed to submit blinded proposal"), err) } diff --git a/http/submitproposal.go b/http/submitproposal.go index b20bd355..6580cd4d 100644 --- a/http/submitproposal.go +++ b/http/submitproposal.go @@ -26,17 +26,85 @@ import ( ) // SubmitProposal submits a proposal. -func (s *Service) SubmitProposal(ctx context.Context, proposal *api.VersionedSignedProposal) error { +func (s *Service) SubmitProposal(ctx context.Context, + opts *api.SubmitProposalOpts, +) error { if err := s.assertIsSynced(ctx); err != nil { return err } - if proposal == nil { + if opts == nil { + return client.ErrNoOptions + } + if opts.Proposal == nil { return errors.Join(errors.New("no proposal supplied"), client.ErrInvalidOptions) } + body, contentType, err := s.submitProposalData(ctx, opts.Proposal) + if err != nil { + return err + } + + endpoint := "/eth/v2/beacon/blocks" + query := "" + if opts.BroadcastValidation != nil { + query = "broadcast_validation=" + opts.BroadcastValidation.String() + } + + headers := make(map[string]string) + headers["Eth-Consensus-Version"] = strings.ToLower(opts.Proposal.Version.String()) + _, err = s.post2(ctx, endpoint, query, &opts.Common, bytes.NewBuffer(body), contentType, headers) + if err != nil { + return errors.Join(errors.New("failed to submit proposal"), err) + } + + return nil +} + +func (s *Service) submitProposalData(ctx context.Context, + proposal *api.VersionedSignedProposal, +) ( + []byte, + ContentType, + error, +) { + var body []byte + var contentType ContentType + var err error + + nodeClientResponse, err := s.NodeClient(ctx) + nodeClient := "unknown" + if err == nil { + nodeClient = nodeClientResponse.Data + } + + if s.enforceJSON || nodeClient == "lodestar" { + contentType = ContentTypeJSON + body, err = s.submitProposalJSON(ctx, proposal) + } else { + contentType = ContentTypeSSZ + body, err = s.submitProposalSSZ(ctx, proposal) + } + + if err != nil { + return nil, ContentTypeUnknown, err + } + + return body, contentType, nil +} + +func (s *Service) submitProposalJSON(_ context.Context, + proposal *api.VersionedSignedProposal, +) ( + []byte, + error, +) { var specJSON []byte var err error + if err := proposal.AssertPresent(); err != nil { + return nil, err + } + switch proposal.Version { case spec.DataVersionPhase0: specJSON, err = json.Marshal(proposal.Phase0) @@ -52,15 +120,42 @@ func (s *Service) SubmitProposal(ctx context.Context, proposal *api.VersionedSig err = errors.New("unknown proposal version") } if err != nil { - return errors.Join(errors.New("failed to marshal JSON"), err) + return nil, errors.Join(errors.New("failed to marshal JSON"), err) } - headers := make(map[string]string) - headers["Eth-Consensus-Version"] = strings.ToLower(proposal.Version.String()) - _, err = s.post2(ctx, "/eth/v1/beacon/blocks", bytes.NewBuffer(specJSON), ContentTypeJSON, headers) + return specJSON, nil +} + +func (s *Service) submitProposalSSZ(_ context.Context, + proposal *api.VersionedSignedProposal, +) ( + []byte, + error, +) { + var specSSZ []byte + var err error + + if err := proposal.AssertPresent(); err != nil { + return nil, err + } + + switch proposal.Version { + case spec.DataVersionPhase0: + specSSZ, err = proposal.Phase0.MarshalSSZ() + case spec.DataVersionAltair: + specSSZ, err = proposal.Altair.MarshalSSZ() + case spec.DataVersionBellatrix: + specSSZ, err = proposal.Bellatrix.MarshalSSZ() + case spec.DataVersionCapella: + specSSZ, err = proposal.Capella.MarshalSSZ() + case spec.DataVersionDeneb: + specSSZ, err = proposal.Deneb.MarshalSSZ() + default: + err = errors.New("unknown proposal version") + } if err != nil { - return errors.Join(errors.New("failed to submit proposal"), err) + return nil, errors.Join(errors.New("failed to marshal SSZ"), err) } - return nil + return specSSZ, nil } diff --git a/http/submitproposal_test.go b/http/submitproposal_test.go index 9cd935fc..50be39bb 100644 --- a/http/submitproposal_test.go +++ b/http/submitproposal_test.go @@ -113,7 +113,9 @@ func TestSubmitProposal(t *testing.T) { t.Fatalf("unknown block version %s", res.Data.Version.String()) } // Some implementations return an error and some don't, so do not check. - service.(client.ProposalSubmitter).SubmitProposal(ctx, signedBeaconBlock) // nolint:errcheck + service.(client.ProposalSubmitter).SubmitProposal(ctx, &api.SubmitProposalOpts{ + Proposal: signedBeaconBlock, + }) // nolint:errcheck }) } } diff --git a/multi/blindedproposal.go b/multi/blindedproposal.go deleted file mode 100644 index 7bf04342..00000000 --- a/multi/blindedproposal.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright © 2021, 2023 Attestant Limited. -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package multi - -import ( - "context" - - consensusclient "github.com/attestantio/go-eth2-client" - "github.com/attestantio/go-eth2-client/api" -) - -// BlindedProposal fetches a blinded proposal for signing. -func (s *Service) BlindedProposal(ctx context.Context, - opts *api.BlindedProposalOpts, -) ( - *api.Response[*api.VersionedBlindedProposal], - error, -) { - res, err := s.doCall(ctx, func(ctx context.Context, client consensusclient.Service) (interface{}, error) { - block, err := client.(consensusclient.BlindedProposalProvider).BlindedProposal(ctx, opts) - if err != nil { - return nil, err - } - - return block, nil - }, nil) - if err != nil { - return nil, err - } - - return res.(*api.Response[*api.VersionedBlindedProposal]), nil -} diff --git a/multi/submitproposal.go b/multi/submitproposal.go index 1717c16e..df058c66 100644 --- a/multi/submitproposal.go +++ b/multi/submitproposal.go @@ -21,9 +21,11 @@ import ( ) // SubmitProposal submits a beacon block. -func (s *Service) SubmitProposal(ctx context.Context, proposal *api.VersionedSignedProposal) error { +func (s *Service) SubmitProposal(ctx context.Context, + opts *api.SubmitProposalOpts, +) error { _, err := s.doCall(ctx, func(ctx context.Context, client consensusclient.Service) (interface{}, error) { - err := client.(consensusclient.ProposalSubmitter).SubmitProposal(ctx, proposal) + err := client.(consensusclient.ProposalSubmitter).SubmitProposal(ctx, opts) if err != nil { return nil, err } diff --git a/service.go b/service.go index 5c6de46b..90544a71 100644 --- a/service.go +++ b/service.go @@ -256,7 +256,7 @@ type BeaconBlockSubmitter interface { // ProposalSubmitter is the interface for submitting proposals. type ProposalSubmitter interface { // SubmitProposal submits a proposal. - SubmitProposal(ctx context.Context, block *api.VersionedSignedProposal) error + SubmitProposal(ctx context.Context, opts *api.SubmitProposalOpts) error } // BeaconCommitteeSubscriptionsSubmitter is the interface for submitting beacon committee subnet subscription requests. @@ -283,12 +283,6 @@ type BeaconStateRootProvider interface { BeaconStateRoot(ctx context.Context, opts *api.BeaconStateRootOpts) (*api.Response[*phase0.Root], error) } -// BlindedProposalProvider is the interface for providing blinded beacon block proposals. -type BlindedProposalProvider interface { - // BlindedProposal fetches a blinded proposed beacon block for signing. - BlindedProposal(ctx context.Context, opts *api.BlindedProposalOpts) (*api.Response[*api.VersionedBlindedProposal], error) -} - // BlindedBeaconBlockSubmitter is the interface for submitting blinded beacon blocks. type BlindedBeaconBlockSubmitter interface { // SubmitBlindedBeaconBlock submits a beacon block. @@ -300,7 +294,7 @@ type BlindedBeaconBlockSubmitter interface { // BlindedProposalSubmitter is the interface for submitting blinded proposals. type BlindedProposalSubmitter interface { // SubmitBlindedProposal submits a beacon block. - SubmitBlindedProposal(ctx context.Context, block *api.VersionedSignedBlindedProposal) error + SubmitBlindedProposal(ctx context.Context, opts *api.SubmitBlindedProposalOpts) error } // ValidatorRegistrationsSubmitter is the interface for submitting validator registrations.