diff --git a/core/chains/evm/types/models.go b/core/chains/evm/types/models.go index a71c9e8716c..6db5d49575c 100644 --- a/core/chains/evm/types/models.go +++ b/core/chains/evm/types/models.go @@ -227,6 +227,21 @@ func (h *Head) NextInt() *big.Int { return new(big.Int).Add(h.ToInt(), big.NewInt(1)) } +// AsSlice returns a slice of heads up to length k +// len(heads) may be less than k if the available chain is not long enough +func (h *Head) AsSlice(k int) (heads []*Head) { + if k < 1 || h == nil { + return + } + heads = make([]*Head, 1) + heads[0] = h + for len(heads) < k && h.Parent != nil { + h = h.Parent + heads = append(heads, h) + } + return +} + func (h *Head) UnmarshalJSON(bs []byte) error { type head struct { Hash common.Hash `json:"hash"` diff --git a/core/chains/evm/types/models_test.go b/core/chains/evm/types/models_test.go index 2f9dc7dd7c3..2911e426e86 100644 --- a/core/chains/evm/types/models_test.go +++ b/core/chains/evm/types/models_test.go @@ -129,6 +129,29 @@ func TestHead_ChainLength(t *testing.T) { assert.Equal(t, uint32(0), head2.ChainLength()) } +func TestHead_AsSlice(t *testing.T) { + h1 := &evmtypes.Head{ + Number: 1, + } + h2 := &evmtypes.Head{ + Number: 2, + Parent: h1, + } + h3 := &evmtypes.Head{ + Number: 3, + Parent: h2, + } + + assert.Len(t, (*evmtypes.Head)(nil).AsSlice(0), 0) + assert.Len(t, (*evmtypes.Head)(nil).AsSlice(1), 0) + + assert.Len(t, h3.AsSlice(0), 0) + assert.Equal(t, []*evmtypes.Head{h3}, h3.AsSlice(1)) + assert.Equal(t, []*evmtypes.Head{h3, h2}, h3.AsSlice(2)) + assert.Equal(t, []*evmtypes.Head{h3, h2, h1}, h3.AsSlice(3)) + assert.Equal(t, []*evmtypes.Head{h3, h2, h1}, h3.AsSlice(4)) +} + func TestModels_HexToFunctionSelector(t *testing.T) { t.Parallel() fid := evmtypes.HexToFunctionSelector("0xb3f98adc") diff --git a/core/scripts/go.mod b/core/scripts/go.mod index 17c2cff1039..6b0a33451e2 100644 --- a/core/scripts/go.mod +++ b/core/scripts/go.mod @@ -305,7 +305,7 @@ require ( github.com/sirupsen/logrus v1.9.3 // indirect github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 // indirect github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20231101160906-7acebcc1b353 // indirect - github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231101203911-c686b4d48672 // indirect + github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231102162027-5fdce33763de // indirect github.com/smartcontractkit/chainlink-solana v1.0.3-0.20231023133638-72f4e799ab05 // indirect github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20231024133459-1ef3a11319eb // indirect github.com/smartcontractkit/tdh2/go/ocr2/decryptionplugin v0.0.0-20230906073235-9e478e5e19f1 // indirect diff --git a/core/scripts/go.sum b/core/scripts/go.sum index ae1f924c0f7..2cb0eb25826 100644 --- a/core/scripts/go.sum +++ b/core/scripts/go.sum @@ -1466,8 +1466,8 @@ github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 h1:T3lFWumv github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704/go.mod h1:2QuJdEouTWjh5BDy5o/vgGXQtR4Gz8yH1IYB5eT7u4M= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20231101160906-7acebcc1b353 h1:4iO3Ei1b/Lb0yprzclk93e1aQnYF92sIe+EJzMG87y4= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20231101160906-7acebcc1b353/go.mod h1:hMhGr9ok3p4442keFtK6u6Ei9yWfG66fmDwsFi3aHcw= -github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231101203911-c686b4d48672 h1:59vz5H52EpwWE/64ZQpNCs7Gtnyi7/ytjyoGjlbKhBA= -github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231101203911-c686b4d48672/go.mod h1:M9U1JV7IQi8Sfj4JR1qSi1tIh6omgW78W/8SHN/8BUQ= +github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231102162027-5fdce33763de h1:CeVpn5xEdmuEsYE8ss2b7bSq9h3BY4OPvpqXeYIPnHw= +github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231102162027-5fdce33763de/go.mod h1:M9U1JV7IQi8Sfj4JR1qSi1tIh6omgW78W/8SHN/8BUQ= github.com/smartcontractkit/chainlink-solana v1.0.3-0.20231023133638-72f4e799ab05 h1:DaPSVnxe7oz1QJ+AVIhQWs1W3ubQvwvGo9NbHpMs1OQ= github.com/smartcontractkit/chainlink-solana v1.0.3-0.20231023133638-72f4e799ab05/go.mod h1:o0Pn1pbaUluboaK6/yhf8xf7TiFCkyFl6WUOdwqamuU= github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20231024133459-1ef3a11319eb h1:HiluOfEVGOQTM6BTDImOqYdMZZ7qq7fkZ3TJdmItNr8= diff --git a/core/services/relay/evm/mercury/mocks/chain_head_tracker.go b/core/services/relay/evm/mercury/mocks/chain_head_tracker.go index 1a5a7e47c5b..b6f2981cf07 100644 --- a/core/services/relay/evm/mercury/mocks/chain_head_tracker.go +++ b/core/services/relay/evm/mercury/mocks/chain_head_tracker.go @@ -4,13 +4,11 @@ package mocks import ( common "github.com/ethereum/go-ethereum/common" - client "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" - - commontypes "github.com/smartcontractkit/chainlink/v2/common/types" - evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" mock "github.com/stretchr/testify/mock" + + types "github.com/smartcontractkit/chainlink/v2/common/types" ) // ChainHeadTracker is an autogenerated mock type for the ChainHeadTracker type @@ -18,32 +16,16 @@ type ChainHeadTracker struct { mock.Mock } -// Client provides a mock function with given fields: -func (_m *ChainHeadTracker) Client() client.Client { - ret := _m.Called() - - var r0 client.Client - if rf, ok := ret.Get(0).(func() client.Client); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(client.Client) - } - } - - return r0 -} - // HeadTracker provides a mock function with given fields: -func (_m *ChainHeadTracker) HeadTracker() commontypes.HeadTracker[*evmtypes.Head, common.Hash] { +func (_m *ChainHeadTracker) HeadTracker() types.HeadTracker[*evmtypes.Head, common.Hash] { ret := _m.Called() - var r0 commontypes.HeadTracker[*evmtypes.Head, common.Hash] - if rf, ok := ret.Get(0).(func() commontypes.HeadTracker[*evmtypes.Head, common.Hash]); ok { + var r0 types.HeadTracker[*evmtypes.Head, common.Hash] + if rf, ok := ret.Get(0).(func() types.HeadTracker[*evmtypes.Head, common.Hash]); ok { r0 = rf() } else { if ret.Get(0) != nil { - r0 = ret.Get(0).(commontypes.HeadTracker[*evmtypes.Head, common.Hash]) + r0 = ret.Get(0).(types.HeadTracker[*evmtypes.Head, common.Hash]) } } diff --git a/core/services/relay/evm/mercury/types/types.go b/core/services/relay/evm/mercury/types/types.go index ca266ca8ccd..7059689939a 100644 --- a/core/services/relay/evm/mercury/types/types.go +++ b/core/services/relay/evm/mercury/types/types.go @@ -8,14 +8,12 @@ import ( "github.com/prometheus/client_golang/prometheus/promauto" ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" - evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" httypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/headtracker/types" "github.com/smartcontractkit/chainlink/v2/core/services/pg" ) //go:generate mockery --quiet --name ChainHeadTracker --output ../mocks/ --case=underscore type ChainHeadTracker interface { - Client() evmclient.Client HeadTracker() httypes.HeadTracker } diff --git a/core/services/relay/evm/mercury/v1/data_source.go b/core/services/relay/evm/mercury/v1/data_source.go index d225dbee68e..1b16dc76f98 100644 --- a/core/services/relay/evm/mercury/v1/data_source.go +++ b/core/services/relay/evm/mercury/v1/data_source.go @@ -8,6 +8,8 @@ import ( "sync" pkgerrors "github.com/pkg/errors" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" @@ -25,6 +27,24 @@ import ( "github.com/smartcontractkit/chainlink/v2/core/utils" ) +var ( + insufficientBlocksCount = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "mercury_insufficient_blocks_count", + Help: fmt.Sprintf("Count of times that there were not enough blocks in the chain during observation (need: %d)", nBlocksObservation), + }, + []string{"feedID"}, + ) + zeroBlocksCount = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "mercury_zero_blocks_count", + Help: "Count of times that there were zero blocks in the chain during observation", + }, + []string{"feedID"}, + ) +) + +// nBlocksObservation controls how many blocks are included in the LatestBlocks observation +const nBlocksObservation int = 5 + type Runner interface { ExecuteRun(ctx context.Context, spec pipeline.Spec, vars pipeline.Vars, l logger.Logger) (run *pipeline.Run, trrs pipeline.TaskRunResults, err error) } @@ -51,18 +71,31 @@ type datasource struct { chainHeadTracker types.ChainHeadTracker fetcher Fetcher initialBlockNumber *int64 + + insufficientBlocksCounter prometheus.Counter + zeroBlocksCounter prometheus.Counter } var _ relaymercuryv1.DataSource = &datasource{} -func NewDataSource(orm types.DataSourceORM, pr pipeline.Runner, jb job.Job, spec pipeline.Spec, lggr logger.Logger, rr chan *pipeline.Run, enhancedTelemChan chan ocrcommon.EnhancedTelemetryMercuryData, chainHeadTracker types.ChainHeadTracker, fetcher Fetcher, initialBlockNumber *int64, feedID [32]byte) *datasource { - return &datasource{pr, jb, spec, lggr, rr, orm, reportcodec.ReportCodec{}, feedID, sync.RWMutex{}, enhancedTelemChan, chainHeadTracker, fetcher, initialBlockNumber} +func NewDataSource(orm types.DataSourceORM, pr pipeline.Runner, jb job.Job, spec pipeline.Spec, lggr logger.Logger, rr chan *pipeline.Run, enhancedTelemChan chan ocrcommon.EnhancedTelemetryMercuryData, chainHeadTracker types.ChainHeadTracker, fetcher Fetcher, initialBlockNumber *int64, feedID mercuryutils.FeedID) *datasource { + return &datasource{pr, jb, spec, lggr, rr, orm, reportcodec.ReportCodec{}, feedID, sync.RWMutex{}, enhancedTelemChan, chainHeadTracker, fetcher, initialBlockNumber, insufficientBlocksCount.WithLabelValues(feedID.String()), zeroBlocksCount.WithLabelValues(feedID.String())} +} + +type ErrEmptyLatestReport struct { + Err error +} + +func (e ErrEmptyLatestReport) Unwrap() error { return e.Err } + +func (e ErrEmptyLatestReport) Error() string { + return fmt.Sprintf("FetchInitialMaxFinalizedBlockNumber returned empty LatestReport; this is a new feed. No initialBlockNumber was set, tried to use current block number to determine maxFinalizedBlockNumber but got error: %v", e.Err) } func (ds *datasource) Observe(ctx context.Context, repts ocrtypes.ReportTimestamp, fetchMaxFinalizedBlockNum bool) (obs relaymercuryv1.Observation, pipelineExecutionErr error) { - // setCurrentBlock must come first, along with observationTimestamp, to - // avoid front-running - ds.setCurrentBlock(ctx, &obs) + // setLatestBlocks must come chronologically before observations, along + // with observationTimestamp, to avoid front-running + ds.setLatestBlocks(ctx, &obs) var wg sync.WaitGroup if fetchMaxFinalizedBlockNum { @@ -89,7 +122,7 @@ func (ds *datasource) Observe(ctx context.Context, repts ocrtypes.ReportTimestam } if ds.initialBlockNumber == nil { if obs.CurrentBlockNum.Err != nil { - obs.MaxFinalizedBlockNumber.Err = fmt.Errorf("FetchInitialMaxFinalizedBlockNumber returned empty LatestReport; this is a new feed. No initialBlockNumber was set, tried to use current block number to determine maxFinalizedBlockNumber but got error: %w", obs.CurrentBlockNum.Err) + obs.MaxFinalizedBlockNumber.Err = ErrEmptyLatestReport{Err: obs.CurrentBlockNum.Err} } else { // Subract 1 here because we will later add 1 to the // maxFinalizedBlockNumber to get the first validFromBlockNum, which @@ -258,37 +291,40 @@ func (ds *datasource) executeRun(ctx context.Context) (*pipeline.Run, pipeline.T return run, trrs, err } -func (ds *datasource) setCurrentBlock(ctx context.Context, obs *relaymercuryv1.Observation) { - latestHead, err := ds.getCurrentBlock(ctx) - if err != nil { +func (ds *datasource) setLatestBlocks(ctx context.Context, obs *relaymercuryv1.Observation) { + latestBlocks := ds.getLatestBlocks(ctx, nBlocksObservation) + if len(latestBlocks) < nBlocksObservation { + ds.insufficientBlocksCounter.Inc() + ds.lggr.Warnw("Insufficient blocks", "latestBlocks", latestBlocks, "lenLatestBlocks", len(latestBlocks), "nBlocksObservation", nBlocksObservation) + } + + // TODO: remove with https://smartcontract-it.atlassian.net/browse/BCF-2209 + if len(latestBlocks) == 0 { + ds.zeroBlocksCounter.Inc() + err := errors.New("no blocks available") obs.CurrentBlockNum.Err = err obs.CurrentBlockHash.Err = err obs.CurrentBlockTimestamp.Err = err - return + } else { + obs.CurrentBlockNum.Val = latestBlocks[0].Number + obs.CurrentBlockHash.Val = latestBlocks[0].Hash.Bytes() + if latestBlocks[0].Timestamp.IsZero() { + obs.CurrentBlockTimestamp.Val = 0 + } else { + obs.CurrentBlockTimestamp.Val = uint64(latestBlocks[0].Timestamp.Unix()) + } } - obs.CurrentBlockNum.Val = latestHead.Number - obs.CurrentBlockHash.Val = latestHead.Hash.Bytes() - if latestHead.Timestamp.IsZero() { - obs.CurrentBlockTimestamp.Val = 0 - } else { - obs.CurrentBlockTimestamp.Val = uint64(latestHead.Timestamp.Unix()) + for _, block := range latestBlocks { + obs.LatestBlocks = append(obs.LatestBlocks, relaymercuryv1.NewBlock(block.Number, block.Hash.Bytes(), uint64(block.Timestamp.Unix()))) } } -func (ds *datasource) getCurrentBlock(ctx context.Context) (*evmtypes.Head, error) { - // Use the headtracker's view of the latest block, this is very fast since +func (ds *datasource) getLatestBlocks(ctx context.Context, k int) (blocks []*evmtypes.Head) { + // Use the headtracker's view of the chain, this is very fast since // it doesn't make any external network requests, and it is the // headtracker's job to ensure it has an up-to-date view of the chain based // on responses from all available RPC nodes latestHead := ds.chainHeadTracker.HeadTracker().LatestChain() - if latestHead == nil { - logger.Sugared(ds.lggr).AssumptionViolation("HeadTracker unexpectedly returned nil head, falling back to RPC call") - var err error - latestHead, err = ds.chainHeadTracker.Client().HeadByNumber(ctx, nil) - if err != nil { - return nil, err - } - } - return latestHead, nil + return latestHead.AsSlice(k) } diff --git a/core/services/relay/evm/mercury/v1/data_source_test.go b/core/services/relay/evm/mercury/v1/data_source_test.go index 6e460951301..42983fa0022 100644 --- a/core/services/relay/evm/mercury/v1/data_source_test.go +++ b/core/services/relay/evm/mercury/v1/data_source_test.go @@ -11,7 +11,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/pkg/errors" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" ocrtypes "github.com/smartcontractkit/libocr/offchainreporting2plus/types" @@ -19,17 +18,17 @@ import ( relaymercuryv1 "github.com/smartcontractkit/chainlink-relay/pkg/reportingplugins/mercury/v1" commonmocks "github.com/smartcontractkit/chainlink/v2/common/mocks" "github.com/smartcontractkit/chainlink/v2/core/assets" - evmclient "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client" - evmclimocks "github.com/smartcontractkit/chainlink/v2/core/chains/evm/client/mocks" httypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/headtracker/types" evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types" "github.com/smartcontractkit/chainlink/v2/core/internal/testutils" "github.com/smartcontractkit/chainlink/v2/core/logger" + "github.com/smartcontractkit/chainlink/v2/core/services/job" "github.com/smartcontractkit/chainlink/v2/core/services/pg" "github.com/smartcontractkit/chainlink/v2/core/services/pipeline" mercurymocks "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/mercury/mocks" "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/mercury/types" + mercuryutils "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/mercury/utils" reportcodecv1 "github.com/smartcontractkit/chainlink/v2/core/services/relay/evm/mercury/v1/reportcodec" "github.com/smartcontractkit/chainlink/v2/core/utils" ) @@ -56,11 +55,9 @@ func (m *mockFetcher) LatestTimestamp(context.Context) (int64, error) { var _ types.ChainHeadTracker = &mockHeadTracker{} type mockHeadTracker struct { - c evmclient.Client h httypes.HeadTracker } -func (m *mockHeadTracker) Client() evmclient.Client { return m.c } func (m *mockHeadTracker) HeadTracker() httypes.HeadTracker { return m.h } type mockORM struct { @@ -74,7 +71,8 @@ func (m *mockORM) LatestReport(ctx context.Context, feedID [32]byte, qopts ...pg func TestMercury_Observe(t *testing.T) { orm := &mockORM{} - ds := &datasource{lggr: logger.TestLogger(t), orm: orm, codec: (reportcodecv1.ReportCodec{})} + lggr := logger.TestLogger(t) + ds := NewDataSource(orm, nil, job.Job{}, pipeline.Spec{}, lggr, nil, nil, nil, nil, nil, mercuryutils.FeedID{}) ctx := testutils.Context(t) repts := ocrtypes.ReportTimestamp{} @@ -108,9 +106,7 @@ func TestMercury_Observe(t *testing.T) { ds.spec = spec h := commonmocks.NewHeadTracker[*evmtypes.Head, common.Hash](t) - c := evmclimocks.NewClient(t) ht := &mockHeadTracker{ - c: c, h: h, } ds.chainHeadTracker = ht @@ -202,25 +198,21 @@ func TestMercury_Observe(t *testing.T) { assert.NoError(t, obs.MaxFinalizedBlockNumber.Err) assert.Equal(t, head.Number-1, obs.MaxFinalizedBlockNumber.Val) }) - t.Run("if current block num errored", func(t *testing.T) { + t.Run("if no current block available", func(t *testing.T) { h2 := commonmocks.NewHeadTracker[*evmtypes.Head, common.Hash](t) h2.On("LatestChain").Return((*evmtypes.Head)(nil)) ht.h = h2 - c2 := evmclimocks.NewClient(t) - c2.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(nil, errors.New("head retrieval failed")) - ht.c = c2 obs, err := ds.Observe(ctx, repts, true) assert.NoError(t, err) - assert.EqualError(t, obs.MaxFinalizedBlockNumber.Err, "FetchInitialMaxFinalizedBlockNumber returned empty LatestReport; this is a new feed. No initialBlockNumber was set, tried to use current block number to determine maxFinalizedBlockNumber but got error: head retrieval failed") + assert.EqualError(t, obs.MaxFinalizedBlockNumber.Err, "FetchInitialMaxFinalizedBlockNumber returned empty LatestReport; this is a new feed. No initialBlockNumber was set, tried to use current block number to determine maxFinalizedBlockNumber but got error: no blocks available") }) }) }) }) ht.h = h - ht.c = c t.Run("when fetchMaxFinalizedBlockNum=false", func(t *testing.T) { t.Run("when run execution fails, returns error", func(t *testing.T) { @@ -322,52 +314,96 @@ func TestMercury_Observe(t *testing.T) { t.Fatal("expected run on channel") } }) - t.Run("if head tracker returns nil, falls back to RPC method", func(t *testing.T) { - t.Run("if call succeeds", func(t *testing.T) { - h = commonmocks.NewHeadTracker[*evmtypes.Head, common.Hash](t) - h.On("LatestChain").Return((*evmtypes.Head)(nil)) - ht.h = h - c.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(head, nil).Once() - - obs, err := ds.Observe(ctx, repts, false) - assert.NoError(t, err) + }) - assert.Equal(t, head.Number, obs.CurrentBlockNum.Val) - assert.NoError(t, obs.CurrentBlockNum.Err) - assert.Equal(t, fmt.Sprintf("%x", head.Hash), fmt.Sprintf("%x", obs.CurrentBlockHash.Val)) - assert.NoError(t, obs.CurrentBlockHash.Err) - assert.Equal(t, uint64(head.Timestamp.Unix()), obs.CurrentBlockTimestamp.Val) - assert.NoError(t, obs.CurrentBlockTimestamp.Err) + t.Run("LatestBlocks is populated correctly", func(t *testing.T) { + t.Run("when chain length is zero", func(t *testing.T) { + ht2 := commonmocks.NewHeadTracker[*evmtypes.Head, common.Hash](t) + ht2.On("LatestChain").Return((*evmtypes.Head)(nil)) + ht.h = ht2 - h.AssertExpectations(t) - c.AssertExpectations(t) - }) - t.Run("if call fails, returns error for that observation", func(t *testing.T) { - c = evmclimocks.NewClient(t) - ht.c = c - c.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(nil, errors.New("client call failed")).Once() + obs, err := ds.Observe(ctx, repts, true) + assert.NoError(t, err) - obs, err := ds.Observe(ctx, repts, false) - assert.NoError(t, err) + assert.Len(t, obs.LatestBlocks, 0) - assert.Zero(t, obs.CurrentBlockNum.Val) - assert.EqualError(t, obs.CurrentBlockNum.Err, "client call failed") - assert.Zero(t, obs.CurrentBlockHash.Val) - assert.EqualError(t, obs.CurrentBlockHash.Err, "client call failed") - assert.Zero(t, obs.CurrentBlockTimestamp.Val) - assert.EqualError(t, obs.CurrentBlockTimestamp.Err, "client call failed") + ht2.AssertExpectations(t) + }) + t.Run("when chain is too short", func(t *testing.T) { + h4 := &evmtypes.Head{ + Number: 4, + Parent: nil, + } + h5 := &evmtypes.Head{ + Number: 5, + Parent: h4, + } + h6 := &evmtypes.Head{ + Number: 6, + Parent: h5, + } - c.AssertExpectations(t) - }) + ht2 := commonmocks.NewHeadTracker[*evmtypes.Head, common.Hash](t) + ht2.On("LatestChain").Return(h6) + ht.h = ht2 + + obs, err := ds.Observe(ctx, repts, true) + assert.NoError(t, err) + + assert.Len(t, obs.LatestBlocks, 3) + assert.Equal(t, 6, int(obs.LatestBlocks[0].Num)) + assert.Equal(t, 5, int(obs.LatestBlocks[1].Num)) + assert.Equal(t, 4, int(obs.LatestBlocks[2].Num)) + + ht2.AssertExpectations(t) + }) + t.Run("when chain is long enough", func(t *testing.T) { + h1 := &evmtypes.Head{ + Number: 1, + } + h2 := &evmtypes.Head{ + Number: 2, + Parent: h1, + } + h3 := &evmtypes.Head{ + Number: 3, + Parent: h2, + } + h4 := &evmtypes.Head{ + Number: 4, + Parent: h3, + } + h5 := &evmtypes.Head{ + Number: 5, + Parent: h4, + } + h6 := &evmtypes.Head{ + Number: 6, + Parent: h5, + } + + ht2 := commonmocks.NewHeadTracker[*evmtypes.Head, common.Hash](t) + ht2.On("LatestChain").Return(h6) + ht.h = ht2 + + obs, err := ds.Observe(ctx, repts, true) + assert.NoError(t, err) + + assert.Len(t, obs.LatestBlocks, 5) + assert.Equal(t, 6, int(obs.LatestBlocks[0].Num)) + assert.Equal(t, 5, int(obs.LatestBlocks[1].Num)) + assert.Equal(t, 4, int(obs.LatestBlocks[2].Num)) + assert.Equal(t, 3, int(obs.LatestBlocks[3].Num)) + assert.Equal(t, 2, int(obs.LatestBlocks[4].Num)) + + ht2.AssertExpectations(t) }) }) } -func TestMercury_SetCurrentBlock(t *testing.T) { +func TestMercury_SetLatestBlocks(t *testing.T) { lggr := logger.TestLogger(t) - ds := datasource{ - lggr: lggr, - } + ds := NewDataSource(nil, nil, job.Job{}, pipeline.Spec{}, lggr, nil, nil, nil, nil, nil, mercuryutils.FeedID{}) h := evmtypes.Head{ Number: testutils.NewRandomPositiveInt64(), @@ -390,64 +426,41 @@ func TestMercury_SetCurrentBlock(t *testing.T) { ds.chainHeadTracker = chainHeadTracker obs := relaymercuryv1.Observation{} - ds.setCurrentBlock(context.Background(), &obs) + ds.setLatestBlocks(context.Background(), &obs) assert.Equal(t, h.Number, obs.CurrentBlockNum.Val) assert.Equal(t, h.Hash.Bytes(), obs.CurrentBlockHash.Val) assert.Equal(t, uint64(h.Timestamp.Unix()), obs.CurrentBlockTimestamp.Val) - chainHeadTracker.AssertExpectations(t) - headTracker.AssertExpectations(t) - }) - - t.Run("if headtracker returns nil head and eth call succeeds", func(t *testing.T) { - ethClient := evmclimocks.NewClient(t) - headTracker := commonmocks.NewHeadTracker[*evmtypes.Head, common.Hash](t) - chainHeadTracker := mercurymocks.NewChainHeadTracker(t) - - chainHeadTracker.On("Client").Return(ethClient) - chainHeadTracker.On("HeadTracker").Return(headTracker) - // This can happen in some cases e.g. RPC node is offline - headTracker.On("LatestChain").Return((*evmtypes.Head)(nil)) - ethClient.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(&h, nil) - - ds.chainHeadTracker = chainHeadTracker - - obs := relaymercuryv1.Observation{} - ds.setCurrentBlock(context.Background(), &obs) - - assert.Equal(t, h.Number, obs.CurrentBlockNum.Val) - assert.Equal(t, h.Hash.Bytes(), obs.CurrentBlockHash.Val) - assert.Equal(t, uint64(h.Timestamp.Unix()), obs.CurrentBlockTimestamp.Val) + assert.Len(t, obs.LatestBlocks, 1) chainHeadTracker.AssertExpectations(t) - ethClient.AssertExpectations(t) headTracker.AssertExpectations(t) }) - t.Run("if headtracker returns nil head and eth call fails", func(t *testing.T) { - ethClient := evmclimocks.NewClient(t) + t.Run("if headtracker returns nil head", func(t *testing.T) { headTracker := commonmocks.NewHeadTracker[*evmtypes.Head, common.Hash](t) chainHeadTracker := mercurymocks.NewChainHeadTracker(t) - chainHeadTracker.On("Client").Return(ethClient) chainHeadTracker.On("HeadTracker").Return(headTracker) // This can happen in some cases e.g. RPC node is offline headTracker.On("LatestChain").Return((*evmtypes.Head)(nil)) - err := errors.New("foo") - ethClient.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(nil, err) ds.chainHeadTracker = chainHeadTracker obs := relaymercuryv1.Observation{} - ds.setCurrentBlock(context.Background(), &obs) + ds.setLatestBlocks(context.Background(), &obs) + + assert.Zero(t, obs.CurrentBlockNum.Val) + assert.Zero(t, obs.CurrentBlockHash.Val) + assert.Zero(t, obs.CurrentBlockTimestamp.Val) + assert.EqualError(t, obs.CurrentBlockNum.Err, "no blocks available") + assert.EqualError(t, obs.CurrentBlockHash.Err, "no blocks available") + assert.EqualError(t, obs.CurrentBlockTimestamp.Err, "no blocks available") - assert.Equal(t, err, obs.CurrentBlockNum.Err) - assert.Equal(t, err, obs.CurrentBlockHash.Err) - assert.Equal(t, err, obs.CurrentBlockTimestamp.Err) + assert.Len(t, obs.LatestBlocks, 0) chainHeadTracker.AssertExpectations(t) - ethClient.AssertExpectations(t) headTracker.AssertExpectations(t) }) } diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b5b393542be..a9f9d080f42 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -22,6 +22,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Removed `Optimism2` as a supported gas estimator mode +### Added + +- Mercury v0.2 has improved consensus around current block that uses the most recent 5 blocks instead of only the latest one +- Two new prom metrics for mercury, nops should consider adding alerting on these: + - `mercury_insufficient_blocks_count` + - `mercury_zero_blocks_count` + ... ## 2.7.0 - UNRELEASED diff --git a/go.mod b/go.mod index 999c1b0402f..f271bf421f6 100644 --- a/go.mod +++ b/go.mod @@ -66,7 +66,7 @@ require ( github.com/shopspring/decimal v1.3.1 github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20231101160906-7acebcc1b353 - github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231101203911-c686b4d48672 + github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231102162027-5fdce33763de github.com/smartcontractkit/chainlink-solana v1.0.3-0.20231023133638-72f4e799ab05 github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20231024133459-1ef3a11319eb github.com/smartcontractkit/libocr v0.0.0-20231020123319-d255366a6545 diff --git a/go.sum b/go.sum index ee06cc9b751..8426876e239 100644 --- a/go.sum +++ b/go.sum @@ -1467,8 +1467,8 @@ github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 h1:T3lFWumv github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704/go.mod h1:2QuJdEouTWjh5BDy5o/vgGXQtR4Gz8yH1IYB5eT7u4M= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20231101160906-7acebcc1b353 h1:4iO3Ei1b/Lb0yprzclk93e1aQnYF92sIe+EJzMG87y4= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20231101160906-7acebcc1b353/go.mod h1:hMhGr9ok3p4442keFtK6u6Ei9yWfG66fmDwsFi3aHcw= -github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231101203911-c686b4d48672 h1:59vz5H52EpwWE/64ZQpNCs7Gtnyi7/ytjyoGjlbKhBA= -github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231101203911-c686b4d48672/go.mod h1:M9U1JV7IQi8Sfj4JR1qSi1tIh6omgW78W/8SHN/8BUQ= +github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231102162027-5fdce33763de h1:CeVpn5xEdmuEsYE8ss2b7bSq9h3BY4OPvpqXeYIPnHw= +github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231102162027-5fdce33763de/go.mod h1:M9U1JV7IQi8Sfj4JR1qSi1tIh6omgW78W/8SHN/8BUQ= github.com/smartcontractkit/chainlink-solana v1.0.3-0.20231023133638-72f4e799ab05 h1:DaPSVnxe7oz1QJ+AVIhQWs1W3ubQvwvGo9NbHpMs1OQ= github.com/smartcontractkit/chainlink-solana v1.0.3-0.20231023133638-72f4e799ab05/go.mod h1:o0Pn1pbaUluboaK6/yhf8xf7TiFCkyFl6WUOdwqamuU= github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20231024133459-1ef3a11319eb h1:HiluOfEVGOQTM6BTDImOqYdMZZ7qq7fkZ3TJdmItNr8= diff --git a/integration-tests/go.mod b/integration-tests/go.mod index 93820c6ebfe..c4e6fd38482 100644 --- a/integration-tests/go.mod +++ b/integration-tests/go.mod @@ -388,7 +388,7 @@ require ( github.com/sirupsen/logrus v1.9.3 // indirect github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 // indirect github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20231101160906-7acebcc1b353 // indirect - github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231101203911-c686b4d48672 // indirect + github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231102162027-5fdce33763de // indirect github.com/smartcontractkit/chainlink-solana v1.0.3-0.20231023133638-72f4e799ab05 // indirect github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20231024133459-1ef3a11319eb // indirect github.com/smartcontractkit/tdh2/go/ocr2/decryptionplugin v0.0.0-20230906073235-9e478e5e19f1 // indirect diff --git a/integration-tests/go.sum b/integration-tests/go.sum index 60805eae825..4744fc086a3 100644 --- a/integration-tests/go.sum +++ b/integration-tests/go.sum @@ -2370,8 +2370,8 @@ github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704 h1:T3lFWumv github.com/smartcontractkit/caigo v0.0.0-20230621050857-b29a4ca8c704/go.mod h1:2QuJdEouTWjh5BDy5o/vgGXQtR4Gz8yH1IYB5eT7u4M= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20231101160906-7acebcc1b353 h1:4iO3Ei1b/Lb0yprzclk93e1aQnYF92sIe+EJzMG87y4= github.com/smartcontractkit/chainlink-cosmos v0.4.1-0.20231101160906-7acebcc1b353/go.mod h1:hMhGr9ok3p4442keFtK6u6Ei9yWfG66fmDwsFi3aHcw= -github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231101203911-c686b4d48672 h1:59vz5H52EpwWE/64ZQpNCs7Gtnyi7/ytjyoGjlbKhBA= -github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231101203911-c686b4d48672/go.mod h1:M9U1JV7IQi8Sfj4JR1qSi1tIh6omgW78W/8SHN/8BUQ= +github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231102162027-5fdce33763de h1:CeVpn5xEdmuEsYE8ss2b7bSq9h3BY4OPvpqXeYIPnHw= +github.com/smartcontractkit/chainlink-relay v0.1.7-0.20231102162027-5fdce33763de/go.mod h1:M9U1JV7IQi8Sfj4JR1qSi1tIh6omgW78W/8SHN/8BUQ= github.com/smartcontractkit/chainlink-solana v1.0.3-0.20231023133638-72f4e799ab05 h1:DaPSVnxe7oz1QJ+AVIhQWs1W3ubQvwvGo9NbHpMs1OQ= github.com/smartcontractkit/chainlink-solana v1.0.3-0.20231023133638-72f4e799ab05/go.mod h1:o0Pn1pbaUluboaK6/yhf8xf7TiFCkyFl6WUOdwqamuU= github.com/smartcontractkit/chainlink-starknet/relayer v0.0.1-beta-test.0.20231024133459-1ef3a11319eb h1:HiluOfEVGOQTM6BTDImOqYdMZZ7qq7fkZ3TJdmItNr8=