From af69cf5edeb664640fc16204aad8033507bcf180 Mon Sep 17 00:00:00 2001 From: avalonche Date: Wed, 2 Aug 2023 14:15:30 +1000 Subject: [PATCH] Add deneb signature checking for block contents --- services/api/service.go | 50 ++++++++++++++++++++++++++++----- services/api/service_test.go | 54 ++++++++++++++++++++++++++++++++++++ services/api/utils.go | 2 +- 3 files changed, 98 insertions(+), 8 deletions(-) diff --git a/services/api/service.go b/services/api/service.go index c9e450bf..2e746d14 100644 --- a/services/api/service.go +++ b/services/api/service.go @@ -7,7 +7,6 @@ import ( "context" "database/sql" "encoding/json" - "errors" "fmt" "io" "math/big" @@ -22,6 +21,7 @@ import ( "github.com/NYTimes/gziphandler" apiv1 "github.com/attestantio/go-builder-client/api/v1" + "github.com/attestantio/go-eth2-client/spec" "github.com/attestantio/go-eth2-client/spec/phase0" "github.com/buger/jsonparser" "github.com/flashbots/go-boost-utils/bls" @@ -36,6 +36,7 @@ import ( "github.com/go-redis/redis/v9" "github.com/gorilla/mux" "github.com/holiman/uint256" + "github.com/pkg/errors" "github.com/sirupsen/logrus" uberatomic "go.uber.org/atomic" "golang.org/x/exp/slices" @@ -1184,6 +1185,42 @@ func (api *RelayAPI) handleGetHeader(w http.ResponseWriter, req *http.Request) { api.RespondOK(w, bid) } +func (api *RelayAPI) checkProposerSignature(block *common.VersionedSignedBlindedBlockRequest, pubKey []byte) (bool, error) { + switch block.Version { + case spec.DataVersionCapella: + return verifyBlockSignature(block, api.opts.EthNetDetails.DomainBeaconProposerCapella, pubKey) + case spec.DataVersionDeneb: + domain := api.opts.EthNetDetails.DomainBeaconProposerDeneb + if ok, err := verifyBlockSignature(block, domain, pubKey); !ok || err != nil { + return false, errors.Wrap(err, "failed to verify block signature for deneb") + } + // verify sidecar signatures + for i, sidecar := range block.Deneb.SignedBlindedBlobSidecars { + if sidecar == nil || sidecar.Message == nil { + return false, errors.New("nil sidecar or message") + } + root, err := sidecar.Message.HashTreeRoot() + if err != nil { + return false, errors.Wrap(err, fmt.Sprintf("failed to calculate hash tree root for sidecar index %d", i)) + } + signingData := phase0.SigningData{ObjectRoot: root, Domain: domain} + msg, err := signingData.HashTreeRoot() + if err != nil { + return false, err + } + + if ok, err := bls.VerifySignatureBytes(msg[:], sidecar.Signature[:], pubKey[:]); !ok || err != nil { + return false, errors.Wrap(err, fmt.Sprintf("failed to verify signature for sidecar index %d ", i)) + } + } + case spec.DataVersionUnknown, spec.DataVersionPhase0, spec.DataVersionAltair, spec.DataVersionBellatrix: + fallthrough + default: + return false, errors.New("unsupported consensus data version") + } + return true, nil +} + func (api *RelayAPI) handleGetPayload(w http.ResponseWriter, req *http.Request) { api.getPayloadCallsInFlight.Add(1) defer api.getPayloadCallsInFlight.Done() @@ -1228,8 +1265,8 @@ func (api *RelayAPI) handleGetPayload(w http.ResponseWriter, req *http.Request) // Decode payload payload := new(common.VersionedSignedBlindedBlockRequest) if err := json.NewDecoder(bytes.NewReader(body)).Decode(payload); err != nil { - log.WithError(err).Warn("failed to decode capella getPayload request") - api.RespondError(w, http.StatusBadRequest, "failed to decode capella payload") + log.WithError(err).Warn("failed to decode getPayload request") + api.RespondError(w, http.StatusBadRequest, "failed to decode payload") return } @@ -1297,14 +1334,13 @@ func (api *RelayAPI) handleGetPayload(w http.ResponseWriter, req *http.Request) } // Validate proposer signature - // TODO: add deneb support. - ok, err := checkProposerSignature(payload, api.opts.EthNetDetails.DomainBeaconProposerCapella, pk[:]) + ok, err := api.checkProposerSignature(payload, pk[:]) if !ok || err != nil { if api.ffLogInvalidSignaturePayload { txt, _ := json.Marshal(payload) //nolint:errchkjson - log.Info("payload_invalid_sig_capella: ", string(txt), "pubkey:", proposerPubkey.String()) + log.Info("payload_invalid_sig: ", string(txt), "pubkey:", proposerPubkey.String()) } - log.WithError(err).Warn("could not verify capella payload signature") + log.WithError(err).Warn("could not verify payload signature") api.RespondError(w, http.StatusBadRequest, "could not verify payload signature") return } diff --git a/services/api/service_test.go b/services/api/service_test.go index c103e770..e61990c3 100644 --- a/services/api/service_test.go +++ b/services/api/service_test.go @@ -750,6 +750,60 @@ func TestCheckSubmissionSlotDetails(t *testing.T) { } } +func TestCheckProposerSignature(t *testing.T) { + t.Run("Unsupported version", func(t *testing.T) { + _, _, backend := startTestBackend(t) + payload := new(common.VersionedSignedBlindedBlockRequest) + payload.Version = consensusspec.DataVersionBellatrix + ok, err := backend.relay.checkProposerSignature(payload, []byte{}) + require.Error(t, err, "unsupported consensus data version") + require.False(t, ok) + }) + + t.Run("Valid Capella Signature", func(t *testing.T) { + jsonBytes := common.LoadGzippedBytes(t, "../../testdata/signedBlindedBeaconBlock_Goerli.json.gz") + payload := new(common.VersionedSignedBlindedBlockRequest) + err := json.Unmarshal(jsonBytes, payload) + require.NoError(t, err) + // start backend with goerli network + _, _, backend := startTestBackend(t) + goerli, err := common.NewEthNetworkDetails(common.EthNetworkGoerli) + require.NoError(t, err) + backend.relay.opts.EthNetDetails = *goerli + // check signature + pubkey, err := utils.HexToPubkey("0xa8afcb5313602f936864b30600f568e04069e596ceed9b55e2a1c872c959ddcb90589636469c15d97e7565344d9ed4ad") + require.NoError(t, err) + ok, err := backend.relay.checkProposerSignature(payload, pubkey[:]) + require.NoError(t, err) + require.True(t, ok) + }) + + t.Run("Invalid Capella Signature", func(t *testing.T) { + jsonBytes := common.LoadGzippedBytes(t, "../../testdata/signedBlindedBeaconBlock_Goerli.json.gz") + payload := new(common.VersionedSignedBlindedBlockRequest) + err := json.Unmarshal(jsonBytes, payload) + require.NoError(t, err) + // change signature + signature, err := utils.HexToSignature( + "0x942d85822e86a182b0a535361b379015a03e5ce4416863d3baa46b42eef06f070462742b79fbc77c0802699ba6d2ab00" + + "11740dad6bfcf05b1f15c5a11687ae2aa6a08c03ad1ff749d7a48e953d13b5d7c2bd1da4cfcf30ba6d918b587d6525f0", + ) + require.NoError(t, err) + payload.Capella.Signature = signature + // start backend with goerli network + _, _, backend := startTestBackend(t) + goerli, err := common.NewEthNetworkDetails(common.EthNetworkGoerli) + require.NoError(t, err) + backend.relay.opts.EthNetDetails = *goerli + // check signature + pubkey, err := utils.HexToPubkey("0xa8afcb5313602f936864b30600f568e04069e596ceed9b55e2a1c872c959ddcb90589636469c15d97e7565344d9ed4ad") + require.NoError(t, err) + ok, err := backend.relay.checkProposerSignature(payload, pubkey[:]) + require.NoError(t, err) + require.False(t, ok) + }) +} + func gzipBytes(t *testing.T, b []byte) []byte { t.Helper() var buf bytes.Buffer diff --git a/services/api/utils.go b/services/api/utils.go index 48b0fd7e..5de0f096 100644 --- a/services/api/utils.go +++ b/services/api/utils.go @@ -91,7 +91,7 @@ func hasReachedFork(slot, forkEpoch uint64) bool { return currentEpoch >= forkEpoch } -func checkProposerSignature(block *common.VersionedSignedBlindedBlockRequest, domain phase0.Domain, pubKey []byte) (bool, error) { +func verifyBlockSignature(block *common.VersionedSignedBlindedBlockRequest, domain phase0.Domain, pubKey []byte) (bool, error) { root, err := block.Root() if err != nil { return false, err