Skip to content

Commit

Permalink
app/eth2wrap: fix error comparison for synthetic blocks (#2705)
Browse files Browse the repository at this point in the history
This PR cherry-picks #2702 onto `main-v0.18`

---

There was a small issue when comparing eth2api errors when searching for signed beacon block. The error returned should be part of eth2wrap multi implementation which was not able to decompose eth2 errors properly. This PR fixes decomposition of eth2 errors and adds a function to check if the relevant field is present as part of the error chain.

category: bug
ticket: #2695
  • Loading branch information
gsora authored Nov 10, 2023
1 parent ddf4d0a commit 67725e4
Show file tree
Hide file tree
Showing 4 changed files with 148 additions and 5 deletions.
2 changes: 1 addition & 1 deletion app/eth2wrap/eth2wrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ func incError(endpoint string) {
// wrapError returns the error as a wrapped structured error.
func wrapError(ctx context.Context, err error, label string, fields ...z.Field) error {
// Decompose go-eth2-client http errors
if apiErr := new(eth2api.Error); errors.As(err, apiErr) {
if apiErr := new(eth2api.Error); errors.As(err, &apiErr) {
err = errors.New("nok http response",
z.Int("status_code", apiErr.StatusCode),
z.Str("endpoint", apiErr.Endpoint),
Expand Down
24 changes: 24 additions & 0 deletions app/eth2wrap/eth2wrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package eth2wrap_test
import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"net/http/httptest"
Expand All @@ -15,7 +16,9 @@ import (
"testing"
"time"

eth2api "github.com/attestantio/go-eth2-client/api"
eth2v1 "github.com/attestantio/go-eth2-client/api/v1"
eth2spec "github.com/attestantio/go-eth2-client/spec"
eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -217,6 +220,27 @@ func TestErrors(t *testing.T) {
require.Error(t, err)
require.ErrorContains(t, err, "beacon api genesis_time: network operation error: :")
})

t.Run("eth2api error", func(t *testing.T) {
bmock, err := beaconmock.New()
require.NoError(t, err)
bmock.SignedBeaconBlockFunc = func(_ context.Context, blockID string) (*eth2spec.VersionedSignedBeaconBlock, error) {
return nil, &eth2api.Error{
Method: http.MethodGet,
Endpoint: fmt.Sprintf("/eth/v2/beacon/blocks/%s", blockID),
StatusCode: http.StatusNotFound,
Data: []byte(fmt.Sprintf(`{"code":404,"message":"NOT_FOUND: beacon block at slot %s","stacktraces":[]}`, blockID)),
}
}

eth2Cl, err := eth2wrap.Instrument(bmock)
require.NoError(t, err)

_, err = eth2Cl.SignedBeaconBlock(ctx, &eth2api.SignedBeaconBlockOpts{Block: "123"})
log.Error(ctx, "See this error log for fields", err)
require.Error(t, err)
require.ErrorContains(t, err, "nok http response")
})
}

func TestCtxCancel(t *testing.T) {
Expand Down
36 changes: 32 additions & 4 deletions app/eth2wrap/synthproposer.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ import (
"github.com/attestantio/go-eth2-client/spec/capella"
eth2p0 "github.com/attestantio/go-eth2-client/spec/phase0"
shuffle "github.com/protolambda/eth2-shuffle"
"go.uber.org/zap"

"github.com/obolnetwork/charon/app/errors"
"github.com/obolnetwork/charon/app/log"
"github.com/obolnetwork/charon/app/z"
)

const (
Expand Down Expand Up @@ -149,10 +151,8 @@ func (h *synthWrapper) syntheticProposal(ctx context.Context, slot eth2p0.Slot,
}
signed, err := h.Client.SignedBeaconBlock(ctx, opts)
if err != nil {
if apiErr := new(eth2api.Error); errors.As(err, apiErr) { // Continue if block is not found in the given slot.
if apiErr.StatusCode == http.StatusNotFound {
continue
}
if fieldExists(err, zap.Int("status_code", http.StatusNotFound)) {
continue
}

return nil, err
Expand Down Expand Up @@ -204,6 +204,34 @@ func (h *synthWrapper) syntheticProposal(ctx context.Context, slot eth2p0.Slot,
return proposal, nil
}

// fieldExists checks if the given field exists as part of the given error.
func fieldExists(err error, field zap.Field) bool {
type structErr interface {
Fields() []z.Field
}

sterr, ok := err.(structErr) //nolint:errorlint
if !ok {
return false
}

zfs := sterr.Fields()
var zapFs []zap.Field
for _, field := range zfs {
field(func(zp zap.Field) {
zapFs = append(zapFs, zp)
})
}

for _, zaps := range zapFs {
if zaps.Equals(field) {
return true
}
}

return false
}

// fraction returns a fraction of the transactions in the block.
// This is used to reduce the size of synthetic blocks to manageable levels.
func fraction(transactions []bellatrix.Transaction) []bellatrix.Transaction {
Expand Down
91 changes: 91 additions & 0 deletions app/eth2wrap/synthproposer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ package eth2wrap_test

import (
"context"
"fmt"
"net/http"
"testing"

eth2api "github.com/attestantio/go-eth2-client/api"
Expand Down Expand Up @@ -161,3 +163,92 @@ func TestSynthProposer(t *testing.T) {

<-done
}

func TestSynthProposerBlockNotFound(t *testing.T) {
ctx := context.Background()

var (
set = beaconmock.ValidatorSetA
feeRecipient = bellatrix.ExecutionAddress{0x00, 0x01, 0x02}
slotsPerEpoch = 3
epoch eth2p0.Epoch = 1
realBlockSlot = eth2p0.Slot(slotsPerEpoch) * eth2p0.Slot(epoch)
activeVals = 0
timesCalled int
)

bmock, err := beaconmock.New(beaconmock.WithValidatorSet(set), beaconmock.WithSlotsPerEpoch(slotsPerEpoch))
require.NoError(t, err)

bmock.ProposerDutiesFunc = func(ctx context.Context, e eth2p0.Epoch, indices []eth2p0.ValidatorIndex) ([]*eth2v1.ProposerDuty, error) {
require.Equal(t, int(epoch), int(e))

return []*eth2v1.ProposerDuty{ // First validator is the proposer for first slot in the epoch.
{
PubKey: set[1].Validator.PublicKey,
Slot: realBlockSlot,
ValidatorIndex: set[1].Index,
},
}, nil
}
cached := bmock.ActiveValidatorsFunc
bmock.ActiveValidatorsFunc = func(ctx context.Context) (eth2wrap.ActiveValidators, error) {
activeVals++
return cached(ctx)
}

// Return eth2api Error when SignedBeaconBlock is requested.
bmock.SignedBeaconBlockFunc = func(ctx context.Context, blockID string) (*eth2spec.VersionedSignedBeaconBlock, error) {
timesCalled++

return nil, &eth2api.Error{
Method: http.MethodGet,
Endpoint: fmt.Sprintf("/eth/v2/beacon/blocks/%s", blockID),
StatusCode: http.StatusNotFound,
Data: []byte(fmt.Sprintf(`{"code":404,"message":"NOT_FOUND: beacon block at slot %s","stacktraces":[]}`, blockID)),
}
}

// Wrap beacon mock with multi eth2 client implementation which returns wrapped error.
eth2Cl, err := eth2wrap.Instrument(bmock)
require.NoError(t, err)

eth2Cl = eth2wrap.WithSyntheticDuties(eth2Cl)

var preps []*eth2v1.ProposalPreparation
for vIdx := range set {
preps = append(preps, &eth2v1.ProposalPreparation{
ValidatorIndex: vIdx,
FeeRecipient: feeRecipient,
})
}
require.NoError(t, eth2Cl.SubmitProposalPreparations(ctx, preps))

// Get synthetic duties
opts := &eth2api.ProposerDutiesOpts{
Epoch: epoch,
Indices: nil,
}
resp1, err := eth2Cl.ProposerDuties(ctx, opts)
require.NoError(t, err)
duties := resp1.Data
require.Len(t, duties, len(set))
require.Equal(t, 1, activeVals)

// Submit blocks
for _, duty := range duties {
timesCalled = 0
var graff [32]byte
copy(graff[:], "test")
opts1 := &eth2api.ProposalOpts{
Slot: duty.Slot,
RandaoReveal: testutil.RandomEth2Signature(),
Graffiti: graff,
}
_, err = eth2Cl.Proposal(ctx, opts1)
require.ErrorContains(t, err, "no proposal found to base synthetic proposal on")

// SignedBeaconBlock will be called for previous slots starting from duty.Slot-1 upto slot 0 (exclusive).
require.Equal(t, timesCalled, int(duty.Slot)-1)
}
}

0 comments on commit 67725e4

Please sign in to comment.