From fe3a5206800663be733f0503b03b8eb4aadceaab Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Fri, 9 Jun 2023 14:37:30 -0700 Subject: [PATCH 01/47] Added solana hash and hash test --- pkg/common/types/hashable.go | 12 +++++++++++ pkg/solana/headtracker/types/hash.go | 13 ++++++++++++ pkg/solana/headtracker/types/hash_test.go | 26 +++++++++++++++++++++++ 3 files changed, 51 insertions(+) create mode 100644 pkg/common/types/hashable.go create mode 100644 pkg/solana/headtracker/types/hash.go create mode 100644 pkg/solana/headtracker/types/hash_test.go diff --git a/pkg/common/types/hashable.go b/pkg/common/types/hashable.go new file mode 100644 index 000000000..2d166505b --- /dev/null +++ b/pkg/common/types/hashable.go @@ -0,0 +1,12 @@ +package types + +import "fmt" + +// A chain-agnostic generic interface to represent the following native types on various chains: +// PublicKey, Address, Account, BlockHash, TxHash +type Hashable interface { + fmt.Stringer + comparable + + Bytes() []byte +} diff --git a/pkg/solana/headtracker/types/hash.go b/pkg/solana/headtracker/types/hash.go new file mode 100644 index 000000000..373529921 --- /dev/null +++ b/pkg/solana/headtracker/types/hash.go @@ -0,0 +1,13 @@ +package headtracker + +import ( + "github.com/gagliardetto/solana-go" +) + +type Hash struct { + solana.Hash +} + +func (h Hash) Bytes() []byte { + return h.Hash[:] +} diff --git a/pkg/solana/headtracker/types/hash_test.go b/pkg/solana/headtracker/types/hash_test.go new file mode 100644 index 000000000..116927bfc --- /dev/null +++ b/pkg/solana/headtracker/types/hash_test.go @@ -0,0 +1,26 @@ +package headtracker + +import ( + "bytes" + "testing" + + "github.com/gagliardetto/solana-go" +) + +func TestHash_Bytes(t *testing.T) { + t.Run("happy path", func(t *testing.T) { + // Create a solana.Hash with 32 bytes. + expectedBytes := []byte("abcdefghabcdefghabcdefghabcdefgh") + var solanaHash solana.Hash + copy(solanaHash[:], expectedBytes) + + // Create a Hash instance with the solana.Hash we just created. + testHash := Hash{solanaHash} + actualBytes := testHash.Bytes() + + // Check that the bytes returned by the method match the bytes we put into the solana.Hash. + if !bytes.Equal(actualBytes, expectedBytes) { + t.Errorf("Bytes() returned %v, want %v", actualBytes, expectedBytes) + } + }) +} From 15a48ba8aeba3d6b6d7c460c88d120c94a7c83bc Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Fri, 9 Jun 2023 14:38:33 -0700 Subject: [PATCH 02/47] Added Head --- pkg/common/headtracker/types/head.go | 16 ++++ pkg/common/types/chain.go | 7 ++ pkg/common/types/head.go | 27 +++++++ pkg/solana/headtracker/types/chain.go | 26 +++++++ pkg/solana/headtracker/types/head.go | 102 ++++++++++++++++++++++++++ 5 files changed, 178 insertions(+) create mode 100644 pkg/common/headtracker/types/head.go create mode 100644 pkg/common/types/chain.go create mode 100644 pkg/common/types/head.go create mode 100644 pkg/solana/headtracker/types/chain.go create mode 100644 pkg/solana/headtracker/types/head.go diff --git a/pkg/common/headtracker/types/head.go b/pkg/common/headtracker/types/head.go new file mode 100644 index 000000000..9d9c383f5 --- /dev/null +++ b/pkg/common/headtracker/types/head.go @@ -0,0 +1,16 @@ +package headtracker + +import ( + "github.com/smartcontractkit/chainlink-solana/pkg/common/types" +) + +//go:generate mockery --quiet --name Head --output ./mocks/ --case=underscore +type Head[BLOCK_HASH types.Hashable, CHAIN_ID types.ID] interface { + types.Head[BLOCK_HASH] + // ChainID returns the chain ID that the head is for + ChainID() CHAIN_ID + // Returns true if the head has a chain Id + HasChainID() bool + // IsValid returns true if the head is valid. + IsValid() bool +} diff --git a/pkg/common/types/chain.go b/pkg/common/types/chain.go new file mode 100644 index 000000000..d9d717bb9 --- /dev/null +++ b/pkg/common/types/chain.go @@ -0,0 +1,7 @@ +package types + +import "fmt" + +// ID represents the base type, for any chain's ID. +// It should be convertible to a string, that can uniquely identify this chain +type ID fmt.Stringer diff --git a/pkg/common/types/head.go b/pkg/common/types/head.go new file mode 100644 index 000000000..4d339b1cd --- /dev/null +++ b/pkg/common/types/head.go @@ -0,0 +1,27 @@ +package types + +// Head provides access to a chain's head, as needed by the TxManager. +// This is a generic interface which ALL chains will implement. +// +//go:generate mockery --quiet --name Head --output ./mocks/ --case=underscore +type Head[BLOCK_HASH Hashable] interface { + // BlockNumber is the head's block number + BlockNumber() int64 + + // ChainLength returns the length of the chain followed by recursively looking up parents + ChainLength() uint32 + + // EarliestHeadInChain traverses through parents until it finds the earliest one + EarliestHeadInChain() Head[BLOCK_HASH] + + // Parent is the head's parent block + GetParent() Head[BLOCK_HASH] + + // Hash is the head's block hash + BlockHash() BLOCK_HASH + GetParentHash() BLOCK_HASH + + // HashAtHeight returns the hash of the block at the given height, if it is in the chain. + // If not in chain, returns the zero hash + HashAtHeight(blockNum int64) BLOCK_HASH +} diff --git a/pkg/solana/headtracker/types/chain.go b/pkg/solana/headtracker/types/chain.go new file mode 100644 index 000000000..6ff8035df --- /dev/null +++ b/pkg/solana/headtracker/types/chain.go @@ -0,0 +1,26 @@ +package headtracker + +type ChainID int + +const ( + Mainnet ChainID = iota + Testnet + Devnet + Localnet +) + +// String returns the string representation of the Network value. +func (id ChainID) String() string { + switch id { + case Mainnet: + return "mainnet" + case Testnet: + return "testnet" + case Devnet: + return "devnet" + case Localnet: + return "localnet" + default: + return "unknown" + } +} diff --git a/pkg/solana/headtracker/types/head.go b/pkg/solana/headtracker/types/head.go new file mode 100644 index 000000000..a86a0433e --- /dev/null +++ b/pkg/solana/headtracker/types/head.go @@ -0,0 +1,102 @@ +package headtracker + +import ( + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" + htrktypes "github.com/smartcontractkit/chainlink-solana/pkg/common/headtracker/types" + commontypes "github.com/smartcontractkit/chainlink-solana/pkg/common/types" +) + +var _ commontypes.Head[Hash] = (*SolanaHead)(nil) +var _ htrktypes.Head[Hash, ChainID] = (*SolanaHead)(nil) + +type SolanaHead struct { + Slot int64 + Block rpc.GetBlockResult + Parent *SolanaHead + ID ChainID +} + +func (h *SolanaHead) BlockNumber() int64 { + return h.Slot +} + +// ChainLength returns the length of the chain followed by recursively looking up parents +func (h *SolanaHead) ChainLength() uint32 { + if h == nil { + return 0 + } + l := uint32(1) + + for { + if h.Parent != nil { + l++ + if h == h.Parent { + panic("circular reference detected") + } + h = h.Parent + } else { + break + } + } + return l +} + +func (h *SolanaHead) EarliestHeadInChain() commontypes.Head[Hash] { + return h.earliestInChain() +} + +func (h *SolanaHead) earliestInChain() *SolanaHead { + for h.Parent != nil { + h = h.Parent + } + return h +} + +func (h *SolanaHead) BlockHash() Hash { + return Hash{Hash: h.blockHash()} +} + +func (h *SolanaHead) blockHash() solana.Hash { + return h.Block.Blockhash +} + +func (h *SolanaHead) GetParent() commontypes.Head[Hash] { + if h.Parent == nil { + return nil + } + return h.Parent +} + +func (h *SolanaHead) GetParentHash() Hash { + if h.Parent == nil { + return Hash{} + } + return h.Parent.BlockHash() +} + +func (h *SolanaHead) HashAtHeight(slotNum int64) Hash { + for { + if h.Slot == slotNum { + return h.BlockHash() + } + if h.Parent != nil { + h = h.Parent + } else { + break + } + } + return Hash{} +} + +func (h *SolanaHead) ChainID() ChainID { + return h.ID +} + +func (h *SolanaHead) HasChainID() bool { + return h.ID.String() != "unknown" // TODO: Refactor this into a more coherent check +} + +func (h *SolanaHead) IsValid() bool { + return h != nil +} From 64f3e54e350af2c910a3808294a5e21953a2abce Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Fri, 9 Jun 2023 16:30:37 -0700 Subject: [PATCH 03/47] Added Hash Helper for tests --- pkg/solana/utils/hash_helper.go | 17 +++++++++++++++++ pkg/solana/utils/hash_helper_test.go | 23 +++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 pkg/solana/utils/hash_helper.go create mode 100644 pkg/solana/utils/hash_helper_test.go diff --git a/pkg/solana/utils/hash_helper.go b/pkg/solana/utils/hash_helper.go new file mode 100644 index 000000000..829454023 --- /dev/null +++ b/pkg/solana/utils/hash_helper.go @@ -0,0 +1,17 @@ +package utils + +import ( + "crypto/rand" + + "github.com/gagliardetto/solana-go" +) + +// NewHash returns a random solana.Hash using SHA-256. +func NewHash() solana.Hash { + b := make([]byte, 32) + _, err := rand.Read(b) + if err != nil { + panic(err) + } + return solana.HashFromBytes(b) +} diff --git a/pkg/solana/utils/hash_helper_test.go b/pkg/solana/utils/hash_helper_test.go new file mode 100644 index 000000000..0bcff880c --- /dev/null +++ b/pkg/solana/utils/hash_helper_test.go @@ -0,0 +1,23 @@ +package utils_test + +import ( + "testing" + + "github.com/gagliardetto/solana-go" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/utils" + "github.com/stretchr/testify/assert" +) + +func TestNewHash(t *testing.T) { + t.Parallel() + + h1 := utils.NewHash() + h2 := utils.NewHash() + // Check that the two hashes are not the same. + assert.NotEqual(t, h1, h2) + + // Check that neither hash is equal to a zero hash. + zeroHash := solana.Hash{} + assert.NotEqual(t, h1, zeroHash) + assert.NotEqual(t, h2, zeroHash) +} From 87c113cf485c46ff9a6da13a5fb4a3b392b01a5f Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Fri, 9 Jun 2023 16:30:50 -0700 Subject: [PATCH 04/47] Added Head --- pkg/solana/headtracker/types/head.go | 42 +++++++++++++++++----------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/pkg/solana/headtracker/types/head.go b/pkg/solana/headtracker/types/head.go index a86a0433e..5bf19354f 100644 --- a/pkg/solana/headtracker/types/head.go +++ b/pkg/solana/headtracker/types/head.go @@ -7,22 +7,32 @@ import ( commontypes "github.com/smartcontractkit/chainlink-solana/pkg/common/types" ) -var _ commontypes.Head[Hash] = (*SolanaHead)(nil) -var _ htrktypes.Head[Hash, ChainID] = (*SolanaHead)(nil) +var _ commontypes.Head[Hash] = (*Head)(nil) +var _ htrktypes.Head[Hash, ChainID] = (*Head)(nil) -type SolanaHead struct { +type Head struct { Slot int64 Block rpc.GetBlockResult - Parent *SolanaHead + Parent *Head ID ChainID } -func (h *SolanaHead) BlockNumber() int64 { +// NewHead returns an instance of Head +func NewHead(slot int64, block rpc.GetBlockResult, parent *Head, id ChainID) *Head { + return &Head{ + Slot: slot, + Block: block, + Parent: parent, + ID: id, + } +} + +func (h *Head) BlockNumber() int64 { return h.Slot } // ChainLength returns the length of the chain followed by recursively looking up parents -func (h *SolanaHead) ChainLength() uint32 { +func (h *Head) ChainLength() uint32 { if h == nil { return 0 } @@ -42,40 +52,40 @@ func (h *SolanaHead) ChainLength() uint32 { return l } -func (h *SolanaHead) EarliestHeadInChain() commontypes.Head[Hash] { +func (h *Head) EarliestHeadInChain() commontypes.Head[Hash] { return h.earliestInChain() } -func (h *SolanaHead) earliestInChain() *SolanaHead { +func (h *Head) earliestInChain() *Head { for h.Parent != nil { h = h.Parent } return h } -func (h *SolanaHead) BlockHash() Hash { +func (h *Head) BlockHash() Hash { return Hash{Hash: h.blockHash()} } -func (h *SolanaHead) blockHash() solana.Hash { +func (h *Head) blockHash() solana.Hash { return h.Block.Blockhash } -func (h *SolanaHead) GetParent() commontypes.Head[Hash] { +func (h *Head) GetParent() commontypes.Head[Hash] { if h.Parent == nil { return nil } return h.Parent } -func (h *SolanaHead) GetParentHash() Hash { +func (h *Head) GetParentHash() Hash { if h.Parent == nil { return Hash{} } return h.Parent.BlockHash() } -func (h *SolanaHead) HashAtHeight(slotNum int64) Hash { +func (h *Head) HashAtHeight(slotNum int64) Hash { for { if h.Slot == slotNum { return h.BlockHash() @@ -89,14 +99,14 @@ func (h *SolanaHead) HashAtHeight(slotNum int64) Hash { return Hash{} } -func (h *SolanaHead) ChainID() ChainID { +func (h *Head) ChainID() ChainID { return h.ID } -func (h *SolanaHead) HasChainID() bool { +func (h *Head) HasChainID() bool { return h.ID.String() != "unknown" // TODO: Refactor this into a more coherent check } -func (h *SolanaHead) IsValid() bool { +func (h *Head) IsValid() bool { return h != nil } From 69005ea58319dec539e6bdb931ef2a16559e91fc Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Fri, 9 Jun 2023 17:06:39 -0700 Subject: [PATCH 05/47] Added Head and Head test for solana --- pkg/solana/headtracker/types/head.go | 5 +- pkg/solana/headtracker/types/head_test.go | 142 ++++++++++++++++++++++ 2 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 pkg/solana/headtracker/types/head_test.go diff --git a/pkg/solana/headtracker/types/head.go b/pkg/solana/headtracker/types/head.go index 5bf19354f..6d64f216d 100644 --- a/pkg/solana/headtracker/types/head.go +++ b/pkg/solana/headtracker/types/head.go @@ -104,7 +104,10 @@ func (h *Head) ChainID() ChainID { } func (h *Head) HasChainID() bool { - return h.ID.String() != "unknown" // TODO: Refactor this into a more coherent check + if h == nil { + return false + } + return h.ChainID().String() != "unknown" // TODO: Refactor this into a more coherent check } func (h *Head) IsValid() bool { diff --git a/pkg/solana/headtracker/types/head_test.go b/pkg/solana/headtracker/types/head_test.go new file mode 100644 index 000000000..609a87dc7 --- /dev/null +++ b/pkg/solana/headtracker/types/head_test.go @@ -0,0 +1,142 @@ +package headtracker_test + +import ( + "strconv" + "testing" + + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" + headtracker "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/utils" + "github.com/stretchr/testify/assert" +) + +func configureBlockResult() rpc.GetBlockResult { + result := rpc.GetBlockResult{ + Blockhash: utils.NewHash(), + PreviousBlockhash: utils.NewHash(), + ParentSlot: 0, + Transactions: []rpc.TransactionWithMeta{}, + Signatures: []solana.Signature{}, + Rewards: []rpc.BlockReward{}, + BlockTime: nil, + BlockHeight: nil, + } + return result +} + +func TestHead_NewHead(t *testing.T) { + emptyBlockResult := configureBlockResult() + t.Parallel() + + tests := []struct { + slot int64 + block rpc.GetBlockResult + parent *headtracker.Head + id headtracker.ChainID + wantSlot int64 + }{ + // with no parent + {10, emptyBlockResult, nil, headtracker.Mainnet, 10}, + // with parent + {20, emptyBlockResult, + headtracker.NewHead(10, emptyBlockResult, nil, headtracker.Mainnet), + headtracker.Mainnet, 20}, + {30, emptyBlockResult, + headtracker.NewHead(20, emptyBlockResult, + headtracker.NewHead(10, emptyBlockResult, nil, headtracker.Mainnet), + headtracker.Mainnet), + headtracker.Mainnet, 30}, + } + + for _, test := range tests { + t.Run( + strconv.FormatInt(test.wantSlot, 10), // convert to base 10 + func(t *testing.T) { + head := headtracker.NewHead(test.slot, test.block, test.parent, test.id) + assert.Equal(t, test.wantSlot, head.Slot) + assert.Equal(t, test.block, head.Block) + assert.Equal(t, test.parent, head.Parent) + assert.Equal(t, test.id, head.ID) + }) + } +} + +func TestHead_ChainLength(t *testing.T) { + blockResult := configureBlockResult() + id := headtracker.Mainnet + + head := headtracker.NewHead(0, blockResult, headtracker.NewHead(0, blockResult, headtracker.NewHead(0, blockResult, nil, id), id), id) + + assert.Equal(t, uint32(3), head.ChainLength()) + + var head2 *headtracker.Head + assert.Equal(t, uint32(0), head2.ChainLength()) +} + +func TestHead_EarliestHeadInChain(t *testing.T) { + blockResult := configureBlockResult() + id := headtracker.Mainnet + + head := headtracker.NewHead(3, blockResult, + headtracker.NewHead(2, blockResult, + headtracker.NewHead(1, blockResult, nil, id), id), id) + + assert.Equal(t, int64(1), head.EarliestHeadInChain().BlockNumber()) +} + +func TestHead_GetParentHash(t *testing.T) { + blockResult := configureBlockResult() + id := headtracker.Mainnet + + head := headtracker.NewHead(3, blockResult, + headtracker.NewHead(2, blockResult, + headtracker.NewHead(1, blockResult, nil, id), id), id) + + assert.Equal(t, head.Parent.BlockHash(), head.GetParentHash()) +} + +func TestHead_GetParent(t *testing.T) { + blockResult := configureBlockResult() + id := headtracker.Mainnet + + head := headtracker.NewHead(3, blockResult, + headtracker.NewHead(2, blockResult, + headtracker.NewHead(1, blockResult, nil, id), id), id) + + assert.Equal(t, head.Parent, head.GetParent()) +} + +func TestHead_HasChainID(t *testing.T) { + t.Parallel() + blockResult := configureBlockResult() // Assuming this function creates a mock rpc.GetBlockResult + + tests := []struct { + name string + chainID headtracker.ChainID + want bool + }{ + { + "HasChainID returns true when ChainID is not 'unknown'", + headtracker.Devnet, // replace with correct initialization + true, + }, + { + "HasChainID returns false when ChainID is 'unknown'", + 99, + false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + head := headtracker.NewHead(0, blockResult, nil, test.chainID) + assert.Equal(t, test.want, head.HasChainID()) + }) + } + + t.Run("HasChainID returns false when Head is nil", func(t *testing.T) { + var head *headtracker.Head + assert.False(t, head.HasChainID()) + }) +} From f067f037fa59d1f77430328b3f4f25493f7f8308 Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Mon, 12 Jun 2023 11:32:07 -0700 Subject: [PATCH 06/47] added mocks --- pkg/common/headtracker/types/config.go | 11 ++ pkg/common/headtracker/types/mocks/head.go | 172 +++++++++++++++++++++ pkg/common/types/mocks/head.go | 130 ++++++++++++++++ pkg/monitoring/mocks/ChainReader.go | 47 ++++-- pkg/solana/config/config.go | 86 +++++++++-- pkg/solana/config/mocks/config.go | 72 ++++++++- 6 files changed, 489 insertions(+), 29 deletions(-) create mode 100644 pkg/common/headtracker/types/config.go create mode 100644 pkg/common/headtracker/types/mocks/head.go create mode 100644 pkg/common/types/mocks/head.go diff --git a/pkg/common/headtracker/types/config.go b/pkg/common/headtracker/types/config.go new file mode 100644 index 000000000..6c7e608c5 --- /dev/null +++ b/pkg/common/headtracker/types/config.go @@ -0,0 +1,11 @@ +package headtracker + +import "time" + +type Config interface { + BlockEmissionIdleWarningThreshold() time.Duration + FinalityDepth() uint32 + HeadTrackerHistoryDepth() uint32 + HeadTrackerMaxBufferSize() uint32 + HeadTrackerSamplingInterval() time.Duration +} diff --git a/pkg/common/headtracker/types/mocks/head.go b/pkg/common/headtracker/types/mocks/head.go new file mode 100644 index 000000000..979c3899c --- /dev/null +++ b/pkg/common/headtracker/types/mocks/head.go @@ -0,0 +1,172 @@ +// Code generated by mockery v2.28.1. DO NOT EDIT. + +package mocks + +import ( + types "github.com/smartcontractkit/chainlink-solana/pkg/common/types" + mock "github.com/stretchr/testify/mock" +) + +// Head is an autogenerated mock type for the Head type +type Head[BLOCK_HASH types.Hashable, CHAIN_ID types.ID] struct { + mock.Mock +} + +// BlockHash provides a mock function with given fields: +func (_m *Head[BLOCK_HASH, CHAIN_ID]) BlockHash() BLOCK_HASH { + ret := _m.Called() + + var r0 BLOCK_HASH + if rf, ok := ret.Get(0).(func() BLOCK_HASH); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(BLOCK_HASH) + } + + return r0 +} + +// BlockNumber provides a mock function with given fields: +func (_m *Head[BLOCK_HASH, CHAIN_ID]) BlockNumber() int64 { + ret := _m.Called() + + var r0 int64 + if rf, ok := ret.Get(0).(func() int64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(int64) + } + + return r0 +} + +// ChainID provides a mock function with given fields: +func (_m *Head[BLOCK_HASH, CHAIN_ID]) ChainID() CHAIN_ID { + ret := _m.Called() + + var r0 CHAIN_ID + if rf, ok := ret.Get(0).(func() CHAIN_ID); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(CHAIN_ID) + } + + return r0 +} + +// ChainLength provides a mock function with given fields: +func (_m *Head[BLOCK_HASH, CHAIN_ID]) ChainLength() uint32 { + ret := _m.Called() + + var r0 uint32 + if rf, ok := ret.Get(0).(func() uint32); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint32) + } + + return r0 +} + +// EarliestHeadInChain provides a mock function with given fields: +func (_m *Head[BLOCK_HASH, CHAIN_ID]) EarliestHeadInChain() types.Head[BLOCK_HASH] { + ret := _m.Called() + + var r0 types.Head[BLOCK_HASH] + if rf, ok := ret.Get(0).(func() types.Head[BLOCK_HASH]); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(types.Head[BLOCK_HASH]) + } + } + + return r0 +} + +// GetParent provides a mock function with given fields: +func (_m *Head[BLOCK_HASH, CHAIN_ID]) GetParent() types.Head[BLOCK_HASH] { + ret := _m.Called() + + var r0 types.Head[BLOCK_HASH] + if rf, ok := ret.Get(0).(func() types.Head[BLOCK_HASH]); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(types.Head[BLOCK_HASH]) + } + } + + return r0 +} + +// GetParentHash provides a mock function with given fields: +func (_m *Head[BLOCK_HASH, CHAIN_ID]) GetParentHash() BLOCK_HASH { + ret := _m.Called() + + var r0 BLOCK_HASH + if rf, ok := ret.Get(0).(func() BLOCK_HASH); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(BLOCK_HASH) + } + + return r0 +} + +// HasChainID provides a mock function with given fields: +func (_m *Head[BLOCK_HASH, CHAIN_ID]) HasChainID() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +// HashAtHeight provides a mock function with given fields: blockNum +func (_m *Head[BLOCK_HASH, CHAIN_ID]) HashAtHeight(blockNum int64) BLOCK_HASH { + ret := _m.Called(blockNum) + + var r0 BLOCK_HASH + if rf, ok := ret.Get(0).(func(int64) BLOCK_HASH); ok { + r0 = rf(blockNum) + } else { + r0 = ret.Get(0).(BLOCK_HASH) + } + + return r0 +} + +// IsValid provides a mock function with given fields: +func (_m *Head[BLOCK_HASH, CHAIN_ID]) IsValid() bool { + ret := _m.Called() + + var r0 bool + if rf, ok := ret.Get(0).(func() bool); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(bool) + } + + return r0 +} + +type mockConstructorTestingTNewHead interface { + mock.TestingT + Cleanup(func()) +} + +// NewHead creates a new instance of Head. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewHead[BLOCK_HASH types.Hashable, CHAIN_ID types.ID](t mockConstructorTestingTNewHead) *Head[BLOCK_HASH, CHAIN_ID] { + mock := &Head[BLOCK_HASH, CHAIN_ID]{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/common/types/mocks/head.go b/pkg/common/types/mocks/head.go new file mode 100644 index 000000000..0567bfca4 --- /dev/null +++ b/pkg/common/types/mocks/head.go @@ -0,0 +1,130 @@ +// Code generated by mockery v2.28.1. DO NOT EDIT. + +package mocks + +import ( + types "github.com/smartcontractkit/chainlink-solana/pkg/common/types" + mock "github.com/stretchr/testify/mock" +) + +// Head is an autogenerated mock type for the Head type +type Head[BLOCK_HASH types.Hashable] struct { + mock.Mock +} + +// BlockHash provides a mock function with given fields: +func (_m *Head[BLOCK_HASH]) BlockHash() BLOCK_HASH { + ret := _m.Called() + + var r0 BLOCK_HASH + if rf, ok := ret.Get(0).(func() BLOCK_HASH); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(BLOCK_HASH) + } + + return r0 +} + +// BlockNumber provides a mock function with given fields: +func (_m *Head[BLOCK_HASH]) BlockNumber() int64 { + ret := _m.Called() + + var r0 int64 + if rf, ok := ret.Get(0).(func() int64); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(int64) + } + + return r0 +} + +// ChainLength provides a mock function with given fields: +func (_m *Head[BLOCK_HASH]) ChainLength() uint32 { + ret := _m.Called() + + var r0 uint32 + if rf, ok := ret.Get(0).(func() uint32); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint32) + } + + return r0 +} + +// EarliestHeadInChain provides a mock function with given fields: +func (_m *Head[BLOCK_HASH]) EarliestHeadInChain() types.Head[BLOCK_HASH] { + ret := _m.Called() + + var r0 types.Head[BLOCK_HASH] + if rf, ok := ret.Get(0).(func() types.Head[BLOCK_HASH]); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(types.Head[BLOCK_HASH]) + } + } + + return r0 +} + +// GetParent provides a mock function with given fields: +func (_m *Head[BLOCK_HASH]) GetParent() types.Head[BLOCK_HASH] { + ret := _m.Called() + + var r0 types.Head[BLOCK_HASH] + if rf, ok := ret.Get(0).(func() types.Head[BLOCK_HASH]); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(types.Head[BLOCK_HASH]) + } + } + + return r0 +} + +// GetParentHash provides a mock function with given fields: +func (_m *Head[BLOCK_HASH]) GetParentHash() BLOCK_HASH { + ret := _m.Called() + + var r0 BLOCK_HASH + if rf, ok := ret.Get(0).(func() BLOCK_HASH); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(BLOCK_HASH) + } + + return r0 +} + +// HashAtHeight provides a mock function with given fields: blockNum +func (_m *Head[BLOCK_HASH]) HashAtHeight(blockNum int64) BLOCK_HASH { + ret := _m.Called(blockNum) + + var r0 BLOCK_HASH + if rf, ok := ret.Get(0).(func(int64) BLOCK_HASH); ok { + r0 = rf(blockNum) + } else { + r0 = ret.Get(0).(BLOCK_HASH) + } + + return r0 +} + +type mockConstructorTestingTNewHead interface { + mock.TestingT + Cleanup(func()) +} + +// NewHead creates a new instance of Head. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewHead[BLOCK_HASH types.Hashable](t mockConstructorTestingTNewHead) *Head[BLOCK_HASH] { + mock := &Head[BLOCK_HASH]{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/pkg/monitoring/mocks/ChainReader.go b/pkg/monitoring/mocks/ChainReader.go index d1d9498d4..7df58df51 100644 --- a/pkg/monitoring/mocks/ChainReader.go +++ b/pkg/monitoring/mocks/ChainReader.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.12.0. DO NOT EDIT. +// Code generated by mockery v2.28.1. DO NOT EDIT. package mocks @@ -12,8 +12,6 @@ import ( rpc "github.com/gagliardetto/solana-go/rpc" solana "github.com/gagliardetto/solana-go" - - testing "testing" ) // ChainReader is an autogenerated mock type for the ChainReader type @@ -26,6 +24,10 @@ func (_m *ChainReader) GetBalance(ctx context.Context, account solana.PublicKey, ret := _m.Called(ctx, account, commitment) var r0 *rpc.GetBalanceResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, rpc.CommitmentType) (*rpc.GetBalanceResult, error)); ok { + return rf(ctx, account, commitment) + } if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, rpc.CommitmentType) *rpc.GetBalanceResult); ok { r0 = rf(ctx, account, commitment) } else { @@ -34,7 +36,6 @@ func (_m *ChainReader) GetBalance(ctx context.Context, account solana.PublicKey, } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, solana.PublicKey, rpc.CommitmentType) error); ok { r1 = rf(ctx, account, commitment) } else { @@ -49,20 +50,23 @@ func (_m *ChainReader) GetLatestTransmission(ctx context.Context, account solana ret := _m.Called(ctx, account, commitment) var r0 pkgsolana.Answer + var r1 uint64 + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, rpc.CommitmentType) (pkgsolana.Answer, uint64, error)); ok { + return rf(ctx, account, commitment) + } if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, rpc.CommitmentType) pkgsolana.Answer); ok { r0 = rf(ctx, account, commitment) } else { r0 = ret.Get(0).(pkgsolana.Answer) } - var r1 uint64 if rf, ok := ret.Get(1).(func(context.Context, solana.PublicKey, rpc.CommitmentType) uint64); ok { r1 = rf(ctx, account, commitment) } else { r1 = ret.Get(1).(uint64) } - var r2 error if rf, ok := ret.Get(2).(func(context.Context, solana.PublicKey, rpc.CommitmentType) error); ok { r2 = rf(ctx, account, commitment) } else { @@ -77,6 +81,10 @@ func (_m *ChainReader) GetSignaturesForAddressWithOpts(ctx context.Context, acco ret := _m.Called(ctx, account, opts) var r0 []*rpc.TransactionSignature + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, *rpc.GetSignaturesForAddressOpts) ([]*rpc.TransactionSignature, error)); ok { + return rf(ctx, account, opts) + } if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, *rpc.GetSignaturesForAddressOpts) []*rpc.TransactionSignature); ok { r0 = rf(ctx, account, opts) } else { @@ -85,7 +93,6 @@ func (_m *ChainReader) GetSignaturesForAddressWithOpts(ctx context.Context, acco } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, solana.PublicKey, *rpc.GetSignaturesForAddressOpts) error); ok { r1 = rf(ctx, account, opts) } else { @@ -100,20 +107,23 @@ func (_m *ChainReader) GetState(ctx context.Context, account solana.PublicKey, c ret := _m.Called(ctx, account, commitment) var r0 pkgsolana.State + var r1 uint64 + var r2 error + if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, rpc.CommitmentType) (pkgsolana.State, uint64, error)); ok { + return rf(ctx, account, commitment) + } if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, rpc.CommitmentType) pkgsolana.State); ok { r0 = rf(ctx, account, commitment) } else { r0 = ret.Get(0).(pkgsolana.State) } - var r1 uint64 if rf, ok := ret.Get(1).(func(context.Context, solana.PublicKey, rpc.CommitmentType) uint64); ok { r1 = rf(ctx, account, commitment) } else { r1 = ret.Get(1).(uint64) } - var r2 error if rf, ok := ret.Get(2).(func(context.Context, solana.PublicKey, rpc.CommitmentType) error); ok { r2 = rf(ctx, account, commitment) } else { @@ -128,6 +138,10 @@ func (_m *ChainReader) GetTokenAccountBalance(ctx context.Context, account solan ret := _m.Called(ctx, account, commitment) var r0 *rpc.GetTokenAccountBalanceResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, rpc.CommitmentType) (*rpc.GetTokenAccountBalanceResult, error)); ok { + return rf(ctx, account, commitment) + } if rf, ok := ret.Get(0).(func(context.Context, solana.PublicKey, rpc.CommitmentType) *rpc.GetTokenAccountBalanceResult); ok { r0 = rf(ctx, account, commitment) } else { @@ -136,7 +150,6 @@ func (_m *ChainReader) GetTokenAccountBalance(ctx context.Context, account solan } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, solana.PublicKey, rpc.CommitmentType) error); ok { r1 = rf(ctx, account, commitment) } else { @@ -151,6 +164,10 @@ func (_m *ChainReader) GetTransaction(ctx context.Context, txSig solana.Signatur ret := _m.Called(ctx, txSig, opts) var r0 *rpc.GetTransactionResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, solana.Signature, *rpc.GetTransactionOpts) (*rpc.GetTransactionResult, error)); ok { + return rf(ctx, txSig, opts) + } if rf, ok := ret.Get(0).(func(context.Context, solana.Signature, *rpc.GetTransactionOpts) *rpc.GetTransactionResult); ok { r0 = rf(ctx, txSig, opts) } else { @@ -159,7 +176,6 @@ func (_m *ChainReader) GetTransaction(ctx context.Context, txSig solana.Signatur } } - var r1 error if rf, ok := ret.Get(1).(func(context.Context, solana.Signature, *rpc.GetTransactionOpts) error); ok { r1 = rf(ctx, txSig, opts) } else { @@ -169,8 +185,13 @@ func (_m *ChainReader) GetTransaction(ctx context.Context, txSig solana.Signatur return r0, r1 } -// NewChainReader creates a new instance of ChainReader. It also registers the testing.TB interface on the mock and a cleanup function to assert the mocks expectations. -func NewChainReader(t testing.TB) *ChainReader { +type mockConstructorTestingTNewChainReader interface { + mock.TestingT + Cleanup(func()) +} + +// NewChainReader creates a new instance of ChainReader. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewChainReader(t mockConstructorTestingTNewChainReader) *ChainReader { mock := &ChainReader{} mock.Mock.Test(t) diff --git a/pkg/solana/config/config.go b/pkg/solana/config/config.go index c680b09fc..c6bd78873 100644 --- a/pkg/solana/config/config.go +++ b/pkg/solana/config/config.go @@ -10,6 +10,7 @@ import ( relaycfg "github.com/smartcontractkit/chainlink-relay/pkg/config" "github.com/smartcontractkit/chainlink-relay/pkg/utils" + htrktypes "github.com/smartcontractkit/chainlink-solana/pkg/common/headtracker/types" "github.com/smartcontractkit/chainlink-solana/pkg/solana/db" "github.com/smartcontractkit/chainlink-solana/pkg/solana/logger" ) @@ -54,6 +55,13 @@ type Config interface { ComputeUnitPriceMin() uint64 ComputeUnitPriceDefault() uint64 FeeBumpPeriod() time.Duration + + // headtracker + BlockEmissionIdleWarningThreshold() time.Duration + FinalityDepth() uint32 + HeadTrackerHistoryDepth() uint32 + HeadTrackerMaxBufferSize() uint32 + HeadTrackerSamplingInterval() time.Duration } // opt: remove @@ -74,9 +82,16 @@ type configSet struct { ComputeUnitPriceMin uint64 ComputeUnitPriceDefault uint64 FeeBumpPeriod time.Duration + + BlockEmissionIdleWarningThreshold time.Duration + FinalityDepth uint32 + HeadTrackerHistoryDepth uint32 + HeadTrackerMaxBufferSize uint32 + HeadTrackerSamplingInterval time.Duration } var _ Config = (*config)(nil) +var _ htrktypes.Config = (*config)(nil) // Deprecated type config struct { @@ -242,22 +257,47 @@ func (c *config) FeeBumpPeriod() time.Duration { return c.defaults.FeeBumpPeriod } +func (c *config) BlockEmissionIdleWarningThreshold() time.Duration { + return c.defaults.BlockEmissionIdleWarningThreshold +} + +func (c *config) FinalityDepth() uint32 { + return c.defaults.FinalityDepth +} + +func (c *config) HeadTrackerHistoryDepth() uint32 { + return c.defaults.HeadTrackerHistoryDepth +} + +func (c *config) HeadTrackerMaxBufferSize() uint32 { + return c.defaults.HeadTrackerMaxBufferSize +} + +func (c *config) HeadTrackerSamplingInterval() time.Duration { + return c.defaults.HeadTrackerSamplingInterval +} + type Chain struct { - BalancePollPeriod *utils.Duration - ConfirmPollPeriod *utils.Duration - OCR2CachePollPeriod *utils.Duration - OCR2CacheTTL *utils.Duration - TxTimeout *utils.Duration - TxRetryTimeout *utils.Duration - TxConfirmTimeout *utils.Duration - SkipPreflight *bool - Commitment *string - MaxRetries *int64 - FeeEstimatorMode *string - ComputeUnitPriceMax *uint64 - ComputeUnitPriceMin *uint64 - ComputeUnitPriceDefault *uint64 - FeeBumpPeriod *utils.Duration + BalancePollPeriod *utils.Duration + ConfirmPollPeriod *utils.Duration + OCR2CachePollPeriod *utils.Duration + OCR2CacheTTL *utils.Duration + TxTimeout *utils.Duration + TxRetryTimeout *utils.Duration + TxConfirmTimeout *utils.Duration + SkipPreflight *bool + Commitment *string + MaxRetries *int64 + FeeEstimatorMode *string + ComputeUnitPriceMax *uint64 + ComputeUnitPriceMin *uint64 + ComputeUnitPriceDefault *uint64 + FeeBumpPeriod *utils.Duration + BlockEmissionIdleWarningThreshold *utils.Duration + FinalityDepth *uint32 + HeadTrackerHistoryDepth *uint32 + HeadTrackerMaxBufferSize *uint32 + HeadTrackerSamplingInterval *utils.Duration } func (c *Chain) SetDefaults() { @@ -307,6 +347,22 @@ func (c *Chain) SetDefaults() { if c.FeeBumpPeriod == nil { c.FeeBumpPeriod = utils.MustNewDuration(defaultConfigSet.FeeBumpPeriod) } + if c.BlockEmissionIdleWarningThreshold == nil { + c.BlockEmissionIdleWarningThreshold = utils.MustNewDuration(defaultConfigSet.BlockEmissionIdleWarningThreshold) + } + if c.FinalityDepth == nil { + c.FinalityDepth = &defaultConfigSet.FinalityDepth + } + if c.HeadTrackerHistoryDepth == nil { + c.HeadTrackerHistoryDepth = &defaultConfigSet.HeadTrackerHistoryDepth + } + if c.HeadTrackerMaxBufferSize == nil { + c.HeadTrackerMaxBufferSize = &defaultConfigSet.HeadTrackerMaxBufferSize + } + if c.HeadTrackerSamplingInterval == nil { + c.HeadTrackerSamplingInterval = utils.MustNewDuration(defaultConfigSet.HeadTrackerSamplingInterval) + } + return } diff --git a/pkg/solana/config/mocks/config.go b/pkg/solana/config/mocks/config.go index f9f35c3a5..26c1a1988 100644 --- a/pkg/solana/config/mocks/config.go +++ b/pkg/solana/config/mocks/config.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.20.0. DO NOT EDIT. +// Code generated by mockery v2.28.1. DO NOT EDIT. package mocks @@ -28,6 +28,20 @@ func (_m *Config) BalancePollPeriod() time.Duration { return r0 } +// BlockEmissionIdleWarningThreshold provides a mock function with given fields: +func (_m *Config) BlockEmissionIdleWarningThreshold() time.Duration { + ret := _m.Called() + + var r0 time.Duration + if rf, ok := ret.Get(0).(func() time.Duration); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(time.Duration) + } + + return r0 +} + // Commitment provides a mock function with given fields: func (_m *Config) Commitment() rpc.CommitmentType { ret := _m.Called() @@ -126,6 +140,62 @@ func (_m *Config) FeeEstimatorMode() string { return r0 } +// FinalityDepth provides a mock function with given fields: +func (_m *Config) FinalityDepth() uint32 { + ret := _m.Called() + + var r0 uint32 + if rf, ok := ret.Get(0).(func() uint32); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint32) + } + + return r0 +} + +// HeadTrackerHistoryDepth provides a mock function with given fields: +func (_m *Config) HeadTrackerHistoryDepth() uint32 { + ret := _m.Called() + + var r0 uint32 + if rf, ok := ret.Get(0).(func() uint32); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint32) + } + + return r0 +} + +// HeadTrackerMaxBufferSize provides a mock function with given fields: +func (_m *Config) HeadTrackerMaxBufferSize() uint32 { + ret := _m.Called() + + var r0 uint32 + if rf, ok := ret.Get(0).(func() uint32); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(uint32) + } + + return r0 +} + +// HeadTrackerSamplingInterval provides a mock function with given fields: +func (_m *Config) HeadTrackerSamplingInterval() time.Duration { + ret := _m.Called() + + var r0 time.Duration + if rf, ok := ret.Get(0).(func() time.Duration); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(time.Duration) + } + + return r0 +} + // MaxRetries provides a mock function with given fields: func (_m *Config) MaxRetries() *uint { ret := _m.Called() From 4ee8456144a97eafea8f9ca34ef965439de0b2fc Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Mon, 12 Jun 2023 16:28:48 -0700 Subject: [PATCH 07/47] Updated Solana Headtracker Config --- pkg/solana/config/config.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/solana/config/config.go b/pkg/solana/config/config.go index c6bd78873..683fbd13a 100644 --- a/pkg/solana/config/config.go +++ b/pkg/solana/config/config.go @@ -34,6 +34,13 @@ var defaultConfigSet = configSet{ ComputeUnitPriceMin: 0, ComputeUnitPriceDefault: 0, FeeBumpPeriod: 3 * time.Second, + + // headtracker + BlockEmissionIdleWarningThreshold: 30 * time.Second, + FinalityDepth: 50, + HeadTrackerHistoryDepth: 100, + HeadTrackerMaxBufferSize: 3, + HeadTrackerSamplingInterval: 1 * time.Second, } //go:generate mockery --name Config --output ./mocks/ --case=underscore --filename config.go From 23a5366a6131ecfaed97d9715b26ae670f6235e5 Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Tue, 13 Jun 2023 12:20:01 -0700 Subject: [PATCH 08/47] Added subscription and client interfaces --- pkg/common/headtracker/types/client.go | 18 ++++++++++++++++++ pkg/common/types/subscription.go | 17 +++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 pkg/common/headtracker/types/client.go create mode 100644 pkg/common/types/subscription.go diff --git a/pkg/common/headtracker/types/client.go b/pkg/common/headtracker/types/client.go new file mode 100644 index 000000000..bfce51967 --- /dev/null +++ b/pkg/common/headtracker/types/client.go @@ -0,0 +1,18 @@ +package headtracker + +import ( + "context" + "math/big" + + "github.com/smartcontractkit/chainlink-solana/pkg/common/types" +) + +type Client[H types.Head[BLOCK_HASH], S types.Subscription, ID types.ID, BLOCK_HASH types.Hashable] interface { + HeadByNumber(ctx context.Context, number *big.Int) (head H, err error) + HeadByHash(ctx context.Context, hash BLOCK_HASH) (head H, err error) + // ConfiguredChainID returns the chain ID that the node is configured to connect to + ConfiguredChainID() (id ID) + // SubscribeNewHead is the method in which the client receives new Head. + // It can be implemented differently for each chain i.e websocket, polling, etc + SubscribeNewHead(ctx context.Context, ch chan<- H) (S, error) +} diff --git a/pkg/common/types/subscription.go b/pkg/common/types/subscription.go new file mode 100644 index 000000000..99247107b --- /dev/null +++ b/pkg/common/types/subscription.go @@ -0,0 +1,17 @@ +package types + +// Subscription represents an event subscription where events are +// delivered on a data channel. +// This is a generic interface for Subscription to represent used by clients. + +//go:generate mockery --quiet --name Subscription --output ./mocks/ --case=underscore +type Subscription interface { + // Unsubscribe cancels the sending of events to the data channel + // and closes the error channel. + Unsubscribe() + // Err returns the subscription error channel. The error channel receives + // a value if there is an issue with the subscription (e.g. the network connection + // delivering the events has been closed). Only one value will ever be sent. + // The error channel is closed by Unsubscribe. + Err() <-chan error +} From 2da21a2dca614e6492ad8d062293239590ec9b79 Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Wed, 14 Jun 2023 19:48:26 -0700 Subject: [PATCH 09/47] Updated config to include polling interval --- pkg/solana/config/config.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pkg/solana/config/config.go b/pkg/solana/config/config.go index 683fbd13a..96f55b6c1 100644 --- a/pkg/solana/config/config.go +++ b/pkg/solana/config/config.go @@ -41,6 +41,7 @@ var defaultConfigSet = configSet{ HeadTrackerHistoryDepth: 100, HeadTrackerMaxBufferSize: 3, HeadTrackerSamplingInterval: 1 * time.Second, + PollingInterval: 2 * time.Second, } //go:generate mockery --name Config --output ./mocks/ --case=underscore --filename config.go @@ -95,6 +96,7 @@ type configSet struct { HeadTrackerHistoryDepth uint32 HeadTrackerMaxBufferSize uint32 HeadTrackerSamplingInterval time.Duration + PollingInterval time.Duration } var _ Config = (*config)(nil) @@ -284,6 +286,10 @@ func (c *config) HeadTrackerSamplingInterval() time.Duration { return c.defaults.HeadTrackerSamplingInterval } +func (c *config) PollingInterval() time.Duration { + return c.defaults.PollingInterval +} + type Chain struct { BalancePollPeriod *utils.Duration ConfirmPollPeriod *utils.Duration @@ -305,6 +311,7 @@ type Chain struct { HeadTrackerHistoryDepth *uint32 HeadTrackerMaxBufferSize *uint32 HeadTrackerSamplingInterval *utils.Duration + PollingInterval *utils.Duration } func (c *Chain) SetDefaults() { @@ -369,6 +376,9 @@ func (c *Chain) SetDefaults() { if c.HeadTrackerSamplingInterval == nil { c.HeadTrackerSamplingInterval = utils.MustNewDuration(defaultConfigSet.HeadTrackerSamplingInterval) } + if c.PollingInterval == nil { + c.PollingInterval = utils.MustNewDuration(defaultConfigSet.PollingInterval) + } return } From ed7192a226fd601433d69d7e6a8af5ec2b31b334 Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Thu, 15 Jun 2023 10:31:50 -0700 Subject: [PATCH 10/47] removed HeadByHash from client interface - not needed --- pkg/common/headtracker/types/client.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/common/headtracker/types/client.go b/pkg/common/headtracker/types/client.go index bfce51967..640014362 100644 --- a/pkg/common/headtracker/types/client.go +++ b/pkg/common/headtracker/types/client.go @@ -9,7 +9,6 @@ import ( type Client[H types.Head[BLOCK_HASH], S types.Subscription, ID types.ID, BLOCK_HASH types.Hashable] interface { HeadByNumber(ctx context.Context, number *big.Int) (head H, err error) - HeadByHash(ctx context.Context, hash BLOCK_HASH) (head H, err error) // ConfiguredChainID returns the chain ID that the node is configured to connect to ConfiguredChainID() (id ID) // SubscribeNewHead is the method in which the client receives new Head. From fc2586e66fb8ba8f4153ef38aa698602c4b9f38b Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Thu, 15 Jun 2023 10:32:22 -0700 Subject: [PATCH 11/47] Added subscription concrete type + tests --- pkg/solana/client/subscription.go | 34 +++++++ pkg/solana/client/subscription_test.go | 120 +++++++++++++++++++++++++ pkg/solana/client/test_helpers.go | 6 ++ 3 files changed, 160 insertions(+) create mode 100644 pkg/solana/client/subscription.go create mode 100644 pkg/solana/client/subscription_test.go diff --git a/pkg/solana/client/subscription.go b/pkg/solana/client/subscription.go new file mode 100644 index 000000000..d3446b069 --- /dev/null +++ b/pkg/solana/client/subscription.go @@ -0,0 +1,34 @@ +package client + +import ( + "context" + + commontypes "github.com/smartcontractkit/chainlink-solana/pkg/common/types" +) + +var _ commontypes.Subscription = (*Subscription)(nil) + +type Subscription struct { + ctx context.Context + cancel context.CancelFunc + errChan chan error + client *Client +} + +func NewSubscription(client *Client, ctx context.Context) *Subscription { + ctx, cancel := context.WithCancel(ctx) + return &Subscription{ + ctx: ctx, + cancel: cancel, + client: client, + errChan: make(chan error), + } +} + +func (s *Subscription) Unsubscribe() { + s.cancel() +} + +func (s *Subscription) Err() <-chan error { + return s.errChan +} diff --git a/pkg/solana/client/subscription_test.go b/pkg/solana/client/subscription_test.go new file mode 100644 index 000000000..91e5013b7 --- /dev/null +++ b/pkg/solana/client/subscription_test.go @@ -0,0 +1,120 @@ +package client + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/pkg/errors" + "github.com/smartcontractkit/chainlink-relay/pkg/logger" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/db" + "github.com/stretchr/testify/assert" + "github.com/test-go/testify/require" +) + +func initClient(t *testing.T) (*Client, context.Context) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + url := DummyUrl(t) + requestTimeout := 5 * time.Second + lggr := logger.Test(t) + cfg := config.NewConfig(db.ChainCfg{}, lggr) + + c, err := NewClient(url, cfg, requestTimeout, lggr) + require.NoError(t, err) + + return c, ctx +} + +func TestSubscription_New(t *testing.T) { + c, ctx := initClient(t) + + t.Run("happy path", func(t *testing.T) { + subscription := NewSubscription(c, ctx) + assert.NotNil(t, subscription) + assert.NotNil(t, subscription.ctx) + assert.NotNil(t, subscription.cancel) + assert.NotNil(t, subscription.client) + assert.NotNil(t, subscription.errChan) + }) + + // Edge case: pass a nil client + t.Run("nil client", func(t *testing.T) { + subscription := NewSubscription(nil, ctx) + assert.NotNil(t, subscription) + assert.Nil(t, subscription.client) + }) +} + +func TestSubscription_Unsubscribe(t *testing.T) { + c, ctx := initClient(t) + + t.Run("happy path", func(t *testing.T) { + subscription := NewSubscription(c, ctx) + + // The Done channel should not be closed yet + select { + case <-subscription.ctx.Done(): + t.Fatal("Expected context to not be done yet") + default: + } + + subscription.Unsubscribe() + + select { + // Success + case <-subscription.ctx.Done(): + fmt.Println("Context is done") + return + case <-time.After(100 * time.Millisecond): + t.Fatal("Expected context to be done") + } + }) + + // Edge case: unsubscribe twice + t.Run("double unsubscribe", func(t *testing.T) { + subscription := NewSubscription(c, ctx) + subscription.Unsubscribe() + subscription.Unsubscribe() // Shouldn't panic or error + }) +} + +func TestSubscription_Err(t *testing.T) { + c, ctx := initClient(t) + t.Run("happy path", func(t *testing.T) { + subscription := NewSubscription(c, ctx) + + errCh := subscription.Err() + assert.NotNil(t, errCh) + + // Send an error to the error channel + expectedError := errors.New("mock error") + go func() { + subscription.errChan <- expectedError + }() + + select { + case err := <-errCh: + assert.ErrorIs(t, err, expectedError) + case <-time.After(100 * time.Millisecond): + t.Fatal("Expected error was not received") + } + }) + + // Edge case: no error sent + t.Run("no error", func(t *testing.T) { + subscription := NewSubscription(c, ctx) + errCh := subscription.Err() + assert.NotNil(t, errCh) + + select { + case err := <-errCh: + t.Fatalf("Did not expect error, got %v", err) + case <-time.After(100 * time.Millisecond): + // Success: no error received as expected + } + }) +} diff --git a/pkg/solana/client/test_helpers.go b/pkg/solana/client/test_helpers.go index e763607df..1150a51a9 100644 --- a/pkg/solana/client/test_helpers.go +++ b/pkg/solana/client/test_helpers.go @@ -68,3 +68,9 @@ func FundTestAccounts(t *testing.T, keys []solana.PublicKey, url string) { require.NoError(t, err) } } + +func DummyUrl(t *testing.T) string { + port := utils.MustRandomPort(t) + url := "http://127.0.0.1:" + port + return url +} From 7fd9084bd3c979cdaa7011be7f6aaae9f1c143a5 Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Thu, 15 Jun 2023 10:33:38 -0700 Subject: [PATCH 12/47] Updated helper for client --- pkg/solana/config/config.go | 1 + pkg/solana/headtracker/types/chain.go | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/pkg/solana/config/config.go b/pkg/solana/config/config.go index 96f55b6c1..47559d930 100644 --- a/pkg/solana/config/config.go +++ b/pkg/solana/config/config.go @@ -70,6 +70,7 @@ type Config interface { HeadTrackerHistoryDepth() uint32 HeadTrackerMaxBufferSize() uint32 HeadTrackerSamplingInterval() time.Duration + PollingInterval() time.Duration } // opt: remove diff --git a/pkg/solana/headtracker/types/chain.go b/pkg/solana/headtracker/types/chain.go index 6ff8035df..fc56bbb10 100644 --- a/pkg/solana/headtracker/types/chain.go +++ b/pkg/solana/headtracker/types/chain.go @@ -21,6 +21,21 @@ func (id ChainID) String() string { case Localnet: return "localnet" default: - return "unknown" + return "localnet" + } +} + +func StringToChainID(id string) ChainID { + switch id { + case "mainnet": + return Mainnet + case "testnet": + return Testnet + case "devnet": + return Devnet + case "localnet": + return Localnet + default: + return Localnet } } From f2cd914b89e83d61484b2560f6e79aac0e55b0ac Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Thu, 15 Jun 2023 12:24:27 -0700 Subject: [PATCH 13/47] Added Client + SubscribeNewHead test for polling --- pkg/solana/client/client.go | 113 +++++++++++++++++++++++++++++- pkg/solana/client/client_test.go | 116 +++++++++++++++++++++++++++++++ 2 files changed, 228 insertions(+), 1 deletion(-) diff --git a/pkg/solana/client/client.go b/pkg/solana/client/client.go index 766959f3a..bdbdcd2d7 100644 --- a/pkg/solana/client/client.go +++ b/pkg/solana/client/client.go @@ -3,14 +3,18 @@ package client import ( "context" "fmt" + "math/big" "time" "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" "github.com/pkg/errors" + "golang.org/x/sync/singleflight" + + htrktypes "github.com/smartcontractkit/chainlink-solana/pkg/common/headtracker/types" "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" + headtracker "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" "github.com/smartcontractkit/chainlink-solana/pkg/solana/logger" - "golang.org/x/sync/singleflight" ) const ( @@ -47,6 +51,8 @@ type Writer interface { var _ ReaderWriter = (*Client)(nil) +var _ htrktypes.Client[*headtracker.Head, *Subscription, headtracker.ChainID, headtracker.Hash] = (*Client)(nil) + type Client struct { rpc *rpc.Client skipPreflight bool // to enable or disable preflight checks @@ -55,6 +61,7 @@ type Client struct { txTimeout time.Duration contextDuration time.Duration log logger.Logger + pollingInterval time.Duration // provides a duplicate function call suppression mechanism requestGroup *singleflight.Group @@ -67,6 +74,7 @@ func NewClient(endpoint string, cfg config.Config, requestTimeout time.Duration, commitment: cfg.Commitment(), maxRetries: cfg.MaxRetries(), txTimeout: cfg.TxTimeout(), + pollingInterval: cfg.PollingInterval(), //TODO: Add this in the config in core contextDuration: requestTimeout, log: log, requestGroup: &singleflight.Group{}, @@ -139,6 +147,16 @@ func (c *Client) ChainID() (string, error) { return network, nil } +// TODO: requires refactor. Do we want to store chainID? how do we want to cache ChainID? +func (c *Client) ConfiguredChainID() headtracker.ChainID { + chainID, err := c.ChainID() + if err != nil { + c.log.Warnf("unable to determine configured chain ID: %v", err) + return headtracker.ChainID(headtracker.Localnet) + } + return headtracker.StringToChainID(chainID) +} + func (c *Client) GetFeeForMessage(msg string) (uint64, error) { // msg is base58 encoded data @@ -209,3 +227,96 @@ func (c *Client) SendTx(ctx context.Context, tx *solana.Transaction) (solana.Sig return c.rpc.SendTransactionWithOpts(ctx, tx, opts) } + +func (c *Client) HeadByNumber(ctx context.Context, number *big.Int) (*headtracker.Head, error) { + ctx, cancel := context.WithTimeout(ctx, c.contextDuration) + defer cancel() + block, err := c.getBlock(ctx, number.Uint64()) + if err != nil { + return nil, err + } + if block == nil { + return nil, errors.New("invalid block in HeadByNumber") + } + chainId := c.ConfiguredChainID() + // TODO: check if parent head will be linked in the headsaver + head := &headtracker.Head{ + Slot: number.Int64(), + Block: *block, + ID: chainId, + } + return head, nil +} + +// SubscribeNewHead polls the RPC endpoint for new blocks. +func (c *Client) SubscribeNewHead(ctx context.Context, ch chan<- *headtracker.Head) (*Subscription, error) { + subscription := NewSubscription(c, ctx) + + go func() { + ticker := time.NewTicker(c.pollingInterval) + + for { + select { + case <-ctx.Done(): + ticker.Stop() + return + case <-ticker.C: + // Fetch latest block + block, slot, err := c.getLatestBlock(ctx) + // TODO: Improve error handling + if err != nil { + subscription.errChan <- err + continue + } + + // Create a new Head object and send to channel + head := &headtracker.Head{ + Slot: int64(slot), + Block: *block, + ID: c.ConfiguredChainID(), + } + ch <- head + } + } + }() + + return subscription, nil +} + +// getLatestBlock queries the latest slot and returns the block. +func (c *Client) getLatestBlock(ctx context.Context) (block *rpc.GetBlockResult, slot uint64, err error) { + ctx, cancel := context.WithTimeout(ctx, c.contextDuration) + defer cancel() + + slot, err = c.getLatestSlot(ctx) + if err != nil { + return nil, slot, err + } + + block, err = c.getBlock(ctx, slot) + if err != nil { + return nil, slot, err + } + + return block, slot, nil +} + +func (c *Client) getBlock(ctx context.Context, number uint64) (out *rpc.GetBlockResult, err error) { + ctx, cancel := context.WithTimeout(ctx, c.contextDuration) + defer cancel() + + v, err, _ := c.requestGroup.Do("GetBlock", func() (interface{}, error) { + return c.rpc.GetBlock(ctx, number) + }) + return v.(*rpc.GetBlockResult), err +} + +func (c *Client) getLatestSlot(ctx context.Context) (uint64, error) { + ctx, cancel := context.WithTimeout(ctx, c.contextDuration) + defer cancel() + + v, err, _ := c.requestGroup.Do("GetSlot", func() (interface{}, error) { + return c.rpc.GetSlot(ctx, c.commitment) + }) + return v.(uint64), err +} diff --git a/pkg/solana/client/client_test.go b/pkg/solana/client/client_test.go index cc378f4fb..20da11b19 100644 --- a/pkg/solana/client/client_test.go +++ b/pkg/solana/client/client_test.go @@ -2,6 +2,7 @@ package client import ( "context" + "encoding/json" "fmt" "math/rand" "net/http" @@ -13,6 +14,7 @@ import ( "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/programs/system" "github.com/gagliardetto/solana-go/rpc" + "github.com/gagliardetto/solana-go/rpc/jsonrpc" "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -21,6 +23,7 @@ import ( "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" "github.com/smartcontractkit/chainlink-solana/pkg/solana/db" + headtracker "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" ) func TestClient_Reader_Integration(t *testing.T) { @@ -276,3 +279,116 @@ func TestClient_SendTxDuplicates_Integration(t *testing.T) { assert.NoError(t, err) assert.Equal(t, initBal-endBal, uint64(5_000)) } + +func TestClient_SubscribeNewHead(t *testing.T) { + requestCounter := 0 + var slot uint64 + + slotResponses := []uint64{ + 428, + 199750878, + 199750877, + } + blockResponses := map[int]string{ + 428: `{ + "blockHeight": 428, + "blockTime": null, + "blockhash": "3Eq21vXNB5s86c62bVuUfTeaMif1N2kUqRPBmGRJhyTA", + "parentSlot": 429, + "previousBlockhash": "mfcyqEXB3DnHXki6KjjmZck6YjmZLvpAByy2fj4nh6B", + "transactions": [] + }`, + 199750878: `{ + "blockHeight": 199750878, + "blockTime": 1686852740, + "blockhash": "7XmsC2yHyHWhF1WQGgHEZGZc9jyvyaY3V7eMhwB2ovEY", + "parentSlot": 199750877, + "previousBlockhash": "5k8ayeVNWk2dXaMmMNYvgwaB6rQwizNqpyRKRScczP34", + "transactions": [] + }`, + 199750877: `{ + "blockHeight": 199750877, + "blockTime": 1686852680, + "blockhash": "5k8ayeVNWk2dXaMmMNYvgwaB6rQwizNqpyRKRScczP34", + "parentSlot": 199750876, + "previousBlockhash": "CLDZ8BDLFtgqk3j4ksEX5HMjws5R9mMDu71X7UvNE5i8", + "transactions": [] + }`, + } + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + requestTimeout := 5 * time.Second + lggr := logger.Test(t) + cfg := config.NewConfig(db.ChainCfg{}, lggr) + + // Mock Server for GetBlock requests. + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var rpcReq jsonrpc.RPCRequest + err := json.NewDecoder(r.Body).Decode(&rpcReq) + require.NoError(t, err) + + switch rpcReq.Method { + case "getSlot": + // respond with slot number for the block + slot = slotResponses[requestCounter] + out := fmt.Sprintf(`{"jsonrpc":"2.0","result":%d,"id":1}`, slot) + _, err := w.Write([]byte(out)) + require.NoError(t, err) + + case "getBlock": + // respond with block info + out := fmt.Sprintf(`{"jsonrpc":"2.0","result":%s,"id":1}`, blockResponses[int(slot)]) + requestCounter++ + _, err := w.Write([]byte(out)) + require.NoError(t, err) + case "getGenesisHash": + out := fmt.Sprintf(`{"jsonrpc":"2.0","result":"%s","id":1}`, MainnetGenesisHash) + _, err := w.Write([]byte(out)) + require.NoError(t, err) + + default: + // Print method for debugging. + t.Logf("RPc: %s", rpcReq.JSONRPC) + fmt.Println("Unrecognized method:", rpcReq.Method) + + // respond with error + http.Error(w, "Method not supported", http.StatusMethodNotAllowed) + } + })) + defer mockServer.Close() + + c, err := NewClient(mockServer.URL, cfg, requestTimeout, lggr) + require.NoError(t, err) + + headCh := make(chan *headtracker.Head) + + subscription, err := c.SubscribeNewHead(ctx, headCh) + require.NoError(t, err) + + // Consume from the head channel and make assertions. + for i := 0; i < len(slotResponses); i++ { + select { + case head := <-headCh: + slotResponse := slotResponses[i] + + require.Equal(t, slotResponse, *head.Block.BlockHeight) + + case <-time.After(5 * time.Second): + t.Fatalf("Did not receive new head in time") + } + } + + // Make sure there are no more heads. + select { + case head := <-headCh: + t.Fatalf("Received unexpected head: %+v", head) + + case <-time.After(time.Second): + // No more heads, as expected. + } + + // Clean up the subscription. + subscription.Unsubscribe() +} From 784b827da3b8432d19fe3f6ed8291ed8e341ee1e Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Thu, 15 Jun 2023 14:10:45 -0700 Subject: [PATCH 14/47] go mod tidy --- go.mod | 1 + 1 file changed, 1 insertion(+) diff --git a/go.mod b/go.mod index 6ccebf82d..02e337ed3 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230525161650-dce1bc58b504 github.com/smartcontractkit/libocr v0.0.0-20230525150148-a75f6e244bb3 github.com/stretchr/testify v1.8.2 + github.com/test-go/testify v1.1.4 go.uber.org/multierr v1.8.0 go.uber.org/zap v1.24.0 golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 From fda3ff0cedf5881f2e3b61a04f3557ee99f93ceb Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Thu, 15 Jun 2023 14:12:50 -0700 Subject: [PATCH 15/47] updated mock file --- pkg/solana/config/mocks/config.go | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/pkg/solana/config/mocks/config.go b/pkg/solana/config/mocks/config.go index 26c1a1988..c765b7d44 100644 --- a/pkg/solana/config/mocks/config.go +++ b/pkg/solana/config/mocks/config.go @@ -240,6 +240,20 @@ func (_m *Config) OCR2CacheTTL() time.Duration { return r0 } +// PollingInterval provides a mock function with given fields: +func (_m *Config) PollingInterval() time.Duration { + ret := _m.Called() + + var r0 time.Duration + if rf, ok := ret.Get(0).(func() time.Duration); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(time.Duration) + } + + return r0 +} + // SkipPreflight provides a mock function with given fields: func (_m *Config) SkipPreflight() bool { ret := _m.Called() From e395c8047c444ea0a4461ace118f6de4f2d4ef95 Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Thu, 15 Jun 2023 15:53:15 -0700 Subject: [PATCH 16/47] Added test for Client HeadByNumber --- pkg/solana/client/client.go | 63 +++++++++++++---- pkg/solana/client/client_test.go | 112 ++++++++++++++++++++++++++++++ pkg/solana/client/test_helpers.go | 14 +++- 3 files changed, 176 insertions(+), 13 deletions(-) diff --git a/pkg/solana/client/client.go b/pkg/solana/client/client.go index bdbdcd2d7..aa6113daa 100644 --- a/pkg/solana/client/client.go +++ b/pkg/solana/client/client.go @@ -231,7 +231,7 @@ func (c *Client) SendTx(ctx context.Context, tx *solana.Transaction) (solana.Sig func (c *Client) HeadByNumber(ctx context.Context, number *big.Int) (*headtracker.Head, error) { ctx, cancel := context.WithTimeout(ctx, c.contextDuration) defer cancel() - block, err := c.getBlock(ctx, number.Uint64()) + block, err := c.GetBlock(ctx, number.Uint64()) if err != nil { return nil, err } @@ -239,7 +239,7 @@ func (c *Client) HeadByNumber(ctx context.Context, number *big.Int) (*headtracke return nil, errors.New("invalid block in HeadByNumber") } chainId := c.ConfiguredChainID() - // TODO: check if parent head will be linked in the headsaver + // TODO: check if parent head can be linked in the headsaver head := &headtracker.Head{ Slot: number.Int64(), Block: *block, @@ -288,12 +288,12 @@ func (c *Client) getLatestBlock(ctx context.Context) (block *rpc.GetBlockResult, ctx, cancel := context.WithTimeout(ctx, c.contextDuration) defer cancel() - slot, err = c.getLatestSlot(ctx) + slot, err = c.GetLatestSlot(ctx) if err != nil { - return nil, slot, err + return nil, slot, errors.Wrap(err, "error in GetLatestSlot") } - block, err = c.getBlock(ctx, slot) + block, err = c.GetBlock(ctx, slot) if err != nil { return nil, slot, err } @@ -301,22 +301,61 @@ func (c *Client) getLatestBlock(ctx context.Context) (block *rpc.GetBlockResult, return block, slot, nil } -func (c *Client) getBlock(ctx context.Context, number uint64) (out *rpc.GetBlockResult, err error) { +func (c *Client) GetBlock(ctx context.Context, slot uint64) (out *rpc.GetBlockResult, err error) { ctx, cancel := context.WithTimeout(ctx, c.contextDuration) defer cancel() - v, err, _ := c.requestGroup.Do("GetBlock", func() (interface{}, error) { - return c.rpc.GetBlock(ctx, number) + res, err, _ := c.requestGroup.Do("GetBlock", func() (interface{}, error) { + return c.rpc.GetBlock(ctx, slot) }) - return v.(*rpc.GetBlockResult), err + // Check for errors and nil pointers + if err != nil { + return nil, errors.Wrap(err, "error in GetBlock") + } + if res == nil { + return nil, errors.New("nil pointer in GetBlock") + } + + return res.(*rpc.GetBlockResult), err } -func (c *Client) getLatestSlot(ctx context.Context) (uint64, error) { +// TODO: confirm commitment for RPC again. Public RPC nodes cannot handle CommitmentProcessed due to requests being too frequent. +func (c *Client) GetLatestSlot(ctx context.Context) (uint64, error) { ctx, cancel := context.WithTimeout(ctx, c.contextDuration) defer cancel() - v, err, _ := c.requestGroup.Do("GetSlot", func() (interface{}, error) { + res, err, _ := c.requestGroup.Do("GetSlot", func() (interface{}, error) { return c.rpc.GetSlot(ctx, c.commitment) }) - return v.(uint64), err + + if err != nil { + return 0, errors.Wrap(err, "error in GetSlot") + } + + if res == nil { + return 0, errors.New("nil pointer in GetSlot") + } + + return res.(uint64), err +} + +func (c *Client) GetBlocks(ctx context.Context, startSlot, endSlot uint64) (blocks []uint64, err error) { + ctx, cancel := context.WithTimeout(ctx, c.contextDuration) + defer cancel() + + res, err, _ := c.requestGroup.Do("GetBlocks", func() (interface{}, error) { + return c.rpc.GetBlocks(ctx, startSlot, &endSlot, c.commitment) + }) + + if err != nil { + return nil, errors.Wrap(err, "error in GetBlocks") + } + if res == nil { + return nil, errors.New("nil pointer in GetBlocks") + } + + blocks = make([]uint64, len(res.(rpc.BlocksResult))) + copy(blocks, res.(rpc.BlocksResult)) + + return blocks, err } diff --git a/pkg/solana/client/client_test.go b/pkg/solana/client/client_test.go index 20da11b19..22b1e585f 100644 --- a/pkg/solana/client/client_test.go +++ b/pkg/solana/client/client_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "math/big" "math/rand" "net/http" "net/http/httptest" @@ -392,3 +393,114 @@ func TestClient_SubscribeNewHead(t *testing.T) { // Clean up the subscription. subscription.Unsubscribe() } + +func TestClient_HeadByNumber(t *testing.T) { + url := SetupLocalSolNode(t) + + requestTimeout := 5 * time.Second + lggr := logger.Test(t) + cfg := config.NewConfig(db.ChainCfg{}, lggr) + c, err := NewClient(url, cfg, requestTimeout, lggr) + assert.NoError(t, err) + + t.Run("happy case, valid block number", func(t *testing.T) { + ctx := context.Background() + // Get most recent height + slotHeight, err := c.SlotHeight() + assert.NoError(t, err) + + // Get List of blocks + blockNumbers, err := c.GetBlocks(ctx, 0, slotHeight) + assert.NoError(t, err) + assert.NotEmpty(t, blockNumbers) + + // Use the first block for our test + firstBlockNumber := blockNumbers[0] + block, err := c.HeadByNumber(ctx, big.NewInt(int64(firstBlockNumber))) + + // Make sure no error is returned. + assert.NoError(t, err) + assert.Equal(t, int64(firstBlockNumber), block.Slot) + }) + + t.Run("negative block number", func(t *testing.T) { + // Call HeadByNumber with zero or a negative number. + ctx := context.Background() + + block, err := c.HeadByNumber(ctx, big.NewInt(-1)) + assert.Error(t, err) // expecting error + assert.Nil(t, block) // expecting no block + }) + + t.Run("block does not exist", func(t *testing.T) { + ctx := context.Background() + + block, err := c.HeadByNumber(ctx, big.NewInt(99999999999)) + assert.Error(t, err) + assert.Nil(t, block) + }) + +} + +func TestClient_GetBlock(t *testing.T) { + requestTimeout := 5 * time.Second + lggr := logger.Test(t) + cfg := config.NewConfig(db.ChainCfg{}, lggr) + + blockHeight := uint64(199750875) + blockHash, err := solana.HashFromBase58("FDJBEXcTgD3Z17BdVM2K6o2j35JHJRXUf7NkHK5w7AbD") + if err != nil { + t.Fatal(err) + } + + previousBlockHash, err := solana.HashFromBase58("3rQRaHFL8uC8jMERbXeTJjhgSomtiuEPVAGYtjrickxr") + if err != nil { + t.Fatal(err) + } + + blockTime := solana.UnixTimeSeconds(1626110123) + + block := &rpc.GetBlockResult{ + BlockHeight: &blockHeight, + Blockhash: blockHash, + ParentSlot: uint64(199750874), + PreviousBlockhash: previousBlockHash, + Rewards: []rpc.BlockReward{}, + Transactions: []rpc.TransactionWithMeta{}, + BlockTime: &blockTime, + } + + // Mock Server + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + out := fmt.Sprintf(`{"jsonrpc":"2.0","result":%s,"id":1}`, MustJSON(block)) + _, err := w.Write([]byte(out)) + require.NoError(t, err) + })) + defer mockServer.Close() + + c, err := NewClient(mockServer.URL, cfg, requestTimeout, lggr) + require.NoError(t, err) + + ctx := context.Background() + out, err := c.GetBlock(ctx, uint64(100)) + // print out for debugging + t.Logf("out: %+v", *out.BlockHeight) + t.Logf("block: %+v", *block.BlockHeight) + assert.NoError(t, err) + assert.Equal(t, block, out) +} + +func TestClient_GetLatestSlot(t *testing.T) { + requestTimeout := 5 * time.Second + lggr := logger.Test(t) + cfg := config.NewConfig(db.ChainCfg{}, lggr) + url := SetupLocalSolNode(t) + + c, err := NewClient(url, cfg, requestTimeout, lggr) + require.NoError(t, err) + + ctx := context.Background() + slot, err := c.GetLatestSlot(ctx) + assert.NoError(t, err) + assert.Greater(t, slot, uint64(0)) +} diff --git a/pkg/solana/client/test_helpers.go b/pkg/solana/client/test_helpers.go index 1150a51a9..ab4c98ce9 100644 --- a/pkg/solana/client/test_helpers.go +++ b/pkg/solana/client/test_helpers.go @@ -3,6 +3,8 @@ package client import ( "bytes" "context" + "encoding/json" + "log" "os/exec" "testing" "time" @@ -45,7 +47,8 @@ func SetupLocalSolNode(t *testing.T) string { client := rpc.New(url) out, err := client.GetHealth(context.Background()) if err != nil || out != rpc.HealthOk { - t.Logf("API server not ready yet (attempt %d)\n", i+1) + t.Logf("API server not ready yet (attempt %d)\nCmd output: %s\nCmd error: %s\n", + i+1, stdOut.String(), stdErr.String()) continue } ready = true @@ -74,3 +77,12 @@ func DummyUrl(t *testing.T) string { url := "http://127.0.0.1:" + port return url } + +// MustJSON marshals an object into a JSON string and panics if there's an error. +func MustJSON(obj interface{}) string { + jsonBytes, err := json.Marshal(obj) + if err != nil { + log.Fatalf("Error marshalling object: %v", err) + } + return string(jsonBytes) +} From 8ee9449b240fd38fe666af17c1a165e11a932816 Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Thu, 15 Jun 2023 16:03:04 -0700 Subject: [PATCH 17/47] cleanup --- pkg/solana/client/client.go | 5 ++--- pkg/solana/client/client_test.go | 14 +------------- pkg/solana/client/subscription.go | 2 +- pkg/solana/client/subscription_test.go | 12 ++++++------ 4 files changed, 10 insertions(+), 23 deletions(-) diff --git a/pkg/solana/client/client.go b/pkg/solana/client/client.go index aa6113daa..cda3b54fc 100644 --- a/pkg/solana/client/client.go +++ b/pkg/solana/client/client.go @@ -250,7 +250,7 @@ func (c *Client) HeadByNumber(ctx context.Context, number *big.Int) (*headtracke // SubscribeNewHead polls the RPC endpoint for new blocks. func (c *Client) SubscribeNewHead(ctx context.Context, ch chan<- *headtracker.Head) (*Subscription, error) { - subscription := NewSubscription(c, ctx) + subscription := NewSubscription(ctx, c) go func() { ticker := time.NewTicker(c.pollingInterval) @@ -261,7 +261,6 @@ func (c *Client) SubscribeNewHead(ctx context.Context, ch chan<- *headtracker.He ticker.Stop() return case <-ticker.C: - // Fetch latest block block, slot, err := c.getLatestBlock(ctx) // TODO: Improve error handling if err != nil { @@ -308,7 +307,7 @@ func (c *Client) GetBlock(ctx context.Context, slot uint64) (out *rpc.GetBlockRe res, err, _ := c.requestGroup.Do("GetBlock", func() (interface{}, error) { return c.rpc.GetBlock(ctx, slot) }) - // Check for errors and nil pointers + if err != nil { return nil, errors.Wrap(err, "error in GetBlock") } diff --git a/pkg/solana/client/client_test.go b/pkg/solana/client/client_test.go index 22b1e585f..94ad18ead 100644 --- a/pkg/solana/client/client_test.go +++ b/pkg/solana/client/client_test.go @@ -332,14 +332,12 @@ func TestClient_SubscribeNewHead(t *testing.T) { switch rpcReq.Method { case "getSlot": - // respond with slot number for the block slot = slotResponses[requestCounter] out := fmt.Sprintf(`{"jsonrpc":"2.0","result":%d,"id":1}`, slot) _, err := w.Write([]byte(out)) require.NoError(t, err) case "getBlock": - // respond with block info out := fmt.Sprintf(`{"jsonrpc":"2.0","result":%s,"id":1}`, blockResponses[int(slot)]) requestCounter++ _, err := w.Write([]byte(out)) @@ -350,10 +348,6 @@ func TestClient_SubscribeNewHead(t *testing.T) { require.NoError(t, err) default: - // Print method for debugging. - t.Logf("RPc: %s", rpcReq.JSONRPC) - fmt.Println("Unrecognized method:", rpcReq.Method) - // respond with error http.Error(w, "Method not supported", http.StatusMethodNotAllowed) } @@ -390,7 +384,6 @@ func TestClient_SubscribeNewHead(t *testing.T) { // No more heads, as expected. } - // Clean up the subscription. subscription.Unsubscribe() } @@ -418,13 +411,11 @@ func TestClient_HeadByNumber(t *testing.T) { firstBlockNumber := blockNumbers[0] block, err := c.HeadByNumber(ctx, big.NewInt(int64(firstBlockNumber))) - // Make sure no error is returned. assert.NoError(t, err) assert.Equal(t, int64(firstBlockNumber), block.Slot) }) t.Run("negative block number", func(t *testing.T) { - // Call HeadByNumber with zero or a negative number. ctx := context.Background() block, err := c.HeadByNumber(ctx, big.NewInt(-1)) @@ -470,7 +461,6 @@ func TestClient_GetBlock(t *testing.T) { BlockTime: &blockTime, } - // Mock Server mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { out := fmt.Sprintf(`{"jsonrpc":"2.0","result":%s,"id":1}`, MustJSON(block)) _, err := w.Write([]byte(out)) @@ -483,9 +473,7 @@ func TestClient_GetBlock(t *testing.T) { ctx := context.Background() out, err := c.GetBlock(ctx, uint64(100)) - // print out for debugging - t.Logf("out: %+v", *out.BlockHeight) - t.Logf("block: %+v", *block.BlockHeight) + assert.NoError(t, err) assert.Equal(t, block, out) } diff --git a/pkg/solana/client/subscription.go b/pkg/solana/client/subscription.go index d3446b069..34059c953 100644 --- a/pkg/solana/client/subscription.go +++ b/pkg/solana/client/subscription.go @@ -15,7 +15,7 @@ type Subscription struct { client *Client } -func NewSubscription(client *Client, ctx context.Context) *Subscription { +func NewSubscription(ctx context.Context, client *Client) *Subscription { ctx, cancel := context.WithCancel(ctx) return &Subscription{ ctx: ctx, diff --git a/pkg/solana/client/subscription_test.go b/pkg/solana/client/subscription_test.go index 91e5013b7..1478cb50f 100644 --- a/pkg/solana/client/subscription_test.go +++ b/pkg/solana/client/subscription_test.go @@ -33,7 +33,7 @@ func TestSubscription_New(t *testing.T) { c, ctx := initClient(t) t.Run("happy path", func(t *testing.T) { - subscription := NewSubscription(c, ctx) + subscription := NewSubscription(ctx, c) assert.NotNil(t, subscription) assert.NotNil(t, subscription.ctx) assert.NotNil(t, subscription.cancel) @@ -43,7 +43,7 @@ func TestSubscription_New(t *testing.T) { // Edge case: pass a nil client t.Run("nil client", func(t *testing.T) { - subscription := NewSubscription(nil, ctx) + subscription := NewSubscription(ctx, nil) assert.NotNil(t, subscription) assert.Nil(t, subscription.client) }) @@ -53,7 +53,7 @@ func TestSubscription_Unsubscribe(t *testing.T) { c, ctx := initClient(t) t.Run("happy path", func(t *testing.T) { - subscription := NewSubscription(c, ctx) + subscription := NewSubscription(ctx, c) // The Done channel should not be closed yet select { @@ -76,7 +76,7 @@ func TestSubscription_Unsubscribe(t *testing.T) { // Edge case: unsubscribe twice t.Run("double unsubscribe", func(t *testing.T) { - subscription := NewSubscription(c, ctx) + subscription := NewSubscription(ctx, c) subscription.Unsubscribe() subscription.Unsubscribe() // Shouldn't panic or error }) @@ -85,7 +85,7 @@ func TestSubscription_Unsubscribe(t *testing.T) { func TestSubscription_Err(t *testing.T) { c, ctx := initClient(t) t.Run("happy path", func(t *testing.T) { - subscription := NewSubscription(c, ctx) + subscription := NewSubscription(ctx, c) errCh := subscription.Err() assert.NotNil(t, errCh) @@ -106,7 +106,7 @@ func TestSubscription_Err(t *testing.T) { // Edge case: no error sent t.Run("no error", func(t *testing.T) { - subscription := NewSubscription(c, ctx) + subscription := NewSubscription(ctx, c) errCh := subscription.Err() assert.NotNil(t, errCh) From c0734ae0e3eb613c6261e06e83767444bfdd5e80 Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Fri, 16 Jun 2023 17:08:49 -0700 Subject: [PATCH 18/47] moved interfaces over for headtracker services --- pkg/common/config/presenter.go | 14 + pkg/common/headtracker/head_broadcaster.go | 163 ++++++++++ pkg/common/headtracker/head_listener.go | 218 +++++++++++++ pkg/common/headtracker/head_tracker.go | 359 +++++++++++++++++++++ pkg/common/types/head_tracker.go | 68 ++++ 5 files changed, 822 insertions(+) create mode 100644 pkg/common/config/presenter.go create mode 100644 pkg/common/headtracker/head_broadcaster.go create mode 100644 pkg/common/headtracker/head_listener.go create mode 100644 pkg/common/headtracker/head_tracker.go create mode 100644 pkg/common/types/head_tracker.go diff --git a/pkg/common/config/presenter.go b/pkg/common/config/presenter.go new file mode 100644 index 000000000..f4da66fd1 --- /dev/null +++ b/pkg/common/config/presenter.go @@ -0,0 +1,14 @@ +package config + +import ( + "fmt" + "math/big" + + "golang.org/x/exp/constraints" +) + +// FriendlyNumber returns a string printing the integer or big.Int in both +// decimal and hexadecimal formats. +func FriendlyNumber[N constraints.Integer | *big.Int](n N) string { + return fmt.Sprintf("#%[1]v (0x%[1]x)", n) +} diff --git a/pkg/common/headtracker/head_broadcaster.go b/pkg/common/headtracker/head_broadcaster.go new file mode 100644 index 000000000..31584bb48 --- /dev/null +++ b/pkg/common/headtracker/head_broadcaster.go @@ -0,0 +1,163 @@ +package headtracker + +import ( + "context" + "fmt" + "reflect" + "sync" + "time" + + "github.com/smartcontractkit/chainlink-relay/pkg/logger" + "github.com/smartcontractkit/chainlink-relay/pkg/utils" + commontypes "github.com/smartcontractkit/chainlink-solana/pkg/common/types" +) + +const TrackableCallbackTimeout = 2 * time.Second + +type callbackSet[H commontypes.Head[BLOCK_HASH], BLOCK_HASH commontypes.Hashable] map[int]commontypes.HeadTrackable[H, BLOCK_HASH] + +func (set callbackSet[H, BLOCK_HASH]) values() []commontypes.HeadTrackable[H, BLOCK_HASH] { + var values []commontypes.HeadTrackable[H, BLOCK_HASH] + for _, callback := range set { + values = append(values, callback) + } + return values +} + +type HeadBroadcaster[H commontypes.Head[BLOCK_HASH], BLOCK_HASH commontypes.Hashable] struct { + logger logger.Logger + callbacks callbackSet[H, BLOCK_HASH] + mailbox *utils.Mailbox[H] + mutex *sync.Mutex + chClose utils.StopChan + wgDone sync.WaitGroup + utils.StartStopOnce + latest H + lastCallbackID int +} + +// NewHeadBroadcaster creates a new HeadBroadcaster +func NewHeadBroadcaster[ + H commontypes.Head[BLOCK_HASH], + BLOCK_HASH commontypes.Hashable, +]( + lggr logger.Logger, +) *HeadBroadcaster[H, BLOCK_HASH] { + return &HeadBroadcaster[H, BLOCK_HASH]{ + logger: logger.Named(lggr, "HeadBroadcaster"), + callbacks: make(callbackSet[H, BLOCK_HASH]), + mailbox: utils.NewSingleMailbox[H](), + mutex: &sync.Mutex{}, + chClose: make(chan struct{}), + wgDone: sync.WaitGroup{}, + StartStopOnce: utils.StartStopOnce{}, + } +} + +func (hb *HeadBroadcaster[H, BLOCK_HASH]) Start(context.Context) error { + return hb.StartOnce("HeadBroadcaster", func() error { + hb.wgDone.Add(1) + go hb.run() + return nil + }) +} + +func (hb *HeadBroadcaster[H, BLOCK_HASH]) Close() error { + return hb.StopOnce("HeadBroadcaster", func() error { + hb.mutex.Lock() + // clear all callbacks + hb.callbacks = make(callbackSet[H, BLOCK_HASH]) + hb.mutex.Unlock() + + close(hb.chClose) + hb.wgDone.Wait() + return nil + }) +} + +func (hb *HeadBroadcaster[H, BLOCK_HASH]) Name() string { + return hb.logger.Name() +} + +func (hb *HeadBroadcaster[H, BLOCK_HASH]) HealthReport() map[string]error { + return map[string]error{hb.Name(): hb.StartStopOnce.Healthy()} +} + +func (hb *HeadBroadcaster[H, BLOCK_HASH]) BroadcastNewLongestChain(head H) { + hb.mailbox.Deliver(head) +} + +// Subscribe subscribes to OnNewLongestChain and Connect until HeadBroadcaster is closed, +// or unsubscribe callback is called explicitly +func (hb *HeadBroadcaster[H, BLOCK_HASH]) Subscribe(callback commontypes.HeadTrackable[H, BLOCK_HASH]) (currentLongestChain H, unsubscribe func()) { + hb.mutex.Lock() + defer hb.mutex.Unlock() + + currentLongestChain = hb.latest + + hb.lastCallbackID++ + callbackID := hb.lastCallbackID + hb.callbacks[callbackID] = callback + unsubscribe = func() { + hb.mutex.Lock() + defer hb.mutex.Unlock() + delete(hb.callbacks, callbackID) + } + + return +} + +func (hb *HeadBroadcaster[H, BLOCK_HASH]) run() { + defer hb.wgDone.Done() + + for { + select { + case <-hb.chClose: + return + case <-hb.mailbox.Notify(): + hb.executeCallbacks() + } + } +} + +// DEV: the head relayer makes no promises about head delivery! Subscribing +// Jobs should expect to the relayer to skip heads if there is a large number of listeners +// and all callbacks cannot be completed in the allotted time. +func (hb *HeadBroadcaster[H, BLOCK_HASH]) executeCallbacks() { + head, exists := hb.mailbox.Retrieve() + if !exists { + hb.logger.Info("No head to retrieve. It might have been skipped") + return + } + + hb.mutex.Lock() + callbacks := hb.callbacks.values() + hb.latest = head + hb.mutex.Unlock() + + hb.logger.Debugw("Initiating callbacks", + "headNum", head.BlockNumber(), + "numCallbacks", len(callbacks), + ) + + wg := sync.WaitGroup{} + wg.Add(len(callbacks)) + + ctx, cancel := hb.chClose.NewCtx() + defer cancel() + + for _, callback := range callbacks { + go func(trackable commontypes.HeadTrackable[H, BLOCK_HASH]) { + defer wg.Done() + start := time.Now() + cctx, cancel := context.WithTimeout(ctx, TrackableCallbackTimeout) + defer cancel() + trackable.OnNewLongestChain(cctx, head) + elapsed := time.Since(start) + hb.logger.Debugw(fmt.Sprintf("Finished callback in %s", elapsed), + "callbackType", reflect.TypeOf(trackable), "blockNumber", head.BlockNumber(), "time", elapsed) + }(callback) + } + + wg.Wait() +} diff --git a/pkg/common/headtracker/head_listener.go b/pkg/common/headtracker/head_listener.go new file mode 100644 index 000000000..0030afadc --- /dev/null +++ b/pkg/common/headtracker/head_listener.go @@ -0,0 +1,218 @@ +package headtracker + +import ( + "context" + "sync/atomic" + "time" + + "github.com/pkg/errors" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + + "github.com/smartcontractkit/chainlink-relay/pkg/logger" + "github.com/smartcontractkit/chainlink-relay/pkg/utils" + htrktypes "github.com/smartcontractkit/chainlink-solana/pkg/common/headtracker/types" + "github.com/smartcontractkit/chainlink-solana/pkg/common/types" +) + +var ( + promNumHeadsReceived = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "head_tracker_heads_received", + Help: "The total number of heads seen", + }, []string{"ChainID"}) + promEthConnectionErrors = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "head_tracker_connection_errors", + Help: "The total number of node connection errors", + }, []string{"ChainID"}) +) + +type HeadListener[ + HTH htrktypes.Head[BLOCK_HASH, ID], + S types.Subscription, + ID types.ID, + BLOCK_HASH types.Hashable, +] struct { + config htrktypes.Config + client htrktypes.Client[HTH, S, ID, BLOCK_HASH] + logger logger.Logger + chStop utils.StopChan + chHeaders chan HTH + headSubscription types.Subscription + connected atomic.Bool + receivingHeads atomic.Bool +} + +func NewHeadListener[ + HTH htrktypes.Head[BLOCK_HASH, ID], + S types.Subscription, + ID types.ID, + BLOCK_HASH types.Hashable, + CLIENT htrktypes.Client[HTH, S, ID, BLOCK_HASH], +]( + lggr logger.Logger, + client CLIENT, + config htrktypes.Config, + chStop chan struct{}, +) *HeadListener[HTH, S, ID, BLOCK_HASH] { + return &HeadListener[HTH, S, ID, BLOCK_HASH]{ + config: config, + client: client, + logger: logger.Named(lggr, "HeadListener"), + chStop: chStop, + } +} + +func (hl *HeadListener[HTH, S, ID, BLOCK_HASH]) Name() string { + return hl.logger.Name() +} + +func (hl *HeadListener[HTH, S, ID, BLOCK_HASH]) ListenForNewHeads(handleNewHead types.NewHeadHandler[HTH, BLOCK_HASH], done func()) { + defer done() + defer hl.unsubscribe() + + ctx, cancel := hl.chStop.NewCtx() + defer cancel() + + for { + if !hl.subscribe(ctx) { + break + } + err := hl.receiveHeaders(ctx, handleNewHead) + if ctx.Err() != nil { + break + } else if err != nil { + hl.logger.Errorw("Error in new head subscription, unsubscribed", "err", err) + continue + } else { + break + } + } +} + +func (hl *HeadListener[HTH, S, ID, BLOCK_HASH]) ReceivingHeads() bool { + return hl.receivingHeads.Load() +} + +func (hl *HeadListener[HTH, S, ID, BLOCK_HASH]) Connected() bool { + return hl.connected.Load() +} + +func (hl *HeadListener[HTH, S, ID, BLOCK_HASH]) HealthReport() map[string]error { + var err error + if !hl.ReceivingHeads() { + err = errors.New("Listener is not receiving heads") + } + if !hl.Connected() { + err = errors.New("Listener is not connected") + } + return map[string]error{hl.Name(): err} +} + +func (hl *HeadListener[HTH, S, ID, BLOCK_HASH]) receiveHeaders(ctx context.Context, handleNewHead types.NewHeadHandler[HTH, BLOCK_HASH]) error { + var noHeadsAlarmC <-chan time.Time + var noHeadsAlarmT *time.Ticker + noHeadsAlarmDuration := hl.config.BlockEmissionIdleWarningThreshold() + if noHeadsAlarmDuration > 0 { + noHeadsAlarmT = time.NewTicker(noHeadsAlarmDuration) + noHeadsAlarmC = noHeadsAlarmT.C + } + + for { + select { + case <-hl.chStop: + return nil + + case blockHeader, open := <-hl.chHeaders: + chainId := hl.client.ConfiguredChainID() + if noHeadsAlarmT != nil { + // We've received a head, reset the no heads alarm + noHeadsAlarmT.Stop() + noHeadsAlarmT = time.NewTicker(noHeadsAlarmDuration) + noHeadsAlarmC = noHeadsAlarmT.C + } + hl.receivingHeads.Store(true) + if !open { + return errors.New("head listener: chHeaders prematurely closed") + } + if !blockHeader.IsValid() { + hl.logger.Error("got nil block header") + continue + } + + // Compare the chain ID of the block header to the chain ID of the client + if !blockHeader.HasChainID() || blockHeader.ChainID().String() != chainId.String() { + hl.logger.Panicf("head listener for %s received block header for %s", chainId, blockHeader.ChainID()) + } + promNumHeadsReceived.WithLabelValues(chainId.String()).Inc() + + err := handleNewHead(ctx, blockHeader) + if ctx.Err() != nil { + return nil + } else if err != nil { + return err + } + + case err, open := <-hl.headSubscription.Err(): + // err can be nil, because of using chainIDSubForwarder + if !open || err == nil { + return errors.New("head listener: subscription Err channel prematurely closed") + } + return err + + case <-noHeadsAlarmC: + // We haven't received a head on the channel for a long time, log a warning + hl.logger.Warnf("have not received a head for %v", noHeadsAlarmDuration) + hl.receivingHeads.Store(false) + } + } +} + +func (hl *HeadListener[HTH, S, ID, BLOCK_HASH]) subscribe(ctx context.Context) bool { + subscribeRetryBackoff := utils.NewRedialBackoff() + + chainId := hl.client.ConfiguredChainID() + + for { + hl.unsubscribe() + + hl.logger.Debugf("Subscribing to new heads on chain %s", chainId.String()) + + select { + case <-hl.chStop: + return false + + case <-time.After(subscribeRetryBackoff.Duration()): + err := hl.subscribeToHead(ctx) + if err != nil { + promEthConnectionErrors.WithLabelValues(chainId.String()).Inc() + hl.logger.Warnw("Failed to subscribe to heads on chain", "chainID", chainId.String(), "err", err) + } else { + hl.logger.Debugf("Subscribed to heads on chain %s", chainId.String()) + return true + } + } + } +} + +func (hl *HeadListener[HTH, S, ID, BLOCK_HASH]) subscribeToHead(ctx context.Context) error { + hl.chHeaders = make(chan HTH) + + var err error + hl.headSubscription, err = hl.client.SubscribeNewHead(ctx, hl.chHeaders) + if err != nil { + close(hl.chHeaders) + return errors.Wrap(err, "Client#SubscribeNewHead") + } + + hl.connected.Store(true) + + return nil +} + +func (hl *HeadListener[HTH, S, ID, BLOCK_HASH]) unsubscribe() { + if hl.headSubscription != nil { + hl.connected.Store(false) + hl.headSubscription.Unsubscribe() + hl.headSubscription = nil + } +} diff --git a/pkg/common/headtracker/head_tracker.go b/pkg/common/headtracker/head_tracker.go new file mode 100644 index 000000000..4d7126fb1 --- /dev/null +++ b/pkg/common/headtracker/head_tracker.go @@ -0,0 +1,359 @@ +package headtracker + +import ( + "context" + "fmt" + "math/big" + "sync" + "time" + + "github.com/pkg/errors" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "golang.org/x/exp/maps" + + "github.com/smartcontractkit/chainlink-relay/pkg/logger" + "github.com/smartcontractkit/chainlink-relay/pkg/utils" + "github.com/smartcontractkit/chainlink-solana/pkg/common/config" + htrktypes "github.com/smartcontractkit/chainlink-solana/pkg/common/headtracker/types" + "github.com/smartcontractkit/chainlink-solana/pkg/common/types" +) + +var ( + promCurrentHead = promauto.NewGaugeVec(prometheus.GaugeOpts{ + Name: "head_tracker_current_head", + Help: "The highest seen head number", + }, []string{"evmChainID"}) + + promOldHead = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "head_tracker_very_old_head", + Help: "Counter is incremented every time we get a head that is much lower than the highest seen head ('much lower' is defined as a block that is EVM.FinalityDepth or greater below the highest seen head)", + }, []string{"evmChainID"}) +) + +// HeadsBufferSize - The buffer is used when heads sampling is disabled, to ensure the callback is run for every head +const HeadsBufferSize = 10 + +type HeadTracker[ + HTH htrktypes.Head[BLOCK_HASH, ID], + S types.Subscription, + ID types.ID, + BLOCK_HASH types.Hashable, +] struct { + log logger.Logger + headBroadcaster types.HeadBroadcaster[HTH, BLOCK_HASH] + headSaver types.HeadSaver[HTH, BLOCK_HASH] + mailMon *utils.MailboxMonitor + client htrktypes.Client[HTH, S, ID, BLOCK_HASH] + chainID ID + config htrktypes.Config + + backfillMB *utils.Mailbox[HTH] + broadcastMB *utils.Mailbox[HTH] + headListener types.HeadListener[HTH, BLOCK_HASH] + chStop utils.StopChan + wgDone sync.WaitGroup + utils.StartStopOnce + getNilHead func() HTH +} + +// NewHeadTracker instantiates a new HeadTracker using HeadSaver to persist new block numbers. +func NewHeadTracker[ + HTH htrktypes.Head[BLOCK_HASH, ID], + S types.Subscription, + ID types.ID, + BLOCK_HASH types.Hashable, +]( + lggr logger.Logger, + client htrktypes.Client[HTH, S, ID, BLOCK_HASH], + config htrktypes.Config, + headBroadcaster types.HeadBroadcaster[HTH, BLOCK_HASH], + headSaver types.HeadSaver[HTH, BLOCK_HASH], + mailMon *utils.MailboxMonitor, + getNilHead func() HTH, +) types.HeadTracker[HTH, BLOCK_HASH] { + chStop := make(chan struct{}) + lggr = logger.Named(lggr, "HeadTracker") + return &HeadTracker[HTH, S, ID, BLOCK_HASH]{ + headBroadcaster: headBroadcaster, + client: client, + chainID: client.ConfiguredChainID(), + config: config, + log: lggr, + backfillMB: utils.NewSingleMailbox[HTH](), + broadcastMB: utils.NewMailbox[HTH](HeadsBufferSize), + chStop: chStop, + headListener: NewHeadListener[HTH, S, ID, BLOCK_HASH](lggr, client, config, chStop), + headSaver: headSaver, + mailMon: mailMon, + getNilHead: getNilHead, + } +} + +// Start starts HeadTracker service. +func (ht *HeadTracker[HTH, S, ID, BLOCK_HASH]) Start(ctx context.Context) error { + return ht.StartOnce("HeadTracker", func() error { + ht.log.Debugw("Starting HeadTracker", "chainID", ht.chainID) + latestChain, err := ht.headSaver.Load(ctx) + if err != nil { + return err + } + if latestChain.IsValid() { + ht.log.Debugw( + fmt.Sprintf("HeadTracker: Tracking logs from last block %v with hash %s", config.FriendlyNumber(latestChain.BlockNumber()), latestChain.BlockHash()), + "blockNumber", latestChain.BlockNumber(), + "blockHash", latestChain.BlockHash(), + ) + } + + // NOTE: Always try to start the head tracker off with whatever the + // latest head is, without waiting for the subscription to send us one. + // + // In some cases the subscription will send us the most recent head + // anyway when we connect (but we should not rely on this because it is + // not specced). If it happens this is fine, and the head will be + // ignored as a duplicate. + initialHead, err := ht.getInitialHead(ctx) + if err != nil { + if errors.Is(err, ctx.Err()) { + return nil + } + ht.log.Errorw("Error getting initial head", "err", err) + } else if initialHead.IsValid() { + if err := ht.handleNewHead(ctx, initialHead); err != nil { + return errors.Wrap(err, "error handling initial head") + } + } else { + ht.log.Debug("Got nil initial head") + } + + ht.wgDone.Add(3) + go ht.headListener.ListenForNewHeads(ht.handleNewHead, ht.wgDone.Done) + go ht.backfillLoop() + go ht.broadcastLoop() + + ht.mailMon.Monitor(ht.broadcastMB, "HeadTracker", "Broadcast", ht.chainID.String()) + + return nil + }) +} + +// Close stops HeadTracker service. +func (ht *HeadTracker[HTH, S, ID, BLOCK_HASH]) Close() error { + return ht.StopOnce("HeadTracker", func() error { + close(ht.chStop) + ht.wgDone.Wait() + return ht.broadcastMB.Close() + }) +} + +func (ht *HeadTracker[HTH, S, ID, BLOCK_HASH]) Name() string { + return ht.log.Name() +} + +func (ht *HeadTracker[HTH, S, ID, BLOCK_HASH]) HealthReport() map[string]error { + report := map[string]error{ + ht.Name(): ht.StartStopOnce.Healthy(), + } + maps.Copy(report, ht.headListener.HealthReport()) + return report +} + +func (ht *HeadTracker[HTH, S, ID, BLOCK_HASH]) Backfill(ctx context.Context, headWithChain HTH, depth uint) (err error) { + if uint(headWithChain.ChainLength()) >= depth { + return nil + } + + baseHeight := headWithChain.BlockNumber() - int64(depth-1) + if baseHeight < 0 { + baseHeight = 0 + } + + return ht.backfill(ctx, headWithChain.EarliestHeadInChain(), baseHeight) +} + +func (ht *HeadTracker[HTH, S, ID, BLOCK_HASH]) LatestChain() HTH { + return ht.headSaver.LatestChain() +} + +func (ht *HeadTracker[HTH, S, ID, BLOCK_HASH]) getInitialHead(ctx context.Context) (HTH, error) { + head, err := ht.client.HeadByNumber(ctx, nil) + if err != nil { + return ht.getNilHead(), errors.Wrap(err, "failed to fetch initial head") + } + loggerFields := []interface{}{"head", head} + if head.IsValid() { + loggerFields = append(loggerFields, "blockNumber", head.BlockNumber(), "blockHash", head.BlockHash()) + } + ht.log.Debugw("Got initial head", loggerFields...) + return head, nil +} + +func (ht *HeadTracker[HTH, S, ID, BLOCK_HASH]) handleNewHead(ctx context.Context, head HTH) error { + prevHead := ht.headSaver.LatestChain() + + ht.log.Debugw(fmt.Sprintf("Received new head %v", config.FriendlyNumber(head.BlockNumber())), + "blockHeight", head.BlockNumber(), + "blockHash", head.BlockHash(), + "parentHeadHash", head.GetParentHash(), + ) + + err := ht.headSaver.Save(ctx, head) + if ctx.Err() != nil { + return nil + } else if err != nil { + return errors.Wrapf(err, "failed to save head: %#v", head) + } + + if !prevHead.IsValid() || head.BlockNumber() > prevHead.BlockNumber() { + promCurrentHead.WithLabelValues(ht.chainID.String()).Set(float64(head.BlockNumber())) + + headWithChain := ht.headSaver.Chain(head.BlockHash()) + if !headWithChain.IsValid() { + return errors.Errorf("HeadTracker#handleNewHighestHead headWithChain was unexpectedly nil") + } + ht.backfillMB.Deliver(headWithChain) + ht.broadcastMB.Deliver(headWithChain) + } else if head.BlockNumber() == prevHead.BlockNumber() { + if head.BlockHash() != prevHead.BlockHash() { + ht.log.Debugw("Got duplicate head", "blockNum", head.BlockNumber(), "head", head.BlockHash(), "prevHead", prevHead.BlockHash()) + } else { + ht.log.Debugw("Head already in the database", "head", head.BlockHash()) + } + } else { + ht.log.Debugw("Got out of order head", "blockNum", head.BlockNumber(), "head", head.BlockHash(), "prevHead", prevHead.BlockNumber()) + prevUnFinalizedHead := prevHead.BlockNumber() - int64(ht.config.FinalityDepth()) + if head.BlockNumber() < prevUnFinalizedHead { + promOldHead.WithLabelValues(ht.chainID.String()).Inc() + ht.log.Criticalf("Got very old block with number %d (highest seen was %d). This is a problem and either means a very deep re-org occurred, one of the RPC nodes has gotten far out of sync, or the chain went backwards in block numbers. This node may not function correctly without manual intervention.", head.BlockNumber(), prevHead.BlockNumber()) + ht.SvcErrBuffer.Append(errors.New("got very old block")) + } + } + return nil +} + +func (ht *HeadTracker[HTH, S, ID, BLOCK_HASH]) broadcastLoop() { + defer ht.wgDone.Done() + + samplingInterval := ht.config.HeadTrackerSamplingInterval() + if samplingInterval > 0 { + ht.log.Debugf("Head sampling is enabled - sampling interval is set to: %v", samplingInterval) + debounceHead := time.NewTicker(samplingInterval) + defer debounceHead.Stop() + for { + select { + case <-ht.chStop: + return + case <-debounceHead.C: + item := ht.broadcastMB.RetrieveLatestAndClear() + if !item.IsValid() { + continue + } + ht.headBroadcaster.BroadcastNewLongestChain(item) + } + } + } else { + ht.log.Info("Head sampling is disabled - callback will be called on every head") + for { + select { + case <-ht.chStop: + return + case <-ht.broadcastMB.Notify(): + for { + item, exists := ht.broadcastMB.Retrieve() + if !exists { + break + } + ht.headBroadcaster.BroadcastNewLongestChain(item) + } + } + } + } +} + +func (ht *HeadTracker[HTH, S, ID, BLOCK_HASH]) backfillLoop() { + defer ht.wgDone.Done() + + ctx, cancel := ht.chStop.NewCtx() + defer cancel() + + for { + select { + case <-ht.chStop: + return + case <-ht.backfillMB.Notify(): + for { + head, exists := ht.backfillMB.Retrieve() + if !exists { + break + } + { + err := ht.Backfill(ctx, head, uint(ht.config.FinalityDepth())) + if err != nil { + ht.log.Warnw("Unexpected error while backfilling heads", "err", err) + } else if ctx.Err() != nil { + break + } + } + } + } + } +} + +// backfill fetches all missing heads up until the base height +func (ht *HeadTracker[HTH, S, ID, BLOCK_HASH]) backfill(ctx context.Context, head types.Head[BLOCK_HASH], baseHeight int64) (err error) { + headBlockNumber := head.BlockNumber() + if headBlockNumber <= baseHeight { + return nil + } + mark := time.Now() + fetched := 0 + l := logger.With(ht.log, "blockNumber", headBlockNumber, + "n", headBlockNumber-baseHeight, + "fromBlockHeight", baseHeight, + "toBlockHeight", headBlockNumber-1) + l.Debug("Starting backfill") + defer func() { + if ctx.Err() != nil { + l.Warnw("Backfill context error", "err", ctx.Err()) + return + } + l.Debugw("Finished backfill", + "fetched", fetched, + "time", time.Since(mark), + "err", err) + }() + + for i := head.BlockNumber() - 1; i >= baseHeight; i-- { + // NOTE: Sequential requests here mean it's a potential performance bottleneck, be aware! + existingHead := ht.headSaver.Chain(head.GetParentHash()) + if existingHead.IsValid() { + head = existingHead + continue + } + head, err = ht.fetchAndSaveHead(ctx, i) + fetched++ + if ctx.Err() != nil { + ht.log.Debugw("context canceled, aborting backfill", "err", err, "ctx.Err", ctx.Err()) + break + } else if err != nil { + return errors.Wrap(err, "fetchAndSaveHead failed") + } + } + return +} + +func (ht *HeadTracker[HTH, S, ID, BLOCK_HASH]) fetchAndSaveHead(ctx context.Context, n int64) (HTH, error) { + ht.log.Debugw("Fetching head", "blockHeight", n) + head, err := ht.client.HeadByNumber(ctx, big.NewInt(n)) + if err != nil { + return ht.getNilHead(), err + } else if !head.IsValid() { + return ht.getNilHead(), errors.New("got nil head") + } + err = ht.headSaver.Save(ctx, head) + if err != nil { + return ht.getNilHead(), err + } + return head, nil +} diff --git a/pkg/common/types/head_tracker.go b/pkg/common/types/head_tracker.go new file mode 100644 index 000000000..510933e30 --- /dev/null +++ b/pkg/common/types/head_tracker.go @@ -0,0 +1,68 @@ +package types + +import ( + "context" + + "github.com/smartcontractkit/chainlink-relay/pkg/types" +) + +//go:generate mockery --quiet --name HeadTracker --output ../mocks/ --case=underscore +type HeadTracker[H Head[BLOCK_HASH], BLOCK_HASH Hashable] interface { + types.Service + // Backfill given a head will fill in any missing heads up to the given depth + // (used for testing) + Backfill(ctx context.Context, headWithChain H, depth uint) (err error) + LatestChain() H +} + +// HeadTrackable is implemented by the core txm, +// to be able to receive head events from any chain. +// Chain implementations should notify head events to the core txm via this interface. +// +//go:generate mockery --quiet --name HeadTrackable --output ./mocks/ --case=underscore +type HeadTrackable[H Head[BLOCK_HASH], BLOCK_HASH Hashable] interface { + OnNewLongestChain(ctx context.Context, head H) +} + +// HeadSaver is an chain agnostic interface for saving and loading heads +// Different chains will instantiate generic HeadSaver type with their native Head and BlockHash types. +type HeadSaver[H Head[BLOCK_HASH], BLOCK_HASH Hashable] interface { + // Save updates the latest block number, if indeed the latest, and persists + // this number in case of reboot. + Save(ctx context.Context, head H) error + // Load loads latest EvmHeadTrackerHistoryDepth heads, returns the latest chain. + Load(ctx context.Context) (H, error) + // LatestChain returns the block header with the highest number that has been seen, or nil. + LatestChain() H + // Chain returns a head for the specified hash, or nil. + Chain(hash BLOCK_HASH) H +} + +// HeadListener is a chain agnostic interface that manages connection of Client that receives heads from the blockchain node +type HeadListener[H Head[BLOCK_HASH], BLOCK_HASH Hashable] interface { + // ListenForNewHeads kicks off the listen loop (not thread safe) + // done() must be executed upon leaving ListenForNewHeads() + ListenForNewHeads(handleNewHead NewHeadHandler[H, BLOCK_HASH], done func()) + + // ReceivingHeads returns true if the listener is receiving heads (thread safe) + ReceivingHeads() bool + + // Connected returns true if the listener is connected (thread safe) + Connected() bool + + // HealthReport returns report of errors within HeadListener + HealthReport() map[string]error +} + +// NewHeadHandler is a callback that handles incoming heads +type NewHeadHandler[H Head[BLOCK_HASH], BLOCK_HASH Hashable] func(ctx context.Context, header H) error + +type HeadBroadcaster[H Head[BLOCK_HASH], BLOCK_HASH Hashable] interface { + types.Service + BroadcastNewLongestChain(H) + HeadBroadcasterRegistry[H, BLOCK_HASH] +} + +type HeadBroadcasterRegistry[H Head[BLOCK_HASH], BLOCK_HASH Hashable] interface { + Subscribe(callback HeadTrackable[H, BLOCK_HASH]) (currentLongestChain H, unsubscribe func()) +} From 623d63bafe07c10e50c38255ed547c680c19ae98 Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Fri, 16 Jun 2023 17:09:01 -0700 Subject: [PATCH 19/47] update go mod --- go.mod | 34 +++++++++++++++++----------------- go.sum | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index 02e337ed3..14292c01a 100644 --- a/go.mod +++ b/go.mod @@ -10,12 +10,12 @@ require ( github.com/gagliardetto/treeout v0.1.4 github.com/google/uuid v1.3.0 github.com/pkg/errors v0.9.1 - github.com/prometheus/client_golang v1.14.0 - github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230525161650-dce1bc58b504 - github.com/smartcontractkit/libocr v0.0.0-20230525150148-a75f6e244bb3 - github.com/stretchr/testify v1.8.2 + github.com/prometheus/client_golang v1.15.0 + github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230609013816-dc5d37505ce4 + github.com/smartcontractkit/libocr v0.0.0-20230606215712-82b910bef5c1 + github.com/stretchr/testify v1.8.4 github.com/test-go/testify v1.1.4 - go.uber.org/multierr v1.8.0 + go.uber.org/multierr v1.11.0 go.uber.org/zap v1.24.0 golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 golang.org/x/sync v0.1.0 @@ -27,7 +27,7 @@ require ( filippo.io/edwards25519 v1.0.0-rc.1 // indirect github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 // indirect - github.com/benbjohnson/clock v1.1.0 // indirect + github.com/benbjohnson/clock v1.3.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blendle/zapdriver v1.3.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect @@ -35,8 +35,8 @@ require ( github.com/dfuse-io/logging v0.0.0-20210109005628-b97a57253f70 // indirect github.com/fatih/color v1.13.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.2 // indirect - github.com/golang/snappy v0.0.4 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -46,17 +46,17 @@ require ( github.com/magiconair/properties v1.8.6 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.16 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect github.com/mr-tron/base58 v1.2.0 // indirect github.com/onsi/gomega v1.24.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.3.0 // indirect - github.com/prometheus/common v0.37.0 // indirect - github.com/prometheus/procfs v0.8.0 // indirect + github.com/prometheus/common v0.42.0 // indirect + github.com/prometheus/procfs v0.9.0 // indirect github.com/riferrei/srclient v0.5.4 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.1.1 // indirect github.com/stretchr/objx v0.5.0 // indirect @@ -65,11 +65,11 @@ require ( github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect go.opencensus.io v0.23.0 // indirect - go.uber.org/atomic v1.9.0 // indirect + go.uber.org/atomic v1.10.0 // indirect go.uber.org/ratelimit v0.2.0 // indirect - golang.org/x/crypto v0.5.0 // indirect - golang.org/x/sys v0.5.0 // indirect - golang.org/x/term v0.4.0 // indirect - google.golang.org/protobuf v1.28.1 // indirect + golang.org/x/crypto v0.9.0 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/term v0.8.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index bf3dadc9a..ed83de8e2 100644 --- a/go.sum +++ b/go.sum @@ -66,6 +66,8 @@ github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 h1:WWB576BN5zNSZc github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -180,10 +182,13 @@ github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -319,6 +324,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -362,6 +369,7 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= @@ -371,6 +379,10 @@ github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqr github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= +github.com/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM= +github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= +github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= +github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -385,6 +397,8 @@ github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9 github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= @@ -393,6 +407,10 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= +github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/riferrei/srclient v0.5.4 h1:dfwyR5u23QF7beuVl2WemUY2KXh5+Sc4DHKyPXBNYuc= github.com/riferrei/srclient v0.5.4/go.mod h1:vbkLmWcgYa7JgfPvuy/+K8fTS0p1bApqadxrxi/S1MI= @@ -417,8 +435,12 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230525161650-dce1bc58b504 h1:LQB12lOqT0Kg6s9zq/zDF5egSPXi7Hyar3QBmjkD32w= github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230525161650-dce1bc58b504/go.mod h1:zfUba6Okm7zTBxap24I78Vq9z+twHmjXSMBAl2C2Qgc= +github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230609013816-dc5d37505ce4 h1:aBsArskduVo/P7GuMD09Btv7Iyky76PZN1u51EBTJXQ= +github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230609013816-dc5d37505ce4/go.mod h1:MfZBUifutkv3aK7abyw5YmTJbqt8iFwcQDFikrxC/uI= github.com/smartcontractkit/libocr v0.0.0-20230525150148-a75f6e244bb3 h1:/Gel/U5eIZ/BGGr25OrHaXiVDTAJ5DYX5+UlXp3q7Gg= github.com/smartcontractkit/libocr v0.0.0-20230525150148-a75f6e244bb3/go.mod h1:5JnCHuYgmIP9ZyXzgAfI5Iwu0WxBtBKp+ApeT5o1Cjw= +github.com/smartcontractkit/libocr v0.0.0-20230606215712-82b910bef5c1 h1:caG9BWjnCxN/HPBA5ltDGadDraZAsjGIct4S8lh8D5c= +github.com/smartcontractkit/libocr v0.0.0-20230606215712-82b910bef5c1/go.mod h1:2lyRkw/qLQgUWlrWWmq5nj0y90rWeO6Y+v+fCakRgb0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= @@ -448,6 +470,7 @@ github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0= github.com/teris-io/shortid v0.0.0-20201117134242-e59966efd125 h1:3SNcvBmEPE1YlB1JpVZouslJpI3GBNoiqW7+wb0Rz7w= @@ -485,6 +508,8 @@ go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= @@ -492,6 +517,8 @@ go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKY go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/ratelimit v0.2.0 h1:UQE2Bgi7p2B85uP5dC2bbRtig0C+OeNRnNEafLjsLPA= go.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6mUg= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= @@ -510,6 +537,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -606,6 +635,7 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -664,11 +694,14 @@ golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg= golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -825,6 +858,8 @@ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/avro.v0 v0.0.0-20171217001914-a730b5802183/go.mod h1:FvqrFXt+jCsyQibeRv4xxEJBL5iG2DDW5aeJwzDiq4A= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 7d3e7eb3b6dedf66c47b24c2e7843b38039f376e Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Wed, 21 Jun 2023 09:15:28 -0700 Subject: [PATCH 20/47] go mod tidy --- go.mod | 1 - go.sum | 162 ++++----------------------------------------------------- 2 files changed, 9 insertions(+), 154 deletions(-) diff --git a/go.mod b/go.mod index 14292c01a..0e3c47e97 100644 --- a/go.mod +++ b/go.mod @@ -37,7 +37,6 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect - github.com/google/go-cmp v0.5.9 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.15.11 // indirect diff --git a/go.sum b/go.sum index ed83de8e2..597bf1d6a 100644 --- a/go.sum +++ b/go.sum @@ -9,29 +9,19 @@ cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= contrib.go.opencensus.io/exporter/stackdriver v0.12.6/go.mod h1:8x999/OcIPy5ivx/wDiV7Gx4D+VUPODf0mWRGRc5kSk= contrib.go.opencensus.io/exporter/stackdriver v0.13.4 h1:ksUxwH3OD5sxkjzEqGxNTl+Xjsmu3BnC/300MhSVTSc= contrib.go.opencensus.io/exporter/stackdriver v0.13.4/go.mod h1:aXENhDJ1Y4lIg4EUaVTwzvYETVNZk10Pu26tevFKLUc= @@ -50,10 +40,7 @@ github.com/actgardner/gogen-avro/v10 v10.2.1/go.mod h1:QUhjeHPchheYmMDni/Nx7VB0R github.com/actgardner/gogen-avro/v9 v9.1.0/go.mod h1:nyTj6wPqDJoxM3qdnjcLv+EnMDSDFqE0qDpva2QRmKc= github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI= github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= @@ -64,8 +51,6 @@ github.com/aws/aws-sdk-go v1.22.1/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 h1:WWB576BN5zNSZc/M9d/10pqEx5VHNhaQ/yOVAkmj5Yo= github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= -github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -80,7 +65,6 @@ github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx2 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -141,13 +125,8 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= @@ -162,14 +141,11 @@ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -180,14 +156,13 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -196,25 +171,19 @@ github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzr github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20211008130755-947d60d73cc0/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -269,7 +238,6 @@ github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22 github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= @@ -286,13 +254,12 @@ github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47e github.com/klauspost/compress v1.15.11 h1:Lcadnb3RKGin4FYM/orgq0qde+nc15E5Cbqg4B9Sx9c= github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -322,8 +289,6 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= @@ -349,7 +314,6 @@ github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1/go.mod h github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= github.com/nrwiersma/avro-benchmarks v0.0.0-20210913175520-21aec48c8f76/go.mod h1:iKyFMidsk/sVYONJRE372sJuX/QTRPacU7imPqqsu7g= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= @@ -367,50 +331,27 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= -github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= -github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= github.com/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM= github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= -github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= -github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= -github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= -github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= -github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= -github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/riferrei/srclient v0.5.4 h1:dfwyR5u23QF7beuVl2WemUY2KXh5+Sc4DHKyPXBNYuc= github.com/riferrei/srclient v0.5.4/go.mod h1:vbkLmWcgYa7JgfPvuy/+K8fTS0p1bApqadxrxi/S1MI= @@ -419,8 +360,8 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/santhosh-tekuri/jsonschema/v5 v5.0.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= @@ -431,14 +372,8 @@ github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5g github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230525161650-dce1bc58b504 h1:LQB12lOqT0Kg6s9zq/zDF5egSPXi7Hyar3QBmjkD32w= -github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230525161650-dce1bc58b504/go.mod h1:zfUba6Okm7zTBxap24I78Vq9z+twHmjXSMBAl2C2Qgc= github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230609013816-dc5d37505ce4 h1:aBsArskduVo/P7GuMD09Btv7Iyky76PZN1u51EBTJXQ= github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230609013816-dc5d37505ce4/go.mod h1:MfZBUifutkv3aK7abyw5YmTJbqt8iFwcQDFikrxC/uI= -github.com/smartcontractkit/libocr v0.0.0-20230525150148-a75f6e244bb3 h1:/Gel/U5eIZ/BGGr25OrHaXiVDTAJ5DYX5+UlXp3q7Gg= -github.com/smartcontractkit/libocr v0.0.0-20230525150148-a75f6e244bb3/go.mod h1:5JnCHuYgmIP9ZyXzgAfI5Iwu0WxBtBKp+ApeT5o1Cjw= github.com/smartcontractkit/libocr v0.0.0-20230606215712-82b910bef5c1 h1:caG9BWjnCxN/HPBA5ltDGadDraZAsjGIct4S8lh8D5c= github.com/smartcontractkit/libocr v0.0.0-20230606215712-82b910bef5c1/go.mod h1:2lyRkw/qLQgUWlrWWmq5nj0y90rWeO6Y+v+fCakRgb0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= @@ -468,8 +403,7 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0= @@ -490,7 +424,6 @@ github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPU github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= @@ -506,17 +439,13 @@ go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= +go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= -go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/ratelimit v0.2.0 h1:UQE2Bgi7p2B85uP5dC2bbRtig0C+OeNRnNEafLjsLPA= @@ -535,8 +464,6 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= -golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -570,7 +497,6 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -585,9 +511,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -595,33 +519,22 @@ golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200505041828-1ed23360d12c/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -629,13 +542,10 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -647,7 +557,6 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -660,47 +569,29 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg= -golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -709,8 +600,7 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -750,19 +640,9 @@ golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200601175630-2caf76543d99/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -777,20 +657,14 @@ google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsb google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -809,19 +683,9 @@ google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvx google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20220503193339-ba3ae3f07e29/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -834,9 +698,6 @@ google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= @@ -850,14 +711,11 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= -google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= @@ -882,7 +740,6 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -897,7 +754,6 @@ honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= From a57978ddd0bc7897ff0650045a6c44a5a6aa74ce Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Thu, 22 Jun 2023 10:15:15 -0700 Subject: [PATCH 21/47] Added InMemory HeadSaver + Tests --- pkg/internal/cltest/cltest.go | 43 ++++ pkg/internal/testutils/testutils.go | 23 ++ pkg/{solana => internal}/utils/hash_helper.go | 0 .../utils/hash_helper_test.go | 2 +- pkg/solana/client/client.go | 2 +- pkg/solana/config/config.go | 2 +- pkg/solana/headtracker/config.go | 63 +++++ pkg/solana/headtracker/head_saver_in_mem.go | 161 ++++++++++++ .../headtracker/head_saver_in_mem_test.go | 239 ++++++++++++++++++ pkg/solana/headtracker/types/head.go | 4 +- pkg/solana/headtracker/types/head_test.go | 2 +- 11 files changed, 535 insertions(+), 6 deletions(-) create mode 100644 pkg/internal/cltest/cltest.go create mode 100644 pkg/internal/testutils/testutils.go rename pkg/{solana => internal}/utils/hash_helper.go (100%) rename pkg/{solana => internal}/utils/hash_helper_test.go (86%) create mode 100644 pkg/solana/headtracker/config.go create mode 100644 pkg/solana/headtracker/head_saver_in_mem.go create mode 100644 pkg/solana/headtracker/head_saver_in_mem_test.go diff --git a/pkg/internal/cltest/cltest.go b/pkg/internal/cltest/cltest.go new file mode 100644 index 000000000..4960ceb5a --- /dev/null +++ b/pkg/internal/cltest/cltest.go @@ -0,0 +1,43 @@ +package cltest + +import ( + "fmt" + "math/big" + + "github.com/gagliardetto/solana-go" + "github.com/gagliardetto/solana-go/rpc" + + "github.com/smartcontractkit/chainlink-solana/pkg/internal/utils" + headtracker "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" +) + +// Head given the value convert it into an Head +func Head(val interface{}) *headtracker.Head { + var h *headtracker.Head + time := solana.UnixTimeSeconds(0) + blockHeight := uint64(0) + block := rpc.GetBlockResult{ + Blockhash: utils.NewHash(), + PreviousBlockhash: utils.NewHash(), + ParentSlot: 0, + Transactions: nil, + Rewards: nil, + BlockTime: &time, + BlockHeight: &blockHeight, + } + chainId := headtracker.Mainnet + + switch t := val.(type) { + case int: + h = headtracker.NewHead(int64(t), block, nil, chainId) + case uint64: + h = headtracker.NewHead(int64(t), block, nil, chainId) + case int64: + h = headtracker.NewHead(t, block, nil, chainId) + case *big.Int: + h = headtracker.NewHead(t.Int64(), block, nil, chainId) + default: + panic(fmt.Sprintf("Could not convert %v of type %T to Head", val, val)) + } + return h +} diff --git a/pkg/internal/testutils/testutils.go b/pkg/internal/testutils/testutils.go new file mode 100644 index 000000000..5e717aad0 --- /dev/null +++ b/pkg/internal/testutils/testutils.go @@ -0,0 +1,23 @@ +package testutils + +import ( + "context" + "testing" +) + +// Context returns a context with the test's deadline, if available. +func Context(tb testing.TB) context.Context { + ctx := context.Background() + var cancel func() + switch t := tb.(type) { + case *testing.T: + if d, ok := t.Deadline(); ok { + ctx, cancel = context.WithDeadline(ctx, d) + } + } + if cancel == nil { + ctx, cancel = context.WithCancel(ctx) + } + tb.Cleanup(cancel) + return ctx +} diff --git a/pkg/solana/utils/hash_helper.go b/pkg/internal/utils/hash_helper.go similarity index 100% rename from pkg/solana/utils/hash_helper.go rename to pkg/internal/utils/hash_helper.go diff --git a/pkg/solana/utils/hash_helper_test.go b/pkg/internal/utils/hash_helper_test.go similarity index 86% rename from pkg/solana/utils/hash_helper_test.go rename to pkg/internal/utils/hash_helper_test.go index 0bcff880c..18bc294d7 100644 --- a/pkg/solana/utils/hash_helper_test.go +++ b/pkg/internal/utils/hash_helper_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/gagliardetto/solana-go" - "github.com/smartcontractkit/chainlink-solana/pkg/solana/utils" + "github.com/smartcontractkit/chainlink-solana/pkg/internal/utils" "github.com/stretchr/testify/assert" ) diff --git a/pkg/solana/client/client.go b/pkg/solana/client/client.go index cda3b54fc..7776179ce 100644 --- a/pkg/solana/client/client.go +++ b/pkg/solana/client/client.go @@ -11,7 +11,7 @@ import ( "github.com/pkg/errors" "golang.org/x/sync/singleflight" - htrktypes "github.com/smartcontractkit/chainlink-solana/pkg/common/headtracker/types" + htrktypes "github.com/smartcontractkit/chainlink-relay/pkg/headtracker/types" "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" headtracker "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" "github.com/smartcontractkit/chainlink-solana/pkg/solana/logger" diff --git a/pkg/solana/config/config.go b/pkg/solana/config/config.go index 47559d930..85b9cbbfc 100644 --- a/pkg/solana/config/config.go +++ b/pkg/solana/config/config.go @@ -10,7 +10,7 @@ import ( relaycfg "github.com/smartcontractkit/chainlink-relay/pkg/config" "github.com/smartcontractkit/chainlink-relay/pkg/utils" - htrktypes "github.com/smartcontractkit/chainlink-solana/pkg/common/headtracker/types" + htrktypes "github.com/smartcontractkit/chainlink-relay/pkg/headtracker/types" "github.com/smartcontractkit/chainlink-solana/pkg/solana/db" "github.com/smartcontractkit/chainlink-solana/pkg/solana/logger" ) diff --git a/pkg/solana/headtracker/config.go b/pkg/solana/headtracker/config.go new file mode 100644 index 000000000..b80b00bc0 --- /dev/null +++ b/pkg/solana/headtracker/config.go @@ -0,0 +1,63 @@ +package headtracker + +import ( + "time" +) + +// This config serves as a POC for headtracker. +// It should be replaced with a more robust config +// such as the one in pkg/solana/config + +// TODO: replace this config with a more robust config +type config struct { + defaults configSet +} + +type configSet struct { + BlockEmissionIdleWarningThreshold time.Duration + FinalityDepth uint32 + HeadTrackerHistoryDepth uint32 + HeadTrackerMaxBufferSize uint32 + HeadTrackerSamplingInterval time.Duration + PollingInterval time.Duration +} + +var defaultConfigSet = configSet{ + // headtracker + BlockEmissionIdleWarningThreshold: 30 * time.Second, + FinalityDepth: 50, + HeadTrackerHistoryDepth: 100, + HeadTrackerMaxBufferSize: 3, + HeadTrackerSamplingInterval: 1 * time.Second, + PollingInterval: 2 * time.Second, +} + +func NewConfig() *config { + return &config{ + defaults: defaultConfigSet, + } +} + +func (c *config) BlockEmissionIdleWarningThreshold() time.Duration { + return c.defaults.BlockEmissionIdleWarningThreshold +} + +func (c *config) FinalityDepth() uint32 { + return c.defaults.FinalityDepth +} + +func (c *config) HeadTrackerHistoryDepth() uint32 { + return c.defaults.HeadTrackerHistoryDepth +} + +func (c *config) HeadTrackerMaxBufferSize() uint32 { + return c.defaults.HeadTrackerMaxBufferSize +} + +func (c *config) HeadTrackerSamplingInterval() time.Duration { + return c.defaults.HeadTrackerSamplingInterval +} + +func (c *config) PollingInterval() time.Duration { + return c.defaults.PollingInterval +} diff --git a/pkg/solana/headtracker/head_saver_in_mem.go b/pkg/solana/headtracker/head_saver_in_mem.go new file mode 100644 index 000000000..825350b62 --- /dev/null +++ b/pkg/solana/headtracker/head_saver_in_mem.go @@ -0,0 +1,161 @@ +package headtracker + +import ( + "context" + "errors" + "sync" + + htrktypes "github.com/smartcontractkit/chainlink-relay/pkg/headtracker/types" + "github.com/smartcontractkit/chainlink-relay/pkg/logger" + "github.com/smartcontractkit/chainlink-relay/pkg/types" + headtracker "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" +) + +type inMemoryHeadSaver[H htrktypes.Head[BLOCK_HASH, CHAIN_ID], BLOCK_HASH types.Hashable, CHAIN_ID types.ID] struct { + config htrktypes.Config + logger logger.Logger + latestHead H + Heads map[BLOCK_HASH]H + HeadsNumber map[int64][]H + mu sync.RWMutex + getNilHead func() H + setParent func(H, H) +} + +type HeadSaver = inMemoryHeadSaver[*headtracker.Head, headtracker.Hash, headtracker.ChainID] + +var _ types.HeadSaver[*headtracker.Head, headtracker.Hash] = (*HeadSaver)(nil) + +func NewInMemoryHeadSaver[ + H htrktypes.Head[BLOCK_HASH, CHAIN_ID], + BLOCK_HASH types.Hashable, + CHAIN_ID types.ID]( + config htrktypes.Config, + lggr logger.Logger, + getNilHead func() H, + setParent func(H, H), +) *inMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID] { + return &inMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]{ + config: config, + logger: logger.Named(lggr, "InMemoryHeadSaver"), + Heads: make(map[BLOCK_HASH]H), + HeadsNumber: make(map[int64][]H), + getNilHead: getNilHead, + setParent: setParent, + } +} + +// Creates a new In Memory HeadSaver for solana +func NewHeadSaver(config htrktypes.Config, lggr logger.Logger) *HeadSaver { + return NewInMemoryHeadSaver[*headtracker.Head, headtracker.Hash, headtracker.ChainID]( + config, + lggr, + func() *headtracker.Head { return nil }, + func(head, parent *headtracker.Head) { head.Parent = parent }, + ) +} + +func (hs *inMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) Save(ctx context.Context, head H) error { + if !head.IsValid() { + return errors.New("invalid head passed to Save method of InMemoryHeadSaver") + } + + historyDepth := int64(hs.config.HeadTrackerHistoryDepth()) + hs.AddHeads(historyDepth, head) + + return nil +} + +// No OP function for EVM +func (hs *inMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) Load(ctx context.Context) (H, error) { + + return hs.LatestChain(), nil +} + +func (hs *inMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) LatestChain() H { + head := hs.getLatestHead() + + if head.ChainLength() < hs.config.FinalityDepth() { + hs.logger.Debugw("chain shorter than EvmFinalityDepth", "chainLen", head.ChainLength(), "evmFinalityDepth", hs.config.FinalityDepth()) + } + return head +} + +func (hs *inMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) Chain(blockHash BLOCK_HASH) H { + hs.mu.RLock() + defer hs.mu.RUnlock() + + if head, exists := hs.Heads[blockHash]; exists { + return head + } + + return hs.getNilHead() +} + +func (hs *inMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) HeadByNumber(blockNumber int64) []H { + hs.mu.RLock() + defer hs.mu.RUnlock() + + return hs.HeadsNumber[blockNumber] +} + +// Assembles the heads together and populates the Heads Map +func (hs *inMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) AddHeads(historyDepth int64, newHeads ...H) { + hs.mu.Lock() + defer hs.mu.Unlock() + + hs.trimHeads(historyDepth) + + for _, head := range newHeads { + blockHash := head.BlockHash() + blockNumber := head.BlockNumber() + + if _, exists := hs.Heads[blockHash]; exists { + continue + } + + if parent, exists := hs.Heads[blockHash]; exists { + hs.setParent(head, parent) + } else { + // If parent's head is too old, we should set it to nil + hs.setParent(head, hs.getNilHead()) + } + + hs.Heads[blockHash] = head + hs.HeadsNumber[blockNumber] = append(hs.HeadsNumber[blockNumber], head) + + if !hs.latestHead.IsValid() { + hs.latestHead = head + } else if head.BlockNumber() > hs.latestHead.BlockNumber() { + hs.latestHead = head + } + } +} + +func (hs *inMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) TrimOldHeads(historyDepth int64) { + hs.mu.Lock() + defer hs.mu.Unlock() + + hs.trimHeads(historyDepth) +} + +// trimHeads() is should only be called by functions with mutex locking. +// trimHeads() is an internal function without locking to prevent deadlocks +func (hs *inMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) trimHeads(historyDepth int64) { + for headNumber, headNumberList := range hs.HeadsNumber { + if hs.latestHead.BlockNumber()-headNumber > historyDepth { + for _, head := range headNumberList { + delete(hs.Heads, head.BlockHash()) + } + + delete(hs.HeadsNumber, headNumber) + } + } +} + +func (hs *inMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) getLatestHead() H { + hs.mu.RLock() + defer hs.mu.RUnlock() + + return hs.latestHead +} diff --git a/pkg/solana/headtracker/head_saver_in_mem_test.go b/pkg/solana/headtracker/head_saver_in_mem_test.go new file mode 100644 index 000000000..6b6c2dab8 --- /dev/null +++ b/pkg/solana/headtracker/head_saver_in_mem_test.go @@ -0,0 +1,239 @@ +package headtracker_test + +import ( + "sync" + "testing" + + "github.com/smartcontractkit/chainlink-relay/pkg/logger" + "github.com/smartcontractkit/chainlink-solana/pkg/internal/cltest" + "github.com/smartcontractkit/chainlink-solana/pkg/internal/testutils" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker" + "github.com/stretchr/testify/require" +) + +func configureInMemorySaver(t *testing.T) *headtracker.HeadSaver { + htCfg := headtracker.NewConfig() + lggr, _ := logger.New() + return headtracker.NewHeadSaver(htCfg, lggr) +} + +func TestInMemoryHeadSaver_Save(t *testing.T) { + t.Parallel() + saver := configureInMemorySaver(t) + + t.Run("happy path, saving heads", func(t *testing.T) { + head := cltest.Head(1) + err := saver.Save(testutils.Context(t), head) + require.NoError(t, err) + + latest := saver.LatestChain() + require.NoError(t, err) + require.Equal(t, int64(1), latest.BlockNumber()) + + latest = saver.LatestChain() + require.NotNil(t, latest) + require.Equal(t, int64(1), latest.BlockNumber()) + + latest = saver.Chain(head.BlockHash()) + require.NotNil(t, latest) + require.Equal(t, int64(1), latest.BlockNumber()) + + // Add more heads + head = cltest.Head(2) + err = saver.Save(testutils.Context(t), head) + require.NoError(t, err) + head = cltest.Head(3) + err = saver.Save(testutils.Context(t), head) + require.NoError(t, err) + + latest = saver.LatestChain() + require.Equal(t, int64(3), latest.BlockNumber()) + }) + + t.Run("save invalid head", func(t *testing.T) { + err := saver.Save(testutils.Context(t), nil) + require.Error(t, err) + }) + + t.Run("saving heads with same block number", func(t *testing.T) { + head := cltest.Head(4) + err := saver.Save(testutils.Context(t), head) + require.NoError(t, err) + + head = cltest.Head(4) + err = saver.Save(testutils.Context(t), head) + require.NoError(t, err) + + head = cltest.Head(4) + err = saver.Save(testutils.Context(t), head) + require.NoError(t, err) + + latest := saver.LatestChain() + require.NoError(t, err) + require.Equal(t, int64(4), latest.BlockNumber()) + + headsWithSameNumber := len(saver.HeadByNumber(4)) + require.Equal(t, 3, headsWithSameNumber) + }) + t.Run("concurrent calls to Save", func(t *testing.T) { + var wg sync.WaitGroup + numRoutines := 10 + wg.Add(numRoutines) + + for i := 1; i <= numRoutines; i++ { + go func(num int) { + defer wg.Done() + head := cltest.Head(num) + err := saver.Save(testutils.Context(t), head) + require.NoError(t, err) + }(i) + } + + wg.Wait() + + latest := saver.LatestChain() + require.Equal(t, int64(numRoutines), latest.BlockNumber()) + }) +} + +func TestInMemoryHeadSaver_TrimOldHeads(t *testing.T) { + t.Parallel() + saver := configureInMemorySaver(t) + + t.Run("happy path, trimming old heads", func(t *testing.T) { + // Save heads with block numbers 1, 2, 3, and 4 + for i := 1; i <= 4; i++ { + head := cltest.Head(i) + err := saver.Save(testutils.Context(t), head) + require.NoError(t, err) + } + + // Trim old heads, keeping only the last two (block numbers 3 and 4) + saver.TrimOldHeads(3) + + // Check that the correct heads remain + require.Equal(t, 2, len(saver.Heads)) + require.Equal(t, 1, len(saver.HeadByNumber(3))) + require.Equal(t, 1, len(saver.HeadByNumber(4))) + require.Equal(t, 0, len(saver.HeadByNumber(1))) + + // Check that the latest head is correct + latest := saver.LatestChain() + require.Equal(t, int64(4), latest.BlockNumber()) + + // Clear All Heads + saver.TrimOldHeads(6) + require.Equal(t, 0, len(saver.Heads)) + require.Equal(t, 0, len(saver.HeadsNumber)) + }) + + t.Run("error path, block number lower than highest chain", func(t *testing.T) { + for i := 1; i <= 4; i++ { + head := cltest.Head(i) + err := saver.Save(testutils.Context(t), head) + require.NoError(t, err) + } + + saver.TrimOldHeads(1) + + // Check that no heads are removed + require.Equal(t, 4, len(saver.Heads)) + require.Equal(t, 4, len(saver.HeadsNumber)) + + latest := saver.LatestChain() + require.Equal(t, int64(4), latest.BlockNumber()) + }) + + t.Run("concurrent calls to TrimOldHeads", func(t *testing.T) { + // Save heads with block numbers 1, 2, 3, and 4 + for i := 1; i <= 4; i++ { + head := cltest.Head(i) + err := saver.Save(testutils.Context(t), head) + require.NoError(t, err) + } + + // Concurrently add multiple heads with different block numbers + var wg sync.WaitGroup + wg.Add(4) + for i := 5; i <= 8; i++ { + go func(num int) { + defer wg.Done() + head := cltest.Head(num) + err := saver.Save(testutils.Context(t), head) + require.NoError(t, err) + }(i) + } + wg.Wait() + + // Concurrently trim old heads with different block numbers + wg.Add(7) + for i := 1; i <= 7; i++ { + go func(num int) { + defer wg.Done() + saver.TrimOldHeads(int64(num)) + }(i) + } + wg.Wait() + + // Check that the correct heads remain after concurrent calls to TrimOldHeads + require.Equal(t, 2, len(saver.Heads)) + require.Equal(t, 1, len(saver.HeadByNumber(7))) + require.Equal(t, 1, len(saver.HeadByNumber(8))) + require.Equal(t, 0, len(saver.HeadByNumber(1))) + + latest := saver.LatestChain() + require.Equal(t, int64(8), latest.BlockNumber()) + }) +} + +func TestInMemoryHeadSaver_Chain(t *testing.T) { + t.Parallel() + saver := configureInMemorySaver(t) + + t.Run("happy path, valid block hash", func(t *testing.T) { + head1 := cltest.Head(1) + head2 := cltest.Head(2) + err := saver.Save(testutils.Context(t), head1) + require.NoError(t, err) + err = saver.Save(testutils.Context(t), head2) + require.NoError(t, err) + + retrievedHead1 := saver.Chain(head1.BlockHash()) + retrievedHead2 := saver.Chain(head2.BlockHash()) + + require.Equal(t, head1, retrievedHead1) + require.Equal(t, head2, retrievedHead2) + + }) + + t.Run("invalid block hash", func(t *testing.T) { + head1 := cltest.Head(1) + err := saver.Save(testutils.Context(t), head1) + require.NoError(t, err) + head2 := cltest.Head(2) + err = saver.Save(testutils.Context(t), head2) + require.NoError(t, err) + + saver.TrimOldHeads(2) + + invalidBlockHash := head1.BlockHash() + retrievedHead := saver.Chain(invalidBlockHash) + + require.Nil(t, retrievedHead) + }) +} + +func TestInMemoryHeadSaver_LatestChain(t *testing.T) { + t.Parallel() + saver := configureInMemorySaver(t) + + t.Run("happy path", func(t *testing.T) { + // Save a valid head + head := cltest.Head(1) + err := saver.Save(testutils.Context(t), head) + require.NoError(t, err) + + latest := saver.LatestChain() + require.Equal(t, int64(1), latest.BlockNumber()) + }) +} diff --git a/pkg/solana/headtracker/types/head.go b/pkg/solana/headtracker/types/head.go index 6d64f216d..6d2ca929d 100644 --- a/pkg/solana/headtracker/types/head.go +++ b/pkg/solana/headtracker/types/head.go @@ -3,8 +3,8 @@ package headtracker import ( "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" - htrktypes "github.com/smartcontractkit/chainlink-solana/pkg/common/headtracker/types" - commontypes "github.com/smartcontractkit/chainlink-solana/pkg/common/types" + htrktypes "github.com/smartcontractkit/chainlink-relay/pkg/headtracker/types" + commontypes "github.com/smartcontractkit/chainlink-relay/pkg/types" ) var _ commontypes.Head[Hash] = (*Head)(nil) diff --git a/pkg/solana/headtracker/types/head_test.go b/pkg/solana/headtracker/types/head_test.go index 609a87dc7..7a44ffb39 100644 --- a/pkg/solana/headtracker/types/head_test.go +++ b/pkg/solana/headtracker/types/head_test.go @@ -6,8 +6,8 @@ import ( "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" + "github.com/smartcontractkit/chainlink-solana/pkg/internal/utils" headtracker "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" - "github.com/smartcontractkit/chainlink-solana/pkg/solana/utils" "github.com/stretchr/testify/assert" ) From c3f524ee0e9b6cafb9bc4dd9b0f9b34937c568d8 Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Thu, 22 Jun 2023 10:22:20 -0700 Subject: [PATCH 22/47] use chainlink-relay, removed common folder from solana --- pkg/common/config/presenter.go | 14 - pkg/common/headtracker/head_broadcaster.go | 163 ---------- pkg/common/headtracker/head_listener.go | 218 ------------- pkg/common/headtracker/head_tracker.go | 359 --------------------- pkg/common/headtracker/types/client.go | 17 - pkg/common/headtracker/types/config.go | 11 - pkg/common/headtracker/types/head.go | 16 - pkg/common/headtracker/types/mocks/head.go | 172 ---------- pkg/common/types/chain.go | 7 - pkg/common/types/hashable.go | 12 - pkg/common/types/head.go | 27 -- pkg/common/types/head_tracker.go | 68 ---- pkg/common/types/mocks/head.go | 130 -------- pkg/common/types/subscription.go | 17 - pkg/solana/client/subscription.go | 2 +- 15 files changed, 1 insertion(+), 1232 deletions(-) delete mode 100644 pkg/common/config/presenter.go delete mode 100644 pkg/common/headtracker/head_broadcaster.go delete mode 100644 pkg/common/headtracker/head_listener.go delete mode 100644 pkg/common/headtracker/head_tracker.go delete mode 100644 pkg/common/headtracker/types/client.go delete mode 100644 pkg/common/headtracker/types/config.go delete mode 100644 pkg/common/headtracker/types/head.go delete mode 100644 pkg/common/headtracker/types/mocks/head.go delete mode 100644 pkg/common/types/chain.go delete mode 100644 pkg/common/types/hashable.go delete mode 100644 pkg/common/types/head.go delete mode 100644 pkg/common/types/head_tracker.go delete mode 100644 pkg/common/types/mocks/head.go delete mode 100644 pkg/common/types/subscription.go diff --git a/pkg/common/config/presenter.go b/pkg/common/config/presenter.go deleted file mode 100644 index f4da66fd1..000000000 --- a/pkg/common/config/presenter.go +++ /dev/null @@ -1,14 +0,0 @@ -package config - -import ( - "fmt" - "math/big" - - "golang.org/x/exp/constraints" -) - -// FriendlyNumber returns a string printing the integer or big.Int in both -// decimal and hexadecimal formats. -func FriendlyNumber[N constraints.Integer | *big.Int](n N) string { - return fmt.Sprintf("#%[1]v (0x%[1]x)", n) -} diff --git a/pkg/common/headtracker/head_broadcaster.go b/pkg/common/headtracker/head_broadcaster.go deleted file mode 100644 index 31584bb48..000000000 --- a/pkg/common/headtracker/head_broadcaster.go +++ /dev/null @@ -1,163 +0,0 @@ -package headtracker - -import ( - "context" - "fmt" - "reflect" - "sync" - "time" - - "github.com/smartcontractkit/chainlink-relay/pkg/logger" - "github.com/smartcontractkit/chainlink-relay/pkg/utils" - commontypes "github.com/smartcontractkit/chainlink-solana/pkg/common/types" -) - -const TrackableCallbackTimeout = 2 * time.Second - -type callbackSet[H commontypes.Head[BLOCK_HASH], BLOCK_HASH commontypes.Hashable] map[int]commontypes.HeadTrackable[H, BLOCK_HASH] - -func (set callbackSet[H, BLOCK_HASH]) values() []commontypes.HeadTrackable[H, BLOCK_HASH] { - var values []commontypes.HeadTrackable[H, BLOCK_HASH] - for _, callback := range set { - values = append(values, callback) - } - return values -} - -type HeadBroadcaster[H commontypes.Head[BLOCK_HASH], BLOCK_HASH commontypes.Hashable] struct { - logger logger.Logger - callbacks callbackSet[H, BLOCK_HASH] - mailbox *utils.Mailbox[H] - mutex *sync.Mutex - chClose utils.StopChan - wgDone sync.WaitGroup - utils.StartStopOnce - latest H - lastCallbackID int -} - -// NewHeadBroadcaster creates a new HeadBroadcaster -func NewHeadBroadcaster[ - H commontypes.Head[BLOCK_HASH], - BLOCK_HASH commontypes.Hashable, -]( - lggr logger.Logger, -) *HeadBroadcaster[H, BLOCK_HASH] { - return &HeadBroadcaster[H, BLOCK_HASH]{ - logger: logger.Named(lggr, "HeadBroadcaster"), - callbacks: make(callbackSet[H, BLOCK_HASH]), - mailbox: utils.NewSingleMailbox[H](), - mutex: &sync.Mutex{}, - chClose: make(chan struct{}), - wgDone: sync.WaitGroup{}, - StartStopOnce: utils.StartStopOnce{}, - } -} - -func (hb *HeadBroadcaster[H, BLOCK_HASH]) Start(context.Context) error { - return hb.StartOnce("HeadBroadcaster", func() error { - hb.wgDone.Add(1) - go hb.run() - return nil - }) -} - -func (hb *HeadBroadcaster[H, BLOCK_HASH]) Close() error { - return hb.StopOnce("HeadBroadcaster", func() error { - hb.mutex.Lock() - // clear all callbacks - hb.callbacks = make(callbackSet[H, BLOCK_HASH]) - hb.mutex.Unlock() - - close(hb.chClose) - hb.wgDone.Wait() - return nil - }) -} - -func (hb *HeadBroadcaster[H, BLOCK_HASH]) Name() string { - return hb.logger.Name() -} - -func (hb *HeadBroadcaster[H, BLOCK_HASH]) HealthReport() map[string]error { - return map[string]error{hb.Name(): hb.StartStopOnce.Healthy()} -} - -func (hb *HeadBroadcaster[H, BLOCK_HASH]) BroadcastNewLongestChain(head H) { - hb.mailbox.Deliver(head) -} - -// Subscribe subscribes to OnNewLongestChain and Connect until HeadBroadcaster is closed, -// or unsubscribe callback is called explicitly -func (hb *HeadBroadcaster[H, BLOCK_HASH]) Subscribe(callback commontypes.HeadTrackable[H, BLOCK_HASH]) (currentLongestChain H, unsubscribe func()) { - hb.mutex.Lock() - defer hb.mutex.Unlock() - - currentLongestChain = hb.latest - - hb.lastCallbackID++ - callbackID := hb.lastCallbackID - hb.callbacks[callbackID] = callback - unsubscribe = func() { - hb.mutex.Lock() - defer hb.mutex.Unlock() - delete(hb.callbacks, callbackID) - } - - return -} - -func (hb *HeadBroadcaster[H, BLOCK_HASH]) run() { - defer hb.wgDone.Done() - - for { - select { - case <-hb.chClose: - return - case <-hb.mailbox.Notify(): - hb.executeCallbacks() - } - } -} - -// DEV: the head relayer makes no promises about head delivery! Subscribing -// Jobs should expect to the relayer to skip heads if there is a large number of listeners -// and all callbacks cannot be completed in the allotted time. -func (hb *HeadBroadcaster[H, BLOCK_HASH]) executeCallbacks() { - head, exists := hb.mailbox.Retrieve() - if !exists { - hb.logger.Info("No head to retrieve. It might have been skipped") - return - } - - hb.mutex.Lock() - callbacks := hb.callbacks.values() - hb.latest = head - hb.mutex.Unlock() - - hb.logger.Debugw("Initiating callbacks", - "headNum", head.BlockNumber(), - "numCallbacks", len(callbacks), - ) - - wg := sync.WaitGroup{} - wg.Add(len(callbacks)) - - ctx, cancel := hb.chClose.NewCtx() - defer cancel() - - for _, callback := range callbacks { - go func(trackable commontypes.HeadTrackable[H, BLOCK_HASH]) { - defer wg.Done() - start := time.Now() - cctx, cancel := context.WithTimeout(ctx, TrackableCallbackTimeout) - defer cancel() - trackable.OnNewLongestChain(cctx, head) - elapsed := time.Since(start) - hb.logger.Debugw(fmt.Sprintf("Finished callback in %s", elapsed), - "callbackType", reflect.TypeOf(trackable), "blockNumber", head.BlockNumber(), "time", elapsed) - }(callback) - } - - wg.Wait() -} diff --git a/pkg/common/headtracker/head_listener.go b/pkg/common/headtracker/head_listener.go deleted file mode 100644 index 0030afadc..000000000 --- a/pkg/common/headtracker/head_listener.go +++ /dev/null @@ -1,218 +0,0 @@ -package headtracker - -import ( - "context" - "sync/atomic" - "time" - - "github.com/pkg/errors" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" - - "github.com/smartcontractkit/chainlink-relay/pkg/logger" - "github.com/smartcontractkit/chainlink-relay/pkg/utils" - htrktypes "github.com/smartcontractkit/chainlink-solana/pkg/common/headtracker/types" - "github.com/smartcontractkit/chainlink-solana/pkg/common/types" -) - -var ( - promNumHeadsReceived = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "head_tracker_heads_received", - Help: "The total number of heads seen", - }, []string{"ChainID"}) - promEthConnectionErrors = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "head_tracker_connection_errors", - Help: "The total number of node connection errors", - }, []string{"ChainID"}) -) - -type HeadListener[ - HTH htrktypes.Head[BLOCK_HASH, ID], - S types.Subscription, - ID types.ID, - BLOCK_HASH types.Hashable, -] struct { - config htrktypes.Config - client htrktypes.Client[HTH, S, ID, BLOCK_HASH] - logger logger.Logger - chStop utils.StopChan - chHeaders chan HTH - headSubscription types.Subscription - connected atomic.Bool - receivingHeads atomic.Bool -} - -func NewHeadListener[ - HTH htrktypes.Head[BLOCK_HASH, ID], - S types.Subscription, - ID types.ID, - BLOCK_HASH types.Hashable, - CLIENT htrktypes.Client[HTH, S, ID, BLOCK_HASH], -]( - lggr logger.Logger, - client CLIENT, - config htrktypes.Config, - chStop chan struct{}, -) *HeadListener[HTH, S, ID, BLOCK_HASH] { - return &HeadListener[HTH, S, ID, BLOCK_HASH]{ - config: config, - client: client, - logger: logger.Named(lggr, "HeadListener"), - chStop: chStop, - } -} - -func (hl *HeadListener[HTH, S, ID, BLOCK_HASH]) Name() string { - return hl.logger.Name() -} - -func (hl *HeadListener[HTH, S, ID, BLOCK_HASH]) ListenForNewHeads(handleNewHead types.NewHeadHandler[HTH, BLOCK_HASH], done func()) { - defer done() - defer hl.unsubscribe() - - ctx, cancel := hl.chStop.NewCtx() - defer cancel() - - for { - if !hl.subscribe(ctx) { - break - } - err := hl.receiveHeaders(ctx, handleNewHead) - if ctx.Err() != nil { - break - } else if err != nil { - hl.logger.Errorw("Error in new head subscription, unsubscribed", "err", err) - continue - } else { - break - } - } -} - -func (hl *HeadListener[HTH, S, ID, BLOCK_HASH]) ReceivingHeads() bool { - return hl.receivingHeads.Load() -} - -func (hl *HeadListener[HTH, S, ID, BLOCK_HASH]) Connected() bool { - return hl.connected.Load() -} - -func (hl *HeadListener[HTH, S, ID, BLOCK_HASH]) HealthReport() map[string]error { - var err error - if !hl.ReceivingHeads() { - err = errors.New("Listener is not receiving heads") - } - if !hl.Connected() { - err = errors.New("Listener is not connected") - } - return map[string]error{hl.Name(): err} -} - -func (hl *HeadListener[HTH, S, ID, BLOCK_HASH]) receiveHeaders(ctx context.Context, handleNewHead types.NewHeadHandler[HTH, BLOCK_HASH]) error { - var noHeadsAlarmC <-chan time.Time - var noHeadsAlarmT *time.Ticker - noHeadsAlarmDuration := hl.config.BlockEmissionIdleWarningThreshold() - if noHeadsAlarmDuration > 0 { - noHeadsAlarmT = time.NewTicker(noHeadsAlarmDuration) - noHeadsAlarmC = noHeadsAlarmT.C - } - - for { - select { - case <-hl.chStop: - return nil - - case blockHeader, open := <-hl.chHeaders: - chainId := hl.client.ConfiguredChainID() - if noHeadsAlarmT != nil { - // We've received a head, reset the no heads alarm - noHeadsAlarmT.Stop() - noHeadsAlarmT = time.NewTicker(noHeadsAlarmDuration) - noHeadsAlarmC = noHeadsAlarmT.C - } - hl.receivingHeads.Store(true) - if !open { - return errors.New("head listener: chHeaders prematurely closed") - } - if !blockHeader.IsValid() { - hl.logger.Error("got nil block header") - continue - } - - // Compare the chain ID of the block header to the chain ID of the client - if !blockHeader.HasChainID() || blockHeader.ChainID().String() != chainId.String() { - hl.logger.Panicf("head listener for %s received block header for %s", chainId, blockHeader.ChainID()) - } - promNumHeadsReceived.WithLabelValues(chainId.String()).Inc() - - err := handleNewHead(ctx, blockHeader) - if ctx.Err() != nil { - return nil - } else if err != nil { - return err - } - - case err, open := <-hl.headSubscription.Err(): - // err can be nil, because of using chainIDSubForwarder - if !open || err == nil { - return errors.New("head listener: subscription Err channel prematurely closed") - } - return err - - case <-noHeadsAlarmC: - // We haven't received a head on the channel for a long time, log a warning - hl.logger.Warnf("have not received a head for %v", noHeadsAlarmDuration) - hl.receivingHeads.Store(false) - } - } -} - -func (hl *HeadListener[HTH, S, ID, BLOCK_HASH]) subscribe(ctx context.Context) bool { - subscribeRetryBackoff := utils.NewRedialBackoff() - - chainId := hl.client.ConfiguredChainID() - - for { - hl.unsubscribe() - - hl.logger.Debugf("Subscribing to new heads on chain %s", chainId.String()) - - select { - case <-hl.chStop: - return false - - case <-time.After(subscribeRetryBackoff.Duration()): - err := hl.subscribeToHead(ctx) - if err != nil { - promEthConnectionErrors.WithLabelValues(chainId.String()).Inc() - hl.logger.Warnw("Failed to subscribe to heads on chain", "chainID", chainId.String(), "err", err) - } else { - hl.logger.Debugf("Subscribed to heads on chain %s", chainId.String()) - return true - } - } - } -} - -func (hl *HeadListener[HTH, S, ID, BLOCK_HASH]) subscribeToHead(ctx context.Context) error { - hl.chHeaders = make(chan HTH) - - var err error - hl.headSubscription, err = hl.client.SubscribeNewHead(ctx, hl.chHeaders) - if err != nil { - close(hl.chHeaders) - return errors.Wrap(err, "Client#SubscribeNewHead") - } - - hl.connected.Store(true) - - return nil -} - -func (hl *HeadListener[HTH, S, ID, BLOCK_HASH]) unsubscribe() { - if hl.headSubscription != nil { - hl.connected.Store(false) - hl.headSubscription.Unsubscribe() - hl.headSubscription = nil - } -} diff --git a/pkg/common/headtracker/head_tracker.go b/pkg/common/headtracker/head_tracker.go deleted file mode 100644 index 4d7126fb1..000000000 --- a/pkg/common/headtracker/head_tracker.go +++ /dev/null @@ -1,359 +0,0 @@ -package headtracker - -import ( - "context" - "fmt" - "math/big" - "sync" - "time" - - "github.com/pkg/errors" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promauto" - "golang.org/x/exp/maps" - - "github.com/smartcontractkit/chainlink-relay/pkg/logger" - "github.com/smartcontractkit/chainlink-relay/pkg/utils" - "github.com/smartcontractkit/chainlink-solana/pkg/common/config" - htrktypes "github.com/smartcontractkit/chainlink-solana/pkg/common/headtracker/types" - "github.com/smartcontractkit/chainlink-solana/pkg/common/types" -) - -var ( - promCurrentHead = promauto.NewGaugeVec(prometheus.GaugeOpts{ - Name: "head_tracker_current_head", - Help: "The highest seen head number", - }, []string{"evmChainID"}) - - promOldHead = promauto.NewCounterVec(prometheus.CounterOpts{ - Name: "head_tracker_very_old_head", - Help: "Counter is incremented every time we get a head that is much lower than the highest seen head ('much lower' is defined as a block that is EVM.FinalityDepth or greater below the highest seen head)", - }, []string{"evmChainID"}) -) - -// HeadsBufferSize - The buffer is used when heads sampling is disabled, to ensure the callback is run for every head -const HeadsBufferSize = 10 - -type HeadTracker[ - HTH htrktypes.Head[BLOCK_HASH, ID], - S types.Subscription, - ID types.ID, - BLOCK_HASH types.Hashable, -] struct { - log logger.Logger - headBroadcaster types.HeadBroadcaster[HTH, BLOCK_HASH] - headSaver types.HeadSaver[HTH, BLOCK_HASH] - mailMon *utils.MailboxMonitor - client htrktypes.Client[HTH, S, ID, BLOCK_HASH] - chainID ID - config htrktypes.Config - - backfillMB *utils.Mailbox[HTH] - broadcastMB *utils.Mailbox[HTH] - headListener types.HeadListener[HTH, BLOCK_HASH] - chStop utils.StopChan - wgDone sync.WaitGroup - utils.StartStopOnce - getNilHead func() HTH -} - -// NewHeadTracker instantiates a new HeadTracker using HeadSaver to persist new block numbers. -func NewHeadTracker[ - HTH htrktypes.Head[BLOCK_HASH, ID], - S types.Subscription, - ID types.ID, - BLOCK_HASH types.Hashable, -]( - lggr logger.Logger, - client htrktypes.Client[HTH, S, ID, BLOCK_HASH], - config htrktypes.Config, - headBroadcaster types.HeadBroadcaster[HTH, BLOCK_HASH], - headSaver types.HeadSaver[HTH, BLOCK_HASH], - mailMon *utils.MailboxMonitor, - getNilHead func() HTH, -) types.HeadTracker[HTH, BLOCK_HASH] { - chStop := make(chan struct{}) - lggr = logger.Named(lggr, "HeadTracker") - return &HeadTracker[HTH, S, ID, BLOCK_HASH]{ - headBroadcaster: headBroadcaster, - client: client, - chainID: client.ConfiguredChainID(), - config: config, - log: lggr, - backfillMB: utils.NewSingleMailbox[HTH](), - broadcastMB: utils.NewMailbox[HTH](HeadsBufferSize), - chStop: chStop, - headListener: NewHeadListener[HTH, S, ID, BLOCK_HASH](lggr, client, config, chStop), - headSaver: headSaver, - mailMon: mailMon, - getNilHead: getNilHead, - } -} - -// Start starts HeadTracker service. -func (ht *HeadTracker[HTH, S, ID, BLOCK_HASH]) Start(ctx context.Context) error { - return ht.StartOnce("HeadTracker", func() error { - ht.log.Debugw("Starting HeadTracker", "chainID", ht.chainID) - latestChain, err := ht.headSaver.Load(ctx) - if err != nil { - return err - } - if latestChain.IsValid() { - ht.log.Debugw( - fmt.Sprintf("HeadTracker: Tracking logs from last block %v with hash %s", config.FriendlyNumber(latestChain.BlockNumber()), latestChain.BlockHash()), - "blockNumber", latestChain.BlockNumber(), - "blockHash", latestChain.BlockHash(), - ) - } - - // NOTE: Always try to start the head tracker off with whatever the - // latest head is, without waiting for the subscription to send us one. - // - // In some cases the subscription will send us the most recent head - // anyway when we connect (but we should not rely on this because it is - // not specced). If it happens this is fine, and the head will be - // ignored as a duplicate. - initialHead, err := ht.getInitialHead(ctx) - if err != nil { - if errors.Is(err, ctx.Err()) { - return nil - } - ht.log.Errorw("Error getting initial head", "err", err) - } else if initialHead.IsValid() { - if err := ht.handleNewHead(ctx, initialHead); err != nil { - return errors.Wrap(err, "error handling initial head") - } - } else { - ht.log.Debug("Got nil initial head") - } - - ht.wgDone.Add(3) - go ht.headListener.ListenForNewHeads(ht.handleNewHead, ht.wgDone.Done) - go ht.backfillLoop() - go ht.broadcastLoop() - - ht.mailMon.Monitor(ht.broadcastMB, "HeadTracker", "Broadcast", ht.chainID.String()) - - return nil - }) -} - -// Close stops HeadTracker service. -func (ht *HeadTracker[HTH, S, ID, BLOCK_HASH]) Close() error { - return ht.StopOnce("HeadTracker", func() error { - close(ht.chStop) - ht.wgDone.Wait() - return ht.broadcastMB.Close() - }) -} - -func (ht *HeadTracker[HTH, S, ID, BLOCK_HASH]) Name() string { - return ht.log.Name() -} - -func (ht *HeadTracker[HTH, S, ID, BLOCK_HASH]) HealthReport() map[string]error { - report := map[string]error{ - ht.Name(): ht.StartStopOnce.Healthy(), - } - maps.Copy(report, ht.headListener.HealthReport()) - return report -} - -func (ht *HeadTracker[HTH, S, ID, BLOCK_HASH]) Backfill(ctx context.Context, headWithChain HTH, depth uint) (err error) { - if uint(headWithChain.ChainLength()) >= depth { - return nil - } - - baseHeight := headWithChain.BlockNumber() - int64(depth-1) - if baseHeight < 0 { - baseHeight = 0 - } - - return ht.backfill(ctx, headWithChain.EarliestHeadInChain(), baseHeight) -} - -func (ht *HeadTracker[HTH, S, ID, BLOCK_HASH]) LatestChain() HTH { - return ht.headSaver.LatestChain() -} - -func (ht *HeadTracker[HTH, S, ID, BLOCK_HASH]) getInitialHead(ctx context.Context) (HTH, error) { - head, err := ht.client.HeadByNumber(ctx, nil) - if err != nil { - return ht.getNilHead(), errors.Wrap(err, "failed to fetch initial head") - } - loggerFields := []interface{}{"head", head} - if head.IsValid() { - loggerFields = append(loggerFields, "blockNumber", head.BlockNumber(), "blockHash", head.BlockHash()) - } - ht.log.Debugw("Got initial head", loggerFields...) - return head, nil -} - -func (ht *HeadTracker[HTH, S, ID, BLOCK_HASH]) handleNewHead(ctx context.Context, head HTH) error { - prevHead := ht.headSaver.LatestChain() - - ht.log.Debugw(fmt.Sprintf("Received new head %v", config.FriendlyNumber(head.BlockNumber())), - "blockHeight", head.BlockNumber(), - "blockHash", head.BlockHash(), - "parentHeadHash", head.GetParentHash(), - ) - - err := ht.headSaver.Save(ctx, head) - if ctx.Err() != nil { - return nil - } else if err != nil { - return errors.Wrapf(err, "failed to save head: %#v", head) - } - - if !prevHead.IsValid() || head.BlockNumber() > prevHead.BlockNumber() { - promCurrentHead.WithLabelValues(ht.chainID.String()).Set(float64(head.BlockNumber())) - - headWithChain := ht.headSaver.Chain(head.BlockHash()) - if !headWithChain.IsValid() { - return errors.Errorf("HeadTracker#handleNewHighestHead headWithChain was unexpectedly nil") - } - ht.backfillMB.Deliver(headWithChain) - ht.broadcastMB.Deliver(headWithChain) - } else if head.BlockNumber() == prevHead.BlockNumber() { - if head.BlockHash() != prevHead.BlockHash() { - ht.log.Debugw("Got duplicate head", "blockNum", head.BlockNumber(), "head", head.BlockHash(), "prevHead", prevHead.BlockHash()) - } else { - ht.log.Debugw("Head already in the database", "head", head.BlockHash()) - } - } else { - ht.log.Debugw("Got out of order head", "blockNum", head.BlockNumber(), "head", head.BlockHash(), "prevHead", prevHead.BlockNumber()) - prevUnFinalizedHead := prevHead.BlockNumber() - int64(ht.config.FinalityDepth()) - if head.BlockNumber() < prevUnFinalizedHead { - promOldHead.WithLabelValues(ht.chainID.String()).Inc() - ht.log.Criticalf("Got very old block with number %d (highest seen was %d). This is a problem and either means a very deep re-org occurred, one of the RPC nodes has gotten far out of sync, or the chain went backwards in block numbers. This node may not function correctly without manual intervention.", head.BlockNumber(), prevHead.BlockNumber()) - ht.SvcErrBuffer.Append(errors.New("got very old block")) - } - } - return nil -} - -func (ht *HeadTracker[HTH, S, ID, BLOCK_HASH]) broadcastLoop() { - defer ht.wgDone.Done() - - samplingInterval := ht.config.HeadTrackerSamplingInterval() - if samplingInterval > 0 { - ht.log.Debugf("Head sampling is enabled - sampling interval is set to: %v", samplingInterval) - debounceHead := time.NewTicker(samplingInterval) - defer debounceHead.Stop() - for { - select { - case <-ht.chStop: - return - case <-debounceHead.C: - item := ht.broadcastMB.RetrieveLatestAndClear() - if !item.IsValid() { - continue - } - ht.headBroadcaster.BroadcastNewLongestChain(item) - } - } - } else { - ht.log.Info("Head sampling is disabled - callback will be called on every head") - for { - select { - case <-ht.chStop: - return - case <-ht.broadcastMB.Notify(): - for { - item, exists := ht.broadcastMB.Retrieve() - if !exists { - break - } - ht.headBroadcaster.BroadcastNewLongestChain(item) - } - } - } - } -} - -func (ht *HeadTracker[HTH, S, ID, BLOCK_HASH]) backfillLoop() { - defer ht.wgDone.Done() - - ctx, cancel := ht.chStop.NewCtx() - defer cancel() - - for { - select { - case <-ht.chStop: - return - case <-ht.backfillMB.Notify(): - for { - head, exists := ht.backfillMB.Retrieve() - if !exists { - break - } - { - err := ht.Backfill(ctx, head, uint(ht.config.FinalityDepth())) - if err != nil { - ht.log.Warnw("Unexpected error while backfilling heads", "err", err) - } else if ctx.Err() != nil { - break - } - } - } - } - } -} - -// backfill fetches all missing heads up until the base height -func (ht *HeadTracker[HTH, S, ID, BLOCK_HASH]) backfill(ctx context.Context, head types.Head[BLOCK_HASH], baseHeight int64) (err error) { - headBlockNumber := head.BlockNumber() - if headBlockNumber <= baseHeight { - return nil - } - mark := time.Now() - fetched := 0 - l := logger.With(ht.log, "blockNumber", headBlockNumber, - "n", headBlockNumber-baseHeight, - "fromBlockHeight", baseHeight, - "toBlockHeight", headBlockNumber-1) - l.Debug("Starting backfill") - defer func() { - if ctx.Err() != nil { - l.Warnw("Backfill context error", "err", ctx.Err()) - return - } - l.Debugw("Finished backfill", - "fetched", fetched, - "time", time.Since(mark), - "err", err) - }() - - for i := head.BlockNumber() - 1; i >= baseHeight; i-- { - // NOTE: Sequential requests here mean it's a potential performance bottleneck, be aware! - existingHead := ht.headSaver.Chain(head.GetParentHash()) - if existingHead.IsValid() { - head = existingHead - continue - } - head, err = ht.fetchAndSaveHead(ctx, i) - fetched++ - if ctx.Err() != nil { - ht.log.Debugw("context canceled, aborting backfill", "err", err, "ctx.Err", ctx.Err()) - break - } else if err != nil { - return errors.Wrap(err, "fetchAndSaveHead failed") - } - } - return -} - -func (ht *HeadTracker[HTH, S, ID, BLOCK_HASH]) fetchAndSaveHead(ctx context.Context, n int64) (HTH, error) { - ht.log.Debugw("Fetching head", "blockHeight", n) - head, err := ht.client.HeadByNumber(ctx, big.NewInt(n)) - if err != nil { - return ht.getNilHead(), err - } else if !head.IsValid() { - return ht.getNilHead(), errors.New("got nil head") - } - err = ht.headSaver.Save(ctx, head) - if err != nil { - return ht.getNilHead(), err - } - return head, nil -} diff --git a/pkg/common/headtracker/types/client.go b/pkg/common/headtracker/types/client.go deleted file mode 100644 index 640014362..000000000 --- a/pkg/common/headtracker/types/client.go +++ /dev/null @@ -1,17 +0,0 @@ -package headtracker - -import ( - "context" - "math/big" - - "github.com/smartcontractkit/chainlink-solana/pkg/common/types" -) - -type Client[H types.Head[BLOCK_HASH], S types.Subscription, ID types.ID, BLOCK_HASH types.Hashable] interface { - HeadByNumber(ctx context.Context, number *big.Int) (head H, err error) - // ConfiguredChainID returns the chain ID that the node is configured to connect to - ConfiguredChainID() (id ID) - // SubscribeNewHead is the method in which the client receives new Head. - // It can be implemented differently for each chain i.e websocket, polling, etc - SubscribeNewHead(ctx context.Context, ch chan<- H) (S, error) -} diff --git a/pkg/common/headtracker/types/config.go b/pkg/common/headtracker/types/config.go deleted file mode 100644 index 6c7e608c5..000000000 --- a/pkg/common/headtracker/types/config.go +++ /dev/null @@ -1,11 +0,0 @@ -package headtracker - -import "time" - -type Config interface { - BlockEmissionIdleWarningThreshold() time.Duration - FinalityDepth() uint32 - HeadTrackerHistoryDepth() uint32 - HeadTrackerMaxBufferSize() uint32 - HeadTrackerSamplingInterval() time.Duration -} diff --git a/pkg/common/headtracker/types/head.go b/pkg/common/headtracker/types/head.go deleted file mode 100644 index 9d9c383f5..000000000 --- a/pkg/common/headtracker/types/head.go +++ /dev/null @@ -1,16 +0,0 @@ -package headtracker - -import ( - "github.com/smartcontractkit/chainlink-solana/pkg/common/types" -) - -//go:generate mockery --quiet --name Head --output ./mocks/ --case=underscore -type Head[BLOCK_HASH types.Hashable, CHAIN_ID types.ID] interface { - types.Head[BLOCK_HASH] - // ChainID returns the chain ID that the head is for - ChainID() CHAIN_ID - // Returns true if the head has a chain Id - HasChainID() bool - // IsValid returns true if the head is valid. - IsValid() bool -} diff --git a/pkg/common/headtracker/types/mocks/head.go b/pkg/common/headtracker/types/mocks/head.go deleted file mode 100644 index 979c3899c..000000000 --- a/pkg/common/headtracker/types/mocks/head.go +++ /dev/null @@ -1,172 +0,0 @@ -// Code generated by mockery v2.28.1. DO NOT EDIT. - -package mocks - -import ( - types "github.com/smartcontractkit/chainlink-solana/pkg/common/types" - mock "github.com/stretchr/testify/mock" -) - -// Head is an autogenerated mock type for the Head type -type Head[BLOCK_HASH types.Hashable, CHAIN_ID types.ID] struct { - mock.Mock -} - -// BlockHash provides a mock function with given fields: -func (_m *Head[BLOCK_HASH, CHAIN_ID]) BlockHash() BLOCK_HASH { - ret := _m.Called() - - var r0 BLOCK_HASH - if rf, ok := ret.Get(0).(func() BLOCK_HASH); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(BLOCK_HASH) - } - - return r0 -} - -// BlockNumber provides a mock function with given fields: -func (_m *Head[BLOCK_HASH, CHAIN_ID]) BlockNumber() int64 { - ret := _m.Called() - - var r0 int64 - if rf, ok := ret.Get(0).(func() int64); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(int64) - } - - return r0 -} - -// ChainID provides a mock function with given fields: -func (_m *Head[BLOCK_HASH, CHAIN_ID]) ChainID() CHAIN_ID { - ret := _m.Called() - - var r0 CHAIN_ID - if rf, ok := ret.Get(0).(func() CHAIN_ID); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(CHAIN_ID) - } - - return r0 -} - -// ChainLength provides a mock function with given fields: -func (_m *Head[BLOCK_HASH, CHAIN_ID]) ChainLength() uint32 { - ret := _m.Called() - - var r0 uint32 - if rf, ok := ret.Get(0).(func() uint32); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(uint32) - } - - return r0 -} - -// EarliestHeadInChain provides a mock function with given fields: -func (_m *Head[BLOCK_HASH, CHAIN_ID]) EarliestHeadInChain() types.Head[BLOCK_HASH] { - ret := _m.Called() - - var r0 types.Head[BLOCK_HASH] - if rf, ok := ret.Get(0).(func() types.Head[BLOCK_HASH]); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(types.Head[BLOCK_HASH]) - } - } - - return r0 -} - -// GetParent provides a mock function with given fields: -func (_m *Head[BLOCK_HASH, CHAIN_ID]) GetParent() types.Head[BLOCK_HASH] { - ret := _m.Called() - - var r0 types.Head[BLOCK_HASH] - if rf, ok := ret.Get(0).(func() types.Head[BLOCK_HASH]); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(types.Head[BLOCK_HASH]) - } - } - - return r0 -} - -// GetParentHash provides a mock function with given fields: -func (_m *Head[BLOCK_HASH, CHAIN_ID]) GetParentHash() BLOCK_HASH { - ret := _m.Called() - - var r0 BLOCK_HASH - if rf, ok := ret.Get(0).(func() BLOCK_HASH); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(BLOCK_HASH) - } - - return r0 -} - -// HasChainID provides a mock function with given fields: -func (_m *Head[BLOCK_HASH, CHAIN_ID]) HasChainID() bool { - ret := _m.Called() - - var r0 bool - if rf, ok := ret.Get(0).(func() bool); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(bool) - } - - return r0 -} - -// HashAtHeight provides a mock function with given fields: blockNum -func (_m *Head[BLOCK_HASH, CHAIN_ID]) HashAtHeight(blockNum int64) BLOCK_HASH { - ret := _m.Called(blockNum) - - var r0 BLOCK_HASH - if rf, ok := ret.Get(0).(func(int64) BLOCK_HASH); ok { - r0 = rf(blockNum) - } else { - r0 = ret.Get(0).(BLOCK_HASH) - } - - return r0 -} - -// IsValid provides a mock function with given fields: -func (_m *Head[BLOCK_HASH, CHAIN_ID]) IsValid() bool { - ret := _m.Called() - - var r0 bool - if rf, ok := ret.Get(0).(func() bool); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(bool) - } - - return r0 -} - -type mockConstructorTestingTNewHead interface { - mock.TestingT - Cleanup(func()) -} - -// NewHead creates a new instance of Head. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewHead[BLOCK_HASH types.Hashable, CHAIN_ID types.ID](t mockConstructorTestingTNewHead) *Head[BLOCK_HASH, CHAIN_ID] { - mock := &Head[BLOCK_HASH, CHAIN_ID]{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/common/types/chain.go b/pkg/common/types/chain.go deleted file mode 100644 index d9d717bb9..000000000 --- a/pkg/common/types/chain.go +++ /dev/null @@ -1,7 +0,0 @@ -package types - -import "fmt" - -// ID represents the base type, for any chain's ID. -// It should be convertible to a string, that can uniquely identify this chain -type ID fmt.Stringer diff --git a/pkg/common/types/hashable.go b/pkg/common/types/hashable.go deleted file mode 100644 index 2d166505b..000000000 --- a/pkg/common/types/hashable.go +++ /dev/null @@ -1,12 +0,0 @@ -package types - -import "fmt" - -// A chain-agnostic generic interface to represent the following native types on various chains: -// PublicKey, Address, Account, BlockHash, TxHash -type Hashable interface { - fmt.Stringer - comparable - - Bytes() []byte -} diff --git a/pkg/common/types/head.go b/pkg/common/types/head.go deleted file mode 100644 index 4d339b1cd..000000000 --- a/pkg/common/types/head.go +++ /dev/null @@ -1,27 +0,0 @@ -package types - -// Head provides access to a chain's head, as needed by the TxManager. -// This is a generic interface which ALL chains will implement. -// -//go:generate mockery --quiet --name Head --output ./mocks/ --case=underscore -type Head[BLOCK_HASH Hashable] interface { - // BlockNumber is the head's block number - BlockNumber() int64 - - // ChainLength returns the length of the chain followed by recursively looking up parents - ChainLength() uint32 - - // EarliestHeadInChain traverses through parents until it finds the earliest one - EarliestHeadInChain() Head[BLOCK_HASH] - - // Parent is the head's parent block - GetParent() Head[BLOCK_HASH] - - // Hash is the head's block hash - BlockHash() BLOCK_HASH - GetParentHash() BLOCK_HASH - - // HashAtHeight returns the hash of the block at the given height, if it is in the chain. - // If not in chain, returns the zero hash - HashAtHeight(blockNum int64) BLOCK_HASH -} diff --git a/pkg/common/types/head_tracker.go b/pkg/common/types/head_tracker.go deleted file mode 100644 index 510933e30..000000000 --- a/pkg/common/types/head_tracker.go +++ /dev/null @@ -1,68 +0,0 @@ -package types - -import ( - "context" - - "github.com/smartcontractkit/chainlink-relay/pkg/types" -) - -//go:generate mockery --quiet --name HeadTracker --output ../mocks/ --case=underscore -type HeadTracker[H Head[BLOCK_HASH], BLOCK_HASH Hashable] interface { - types.Service - // Backfill given a head will fill in any missing heads up to the given depth - // (used for testing) - Backfill(ctx context.Context, headWithChain H, depth uint) (err error) - LatestChain() H -} - -// HeadTrackable is implemented by the core txm, -// to be able to receive head events from any chain. -// Chain implementations should notify head events to the core txm via this interface. -// -//go:generate mockery --quiet --name HeadTrackable --output ./mocks/ --case=underscore -type HeadTrackable[H Head[BLOCK_HASH], BLOCK_HASH Hashable] interface { - OnNewLongestChain(ctx context.Context, head H) -} - -// HeadSaver is an chain agnostic interface for saving and loading heads -// Different chains will instantiate generic HeadSaver type with their native Head and BlockHash types. -type HeadSaver[H Head[BLOCK_HASH], BLOCK_HASH Hashable] interface { - // Save updates the latest block number, if indeed the latest, and persists - // this number in case of reboot. - Save(ctx context.Context, head H) error - // Load loads latest EvmHeadTrackerHistoryDepth heads, returns the latest chain. - Load(ctx context.Context) (H, error) - // LatestChain returns the block header with the highest number that has been seen, or nil. - LatestChain() H - // Chain returns a head for the specified hash, or nil. - Chain(hash BLOCK_HASH) H -} - -// HeadListener is a chain agnostic interface that manages connection of Client that receives heads from the blockchain node -type HeadListener[H Head[BLOCK_HASH], BLOCK_HASH Hashable] interface { - // ListenForNewHeads kicks off the listen loop (not thread safe) - // done() must be executed upon leaving ListenForNewHeads() - ListenForNewHeads(handleNewHead NewHeadHandler[H, BLOCK_HASH], done func()) - - // ReceivingHeads returns true if the listener is receiving heads (thread safe) - ReceivingHeads() bool - - // Connected returns true if the listener is connected (thread safe) - Connected() bool - - // HealthReport returns report of errors within HeadListener - HealthReport() map[string]error -} - -// NewHeadHandler is a callback that handles incoming heads -type NewHeadHandler[H Head[BLOCK_HASH], BLOCK_HASH Hashable] func(ctx context.Context, header H) error - -type HeadBroadcaster[H Head[BLOCK_HASH], BLOCK_HASH Hashable] interface { - types.Service - BroadcastNewLongestChain(H) - HeadBroadcasterRegistry[H, BLOCK_HASH] -} - -type HeadBroadcasterRegistry[H Head[BLOCK_HASH], BLOCK_HASH Hashable] interface { - Subscribe(callback HeadTrackable[H, BLOCK_HASH]) (currentLongestChain H, unsubscribe func()) -} diff --git a/pkg/common/types/mocks/head.go b/pkg/common/types/mocks/head.go deleted file mode 100644 index 0567bfca4..000000000 --- a/pkg/common/types/mocks/head.go +++ /dev/null @@ -1,130 +0,0 @@ -// Code generated by mockery v2.28.1. DO NOT EDIT. - -package mocks - -import ( - types "github.com/smartcontractkit/chainlink-solana/pkg/common/types" - mock "github.com/stretchr/testify/mock" -) - -// Head is an autogenerated mock type for the Head type -type Head[BLOCK_HASH types.Hashable] struct { - mock.Mock -} - -// BlockHash provides a mock function with given fields: -func (_m *Head[BLOCK_HASH]) BlockHash() BLOCK_HASH { - ret := _m.Called() - - var r0 BLOCK_HASH - if rf, ok := ret.Get(0).(func() BLOCK_HASH); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(BLOCK_HASH) - } - - return r0 -} - -// BlockNumber provides a mock function with given fields: -func (_m *Head[BLOCK_HASH]) BlockNumber() int64 { - ret := _m.Called() - - var r0 int64 - if rf, ok := ret.Get(0).(func() int64); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(int64) - } - - return r0 -} - -// ChainLength provides a mock function with given fields: -func (_m *Head[BLOCK_HASH]) ChainLength() uint32 { - ret := _m.Called() - - var r0 uint32 - if rf, ok := ret.Get(0).(func() uint32); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(uint32) - } - - return r0 -} - -// EarliestHeadInChain provides a mock function with given fields: -func (_m *Head[BLOCK_HASH]) EarliestHeadInChain() types.Head[BLOCK_HASH] { - ret := _m.Called() - - var r0 types.Head[BLOCK_HASH] - if rf, ok := ret.Get(0).(func() types.Head[BLOCK_HASH]); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(types.Head[BLOCK_HASH]) - } - } - - return r0 -} - -// GetParent provides a mock function with given fields: -func (_m *Head[BLOCK_HASH]) GetParent() types.Head[BLOCK_HASH] { - ret := _m.Called() - - var r0 types.Head[BLOCK_HASH] - if rf, ok := ret.Get(0).(func() types.Head[BLOCK_HASH]); ok { - r0 = rf() - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(types.Head[BLOCK_HASH]) - } - } - - return r0 -} - -// GetParentHash provides a mock function with given fields: -func (_m *Head[BLOCK_HASH]) GetParentHash() BLOCK_HASH { - ret := _m.Called() - - var r0 BLOCK_HASH - if rf, ok := ret.Get(0).(func() BLOCK_HASH); ok { - r0 = rf() - } else { - r0 = ret.Get(0).(BLOCK_HASH) - } - - return r0 -} - -// HashAtHeight provides a mock function with given fields: blockNum -func (_m *Head[BLOCK_HASH]) HashAtHeight(blockNum int64) BLOCK_HASH { - ret := _m.Called(blockNum) - - var r0 BLOCK_HASH - if rf, ok := ret.Get(0).(func(int64) BLOCK_HASH); ok { - r0 = rf(blockNum) - } else { - r0 = ret.Get(0).(BLOCK_HASH) - } - - return r0 -} - -type mockConstructorTestingTNewHead interface { - mock.TestingT - Cleanup(func()) -} - -// NewHead creates a new instance of Head. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewHead[BLOCK_HASH types.Hashable](t mockConstructorTestingTNewHead) *Head[BLOCK_HASH] { - mock := &Head[BLOCK_HASH]{} - mock.Mock.Test(t) - - t.Cleanup(func() { mock.AssertExpectations(t) }) - - return mock -} diff --git a/pkg/common/types/subscription.go b/pkg/common/types/subscription.go deleted file mode 100644 index 99247107b..000000000 --- a/pkg/common/types/subscription.go +++ /dev/null @@ -1,17 +0,0 @@ -package types - -// Subscription represents an event subscription where events are -// delivered on a data channel. -// This is a generic interface for Subscription to represent used by clients. - -//go:generate mockery --quiet --name Subscription --output ./mocks/ --case=underscore -type Subscription interface { - // Unsubscribe cancels the sending of events to the data channel - // and closes the error channel. - Unsubscribe() - // Err returns the subscription error channel. The error channel receives - // a value if there is an issue with the subscription (e.g. the network connection - // delivering the events has been closed). Only one value will ever be sent. - // The error channel is closed by Unsubscribe. - Err() <-chan error -} diff --git a/pkg/solana/client/subscription.go b/pkg/solana/client/subscription.go index 34059c953..4e6323cc0 100644 --- a/pkg/solana/client/subscription.go +++ b/pkg/solana/client/subscription.go @@ -3,7 +3,7 @@ package client import ( "context" - commontypes "github.com/smartcontractkit/chainlink-solana/pkg/common/types" + commontypes "github.com/smartcontractkit/chainlink-relay/pkg/types" ) var _ commontypes.Subscription = (*Subscription)(nil) From bb012ed3faba8f2fc76394858f18dea28ab68b93 Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Thu, 22 Jun 2023 10:26:32 -0700 Subject: [PATCH 23/47] update go mod dependency for chainlink-relay --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 0e3c47e97..a88b8cdc4 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/google/uuid v1.3.0 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.15.0 - github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230609013816-dc5d37505ce4 + github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230621184141-6c65ae346995 github.com/smartcontractkit/libocr v0.0.0-20230606215712-82b910bef5c1 github.com/stretchr/testify v1.8.4 github.com/test-go/testify v1.1.4 diff --git a/go.sum b/go.sum index 597bf1d6a..709550186 100644 --- a/go.sum +++ b/go.sum @@ -374,6 +374,8 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230609013816-dc5d37505ce4 h1:aBsArskduVo/P7GuMD09Btv7Iyky76PZN1u51EBTJXQ= github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230609013816-dc5d37505ce4/go.mod h1:MfZBUifutkv3aK7abyw5YmTJbqt8iFwcQDFikrxC/uI= +github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230621184141-6c65ae346995 h1:uFh1ND4YG/Qontbp6rT+Tf/zoB2pzHQ660SPHz8APXg= +github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230621184141-6c65ae346995/go.mod h1:MfZBUifutkv3aK7abyw5YmTJbqt8iFwcQDFikrxC/uI= github.com/smartcontractkit/libocr v0.0.0-20230606215712-82b910bef5c1 h1:caG9BWjnCxN/HPBA5ltDGadDraZAsjGIct4S8lh8D5c= github.com/smartcontractkit/libocr v0.0.0-20230606215712-82b910bef5c1/go.mod h1:2lyRkw/qLQgUWlrWWmq5nj0y90rWeO6Y+v+fCakRgb0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= From 607e7b641f4362a271146671db84e6d194433236 Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Thu, 22 Jun 2023 10:48:32 -0700 Subject: [PATCH 24/47] improve package naming --- pkg/solana/headtracker/head_saver_in_mem.go | 20 +++---- pkg/solana/headtracker/types/chain.go | 2 +- pkg/solana/headtracker/types/hash.go | 2 +- pkg/solana/headtracker/types/hash_test.go | 2 +- pkg/solana/headtracker/types/head.go | 2 +- pkg/solana/headtracker/types/head_test.go | 61 ++++++++++----------- 6 files changed, 44 insertions(+), 45 deletions(-) diff --git a/pkg/solana/headtracker/head_saver_in_mem.go b/pkg/solana/headtracker/head_saver_in_mem.go index 825350b62..fe5b9bdc8 100644 --- a/pkg/solana/headtracker/head_saver_in_mem.go +++ b/pkg/solana/headtracker/head_saver_in_mem.go @@ -7,11 +7,11 @@ import ( htrktypes "github.com/smartcontractkit/chainlink-relay/pkg/headtracker/types" "github.com/smartcontractkit/chainlink-relay/pkg/logger" - "github.com/smartcontractkit/chainlink-relay/pkg/types" - headtracker "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" + commontypes "github.com/smartcontractkit/chainlink-relay/pkg/types" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" ) -type inMemoryHeadSaver[H htrktypes.Head[BLOCK_HASH, CHAIN_ID], BLOCK_HASH types.Hashable, CHAIN_ID types.ID] struct { +type inMemoryHeadSaver[H htrktypes.Head[BLOCK_HASH, CHAIN_ID], BLOCK_HASH commontypes.Hashable, CHAIN_ID commontypes.ID] struct { config htrktypes.Config logger logger.Logger latestHead H @@ -22,14 +22,14 @@ type inMemoryHeadSaver[H htrktypes.Head[BLOCK_HASH, CHAIN_ID], BLOCK_HASH types. setParent func(H, H) } -type HeadSaver = inMemoryHeadSaver[*headtracker.Head, headtracker.Hash, headtracker.ChainID] +type HeadSaver = inMemoryHeadSaver[*types.Head, types.Hash, types.ChainID] -var _ types.HeadSaver[*headtracker.Head, headtracker.Hash] = (*HeadSaver)(nil) +var _ commontypes.HeadSaver[*types.Head, types.Hash] = (*HeadSaver)(nil) func NewInMemoryHeadSaver[ H htrktypes.Head[BLOCK_HASH, CHAIN_ID], - BLOCK_HASH types.Hashable, - CHAIN_ID types.ID]( + BLOCK_HASH commontypes.Hashable, + CHAIN_ID commontypes.ID]( config htrktypes.Config, lggr logger.Logger, getNilHead func() H, @@ -47,11 +47,11 @@ func NewInMemoryHeadSaver[ // Creates a new In Memory HeadSaver for solana func NewHeadSaver(config htrktypes.Config, lggr logger.Logger) *HeadSaver { - return NewInMemoryHeadSaver[*headtracker.Head, headtracker.Hash, headtracker.ChainID]( + return NewInMemoryHeadSaver[*types.Head, types.Hash, types.ChainID]( config, lggr, - func() *headtracker.Head { return nil }, - func(head, parent *headtracker.Head) { head.Parent = parent }, + func() *types.Head { return nil }, + func(head, parent *types.Head) { head.Parent = parent }, ) } diff --git a/pkg/solana/headtracker/types/chain.go b/pkg/solana/headtracker/types/chain.go index fc56bbb10..1728273c9 100644 --- a/pkg/solana/headtracker/types/chain.go +++ b/pkg/solana/headtracker/types/chain.go @@ -1,4 +1,4 @@ -package headtracker +package types type ChainID int diff --git a/pkg/solana/headtracker/types/hash.go b/pkg/solana/headtracker/types/hash.go index 373529921..152cea45c 100644 --- a/pkg/solana/headtracker/types/hash.go +++ b/pkg/solana/headtracker/types/hash.go @@ -1,4 +1,4 @@ -package headtracker +package types import ( "github.com/gagliardetto/solana-go" diff --git a/pkg/solana/headtracker/types/hash_test.go b/pkg/solana/headtracker/types/hash_test.go index 116927bfc..c619633fd 100644 --- a/pkg/solana/headtracker/types/hash_test.go +++ b/pkg/solana/headtracker/types/hash_test.go @@ -1,4 +1,4 @@ -package headtracker +package types import ( "bytes" diff --git a/pkg/solana/headtracker/types/head.go b/pkg/solana/headtracker/types/head.go index 6d2ca929d..bde9042c3 100644 --- a/pkg/solana/headtracker/types/head.go +++ b/pkg/solana/headtracker/types/head.go @@ -1,4 +1,4 @@ -package headtracker +package types import ( "github.com/gagliardetto/solana-go" diff --git a/pkg/solana/headtracker/types/head_test.go b/pkg/solana/headtracker/types/head_test.go index 7a44ffb39..5db1219ad 100644 --- a/pkg/solana/headtracker/types/head_test.go +++ b/pkg/solana/headtracker/types/head_test.go @@ -1,4 +1,4 @@ -package headtracker_test +package types import ( "strconv" @@ -7,7 +7,6 @@ import ( "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" "github.com/smartcontractkit/chainlink-solana/pkg/internal/utils" - headtracker "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" "github.com/stretchr/testify/assert" ) @@ -32,28 +31,28 @@ func TestHead_NewHead(t *testing.T) { tests := []struct { slot int64 block rpc.GetBlockResult - parent *headtracker.Head - id headtracker.ChainID + parent *Head + id ChainID wantSlot int64 }{ // with no parent - {10, emptyBlockResult, nil, headtracker.Mainnet, 10}, + {10, emptyBlockResult, nil, Mainnet, 10}, // with parent {20, emptyBlockResult, - headtracker.NewHead(10, emptyBlockResult, nil, headtracker.Mainnet), - headtracker.Mainnet, 20}, + NewHead(10, emptyBlockResult, nil, Mainnet), + Mainnet, 20}, {30, emptyBlockResult, - headtracker.NewHead(20, emptyBlockResult, - headtracker.NewHead(10, emptyBlockResult, nil, headtracker.Mainnet), - headtracker.Mainnet), - headtracker.Mainnet, 30}, + NewHead(20, emptyBlockResult, + NewHead(10, emptyBlockResult, nil, Mainnet), + Mainnet), + Mainnet, 30}, } for _, test := range tests { t.Run( strconv.FormatInt(test.wantSlot, 10), // convert to base 10 func(t *testing.T) { - head := headtracker.NewHead(test.slot, test.block, test.parent, test.id) + head := NewHead(test.slot, test.block, test.parent, test.id) assert.Equal(t, test.wantSlot, head.Slot) assert.Equal(t, test.block, head.Block) assert.Equal(t, test.parent, head.Parent) @@ -64,45 +63,45 @@ func TestHead_NewHead(t *testing.T) { func TestHead_ChainLength(t *testing.T) { blockResult := configureBlockResult() - id := headtracker.Mainnet + id := Mainnet - head := headtracker.NewHead(0, blockResult, headtracker.NewHead(0, blockResult, headtracker.NewHead(0, blockResult, nil, id), id), id) + head := NewHead(0, blockResult, NewHead(0, blockResult, NewHead(0, blockResult, nil, id), id), id) assert.Equal(t, uint32(3), head.ChainLength()) - var head2 *headtracker.Head + var head2 *Head assert.Equal(t, uint32(0), head2.ChainLength()) } func TestHead_EarliestHeadInChain(t *testing.T) { blockResult := configureBlockResult() - id := headtracker.Mainnet + id := Mainnet - head := headtracker.NewHead(3, blockResult, - headtracker.NewHead(2, blockResult, - headtracker.NewHead(1, blockResult, nil, id), id), id) + head := NewHead(3, blockResult, + NewHead(2, blockResult, + NewHead(1, blockResult, nil, id), id), id) assert.Equal(t, int64(1), head.EarliestHeadInChain().BlockNumber()) } func TestHead_GetParentHash(t *testing.T) { blockResult := configureBlockResult() - id := headtracker.Mainnet + id := Mainnet - head := headtracker.NewHead(3, blockResult, - headtracker.NewHead(2, blockResult, - headtracker.NewHead(1, blockResult, nil, id), id), id) + head := NewHead(3, blockResult, + NewHead(2, blockResult, + NewHead(1, blockResult, nil, id), id), id) assert.Equal(t, head.Parent.BlockHash(), head.GetParentHash()) } func TestHead_GetParent(t *testing.T) { blockResult := configureBlockResult() - id := headtracker.Mainnet + id := Mainnet - head := headtracker.NewHead(3, blockResult, - headtracker.NewHead(2, blockResult, - headtracker.NewHead(1, blockResult, nil, id), id), id) + head := NewHead(3, blockResult, + NewHead(2, blockResult, + NewHead(1, blockResult, nil, id), id), id) assert.Equal(t, head.Parent, head.GetParent()) } @@ -113,12 +112,12 @@ func TestHead_HasChainID(t *testing.T) { tests := []struct { name string - chainID headtracker.ChainID + chainID ChainID want bool }{ { "HasChainID returns true when ChainID is not 'unknown'", - headtracker.Devnet, // replace with correct initialization + Devnet, // replace with correct initialization true, }, { @@ -130,13 +129,13 @@ func TestHead_HasChainID(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - head := headtracker.NewHead(0, blockResult, nil, test.chainID) + head := NewHead(0, blockResult, nil, test.chainID) assert.Equal(t, test.want, head.HasChainID()) }) } t.Run("HasChainID returns false when Head is nil", func(t *testing.T) { - var head *headtracker.Head + var head *Head assert.False(t, head.HasChainID()) }) } From 832b1025043156b95c243c0f0d8ee83eedf68449 Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Thu, 22 Jun 2023 10:48:53 -0700 Subject: [PATCH 25/47] Added head broadcaster --- pkg/internal/cltest/cltest.go | 33 ++++++++++++++++----- pkg/solana/headtracker/head_broadcaster.go | 18 +++++++++++ pkg/solana/headtracker/head_saver_in_mem.go | 3 +- 3 files changed, 44 insertions(+), 10 deletions(-) create mode 100644 pkg/solana/headtracker/head_broadcaster.go diff --git a/pkg/internal/cltest/cltest.go b/pkg/internal/cltest/cltest.go index 4960ceb5a..c40937545 100644 --- a/pkg/internal/cltest/cltest.go +++ b/pkg/internal/cltest/cltest.go @@ -1,19 +1,21 @@ package cltest import ( + "context" "fmt" "math/big" + "sync/atomic" "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" "github.com/smartcontractkit/chainlink-solana/pkg/internal/utils" - headtracker "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" ) // Head given the value convert it into an Head -func Head(val interface{}) *headtracker.Head { - var h *headtracker.Head +func Head(val interface{}) *types.Head { + var h *types.Head time := solana.UnixTimeSeconds(0) blockHeight := uint64(0) block := rpc.GetBlockResult{ @@ -25,19 +27,34 @@ func Head(val interface{}) *headtracker.Head { BlockTime: &time, BlockHeight: &blockHeight, } - chainId := headtracker.Mainnet + chainId := types.Mainnet switch t := val.(type) { case int: - h = headtracker.NewHead(int64(t), block, nil, chainId) + h = types.NewHead(int64(t), block, nil, chainId) case uint64: - h = headtracker.NewHead(int64(t), block, nil, chainId) + h = types.NewHead(int64(t), block, nil, chainId) case int64: - h = headtracker.NewHead(t, block, nil, chainId) + h = types.NewHead(t, block, nil, chainId) case *big.Int: - h = headtracker.NewHead(t.Int64(), block, nil, chainId) + h = types.NewHead(t.Int64(), block, nil, chainId) default: panic(fmt.Sprintf("Could not convert %v of type %T to Head", val, val)) } return h } + +// MockHeadTrackable allows you to mock HeadTrackable +type MockHeadTrackable struct { + onNewHeadCount atomic.Int32 +} + +// OnNewLongestChain increases the OnNewLongestChainCount count by one +func (m *MockHeadTrackable) OnNewLongestChain(context.Context, *types.Head) { + m.onNewHeadCount.Add(1) +} + +// OnNewLongestChainCount returns the count of new heads, safely. +func (m *MockHeadTrackable) OnNewLongestChainCount() int32 { + return m.onNewHeadCount.Load() +} diff --git a/pkg/solana/headtracker/head_broadcaster.go b/pkg/solana/headtracker/head_broadcaster.go new file mode 100644 index 000000000..d8b258f75 --- /dev/null +++ b/pkg/solana/headtracker/head_broadcaster.go @@ -0,0 +1,18 @@ +package headtracker + +import ( + "github.com/smartcontractkit/chainlink-relay/pkg/headtracker" + "github.com/smartcontractkit/chainlink-relay/pkg/logger" + commontypes "github.com/smartcontractkit/chainlink-relay/pkg/types" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" +) + +type headBroadcaster = headtracker.HeadBroadcaster[*types.Head, types.Hash] + +var _ commontypes.HeadBroadcaster[*types.Head, types.Hash] = &headBroadcaster{} + +func NewHeadBroadcaster( + lggr logger.Logger, +) *headBroadcaster { + return headtracker.NewHeadBroadcaster[*types.Head, types.Hash](lggr) +} diff --git a/pkg/solana/headtracker/head_saver_in_mem.go b/pkg/solana/headtracker/head_saver_in_mem.go index fe5b9bdc8..8385f9a7f 100644 --- a/pkg/solana/headtracker/head_saver_in_mem.go +++ b/pkg/solana/headtracker/head_saver_in_mem.go @@ -66,9 +66,8 @@ func (hs *inMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) Save(ctx context.Context, return nil } -// No OP function for EVM +// No OP function for Solana func (hs *inMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) Load(ctx context.Context) (H, error) { - return hs.LatestChain(), nil } From a48307c170adff8620830bf601c0bfb184117aed Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Thu, 22 Jun 2023 19:32:36 -0700 Subject: [PATCH 26/47] upgrade go mod --- go.mod | 7 +++++-- go.sum | 12 ++++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index a88b8cdc4..4568d0e02 100644 --- a/go.mod +++ b/go.mod @@ -9,9 +9,10 @@ require ( github.com/gagliardetto/solana-go v1.4.1-0.20220428092759-5250b4abbb27 github.com/gagliardetto/treeout v0.1.4 github.com/google/uuid v1.3.0 + github.com/onsi/gomega v1.24.1 github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.15.0 - github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230621184141-6c65ae346995 + github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230623025050-a286a91d29e6 github.com/smartcontractkit/libocr v0.0.0-20230606215712-82b910bef5c1 github.com/stretchr/testify v1.8.4 github.com/test-go/testify v1.1.4 @@ -37,6 +38,7 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect + github.com/google/go-cmp v0.5.9 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.15.11 // indirect @@ -51,7 +53,6 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect github.com/mr-tron/base58 v1.2.0 // indirect - github.com/onsi/gomega v1.24.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.42.0 // indirect @@ -67,8 +68,10 @@ require ( go.uber.org/atomic v1.10.0 // indirect go.uber.org/ratelimit v0.2.0 // indirect golang.org/x/crypto v0.9.0 // indirect + golang.org/x/net v0.10.0 // indirect golang.org/x/sys v0.8.0 // indirect golang.org/x/term v0.8.0 // indirect + golang.org/x/text v0.9.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 709550186..4970389bb 100644 --- a/go.sum +++ b/go.sum @@ -127,6 +127,7 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2 github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= @@ -177,6 +178,7 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -320,6 +322,8 @@ github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.1 h1:jMU0WaQrP0a/YAEq8eJmJKjBoMs+pClEr1vDMlM/Do4= +github.com/onsi/ginkgo/v2 v2.5.0 h1:TRtrvv2vdQqzkwrQ1ke6vtXf7IK34RBUJafIy1wMwls= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.24.1 h1:KORJXNNTzJXzu4ScJWssJfJMnJ+2QJqhoQSRwNlze9E= @@ -372,10 +376,8 @@ github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5g github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230609013816-dc5d37505ce4 h1:aBsArskduVo/P7GuMD09Btv7Iyky76PZN1u51EBTJXQ= -github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230609013816-dc5d37505ce4/go.mod h1:MfZBUifutkv3aK7abyw5YmTJbqt8iFwcQDFikrxC/uI= -github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230621184141-6c65ae346995 h1:uFh1ND4YG/Qontbp6rT+Tf/zoB2pzHQ660SPHz8APXg= -github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230621184141-6c65ae346995/go.mod h1:MfZBUifutkv3aK7abyw5YmTJbqt8iFwcQDFikrxC/uI= +github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230623025050-a286a91d29e6 h1:Prz5n1XFdP4RouzTyy+bBL3C9B+HY6QTuyBTXCl4GMU= +github.com/smartcontractkit/chainlink-relay v0.1.7-0.20230623025050-a286a91d29e6/go.mod h1:MfZBUifutkv3aK7abyw5YmTJbqt8iFwcQDFikrxC/uI= github.com/smartcontractkit/libocr v0.0.0-20230606215712-82b910bef5c1 h1:caG9BWjnCxN/HPBA5ltDGadDraZAsjGIct4S8lh8D5c= github.com/smartcontractkit/libocr v0.0.0-20230606215712-82b910bef5c1/go.mod h1:2lyRkw/qLQgUWlrWWmq5nj0y90rWeO6Y+v+fCakRgb0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= @@ -532,6 +534,7 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -603,6 +606,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= From 7b389100d35fb1c327060015aa7553c0e182d212 Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Thu, 22 Jun 2023 21:45:47 -0700 Subject: [PATCH 27/47] refactored tests --- pkg/internal/cltest/cltest.go | 128 ++++++++++++++++++-- pkg/internal/testutils/testutils.go | 15 +++ pkg/solana/client/client.go | 19 +-- pkg/solana/headtracker/head_saver_in_mem.go | 26 ++-- pkg/solana/headtracker/types/head_test.go | 92 ++++++-------- 5 files changed, 193 insertions(+), 87 deletions(-) diff --git a/pkg/internal/cltest/cltest.go b/pkg/internal/cltest/cltest.go index c40937545..0a14f5670 100644 --- a/pkg/internal/cltest/cltest.go +++ b/pkg/internal/cltest/cltest.go @@ -5,43 +5,76 @@ import ( "fmt" "math/big" "sync/atomic" + "testing" + "time" "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" + clientmocks "github.com/smartcontractkit/chainlink-relay/pkg/headtracker/types/mocks" + commontypes "github.com/smartcontractkit/chainlink-relay/pkg/types" "github.com/smartcontractkit/chainlink-solana/pkg/internal/utils" "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" ) -// Head given the value convert it into an Head +// TODO: write tests for this package + +// Head returns a new Head with the given block height func Head(val interface{}) *types.Head { - var h *types.Head time := solana.UnixTimeSeconds(0) - blockHeight := uint64(0) - block := rpc.GetBlockResult{ + chainId := types.Mainnet + blockHeight := getBlockHeight(val) + parentSlot := getParentSlot(blockHeight) + block := getBlock(blockHeight, parentSlot, time) + h := createHead(val, blockHeight, block, chainId) + return h +} + +func getBlockHeight(val interface{}) uint64 { + switch t := val.(type) { + case int: + return uint64(t) + case uint64: + return t + case int64: + return uint64(t) + case *big.Int: + return t.Uint64() + default: + panic(fmt.Sprintf("Could not convert %v of type %T to Head", val, val)) + } +} + +func getParentSlot(blockHeight uint64) uint64 { + if blockHeight > 1 { + return blockHeight - 1 + } + return 0 +} + +func getBlock(blockHeight, parentSlot uint64, time solana.UnixTimeSeconds) rpc.GetBlockResult { + return rpc.GetBlockResult{ Blockhash: utils.NewHash(), PreviousBlockhash: utils.NewHash(), - ParentSlot: 0, + ParentSlot: parentSlot, Transactions: nil, Rewards: nil, BlockTime: &time, BlockHeight: &blockHeight, } - chainId := types.Mainnet +} +func createHead(val interface{}, blockHeight uint64, block rpc.GetBlockResult, chainId types.ChainID) *types.Head { switch t := val.(type) { - case int: - h = types.NewHead(int64(t), block, nil, chainId) - case uint64: - h = types.NewHead(int64(t), block, nil, chainId) + case int, uint64: + return types.NewHead(int64(blockHeight), block, nil, chainId) case int64: - h = types.NewHead(t, block, nil, chainId) + return types.NewHead(t, block, nil, chainId) case *big.Int: - h = types.NewHead(t.Int64(), block, nil, chainId) + return types.NewHead(t.Int64(), block, nil, chainId) default: panic(fmt.Sprintf("Could not convert %v of type %T to Head", val, val)) } - return h } // MockHeadTrackable allows you to mock HeadTrackable @@ -58,3 +91,72 @@ func (m *MockHeadTrackable) OnNewLongestChain(context.Context, *types.Head) { func (m *MockHeadTrackable) OnNewLongestChainCount() int32 { return m.onNewHeadCount.Load() } + +type Awaiter chan struct{} + +func NewAwaiter() Awaiter { return make(Awaiter) } + +func (a Awaiter) ItHappened() { close(a) } + +func (a Awaiter) AssertHappened(t *testing.T, expected bool) { + t.Helper() + select { + case <-a: + if !expected { + t.Fatal("It happened") + } + default: + if expected { + t.Fatal("It didn't happen") + } + } +} + +func (a Awaiter) AwaitOrFail(t testing.TB, durationParams ...time.Duration) { + t.Helper() + + duration := 10 * time.Second + if len(durationParams) > 0 { + duration = durationParams[0] + } + + select { + case <-a: + case <-time.After(duration): + t.Fatal("Timed out waiting for Awaiter to get ItHappened") + } +} + +func NewClientMock(t *testing.T) *clientmocks.Client[ + *types.Head, + commontypes.Subscription, + types.ChainID, + types.Hash] { + return clientmocks.NewClient[*types.Head, commontypes.Subscription, + types.ChainID, types.Hash](t) +} + +func NewClientMockWithDefaultChain(t *testing.T) *clientmocks.Client[ + *types.Head, + commontypes.Subscription, + types.ChainID, + types.Hash] { + c := NewClientMock(t) + c.On("ConfiguredChainID").Return(types.Mainnet).Maybe() + c.On("IsL2").Return(false).Maybe() + return c +} + +func ConfigureBlockResult() rpc.GetBlockResult { + result := rpc.GetBlockResult{ + Blockhash: utils.NewHash(), + PreviousBlockhash: utils.NewHash(), + ParentSlot: 0, + Transactions: []rpc.TransactionWithMeta{}, + Signatures: []solana.Signature{}, + Rewards: []rpc.BlockReward{}, + BlockTime: nil, + BlockHeight: nil, + } + return result +} diff --git a/pkg/internal/testutils/testutils.go b/pkg/internal/testutils/testutils.go index 5e717aad0..fffeeab69 100644 --- a/pkg/internal/testutils/testutils.go +++ b/pkg/internal/testutils/testutils.go @@ -3,6 +3,7 @@ package testutils import ( "context" "testing" + "time" ) // Context returns a context with the test's deadline, if available. @@ -21,3 +22,17 @@ func Context(tb testing.TB) context.Context { tb.Cleanup(cancel) return ctx } + +// DefaultWaitTimeout is the default wait timeout. If you have a *testing.T, use WaitTimeout instead. +const DefaultWaitTimeout = 30 * time.Second + +// WaitTimeout returns a timeout based on the test's Deadline, if available. +// Especially important to use in parallel tests, as their individual execution +// can get paused for arbitrary amounts of time. +func WaitTimeout(t *testing.T) time.Duration { + if d, ok := t.Deadline(); ok { + // 10% buffer for cleanup and scheduling delay + return time.Until(d) * 9 / 10 + } + return DefaultWaitTimeout +} diff --git a/pkg/solana/client/client.go b/pkg/solana/client/client.go index 7776179ce..c9c30500f 100644 --- a/pkg/solana/client/client.go +++ b/pkg/solana/client/client.go @@ -13,7 +13,7 @@ import ( htrktypes "github.com/smartcontractkit/chainlink-relay/pkg/headtracker/types" "github.com/smartcontractkit/chainlink-solana/pkg/solana/config" - headtracker "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" "github.com/smartcontractkit/chainlink-solana/pkg/solana/logger" ) @@ -51,8 +51,9 @@ type Writer interface { var _ ReaderWriter = (*Client)(nil) -var _ htrktypes.Client[*headtracker.Head, *Subscription, headtracker.ChainID, headtracker.Hash] = (*Client)(nil) +var _ htrktypes.Client[*types.Head, *Subscription, types.ChainID, types.Hash] = (*Client)(nil) +//go:generate mockery --quiet --name Client --output ./mocks/ --case=underscore type Client struct { rpc *rpc.Client skipPreflight bool // to enable or disable preflight checks @@ -148,13 +149,13 @@ func (c *Client) ChainID() (string, error) { } // TODO: requires refactor. Do we want to store chainID? how do we want to cache ChainID? -func (c *Client) ConfiguredChainID() headtracker.ChainID { +func (c *Client) ConfiguredChainID() types.ChainID { chainID, err := c.ChainID() if err != nil { c.log.Warnf("unable to determine configured chain ID: %v", err) - return headtracker.ChainID(headtracker.Localnet) + return types.ChainID(types.Localnet) } - return headtracker.StringToChainID(chainID) + return types.StringToChainID(chainID) } func (c *Client) GetFeeForMessage(msg string) (uint64, error) { @@ -228,7 +229,7 @@ func (c *Client) SendTx(ctx context.Context, tx *solana.Transaction) (solana.Sig return c.rpc.SendTransactionWithOpts(ctx, tx, opts) } -func (c *Client) HeadByNumber(ctx context.Context, number *big.Int) (*headtracker.Head, error) { +func (c *Client) HeadByNumber(ctx context.Context, number *big.Int) (*types.Head, error) { ctx, cancel := context.WithTimeout(ctx, c.contextDuration) defer cancel() block, err := c.GetBlock(ctx, number.Uint64()) @@ -240,7 +241,7 @@ func (c *Client) HeadByNumber(ctx context.Context, number *big.Int) (*headtracke } chainId := c.ConfiguredChainID() // TODO: check if parent head can be linked in the headsaver - head := &headtracker.Head{ + head := &types.Head{ Slot: number.Int64(), Block: *block, ID: chainId, @@ -249,7 +250,7 @@ func (c *Client) HeadByNumber(ctx context.Context, number *big.Int) (*headtracke } // SubscribeNewHead polls the RPC endpoint for new blocks. -func (c *Client) SubscribeNewHead(ctx context.Context, ch chan<- *headtracker.Head) (*Subscription, error) { +func (c *Client) SubscribeNewHead(ctx context.Context, ch chan<- *types.Head) (*Subscription, error) { subscription := NewSubscription(ctx, c) go func() { @@ -269,7 +270,7 @@ func (c *Client) SubscribeNewHead(ctx context.Context, ch chan<- *headtracker.He } // Create a new Head object and send to channel - head := &headtracker.Head{ + head := &types.Head{ Slot: int64(slot), Block: *block, ID: c.ConfiguredChainID(), diff --git a/pkg/solana/headtracker/head_saver_in_mem.go b/pkg/solana/headtracker/head_saver_in_mem.go index 8385f9a7f..283f07ba2 100644 --- a/pkg/solana/headtracker/head_saver_in_mem.go +++ b/pkg/solana/headtracker/head_saver_in_mem.go @@ -11,7 +11,7 @@ import ( "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" ) -type inMemoryHeadSaver[H htrktypes.Head[BLOCK_HASH, CHAIN_ID], BLOCK_HASH commontypes.Hashable, CHAIN_ID commontypes.ID] struct { +type InMemoryHeadSaver[H htrktypes.Head[BLOCK_HASH, CHAIN_ID], BLOCK_HASH commontypes.Hashable, CHAIN_ID commontypes.ID] struct { config htrktypes.Config logger logger.Logger latestHead H @@ -22,7 +22,7 @@ type inMemoryHeadSaver[H htrktypes.Head[BLOCK_HASH, CHAIN_ID], BLOCK_HASH common setParent func(H, H) } -type HeadSaver = inMemoryHeadSaver[*types.Head, types.Hash, types.ChainID] +type HeadSaver = InMemoryHeadSaver[*types.Head, types.Hash, types.ChainID] var _ commontypes.HeadSaver[*types.Head, types.Hash] = (*HeadSaver)(nil) @@ -34,8 +34,8 @@ func NewInMemoryHeadSaver[ lggr logger.Logger, getNilHead func() H, setParent func(H, H), -) *inMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID] { - return &inMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]{ +) *InMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID] { + return &InMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]{ config: config, logger: logger.Named(lggr, "InMemoryHeadSaver"), Heads: make(map[BLOCK_HASH]H), @@ -55,7 +55,7 @@ func NewHeadSaver(config htrktypes.Config, lggr logger.Logger) *HeadSaver { ) } -func (hs *inMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) Save(ctx context.Context, head H) error { +func (hs *InMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) Save(ctx context.Context, head H) error { if !head.IsValid() { return errors.New("invalid head passed to Save method of InMemoryHeadSaver") } @@ -67,11 +67,11 @@ func (hs *inMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) Save(ctx context.Context, } // No OP function for Solana -func (hs *inMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) Load(ctx context.Context) (H, error) { +func (hs *InMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) Load(ctx context.Context) (H, error) { return hs.LatestChain(), nil } -func (hs *inMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) LatestChain() H { +func (hs *InMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) LatestChain() H { head := hs.getLatestHead() if head.ChainLength() < hs.config.FinalityDepth() { @@ -80,7 +80,7 @@ func (hs *inMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) LatestChain() H { return head } -func (hs *inMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) Chain(blockHash BLOCK_HASH) H { +func (hs *InMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) Chain(blockHash BLOCK_HASH) H { hs.mu.RLock() defer hs.mu.RUnlock() @@ -91,7 +91,7 @@ func (hs *inMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) Chain(blockHash BLOCK_HASH return hs.getNilHead() } -func (hs *inMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) HeadByNumber(blockNumber int64) []H { +func (hs *InMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) HeadByNumber(blockNumber int64) []H { hs.mu.RLock() defer hs.mu.RUnlock() @@ -99,7 +99,7 @@ func (hs *inMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) HeadByNumber(blockNumber i } // Assembles the heads together and populates the Heads Map -func (hs *inMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) AddHeads(historyDepth int64, newHeads ...H) { +func (hs *InMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) AddHeads(historyDepth int64, newHeads ...H) { hs.mu.Lock() defer hs.mu.Unlock() @@ -131,7 +131,7 @@ func (hs *inMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) AddHeads(historyDepth int6 } } -func (hs *inMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) TrimOldHeads(historyDepth int64) { +func (hs *InMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) TrimOldHeads(historyDepth int64) { hs.mu.Lock() defer hs.mu.Unlock() @@ -140,7 +140,7 @@ func (hs *inMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) TrimOldHeads(historyDepth // trimHeads() is should only be called by functions with mutex locking. // trimHeads() is an internal function without locking to prevent deadlocks -func (hs *inMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) trimHeads(historyDepth int64) { +func (hs *InMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) trimHeads(historyDepth int64) { for headNumber, headNumberList := range hs.HeadsNumber { if hs.latestHead.BlockNumber()-headNumber > historyDepth { for _, head := range headNumberList { @@ -152,7 +152,7 @@ func (hs *inMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) trimHeads(historyDepth int } } -func (hs *inMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) getLatestHead() H { +func (hs *InMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) getLatestHead() H { hs.mu.RLock() defer hs.mu.RUnlock() diff --git a/pkg/solana/headtracker/types/head_test.go b/pkg/solana/headtracker/types/head_test.go index 5db1219ad..9d9686735 100644 --- a/pkg/solana/headtracker/types/head_test.go +++ b/pkg/solana/headtracker/types/head_test.go @@ -1,58 +1,44 @@ -package types +package types_test import ( "strconv" "testing" - "github.com/gagliardetto/solana-go" "github.com/gagliardetto/solana-go/rpc" - "github.com/smartcontractkit/chainlink-solana/pkg/internal/utils" + "github.com/smartcontractkit/chainlink-solana/pkg/internal/cltest" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" "github.com/stretchr/testify/assert" ) -func configureBlockResult() rpc.GetBlockResult { - result := rpc.GetBlockResult{ - Blockhash: utils.NewHash(), - PreviousBlockhash: utils.NewHash(), - ParentSlot: 0, - Transactions: []rpc.TransactionWithMeta{}, - Signatures: []solana.Signature{}, - Rewards: []rpc.BlockReward{}, - BlockTime: nil, - BlockHeight: nil, - } - return result -} - func TestHead_NewHead(t *testing.T) { - emptyBlockResult := configureBlockResult() + emptyBlockResult := cltest.ConfigureBlockResult() t.Parallel() tests := []struct { slot int64 block rpc.GetBlockResult - parent *Head - id ChainID + parent *types.Head + id types.ChainID wantSlot int64 }{ // with no parent - {10, emptyBlockResult, nil, Mainnet, 10}, + {10, emptyBlockResult, nil, types.Mainnet, 10}, // with parent {20, emptyBlockResult, - NewHead(10, emptyBlockResult, nil, Mainnet), - Mainnet, 20}, + types.NewHead(10, emptyBlockResult, nil, types.Mainnet), + types.Mainnet, 20}, {30, emptyBlockResult, - NewHead(20, emptyBlockResult, - NewHead(10, emptyBlockResult, nil, Mainnet), - Mainnet), - Mainnet, 30}, + types.NewHead(20, emptyBlockResult, + types.NewHead(10, emptyBlockResult, nil, types.Mainnet), + types.Mainnet), + types.Mainnet, 30}, } for _, test := range tests { t.Run( strconv.FormatInt(test.wantSlot, 10), // convert to base 10 func(t *testing.T) { - head := NewHead(test.slot, test.block, test.parent, test.id) + head := types.NewHead(test.slot, test.block, test.parent, test.id) assert.Equal(t, test.wantSlot, head.Slot) assert.Equal(t, test.block, head.Block) assert.Equal(t, test.parent, head.Parent) @@ -62,62 +48,64 @@ func TestHead_NewHead(t *testing.T) { } func TestHead_ChainLength(t *testing.T) { - blockResult := configureBlockResult() - id := Mainnet + blockResult := cltest.ConfigureBlockResult() + id := types.Mainnet - head := NewHead(0, blockResult, NewHead(0, blockResult, NewHead(0, blockResult, nil, id), id), id) + head := types.NewHead(0, blockResult, + types.NewHead(0, blockResult, + types.NewHead(0, blockResult, nil, id), id), id) assert.Equal(t, uint32(3), head.ChainLength()) - var head2 *Head + var head2 *types.Head assert.Equal(t, uint32(0), head2.ChainLength()) } func TestHead_EarliestHeadInChain(t *testing.T) { - blockResult := configureBlockResult() - id := Mainnet + blockResult := cltest.ConfigureBlockResult() + id := types.Mainnet - head := NewHead(3, blockResult, - NewHead(2, blockResult, - NewHead(1, blockResult, nil, id), id), id) + head := types.NewHead(3, blockResult, + types.NewHead(2, blockResult, + types.NewHead(1, blockResult, nil, id), id), id) assert.Equal(t, int64(1), head.EarliestHeadInChain().BlockNumber()) } func TestHead_GetParentHash(t *testing.T) { - blockResult := configureBlockResult() - id := Mainnet + blockResult := cltest.ConfigureBlockResult() + id := types.Mainnet - head := NewHead(3, blockResult, - NewHead(2, blockResult, - NewHead(1, blockResult, nil, id), id), id) + head := types.NewHead(3, blockResult, + types.NewHead(2, blockResult, + types.NewHead(1, blockResult, nil, id), id), id) assert.Equal(t, head.Parent.BlockHash(), head.GetParentHash()) } func TestHead_GetParent(t *testing.T) { - blockResult := configureBlockResult() - id := Mainnet + blockResult := cltest.ConfigureBlockResult() + id := types.Mainnet - head := NewHead(3, blockResult, - NewHead(2, blockResult, - NewHead(1, blockResult, nil, id), id), id) + head := types.NewHead(3, blockResult, + types.NewHead(2, blockResult, + types.NewHead(1, blockResult, nil, id), id), id) assert.Equal(t, head.Parent, head.GetParent()) } func TestHead_HasChainID(t *testing.T) { t.Parallel() - blockResult := configureBlockResult() // Assuming this function creates a mock rpc.GetBlockResult + blockResult := cltest.ConfigureBlockResult() // Assuming this function creates a mock rpc.GetBlockResult tests := []struct { name string - chainID ChainID + chainID types.ChainID want bool }{ { "HasChainID returns true when ChainID is not 'unknown'", - Devnet, // replace with correct initialization + types.Devnet, // replace with correct initialization true, }, { @@ -129,13 +117,13 @@ func TestHead_HasChainID(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { - head := NewHead(0, blockResult, nil, test.chainID) + head := types.NewHead(0, blockResult, nil, test.chainID) assert.Equal(t, test.want, head.HasChainID()) }) } t.Run("HasChainID returns false when Head is nil", func(t *testing.T) { - var head *Head + var head *types.Head assert.False(t, head.HasChainID()) }) } From ecad2534e94310df39fdf5e1ebc21073fe5422ad Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Thu, 22 Jun 2023 21:46:13 -0700 Subject: [PATCH 28/47] added head_broadcaster_test --- .../headtracker/head_broadcaster_test.go | 189 ++++++++++++++++++ 1 file changed, 189 insertions(+) create mode 100644 pkg/solana/headtracker/head_broadcaster_test.go diff --git a/pkg/solana/headtracker/head_broadcaster_test.go b/pkg/solana/headtracker/head_broadcaster_test.go new file mode 100644 index 000000000..f140e11bb --- /dev/null +++ b/pkg/solana/headtracker/head_broadcaster_test.go @@ -0,0 +1,189 @@ +package headtracker_test + +import ( + "context" + "testing" + "time" + + "github.com/onsi/gomega" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + commonhtrk "github.com/smartcontractkit/chainlink-relay/pkg/headtracker" + commontypes "github.com/smartcontractkit/chainlink-relay/pkg/types" + commonmocks "github.com/smartcontractkit/chainlink-relay/pkg/types/mocks" + + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" + + // configtest "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/configtest/v2" + "github.com/smartcontractkit/chainlink-solana/pkg/internal/testutils" + // "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" + "github.com/smartcontractkit/chainlink-relay/pkg/logger" + // "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" + // "github.com/smartcontractkit/chainlink/v2/core/store/models" + "github.com/smartcontractkit/chainlink-relay/pkg/services" + "github.com/smartcontractkit/chainlink-relay/pkg/utils" + "github.com/smartcontractkit/chainlink-solana/pkg/internal/cltest" +) + +func waitHeadBroadcasterToStart(t *testing.T, hb commontypes.HeadBroadcaster[*types.Head, types.Hash]) { + t.Helper() + + subscriber := &cltest.MockHeadTrackable{} + _, unsubscribe := hb.Subscribe(subscriber) + defer unsubscribe() + + hb.BroadcastNewLongestChain(cltest.Head(1)) + g := gomega.NewWithT(t) + g.Eventually(subscriber.OnNewLongestChainCount).Should(gomega.Equal(int32(1))) +} + +func TestHeadBroadcaster_Subscribe(t *testing.T) { + t.Parallel() + g := gomega.NewWithT(t) + + lggr, _ := logger.New() + sub := commonmocks.NewSubscription(t) + client := cltest.NewClientMockWithDefaultChain(t) + cfg := headtracker.NewConfig() + + chchHeaders := make(chan chan<- *types.Head, 1) + + client.On("SubscribeNewHead", mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + chchHeaders <- args.Get(1).(chan<- *types.Head) + }). + Return(sub, nil) + client.On("HeadByNumber", mock.Anything, mock.Anything).Return(cltest.Head(1), nil) + + sub.On("Unsubscribe").Return() + sub.On("Err").Return(nil) + + checker1 := &cltest.MockHeadTrackable{} + checker2 := &cltest.MockHeadTrackable{} + + hb := headtracker.NewHeadBroadcaster(lggr) + hs := headtracker.NewHeadSaver(cfg, lggr) + mailMon := utils.NewMailboxMonitor(t.Name()) + ht := headtracker.NewHeadTracker(lggr, client, cfg, hb, hs, mailMon) + + var ms services.MultiStart + require.NoError(t, ms.Start(testutils.Context(t), mailMon, hb, ht)) + t.Cleanup(func() { require.NoError(t, services.CloseAll(mailMon, hb, ht)) }) + + latest1, unsubscribe1 := hb.Subscribe(checker1) + // "latest head" is nil here because we didn't receive any yet + assert.Equal(t, (*types.Head)(nil), latest1) + + firstHead := cltest.Head(1) + secondHead := cltest.Head(2) + firstHead.Parent = secondHead + secondHead.Block.PreviousBlockhash = firstHead.Block.Blockhash + + headers := <-chchHeaders + headers <- firstHead + g.Eventually(checker1.OnNewLongestChainCount).Should(gomega.Equal(int32(1))) + + latest2, _ := hb.Subscribe(checker2) + // "latest head" is set here to the most recent head received + assert.NotNil(t, latest2) + assert.Equal(t, firstHead.BlockNumber(), latest2.BlockNumber()) + + unsubscribe1() + + headers <- secondHead + g.Eventually(checker2.OnNewLongestChainCount).Should(gomega.Equal(int32(1))) +} + +func TestHeadBroadcaster_BroadcastNewLongestChain(t *testing.T) { + t.Parallel() + g := gomega.NewWithT(t) + + lggr, _ := logger.New() + broadcaster := headtracker.NewHeadBroadcaster(lggr) + + err := broadcaster.Start(testutils.Context(t)) + require.NoError(t, err) + + waitHeadBroadcasterToStart(t, broadcaster) + + subscriber1 := &cltest.MockHeadTrackable{} + subscriber2 := &cltest.MockHeadTrackable{} + _, unsubscribe1 := broadcaster.Subscribe(subscriber1) + _, unsubscribe2 := broadcaster.Subscribe(subscriber2) + + broadcaster.BroadcastNewLongestChain(cltest.Head(1)) + g.Eventually(subscriber1.OnNewLongestChainCount).Should(gomega.Equal(int32(1))) + + unsubscribe1() + + broadcaster.BroadcastNewLongestChain(cltest.Head(2)) + g.Eventually(subscriber2.OnNewLongestChainCount).Should(gomega.Equal(int32(2))) + + unsubscribe2() + + subscriber3 := &cltest.MockHeadTrackable{} + _, unsubscribe3 := broadcaster.Subscribe(subscriber3) + broadcaster.BroadcastNewLongestChain(cltest.Head(1)) + g.Eventually(subscriber3.OnNewLongestChainCount).Should(gomega.Equal(int32(1))) + + unsubscribe3() + + // no subscribers - shall do nothing + broadcaster.BroadcastNewLongestChain(cltest.Head(0)) + + err = broadcaster.Close() + require.NoError(t, err) + + require.Equal(t, int32(1), subscriber3.OnNewLongestChainCount()) +} + +func TestHeadBroadcaster_TrackableCallbackTimeout(t *testing.T) { + t.Parallel() + + lggr, _ := logger.New() + broadcaster := headtracker.NewHeadBroadcaster(lggr) + + err := broadcaster.Start(testutils.Context(t)) + require.NoError(t, err) + + waitHeadBroadcasterToStart(t, broadcaster) + + slowAwaiter := cltest.NewAwaiter() + fastAwaiter := cltest.NewAwaiter() + slow := &sleepySubscriber{awaiter: slowAwaiter, delay: commonhtrk.TrackableCallbackTimeout * 2} + fast := &sleepySubscriber{awaiter: fastAwaiter, delay: commonhtrk.TrackableCallbackTimeout / 2} + _, unsubscribe1 := broadcaster.Subscribe(slow) + _, unsubscribe2 := broadcaster.Subscribe(fast) + + broadcaster.BroadcastNewLongestChain(cltest.Head(1)) + slowAwaiter.AwaitOrFail(t, testutils.WaitTimeout(t)) + fastAwaiter.AwaitOrFail(t, testutils.WaitTimeout(t)) + + require.True(t, slow.contextDone) + require.False(t, fast.contextDone) + + unsubscribe1() + unsubscribe2() + + err = broadcaster.Close() + require.NoError(t, err) +} + +type sleepySubscriber struct { + awaiter cltest.Awaiter + delay time.Duration + contextDone bool +} + +func (ss *sleepySubscriber) OnNewLongestChain(ctx context.Context, head *types.Head) { + time.Sleep(ss.delay) + select { + case <-ctx.Done(): + ss.contextDone = true + default: + } + ss.awaiter.ItHappened() +} From 50c96ccd559e8823544d56703f91f37e04a44c68 Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Thu, 22 Jun 2023 21:46:24 -0700 Subject: [PATCH 29/47] added headtracker --- pkg/solana/headtracker/head_tracker.go | 33 ++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 pkg/solana/headtracker/head_tracker.go diff --git a/pkg/solana/headtracker/head_tracker.go b/pkg/solana/headtracker/head_tracker.go new file mode 100644 index 000000000..bd78b8136 --- /dev/null +++ b/pkg/solana/headtracker/head_tracker.go @@ -0,0 +1,33 @@ +package headtracker + +import ( + "github.com/smartcontractkit/chainlink-relay/pkg/headtracker" + htrktypes "github.com/smartcontractkit/chainlink-relay/pkg/headtracker/types" + "github.com/smartcontractkit/chainlink-relay/pkg/logger" + commontypes "github.com/smartcontractkit/chainlink-relay/pkg/types" + "github.com/smartcontractkit/chainlink-relay/pkg/utils" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" +) + +type headTracker = headtracker.HeadTracker[*types.Head, commontypes.Subscription, types.ChainID, types.Hash] + +var _ commontypes.HeadTracker[*types.Head, types.Hash] = (*headTracker)(nil) + +func NewHeadTracker( + lggr logger.Logger, + solanaClient htrktypes.Client[*types.Head, commontypes.Subscription, types.ChainID, types.Hash], + config htrktypes.Config, + headBroadcaster commontypes.HeadBroadcaster[*types.Head, types.Hash], + headSaver commontypes.HeadSaver[*types.Head, types.Hash], + mailMon *utils.MailboxMonitor, +) commontypes.HeadTracker[*types.Head, types.Hash] { + return headtracker.NewHeadTracker( + lggr, + solanaClient, + config, + headBroadcaster, + headSaver, + mailMon, + func() *types.Head { return nil }, + ) +} From 5d3b2b8ebb72d6d5d3df9fba5a8bcf557f92359a Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Fri, 23 Jun 2023 11:20:58 -0700 Subject: [PATCH 30/47] fixed head broadcaster test --- .../headtracker/head_broadcaster_test.go | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/pkg/solana/headtracker/head_broadcaster_test.go b/pkg/solana/headtracker/head_broadcaster_test.go index f140e11bb..fe8a9895b 100644 --- a/pkg/solana/headtracker/head_broadcaster_test.go +++ b/pkg/solana/headtracker/head_broadcaster_test.go @@ -11,21 +11,15 @@ import ( "github.com/stretchr/testify/require" commonhtrk "github.com/smartcontractkit/chainlink-relay/pkg/headtracker" - commontypes "github.com/smartcontractkit/chainlink-relay/pkg/types" - commonmocks "github.com/smartcontractkit/chainlink-relay/pkg/types/mocks" - - "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker" - "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" - - // configtest "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/configtest/v2" - "github.com/smartcontractkit/chainlink-solana/pkg/internal/testutils" - // "github.com/smartcontractkit/chainlink/v2/core/internal/testutils/pgtest" "github.com/smartcontractkit/chainlink-relay/pkg/logger" - // "github.com/smartcontractkit/chainlink/v2/core/services/chainlink" - // "github.com/smartcontractkit/chainlink/v2/core/store/models" "github.com/smartcontractkit/chainlink-relay/pkg/services" + commontypes "github.com/smartcontractkit/chainlink-relay/pkg/types" + commonmocks "github.com/smartcontractkit/chainlink-relay/pkg/types/mocks" "github.com/smartcontractkit/chainlink-relay/pkg/utils" "github.com/smartcontractkit/chainlink-solana/pkg/internal/cltest" + "github.com/smartcontractkit/chainlink-solana/pkg/internal/testutils" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" ) func waitHeadBroadcasterToStart(t *testing.T, hb commontypes.HeadBroadcaster[*types.Head, types.Hash]) { @@ -43,6 +37,7 @@ func waitHeadBroadcasterToStart(t *testing.T, hb commontypes.HeadBroadcaster[*ty func TestHeadBroadcaster_Subscribe(t *testing.T) { t.Parallel() g := gomega.NewWithT(t) + g.SetDefaultEventuallyTimeout(1 * time.Second) lggr, _ := logger.New() sub := commonmocks.NewSubscription(t) @@ -87,6 +82,7 @@ func TestHeadBroadcaster_Subscribe(t *testing.T) { g.Eventually(checker1.OnNewLongestChainCount).Should(gomega.Equal(int32(1))) latest2, _ := hb.Subscribe(checker2) + // "latest head" is set here to the most recent head received assert.NotNil(t, latest2) assert.Equal(t, firstHead.BlockNumber(), latest2.BlockNumber()) @@ -94,7 +90,9 @@ func TestHeadBroadcaster_Subscribe(t *testing.T) { unsubscribe1() headers <- secondHead - g.Eventually(checker2.OnNewLongestChainCount).Should(gomega.Equal(int32(1))) + // sleep for 3 seconds + time.Sleep(3 * time.Second) + g.Eventually(checker2.OnNewLongestChainCount).Should(gomega.Equal(int32(1))) // TODO: Fix this test } func TestHeadBroadcaster_BroadcastNewLongestChain(t *testing.T) { From 4942db84fa8affd615c9a67782e9d310e645eb37 Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Fri, 23 Jun 2023 11:34:26 -0700 Subject: [PATCH 31/47] Nit changes to New Headtracker components --- pkg/solana/headtracker/head_broadcaster.go | 2 +- pkg/solana/headtracker/head_broadcaster_test.go | 8 ++++---- pkg/solana/headtracker/head_tracker.go | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/solana/headtracker/head_broadcaster.go b/pkg/solana/headtracker/head_broadcaster.go index d8b258f75..53789a2a1 100644 --- a/pkg/solana/headtracker/head_broadcaster.go +++ b/pkg/solana/headtracker/head_broadcaster.go @@ -11,7 +11,7 @@ type headBroadcaster = headtracker.HeadBroadcaster[*types.Head, types.Hash] var _ commontypes.HeadBroadcaster[*types.Head, types.Hash] = &headBroadcaster{} -func NewHeadBroadcaster( +func NewBroadcaster( lggr logger.Logger, ) *headBroadcaster { return headtracker.NewHeadBroadcaster[*types.Head, types.Hash](lggr) diff --git a/pkg/solana/headtracker/head_broadcaster_test.go b/pkg/solana/headtracker/head_broadcaster_test.go index fe8a9895b..141c27244 100644 --- a/pkg/solana/headtracker/head_broadcaster_test.go +++ b/pkg/solana/headtracker/head_broadcaster_test.go @@ -59,10 +59,10 @@ func TestHeadBroadcaster_Subscribe(t *testing.T) { checker1 := &cltest.MockHeadTrackable{} checker2 := &cltest.MockHeadTrackable{} - hb := headtracker.NewHeadBroadcaster(lggr) + hb := headtracker.NewBroadcaster(lggr) hs := headtracker.NewHeadSaver(cfg, lggr) mailMon := utils.NewMailboxMonitor(t.Name()) - ht := headtracker.NewHeadTracker(lggr, client, cfg, hb, hs, mailMon) + ht := headtracker.NewTracker(lggr, client, cfg, hb, hs, mailMon) var ms services.MultiStart require.NoError(t, ms.Start(testutils.Context(t), mailMon, hb, ht)) @@ -100,7 +100,7 @@ func TestHeadBroadcaster_BroadcastNewLongestChain(t *testing.T) { g := gomega.NewWithT(t) lggr, _ := logger.New() - broadcaster := headtracker.NewHeadBroadcaster(lggr) + broadcaster := headtracker.NewBroadcaster(lggr) err := broadcaster.Start(testutils.Context(t)) require.NoError(t, err) @@ -142,7 +142,7 @@ func TestHeadBroadcaster_TrackableCallbackTimeout(t *testing.T) { t.Parallel() lggr, _ := logger.New() - broadcaster := headtracker.NewHeadBroadcaster(lggr) + broadcaster := headtracker.NewBroadcaster(lggr) err := broadcaster.Start(testutils.Context(t)) require.NoError(t, err) diff --git a/pkg/solana/headtracker/head_tracker.go b/pkg/solana/headtracker/head_tracker.go index bd78b8136..86e5e2172 100644 --- a/pkg/solana/headtracker/head_tracker.go +++ b/pkg/solana/headtracker/head_tracker.go @@ -13,7 +13,7 @@ type headTracker = headtracker.HeadTracker[*types.Head, commontypes.Subscription var _ commontypes.HeadTracker[*types.Head, types.Hash] = (*headTracker)(nil) -func NewHeadTracker( +func NewTracker( lggr logger.Logger, solanaClient htrktypes.Client[*types.Head, commontypes.Subscription, types.ChainID, types.Hash], config htrktypes.Config, From 24500a5884e026cd1a7aee8f5b384f79cab8e688 Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Fri, 23 Jun 2023 11:39:38 -0700 Subject: [PATCH 32/47] Added HeadListener --- pkg/solana/headtracker/head_listener.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 pkg/solana/headtracker/head_listener.go diff --git a/pkg/solana/headtracker/head_listener.go b/pkg/solana/headtracker/head_listener.go new file mode 100644 index 000000000..003b40575 --- /dev/null +++ b/pkg/solana/headtracker/head_listener.go @@ -0,0 +1,23 @@ +package headtracker + +import ( + "github.com/smartcontractkit/chainlink-relay/pkg/headtracker" + htrktypes "github.com/smartcontractkit/chainlink-relay/pkg/headtracker/types" + "github.com/smartcontractkit/chainlink-relay/pkg/logger" + commontypes "github.com/smartcontractkit/chainlink-relay/pkg/types" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" +) + +type headListener = headtracker.HeadListener[*types.Head, commontypes.Subscription, types.ChainID, types.Hash] + +var _ commontypes.HeadListener[*types.Head, types.Hash] = &headListener{} + +func NewListener( + lggr logger.Logger, + solanaClient htrktypes.Client[*types.Head, commontypes.Subscription, types.ChainID, types.Hash], + config htrktypes.Config, + chStop chan struct{}, +) *headListener { + return headtracker.NewHeadListener[*types.Head, commontypes.Subscription, + types.ChainID, types.Hash](lggr, solanaClient, config, chStop) +} From 3571cc7f1916e92fc59c9d391765a0a828f44f44 Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Fri, 23 Jun 2023 14:27:09 -0700 Subject: [PATCH 33/47] Added Headlistener test --- pkg/internal/testutils/testutils.go | 6 + pkg/solana/headtracker/head_broadcaster.go | 6 +- pkg/solana/headtracker/head_listener.go | 6 +- pkg/solana/headtracker/head_listener_test.go | 247 +++++++++++++++++++ 4 files changed, 259 insertions(+), 6 deletions(-) create mode 100644 pkg/solana/headtracker/head_listener_test.go diff --git a/pkg/internal/testutils/testutils.go b/pkg/internal/testutils/testutils.go index fffeeab69..a52b2dfa3 100644 --- a/pkg/internal/testutils/testutils.go +++ b/pkg/internal/testutils/testutils.go @@ -6,6 +6,8 @@ import ( "time" ) +// TODO: These can prob refactor to chainlink internal testutils + // Context returns a context with the test's deadline, if available. func Context(tb testing.TB) context.Context { ctx := context.Background() @@ -36,3 +38,7 @@ func WaitTimeout(t *testing.T) time.Duration { } return DefaultWaitTimeout } + +// TestInterval is just a sensible poll interval that gives fast tests without +// risk of spamming +const TestInterval = 100 * time.Millisecond diff --git a/pkg/solana/headtracker/head_broadcaster.go b/pkg/solana/headtracker/head_broadcaster.go index 53789a2a1..4d143ee69 100644 --- a/pkg/solana/headtracker/head_broadcaster.go +++ b/pkg/solana/headtracker/head_broadcaster.go @@ -7,12 +7,12 @@ import ( "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" ) -type headBroadcaster = headtracker.HeadBroadcaster[*types.Head, types.Hash] +type HeadBroadcaster = headtracker.HeadBroadcaster[*types.Head, types.Hash] -var _ commontypes.HeadBroadcaster[*types.Head, types.Hash] = &headBroadcaster{} +var _ commontypes.HeadBroadcaster[*types.Head, types.Hash] = &HeadBroadcaster{} func NewBroadcaster( lggr logger.Logger, -) *headBroadcaster { +) *HeadBroadcaster { return headtracker.NewHeadBroadcaster[*types.Head, types.Hash](lggr) } diff --git a/pkg/solana/headtracker/head_listener.go b/pkg/solana/headtracker/head_listener.go index 003b40575..e6c621d0b 100644 --- a/pkg/solana/headtracker/head_listener.go +++ b/pkg/solana/headtracker/head_listener.go @@ -8,16 +8,16 @@ import ( "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" ) -type headListener = headtracker.HeadListener[*types.Head, commontypes.Subscription, types.ChainID, types.Hash] +type HeadListener = headtracker.HeadListener[*types.Head, commontypes.Subscription, types.ChainID, types.Hash] -var _ commontypes.HeadListener[*types.Head, types.Hash] = &headListener{} +var _ commontypes.HeadListener[*types.Head, types.Hash] = &HeadListener{} func NewListener( lggr logger.Logger, solanaClient htrktypes.Client[*types.Head, commontypes.Subscription, types.ChainID, types.Hash], config htrktypes.Config, chStop chan struct{}, -) *headListener { +) *HeadListener { return headtracker.NewHeadListener[*types.Head, commontypes.Subscription, types.ChainID, types.Hash](lggr, solanaClient, config, chStop) } diff --git a/pkg/solana/headtracker/head_listener_test.go b/pkg/solana/headtracker/head_listener_test.go new file mode 100644 index 000000000..4daac9834 --- /dev/null +++ b/pkg/solana/headtracker/head_listener_test.go @@ -0,0 +1,247 @@ +package headtracker_test + +import ( + "context" + "errors" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/stretchr/testify/mock" + + "github.com/smartcontractkit/chainlink-relay/pkg/logger" + commonmocks "github.com/smartcontractkit/chainlink-relay/pkg/types/mocks" + "github.com/smartcontractkit/chainlink-solana/pkg/internal/cltest" + "github.com/smartcontractkit/chainlink-solana/pkg/internal/testutils" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" +) + +func Test_HeadListener_HappyPath(t *testing.T) { + // Logic: + // - spawn a listener instance + // - mock SubscribeNewHead/Err/Unsubscribe to track these calls + // - send 3 heads + // - ask listener to stop + // Asserts: + // - check Connected()/ReceivingHeads() are updated + // - 3 heads is passed to callback + // - ethClient methods are invoked + + lggr, _ := logger.New() + client := cltest.NewClientMockWithDefaultChain(t) + cfg := headtracker.NewConfig() + chStop := make(chan struct{}) + hl := headtracker.NewListener(lggr, client, cfg, chStop) + + var headCount atomic.Int32 + handler := func(context.Context, *types.Head) error { + headCount.Add(1) + return nil + } + + subscribeAwaiter := cltest.NewAwaiter() + unsubscribeAwaiter := cltest.NewAwaiter() + var chHeads chan<- *types.Head + var chErr = make(chan error) + var chSubErr <-chan error = chErr + sub := commonmocks.NewSubscription(t) + client.On("SubscribeNewHead", mock.Anything, mock.AnythingOfType("chan<- *types.Head")).Return(sub, nil).Once().Run(func(args mock.Arguments) { + chHeads = args.Get(1).(chan<- *types.Head) + subscribeAwaiter.ItHappened() + }) + sub.On("Err").Return(chSubErr) + sub.On("Unsubscribe").Return().Once().Run(func(mock.Arguments) { + unsubscribeAwaiter.ItHappened() + close(chHeads) + close(chErr) + }) + + doneAwaiter := cltest.NewAwaiter() + done := func() { + doneAwaiter.ItHappened() + } + go hl.ListenForNewHeads(handler, done) + + subscribeAwaiter.AwaitOrFail(t, testutils.WaitTimeout(t)) + require.Eventually(t, hl.Connected, testutils.WaitTimeout(t), testutils.TestInterval) + + chHeads <- cltest.Head(0) + chHeads <- cltest.Head(1) + chHeads <- cltest.Head(2) + + require.True(t, hl.ReceivingHeads()) + + close(chStop) + doneAwaiter.AwaitOrFail(t) + + unsubscribeAwaiter.AwaitOrFail(t) + require.Equal(t, int32(3), headCount.Load()) +} + +func Test_HeadListener_NotReceivingHeads(t *testing.T) { + // Logic: + // - same as Test_HeadListener_HappyPath, but + // - send one head, make sure ReceivingHeads() is true + // - do not send any heads within BlockEmissionIdleWarningThreshold and check ReceivingHeads() is false + + lggr, _ := logger.New() + client := cltest.NewClientMockWithDefaultChain(t) + cfg := headtracker.NewConfig() + chStop := make(chan struct{}) + hl := headtracker.NewListener(lggr, client, cfg, chStop) + + firstHeadAwaiter := cltest.NewAwaiter() + // handler := func(context.Context, *types.Head) error { + // firstHeadAwaiter.ItHappened() + // return nil + // } + + var headCount atomic.Int32 + handler := func(context.Context, *types.Head) error { + headCount.Add(1) + return nil + } + + subscribeAwaiter := cltest.NewAwaiter() + unsubscribeAwaiter := cltest.NewAwaiter() + var chHeads chan<- *types.Head + var chErr = make(chan error) + var chSubErr <-chan error = chErr + sub := commonmocks.NewSubscription(t) + client.On("SubscribeNewHead", mock.Anything, mock.AnythingOfType("chan<- *types.Head")).Return(sub, nil).Once().Run(func(args mock.Arguments) { + chHeads = args.Get(1).(chan<- *types.Head) + subscribeAwaiter.ItHappened() + }) + sub.On("Err").Return(chSubErr) + sub.On("Unsubscribe").Return().Once().Run(func(_ mock.Arguments) { + unsubscribeAwaiter.ItHappened() + close(chHeads) + close(chErr) + }) // TODO: Fix error here + + doneAwaiter := cltest.NewAwaiter() + done := func() { + doneAwaiter.ItHappened() + } + go hl.ListenForNewHeads(handler, done) + + subscribeAwaiter.AwaitOrFail(t, testutils.WaitTimeout(t)) + + chHeads <- cltest.Head(0) + firstHeadAwaiter.AwaitOrFail(t) + + require.True(t, hl.ReceivingHeads()) + + time.Sleep(time.Second * 2) + + require.False(t, hl.ReceivingHeads()) + + close(chStop) + doneAwaiter.AwaitOrFail(t) +} + +func Test_HeadListener_SubscriptionErr(t *testing.T) { + tests := []struct { + name string + err error + closeErr bool + }{ + {"nil error", nil, false}, + {"socket error", errors.New("close 1006 (abnormal closure): unexpected EOF"), false}, + {"close Err channel", nil, true}, + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + lggr, _ := logger.New() + client := cltest.NewClientMockWithDefaultChain(t) + cfg := headtracker.NewConfig() + chStop := make(chan struct{}) + hl := headtracker.NewListener(lggr, client, cfg, chStop) + + hnhCalled := make(chan *types.Head) + hnh := func(_ context.Context, header *types.Head) error { + hnhCalled <- header + return nil + } + doneAwaiter := cltest.NewAwaiter() + done := doneAwaiter.ItHappened + + chSubErrTest := make(chan error) + var chSubErr <-chan error = chSubErrTest + sub := commonmocks.NewSubscription(t) + // sub.Err is called twice because we enter the select loop two times: once + // initially and once again after exactly one head has been received + sub.On("Err").Return(chSubErr).Twice() + + subscribeAwaiter := cltest.NewAwaiter() + var headsCh chan<- *types.Head + // Initial subscribe + client.On("SubscribeNewHead", mock.Anything, mock.AnythingOfType("chan<- *types.Head")).Return(sub, nil).Once().Run(func(args mock.Arguments) { + headsCh = args.Get(1).(chan<- *types.Head) + subscribeAwaiter.ItHappened() + }) + go func() { + hl.ListenForNewHeads(hnh, done) + }() + + // Put a head on the channel to ensure we test all code paths + subscribeAwaiter.AwaitOrFail(t, testutils.WaitTimeout(t)) + head := cltest.Head(0) + headsCh <- head + + h := <-hnhCalled + assert.Equal(t, head, h) + + // Expect a call to unsubscribe on error + sub.On("Unsubscribe").Once().Run(func(_ mock.Arguments) { + close(headsCh) + if !test.closeErr { + close(chSubErrTest) + } + }) + // Expect a resubscribe + chSubErrTest2 := make(chan error) + var chSubErr2 <-chan error = chSubErrTest2 + sub2 := commonmocks.NewSubscription(t) + sub2.On("Err").Return(chSubErr2) + subscribeAwaiter2 := cltest.NewAwaiter() + + var headsCh2 chan<- *types.Head + client.On("SubscribeNewHead", mock.Anything, mock.AnythingOfType("chan<- *types.Head")).Return(sub2, nil).Once().Run(func(args mock.Arguments) { + headsCh2 = args.Get(1).(chan<- *types.Head) + subscribeAwaiter2.ItHappened() + }) + + // Sending test error + if test.closeErr { + close(chSubErrTest) + } else { + chSubErrTest <- test.err + } + + // Wait for it to resubscribe + subscribeAwaiter2.AwaitOrFail(t, testutils.WaitTimeout(t)) + + head2 := cltest.Head(1) + headsCh2 <- head2 + + h2 := <-hnhCalled + assert.Equal(t, head2, h2) + + // Second call to unsubscribe on close + sub2.On("Unsubscribe").Once().Run(func(_ mock.Arguments) { + close(headsCh2) + // geth guarantees that Unsubscribe closes the errors channel + close(chSubErrTest2) + }) + close(chStop) + doneAwaiter.AwaitOrFail(t) + }) + } +} From 9e3eec8f103d912a972fcbb9164cb17e7e9160e0 Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Fri, 23 Jun 2023 14:59:21 -0700 Subject: [PATCH 34/47] fixed head listener test --- pkg/internal/testutils/testutils.go | 8 ++++ pkg/solana/headtracker/config.go | 48 +++++++++++--------- pkg/solana/headtracker/head_listener_test.go | 14 +++--- 3 files changed, 40 insertions(+), 30 deletions(-) diff --git a/pkg/internal/testutils/testutils.go b/pkg/internal/testutils/testutils.go index a52b2dfa3..55948e679 100644 --- a/pkg/internal/testutils/testutils.go +++ b/pkg/internal/testutils/testutils.go @@ -4,6 +4,8 @@ import ( "context" "testing" "time" + + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker" ) // TODO: These can prob refactor to chainlink internal testutils @@ -42,3 +44,9 @@ func WaitTimeout(t *testing.T) time.Duration { // TestInterval is just a sensible poll interval that gives fast tests without // risk of spamming const TestInterval = 100 * time.Millisecond + +// NewHeadtrackerConfig returns a new Solana Headtracker Config with overrides. +func NewHeadtrackerConfig(config *headtracker.Config, overrideFn func(*headtracker.Config)) *headtracker.Config { + overrideFn(config) + return config +} diff --git a/pkg/solana/headtracker/config.go b/pkg/solana/headtracker/config.go index b80b00bc0..5dd90bf62 100644 --- a/pkg/solana/headtracker/config.go +++ b/pkg/solana/headtracker/config.go @@ -2,15 +2,17 @@ package headtracker import ( "time" + + htrktypes "github.com/smartcontractkit/chainlink-relay/pkg/headtracker/types" ) -// This config serves as a POC for headtracker. -// It should be replaced with a more robust config -// such as the one in pkg/solana/config +// This Config serves as a POC for headtracker. +// It should be replaced with a more robust Config +// such as the one in pkg/solana/Config -// TODO: replace this config with a more robust config -type config struct { - defaults configSet +// TODO: replace this Config with a more robust Config. Requires research +type Config struct { + Defaults configSet } type configSet struct { @@ -24,7 +26,7 @@ type configSet struct { var defaultConfigSet = configSet{ // headtracker - BlockEmissionIdleWarningThreshold: 30 * time.Second, + BlockEmissionIdleWarningThreshold: 30 * time.Second, // TODO: Check this Config value again FinalityDepth: 50, HeadTrackerHistoryDepth: 100, HeadTrackerMaxBufferSize: 3, @@ -32,32 +34,34 @@ var defaultConfigSet = configSet{ PollingInterval: 2 * time.Second, } -func NewConfig() *config { - return &config{ - defaults: defaultConfigSet, +func NewConfig() *Config { + return &Config{ + Defaults: defaultConfigSet, } } -func (c *config) BlockEmissionIdleWarningThreshold() time.Duration { - return c.defaults.BlockEmissionIdleWarningThreshold +var _ htrktypes.Config = &Config{} + +func (c *Config) BlockEmissionIdleWarningThreshold() time.Duration { + return c.Defaults.BlockEmissionIdleWarningThreshold } -func (c *config) FinalityDepth() uint32 { - return c.defaults.FinalityDepth +func (c *Config) FinalityDepth() uint32 { + return c.Defaults.FinalityDepth } -func (c *config) HeadTrackerHistoryDepth() uint32 { - return c.defaults.HeadTrackerHistoryDepth +func (c *Config) HeadTrackerHistoryDepth() uint32 { + return c.Defaults.HeadTrackerHistoryDepth } -func (c *config) HeadTrackerMaxBufferSize() uint32 { - return c.defaults.HeadTrackerMaxBufferSize +func (c *Config) HeadTrackerMaxBufferSize() uint32 { + return c.Defaults.HeadTrackerMaxBufferSize } -func (c *config) HeadTrackerSamplingInterval() time.Duration { - return c.defaults.HeadTrackerSamplingInterval +func (c *Config) HeadTrackerSamplingInterval() time.Duration { + return c.Defaults.HeadTrackerSamplingInterval } -func (c *config) PollingInterval() time.Duration { - return c.defaults.PollingInterval +func (c *Config) PollingInterval() time.Duration { + return c.Defaults.PollingInterval } diff --git a/pkg/solana/headtracker/head_listener_test.go b/pkg/solana/headtracker/head_listener_test.go index 4daac9834..d50268868 100644 --- a/pkg/solana/headtracker/head_listener_test.go +++ b/pkg/solana/headtracker/head_listener_test.go @@ -91,18 +91,15 @@ func Test_HeadListener_NotReceivingHeads(t *testing.T) { lggr, _ := logger.New() client := cltest.NewClientMockWithDefaultChain(t) cfg := headtracker.NewConfig() + overridenConfig := testutils.NewHeadtrackerConfig(cfg, func(c *headtracker.Config) { + c.Defaults.BlockEmissionIdleWarningThreshold = 1 * time.Second + }) chStop := make(chan struct{}) - hl := headtracker.NewListener(lggr, client, cfg, chStop) + hl := headtracker.NewListener(lggr, client, overridenConfig, chStop) firstHeadAwaiter := cltest.NewAwaiter() - // handler := func(context.Context, *types.Head) error { - // firstHeadAwaiter.ItHappened() - // return nil - // } - - var headCount atomic.Int32 handler := func(context.Context, *types.Head) error { - headCount.Add(1) + firstHeadAwaiter.ItHappened() return nil } @@ -130,6 +127,7 @@ func Test_HeadListener_NotReceivingHeads(t *testing.T) { go hl.ListenForNewHeads(handler, done) subscribeAwaiter.AwaitOrFail(t, testutils.WaitTimeout(t)) + require.Eventually(t, hl.Connected, testutils.WaitTimeout(t), testutils.TestInterval) chHeads <- cltest.Head(0) firstHeadAwaiter.AwaitOrFail(t) From 7f20a24016f402271578e83fb367963e0967e10b Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Mon, 10 Jul 2023 15:28:57 -0500 Subject: [PATCH 35/47] Reduced Verbosity of Headtracker --- pkg/solana/headtracker/head_broadcaster_test.go | 2 +- pkg/solana/headtracker/head_saver_in_mem.go | 2 +- pkg/solana/headtracker/head_saver_in_mem_test.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/solana/headtracker/head_broadcaster_test.go b/pkg/solana/headtracker/head_broadcaster_test.go index 141c27244..54dc85b5f 100644 --- a/pkg/solana/headtracker/head_broadcaster_test.go +++ b/pkg/solana/headtracker/head_broadcaster_test.go @@ -60,7 +60,7 @@ func TestHeadBroadcaster_Subscribe(t *testing.T) { checker2 := &cltest.MockHeadTrackable{} hb := headtracker.NewBroadcaster(lggr) - hs := headtracker.NewHeadSaver(cfg, lggr) + hs := headtracker.NewSaver(cfg, lggr) mailMon := utils.NewMailboxMonitor(t.Name()) ht := headtracker.NewTracker(lggr, client, cfg, hb, hs, mailMon) diff --git a/pkg/solana/headtracker/head_saver_in_mem.go b/pkg/solana/headtracker/head_saver_in_mem.go index 283f07ba2..37cfe1022 100644 --- a/pkg/solana/headtracker/head_saver_in_mem.go +++ b/pkg/solana/headtracker/head_saver_in_mem.go @@ -46,7 +46,7 @@ func NewInMemoryHeadSaver[ } // Creates a new In Memory HeadSaver for solana -func NewHeadSaver(config htrktypes.Config, lggr logger.Logger) *HeadSaver { +func NewSaver(config htrktypes.Config, lggr logger.Logger) *HeadSaver { return NewInMemoryHeadSaver[*types.Head, types.Hash, types.ChainID]( config, lggr, diff --git a/pkg/solana/headtracker/head_saver_in_mem_test.go b/pkg/solana/headtracker/head_saver_in_mem_test.go index 6b6c2dab8..011d0b93c 100644 --- a/pkg/solana/headtracker/head_saver_in_mem_test.go +++ b/pkg/solana/headtracker/head_saver_in_mem_test.go @@ -14,7 +14,7 @@ import ( func configureInMemorySaver(t *testing.T) *headtracker.HeadSaver { htCfg := headtracker.NewConfig() lggr, _ := logger.New() - return headtracker.NewHeadSaver(htCfg, lggr) + return headtracker.NewSaver(htCfg, lggr) } func TestInMemoryHeadSaver_Save(t *testing.T) { From d1aa0d41aaac0e0829cb5542b80d03be1736bc58 Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Mon, 10 Jul 2023 15:36:01 -0500 Subject: [PATCH 36/47] cleanup headlistener test --- pkg/solana/headtracker/head_listener_test.go | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/pkg/solana/headtracker/head_listener_test.go b/pkg/solana/headtracker/head_listener_test.go index d50268868..181c69a71 100644 --- a/pkg/solana/headtracker/head_listener_test.go +++ b/pkg/solana/headtracker/head_listener_test.go @@ -21,16 +21,6 @@ import ( ) func Test_HeadListener_HappyPath(t *testing.T) { - // Logic: - // - spawn a listener instance - // - mock SubscribeNewHead/Err/Unsubscribe to track these calls - // - send 3 heads - // - ask listener to stop - // Asserts: - // - check Connected()/ReceivingHeads() are updated - // - 3 heads is passed to callback - // - ethClient methods are invoked - lggr, _ := logger.New() client := cltest.NewClientMockWithDefaultChain(t) cfg := headtracker.NewConfig() @@ -83,11 +73,6 @@ func Test_HeadListener_HappyPath(t *testing.T) { } func Test_HeadListener_NotReceivingHeads(t *testing.T) { - // Logic: - // - same as Test_HeadListener_HappyPath, but - // - send one head, make sure ReceivingHeads() is true - // - do not send any heads within BlockEmissionIdleWarningThreshold and check ReceivingHeads() is false - lggr, _ := logger.New() client := cltest.NewClientMockWithDefaultChain(t) cfg := headtracker.NewConfig() @@ -118,7 +103,7 @@ func Test_HeadListener_NotReceivingHeads(t *testing.T) { unsubscribeAwaiter.ItHappened() close(chHeads) close(chErr) - }) // TODO: Fix error here + }) doneAwaiter := cltest.NewAwaiter() done := func() { @@ -235,7 +220,6 @@ func Test_HeadListener_SubscriptionErr(t *testing.T) { // Second call to unsubscribe on close sub2.On("Unsubscribe").Once().Run(func(_ mock.Arguments) { close(headsCh2) - // geth guarantees that Unsubscribe closes the errors channel close(chSubErrTest2) }) close(chStop) From e6cd1df430749639b095a8bc9583111b642a8b8c Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Mon, 10 Jul 2023 17:31:01 -0500 Subject: [PATCH 37/47] Headtracker Test added --- pkg/solana/headtracker/head_tracker_test.go | 1095 +++++++++++++++++++ 1 file changed, 1095 insertions(+) create mode 100644 pkg/solana/headtracker/head_tracker_test.go diff --git a/pkg/solana/headtracker/head_tracker_test.go b/pkg/solana/headtracker/head_tracker_test.go new file mode 100644 index 000000000..4997782ee --- /dev/null +++ b/pkg/solana/headtracker/head_tracker_test.go @@ -0,0 +1,1095 @@ +package headtracker_test + +import ( + "context" + "fmt" + "math/big" + "sync" + "testing" + "time" + + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" + + "github.com/onsi/gomega" + + htrktypes "github.com/smartcontractkit/chainlink-relay/pkg/headtracker/types" + "github.com/smartcontractkit/chainlink-relay/pkg/logger" + commontypes "github.com/smartcontractkit/chainlink-relay/pkg/types" + "github.com/smartcontractkit/chainlink-relay/pkg/utils" + "github.com/smartcontractkit/chainlink-solana/pkg/internal/cltest" + "github.com/smartcontractkit/chainlink-solana/pkg/internal/testutils" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" + + "github.com/stretchr/testify/mock" + "github.com/test-go/testify/assert" + "github.com/test-go/testify/require" +) + +// Why do we need this? +// Allow us to retreive the earliest head in our HeadSaver +func firstHead( + t *testing.T, + hs *headtracker.InMemoryHeadSaver[ + *types.Head, + types.Hash, + types.ChainID, + ]) (h types.Head) { + + return h +} + +func TestHeadTracker_New(t *testing.T) { + t.Parallel() + + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + client := cltest.NewClientMockWithDefaultChain(t) + client.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(cltest.Head(0), nil) + + headSaver := headtracker.NewSaver(cfg, lggr) + assert.Nil(t, headSaver.Save(testutils.Context(t), cltest.Head(1))) + last := cltest.Head(16) + assert.Nil(t, headSaver.Save(testutils.Context(t), last)) + assert.Nil(t, headSaver.Save(testutils.Context(t), cltest.Head(10))) + + ht := createHeadTracker(t, cfg, client, headSaver) + ht.Start(t) + + latest := ht.headSaver.LatestChain() + require.NotNil(t, latest) + fmt.Println("latest", latest) + assert.Equal(t, last.BlockNumber(), latest.BlockNumber()) +} + +// // The function `TestHeadTracker_Save_InsertsAndTrimsTable` tests the functionality of inserting and +// // trimming a table in the head tracker. +func TestHeadTracker_Save_InsertsAndTrimsTable(t *testing.T) { + t.Parallel() + + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + headSaver := headtracker.NewSaver(cfg, lggr) + + client := cltest.NewClientMockWithDefaultChain(t) + + var parentHead *types.Head + + for idx := 0; idx < 5; idx++ { + idxHead := cltest.Head(idx) + + if parentHead != nil { + idxHead.Parent = parentHead + } + parentHead = idxHead + fmt.Println("parentHead", idxHead.Parent) + + assert.Nil(t, headSaver.Save(testutils.Context(t), idxHead)) + } + + ht := createHeadTracker(t, headtracker.NewConfig(), client, headSaver) + + h := cltest.Head(200) + require.NoError(t, ht.headSaver.Save(testutils.Context(t), h)) + assert.Equal(t, int64(200), ht.headSaver.LatestChain().BlockNumber()) + + // Recursively get the parent of the head until we reach the first head + var firstHead *types.Head + + for { + // print h.Parent + fmt.Println("h.Parent", h.Parent) + if h.Parent == nil { + firstHead = h + break + } + h = h.Parent + } + + assert.Equal(t, int64(101), firstHead.BlockNumber()) + + // lastHead, err := orm.LatestHead(testutils.Context(t)) + // require.NoError(t, err) + // assert.Equal(t, int64(200), lastHead.Number) +} + +// func TestHeadTracker_Get(t *testing.T) { +// t.Parallel() + +// start := cltest.Head(5) + +// tests := []struct { +// name string +// initial *evmtypes.Head +// toSave *evmtypes.Head +// want *big.Int +// }{ +// {"greater", start, cltest.Head(6), big.NewInt(6)}, +// {"less than", start, cltest.Head(1), big.NewInt(5)}, +// {"zero", start, cltest.Head(0), big.NewInt(5)}, +// {"nil", start, nil, big.NewInt(5)}, +// {"nil no initial", nil, nil, big.NewInt(0)}, +// } + +// for _, test := range tests { +// t.Run(test.name, func(t *testing.T) { +// db := pgtest.NewSqlxDB(t) +// logger := logger.TestLogger(t) +// config := cltest.NewTestChainScopedConfig(t) +// orm := headtracker.NewORM(db, logger, config.Database(), cltest.FixtureChainID) + +// ethClient := evmtest.NewEthClientMockWithDefaultChain(t) +// chStarted := make(chan struct{}) +// mockEth := &evmtest.MockEth{ +// EthClient: ethClient, +// } +// ethClient.On("SubscribeNewHead", mock.Anything, mock.Anything). +// Maybe(). +// Return( +// func(ctx context.Context, ch chan<- *evmtypes.Head) ethereum.Subscription { +// defer close(chStarted) +// return mockEth.NewSub(t) +// }, +// func(ctx context.Context, ch chan<- *evmtypes.Head) error { return nil }, +// ) +// ethClient.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(cltest.Head(0), nil) + +// fnCall := ethClient.On("HeadByNumber", mock.Anything, mock.Anything) +// fnCall.RunFn = func(args mock.Arguments) { +// num := args.Get(1).(*big.Int) +// fnCall.ReturnArguments = mock.Arguments{cltest.Head(num.Int64()), nil} +// } + +// if test.initial != nil { +// assert.Nil(t, orm.IdempotentInsertHead(testutils.Context(t), test.initial)) +// } + +// ht := createHeadTracker(t, ethClient, config.EVM(), config.EVM().HeadTracker(), orm) +// ht.Start(t) + +// if test.toSave != nil { +// err := ht.headSaver.Save(testutils.Context(t), test.toSave) +// assert.NoError(t, err) +// } + +// assert.Equal(t, test.want, ht.headSaver.LatestChain().ToInt()) +// }) +// } +// } + +// func TestHeadTracker_Start_NewHeads(t *testing.T) { +// t.Parallel() + +// db := pgtest.NewSqlxDB(t) +// logger := logger.TestLogger(t) +// config := cltest.NewTestChainScopedConfig(t) +// orm := headtracker.NewORM(db, logger, config.Database(), cltest.FixtureChainID) + +// ethClient := evmtest.NewEthClientMockWithDefaultChain(t) +// chStarted := make(chan struct{}) +// mockEth := &evmtest.MockEth{EthClient: ethClient} +// sub := mockEth.NewSub(t) +// ethClient.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(cltest.Head(0), nil) +// ethClient.On("SubscribeNewHead", mock.Anything, mock.Anything). +// Run(func(mock.Arguments) { +// close(chStarted) +// }). +// Return(sub, nil) + +// ht := createHeadTracker(t, ethClient, config.EVM(), config.EVM().HeadTracker(), orm) +// ht.Start(t) + +// <-chStarted +// } + +// func TestHeadTracker_Start_CancelContext(t *testing.T) { +// t.Parallel() + +// db := pgtest.NewSqlxDB(t) +// logger := logger.TestLogger(t) +// config := cltest.NewTestChainScopedConfig(t) +// orm := headtracker.NewORM(db, logger, config.Database(), cltest.FixtureChainID) +// ethClient := evmtest.NewEthClientMockWithDefaultChain(t) +// chStarted := make(chan struct{}) +// ethClient.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Run(func(args mock.Arguments) { +// ctx := args.Get(0).(context.Context) +// select { +// case <-ctx.Done(): +// return +// case <-time.After(10 * time.Second): +// assert.FailNow(t, "context was not cancelled within 10s") +// } +// }).Return(cltest.Head(0), nil) +// mockEth := &evmtest.MockEth{EthClient: ethClient} +// sub := mockEth.NewSub(t) +// ethClient.On("SubscribeNewHead", mock.Anything, mock.Anything). +// Run(func(mock.Arguments) { +// close(chStarted) +// }). +// Return(sub, nil). +// Maybe() + +// ht := createHeadTracker(t, ethClient, config.EVM(), config.EVM().HeadTracker(), orm) + +// ctx, cancel := context.WithCancel(testutils.Context(t)) +// go func() { +// time.Sleep(1 * time.Second) +// cancel() +// }() +// err := ht.headTracker.Start(ctx) +// require.NoError(t, err) +// require.NoError(t, ht.headTracker.Close()) +// } + +// func TestHeadTracker_CallsHeadTrackableCallbacks(t *testing.T) { +// t.Parallel() +// g := gomega.NewWithT(t) + +// db := pgtest.NewSqlxDB(t) +// logger := logger.TestLogger(t) +// config := cltest.NewTestChainScopedConfig(t) +// orm := headtracker.NewORM(db, logger, config.Database(), cltest.FixtureChainID) + +// ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + +// chchHeaders := make(chan evmtest.RawSub[*evmtypes.Head], 1) +// mockEth := &evmtest.MockEth{EthClient: ethClient} +// ethClient.On("SubscribeNewHead", mock.Anything, mock.Anything). +// Return( +// func(ctx context.Context, ch chan<- *evmtypes.Head) ethereum.Subscription { +// sub := mockEth.NewSub(t) +// chchHeaders <- evmtest.NewRawSub(ch, sub.Err()) +// return sub +// }, +// func(ctx context.Context, ch chan<- *evmtypes.Head) error { return nil }, +// ) +// ethClient.On("HeadByNumber", mock.Anything, mock.Anything).Return(cltest.Head(0), nil) + +// checker := &cltest.MockHeadTrackable{} +// ht := createHeadTrackerWithChecker(t, ethClient, config.EVM(), config.EVM().HeadTracker(), orm, checker) + +// ht.Start(t) +// assert.Equal(t, int32(0), checker.OnNewLongestChainCount()) + +// headers := <-chchHeaders +// headers.TrySend(&evmtypes.Head{Number: 1, Hash: utils.NewHash(), EVMChainID: utils.NewBig(&cltest.FixtureChainID)}) +// g.Eventually(checker.OnNewLongestChainCount).Should(gomega.Equal(int32(1))) + +// ht.Stop(t) +// assert.Equal(t, int32(1), checker.OnNewLongestChainCount()) +// } + +// func TestHeadTracker_ReconnectOnError(t *testing.T) { +// t.Parallel() +// g := gomega.NewWithT(t) + +// db := pgtest.NewSqlxDB(t) +// logger := logger.TestLogger(t) +// config := cltest.NewTestChainScopedConfig(t) +// orm := headtracker.NewORM(db, logger, config.Database(), cltest.FixtureChainID) + +// ethClient := evmtest.NewEthClientMockWithDefaultChain(t) +// mockEth := &evmtest.MockEth{EthClient: ethClient} +// ethClient.On("SubscribeNewHead", mock.Anything, mock.Anything). +// Return( +// func(ctx context.Context, ch chan<- *evmtypes.Head) ethereum.Subscription { return mockEth.NewSub(t) }, +// func(ctx context.Context, ch chan<- *evmtypes.Head) error { return nil }, +// ) +// ethClient.On("SubscribeNewHead", mock.Anything, mock.Anything).Return(nil, errors.New("cannot reconnect")) +// ethClient.On("SubscribeNewHead", mock.Anything, mock.Anything). +// Return( +// func(ctx context.Context, ch chan<- *evmtypes.Head) ethereum.Subscription { return mockEth.NewSub(t) }, +// func(ctx context.Context, ch chan<- *evmtypes.Head) error { return nil }, +// ) +// ethClient.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(cltest.Head(0), nil) + +// checker := &cltest.MockHeadTrackable{} +// ht := createHeadTrackerWithChecker(t, ethClient, config.EVM(), config.EVM().HeadTracker(), orm, checker) + +// // connect +// ht.Start(t) +// assert.Equal(t, int32(0), checker.OnNewLongestChainCount()) + +// // trigger reconnect loop +// mockEth.SubsErr(errors.New("test error to force reconnect")) +// g.Eventually(checker.OnNewLongestChainCount).Should(gomega.Equal(int32(1))) +// } + +// func TestHeadTracker_ResubscribeOnSubscriptionError(t *testing.T) { +// t.Parallel() +// g := gomega.NewWithT(t) + +// db := pgtest.NewSqlxDB(t) +// logger := logger.TestLogger(t) +// config := cltest.NewTestChainScopedConfig(t) +// orm := headtracker.NewORM(db, logger, config.Database(), cltest.FixtureChainID) + +// ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + +// chchHeaders := make(chan evmtest.RawSub[*evmtypes.Head], 1) +// mockEth := &evmtest.MockEth{EthClient: ethClient} +// ethClient.On("SubscribeNewHead", mock.Anything, mock.Anything). +// Return( +// func(ctx context.Context, ch chan<- *evmtypes.Head) ethereum.Subscription { +// sub := mockEth.NewSub(t) +// chchHeaders <- evmtest.NewRawSub(ch, sub.Err()) +// return sub +// }, +// func(ctx context.Context, ch chan<- *evmtypes.Head) error { return nil }, +// ) +// ethClient.On("HeadByNumber", mock.Anything, mock.Anything).Return(cltest.Head(0), nil) + +// checker := &cltest.MockHeadTrackable{} +// ht := createHeadTrackerWithChecker(t, ethClient, config.EVM(), config.EVM().HeadTracker(), orm, checker) + +// ht.Start(t) +// assert.Equal(t, int32(0), checker.OnNewLongestChainCount()) + +// headers := <-chchHeaders +// go func() { +// headers.TrySend(cltest.Head(1)) +// }() + +// g.Eventually(func() bool { +// report := ht.headTracker.HealthReport() +// return !slices.ContainsFunc(maps.Values(report), func(e error) bool { return e != nil }) +// }, 5*time.Second, testutils.TestInterval).Should(gomega.Equal(true)) + +// // trigger reconnect loop +// headers.CloseCh() + +// // wait for full disconnect and a new subscription +// g.Eventually(checker.OnNewLongestChainCount, 5*time.Second, testutils.TestInterval).Should(gomega.Equal(int32(1))) +// } + +// func TestHeadTracker_Start_LoadsLatestChain(t *testing.T) { +// t.Parallel() + +// db := pgtest.NewSqlxDB(t) +// logger := logger.TestLogger(t) +// config := cltest.NewTestChainScopedConfig(t) +// ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + +// heads := []*evmtypes.Head{ +// cltest.Head(0), +// cltest.Head(1), +// cltest.Head(2), +// cltest.Head(3), +// } +// var parentHash gethCommon.Hash +// for i := 0; i < len(heads); i++ { +// if parentHash != (gethCommon.Hash{}) { +// heads[i].ParentHash = parentHash +// } +// parentHash = heads[i].Hash +// } +// ethClient.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(heads[3], nil).Maybe() +// ethClient.On("HeadByNumber", mock.Anything, big.NewInt(2)).Return(heads[2], nil).Maybe() +// ethClient.On("HeadByNumber", mock.Anything, big.NewInt(1)).Return(heads[1], nil).Maybe() +// ethClient.On("HeadByNumber", mock.Anything, big.NewInt(0)).Return(heads[0], nil).Maybe() + +// chchHeaders := make(chan evmtest.RawSub[*evmtypes.Head], 1) +// mockEth := &evmtest.MockEth{EthClient: ethClient} +// ethClient.On("SubscribeNewHead", mock.Anything, mock.Anything). +// Return( +// func(ctx context.Context, ch chan<- *evmtypes.Head) ethereum.Subscription { +// sub := mockEth.NewSub(t) +// chchHeaders <- evmtest.NewRawSub(ch, sub.Err()) +// return sub +// }, +// func(ctx context.Context, ch chan<- *evmtypes.Head) error { return nil }, +// ) + +// orm := headtracker.NewORM(db, logger, config.Database(), cltest.FixtureChainID) +// trackable := &cltest.MockHeadTrackable{} +// ht := createHeadTrackerWithChecker(t, ethClient, config.EVM(), config.EVM().HeadTracker(), orm, trackable) + +// require.NoError(t, orm.IdempotentInsertHead(testutils.Context(t), heads[2])) + +// ht.Start(t) + +// assert.Equal(t, int32(0), trackable.OnNewLongestChainCount()) + +// headers := <-chchHeaders +// go func() { +// headers.TrySend(cltest.Head(1)) +// }() + +// gomega.NewWithT(t).Eventually(func() bool { +// report := ht.headTracker.HealthReport() +// maps.Copy(report, ht.headBroadcaster.HealthReport()) +// return !slices.ContainsFunc(maps.Values(report), func(e error) bool { return e != nil }) +// }, 5*time.Second, testutils.TestInterval).Should(gomega.Equal(true)) + +// h, err := orm.LatestHead(testutils.Context(t)) +// require.NoError(t, err) +// require.NotNil(t, h) +// assert.Equal(t, h.Number, int64(3)) +// } + +// func TestHeadTracker_SwitchesToLongestChainWithHeadSamplingEnabled(t *testing.T) { +// t.Parallel() + +// db := pgtest.NewSqlxDB(t) +// logger := logger.TestLogger(t) + +// config := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { +// c.EVM[0].FinalityDepth = ptr[uint32](50) +// // Need to set the buffer to something large since we inject a lot of heads at once and otherwise they will be dropped +// c.EVM[0].HeadTracker.MaxBufferSize = ptr[uint32](100) +// c.EVM[0].HeadTracker.SamplingInterval = models.MustNewDuration(2500 * time.Millisecond) +// }) + +// ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + +// checker := commonmocks.NewHeadTrackable[*evmtypes.Head, gethCommon.Hash](t) +// orm := headtracker.NewORM(db, logger, config.Database(), *config.DefaultChainID()) +// csCfg := evmtest.NewChainScopedConfig(t, config) +// ht := createHeadTrackerWithChecker(t, ethClient, csCfg.EVM(), csCfg.EVM().HeadTracker(), orm, checker) + +// chchHeaders := make(chan evmtest.RawSub[*evmtypes.Head], 1) +// mockEth := &evmtest.MockEth{EthClient: ethClient} +// ethClient.On("SubscribeNewHead", mock.Anything, mock.Anything). +// Return( +// func(ctx context.Context, ch chan<- *evmtypes.Head) ethereum.Subscription { +// sub := mockEth.NewSub(t) +// chchHeaders <- evmtest.NewRawSub(ch, sub.Err()) +// return sub +// }, +// func(ctx context.Context, ch chan<- *evmtypes.Head) error { return nil }, +// ) + +// // --------------------- +// blocks := cltest.NewBlocks(t, 10) + +// head0 := blocks.Head(0) +// // Initial query +// ethClient.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(head0, nil) +// ht.Start(t) + +// headSeq := cltest.NewHeadBuffer(t) +// headSeq.Append(blocks.Head(0)) +// headSeq.Append(blocks.Head(1)) + +// // Blocks 2 and 3 are out of order +// headSeq.Append(blocks.Head(3)) +// headSeq.Append(blocks.Head(2)) + +// // Block 4 comes in +// headSeq.Append(blocks.Head(4)) + +// // Another block at level 4 comes in, that will be uncled +// headSeq.Append(blocks.NewHead(4)) + +// // Reorg happened forking from block 2 +// blocksForked := blocks.ForkAt(t, 2, 5) +// headSeq.Append(blocksForked.Head(2)) +// headSeq.Append(blocksForked.Head(3)) +// headSeq.Append(blocksForked.Head(4)) +// headSeq.Append(blocksForked.Head(5)) // Now the new chain is longer + +// lastLongestChainAwaiter := cltest.NewAwaiter() + +// // the callback is only called for head number 5 because of head sampling +// checker.On("OnNewLongestChain", mock.Anything, mock.Anything). +// Run(func(args mock.Arguments) { +// h := args.Get(1).(*evmtypes.Head) + +// assert.Equal(t, int64(5), h.Number) +// assert.Equal(t, blocksForked.Head(5).Hash, h.Hash) + +// // This is the new longest chain, check that it came with its parents +// if !assert.NotNil(t, h.Parent) { +// return +// } +// assert.Equal(t, h.Parent.Hash, blocksForked.Head(4).Hash) +// if !assert.NotNil(t, h.Parent.Parent) { +// return +// } +// assert.Equal(t, h.Parent.Parent.Hash, blocksForked.Head(3).Hash) +// if !assert.NotNil(t, h.Parent.Parent.Parent) { +// return +// } +// assert.Equal(t, h.Parent.Parent.Parent.Hash, blocksForked.Head(2).Hash) +// if !assert.NotNil(t, h.Parent.Parent.Parent.Parent) { +// return +// } +// assert.Equal(t, h.Parent.Parent.Parent.Parent.Hash, blocksForked.Head(1).Hash) +// lastLongestChainAwaiter.ItHappened() +// }).Return().Once() + +// headers := <-chchHeaders + +// // This grotesque construction is the only way to do dynamic return values using +// // the mock package. We need dynamic returns because we're simulating reorgs. +// latestHeadByNumber := make(map[int64]*evmtypes.Head) +// latestHeadByNumberMu := new(sync.Mutex) + +// fnCall := ethClient.On("HeadByNumber", mock.Anything, mock.Anything) +// fnCall.RunFn = func(args mock.Arguments) { +// latestHeadByNumberMu.Lock() +// defer latestHeadByNumberMu.Unlock() +// num := args.Get(1).(*big.Int) +// head, exists := latestHeadByNumber[num.Int64()] +// if !exists { +// head = cltest.Head(num.Int64()) +// latestHeadByNumber[num.Int64()] = head +// } +// fnCall.ReturnArguments = mock.Arguments{head, nil} +// } + +// for _, h := range headSeq.Heads { +// latestHeadByNumberMu.Lock() +// latestHeadByNumber[h.Number] = h +// latestHeadByNumberMu.Unlock() +// headers.TrySend(h) +// } + +// // default 10s may not be sufficient, so using testutils.WaitTimeout(t) +// lastLongestChainAwaiter.AwaitOrFail(t, testutils.WaitTimeout(t)) +// ht.Stop(t) +// assert.Equal(t, int64(5), ht.headSaver.LatestChain().Number) + +// for _, h := range headSeq.Heads { +// c := ht.headSaver.Chain(h.Hash) +// require.NotNil(t, c) +// assert.Equal(t, c.ParentHash, h.ParentHash) +// assert.Equal(t, c.Timestamp.Unix(), h.Timestamp.UTC().Unix()) +// assert.Equal(t, c.Number, h.Number) +// } +// } + +// func TestHeadTracker_SwitchesToLongestChainWithHeadSamplingDisabled(t *testing.T) { +// t.Parallel() + +// db := pgtest.NewSqlxDB(t) +// logger := logger.TestLogger(t) + +// config := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { +// c.EVM[0].FinalityDepth = ptr[uint32](50) +// // Need to set the buffer to something large since we inject a lot of heads at once and otherwise they will be dropped +// c.EVM[0].HeadTracker.MaxBufferSize = ptr[uint32](100) +// c.EVM[0].HeadTracker.SamplingInterval = models.MustNewDuration(0) +// }) + +// ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + +// checker := commonmocks.NewHeadTrackable[*evmtypes.Head, gethCommon.Hash](t) +// orm := headtracker.NewORM(db, logger, config.Database(), cltest.FixtureChainID) +// evmcfg := evmtest.NewChainScopedConfig(t, config) +// ht := createHeadTrackerWithChecker(t, ethClient, evmcfg.EVM(), evmcfg.EVM().HeadTracker(), orm, checker) + +// chchHeaders := make(chan evmtest.RawSub[*evmtypes.Head], 1) +// mockEth := &evmtest.MockEth{EthClient: ethClient} +// ethClient.On("SubscribeNewHead", mock.Anything, mock.Anything). +// Return( +// func(ctx context.Context, ch chan<- *evmtypes.Head) ethereum.Subscription { +// sub := mockEth.NewSub(t) +// chchHeaders <- evmtest.NewRawSub(ch, sub.Err()) +// return sub +// }, +// func(ctx context.Context, ch chan<- *evmtypes.Head) error { return nil }, +// ) + +// // --------------------- +// blocks := cltest.NewBlocks(t, 10) + +// head0 := blocks.Head(0) // evmtypes.Head{Number: 0, Hash: utils.NewHash(), ParentHash: utils.NewHash(), Timestamp: time.Unix(0, 0)} +// // Initial query +// ethClient.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(head0, nil) + +// headSeq := cltest.NewHeadBuffer(t) +// headSeq.Append(blocks.Head(0)) +// headSeq.Append(blocks.Head(1)) + +// // Blocks 2 and 3 are out of order +// headSeq.Append(blocks.Head(3)) +// headSeq.Append(blocks.Head(2)) + +// // Block 4 comes in +// headSeq.Append(blocks.Head(4)) + +// // Another block at level 4 comes in, that will be uncled +// headSeq.Append(blocks.NewHead(4)) + +// // Reorg happened forking from block 2 +// blocksForked := blocks.ForkAt(t, 2, 5) +// headSeq.Append(blocksForked.Head(2)) +// headSeq.Append(blocksForked.Head(3)) +// headSeq.Append(blocksForked.Head(4)) +// headSeq.Append(blocksForked.Head(5)) // Now the new chain is longer + +// lastLongestChainAwaiter := cltest.NewAwaiter() + +// checker.On("OnNewLongestChain", mock.Anything, mock.Anything). +// Run(func(args mock.Arguments) { +// h := args.Get(1).(*evmtypes.Head) +// require.Equal(t, int64(0), h.Number) +// require.Equal(t, blocks.Head(0).Hash, h.Hash) +// }).Return().Once() + +// checker.On("OnNewLongestChain", mock.Anything, mock.Anything). +// Run(func(args mock.Arguments) { +// h := args.Get(1).(*evmtypes.Head) +// require.Equal(t, int64(1), h.Number) +// require.Equal(t, blocks.Head(1).Hash, h.Hash) +// }).Return().Once() + +// checker.On("OnNewLongestChain", mock.Anything, mock.Anything). +// Run(func(args mock.Arguments) { +// h := args.Get(1).(*evmtypes.Head) +// require.Equal(t, int64(3), h.Number) +// require.Equal(t, blocks.Head(3).Hash, h.Hash) +// }).Return().Once() + +// checker.On("OnNewLongestChain", mock.Anything, mock.Anything). +// Run(func(args mock.Arguments) { +// h := args.Get(1).(*evmtypes.Head) +// require.Equal(t, int64(4), h.Number) +// require.Equal(t, blocks.Head(4).Hash, h.Hash) + +// // Check that the block came with its parents +// require.NotNil(t, h.Parent) +// require.Equal(t, h.Parent.Hash, blocks.Head(3).Hash) +// require.NotNil(t, h.Parent.Parent.Hash) +// require.Equal(t, h.Parent.Parent.Hash, blocks.Head(2).Hash) +// require.NotNil(t, h.Parent.Parent.Parent) +// require.Equal(t, h.Parent.Parent.Parent.Hash, blocks.Head(1).Hash) +// }).Return().Once() + +// checker.On("OnNewLongestChain", mock.Anything, mock.Anything). +// Run(func(args mock.Arguments) { +// h := args.Get(1).(*evmtypes.Head) + +// require.Equal(t, int64(5), h.Number) +// require.Equal(t, blocksForked.Head(5).Hash, h.Hash) + +// // This is the new longest chain, check that it came with its parents +// require.NotNil(t, h.Parent) +// require.Equal(t, h.Parent.Hash, blocksForked.Head(4).Hash) +// require.NotNil(t, h.Parent.Parent) +// require.Equal(t, h.Parent.Parent.Hash, blocksForked.Head(3).Hash) +// require.NotNil(t, h.Parent.Parent.Parent) +// require.Equal(t, h.Parent.Parent.Parent.Hash, blocksForked.Head(2).Hash) +// require.NotNil(t, h.Parent.Parent.Parent.Parent) +// require.Equal(t, h.Parent.Parent.Parent.Parent.Hash, blocksForked.Head(1).Hash) +// lastLongestChainAwaiter.ItHappened() +// }).Return().Once() + +// ht.Start(t) + +// headers := <-chchHeaders + +// // This grotesque construction is the only way to do dynamic return values using +// // the mock package. We need dynamic returns because we're simulating reorgs. +// latestHeadByNumber := make(map[int64]*evmtypes.Head) +// latestHeadByNumberMu := new(sync.Mutex) + +// fnCall := ethClient.On("HeadByNumber", mock.Anything, mock.Anything) +// fnCall.RunFn = func(args mock.Arguments) { +// latestHeadByNumberMu.Lock() +// defer latestHeadByNumberMu.Unlock() +// num := args.Get(1).(*big.Int) +// head, exists := latestHeadByNumber[num.Int64()] +// if !exists { +// head = cltest.Head(num.Int64()) +// latestHeadByNumber[num.Int64()] = head +// } +// fnCall.ReturnArguments = mock.Arguments{head, nil} +// } + +// for _, h := range headSeq.Heads { +// latestHeadByNumberMu.Lock() +// latestHeadByNumber[h.Number] = h +// latestHeadByNumberMu.Unlock() +// headers.TrySend(h) +// time.Sleep(testutils.TestInterval) +// } + +// // default 10s may not be sufficient, so using testutils.WaitTimeout(t) +// lastLongestChainAwaiter.AwaitOrFail(t, testutils.WaitTimeout(t)) +// ht.Stop(t) +// assert.Equal(t, int64(5), ht.headSaver.LatestChain().Number) + +// for _, h := range headSeq.Heads { +// c := ht.headSaver.Chain(h.Hash) +// require.NotNil(t, c) +// assert.Equal(t, c.ParentHash, h.ParentHash) +// assert.Equal(t, c.Timestamp.Unix(), h.Timestamp.UTC().Unix()) +// assert.Equal(t, c.Number, h.Number) +// } +// } + +// func TestHeadTracker_Backfill(t *testing.T) { +// t.Parallel() + +// // Heads are arranged as follows: +// // headN indicates an unpersisted ethereum header +// // hN indicates a persisted head record +// // +// // (1)->(H0) +// // +// // (14Orphaned)-+ +// // +->(13)->(12)->(11)->(H10)->(9)->(H8) +// // (15)->(14)---------+ + +// now := uint64(time.Now().UTC().Unix()) + +// gethHead0 := &gethTypes.Header{ +// Number: big.NewInt(0), +// ParentHash: gethCommon.BigToHash(big.NewInt(0)), +// Time: now, +// } +// head0 := evmtypes.NewHead(gethHead0.Number, utils.NewHash(), gethHead0.ParentHash, gethHead0.Time, utils.NewBig(&cltest.FixtureChainID)) + +// h1 := *cltest.Head(1) +// h1.ParentHash = head0.Hash + +// gethHead8 := &gethTypes.Header{ +// Number: big.NewInt(8), +// ParentHash: utils.NewHash(), +// Time: now, +// } +// head8 := evmtypes.NewHead(gethHead8.Number, utils.NewHash(), gethHead8.ParentHash, gethHead8.Time, utils.NewBig(&cltest.FixtureChainID)) + +// h9 := *cltest.Head(9) +// h9.ParentHash = head8.Hash + +// gethHead10 := &gethTypes.Header{ +// Number: big.NewInt(10), +// ParentHash: h9.Hash, +// Time: now, +// } +// head10 := evmtypes.NewHead(gethHead10.Number, utils.NewHash(), gethHead10.ParentHash, gethHead10.Time, utils.NewBig(&cltest.FixtureChainID)) + +// h11 := *cltest.Head(11) +// h11.ParentHash = head10.Hash + +// h12 := *cltest.Head(12) +// h12.ParentHash = h11.Hash + +// h13 := *cltest.Head(13) +// h13.ParentHash = h12.Hash + +// h14Orphaned := *cltest.Head(14) +// h14Orphaned.ParentHash = h13.Hash + +// h14 := *cltest.Head(14) +// h14.ParentHash = h13.Hash + +// h15 := *cltest.Head(15) +// h15.ParentHash = h14.Hash + +// heads := []evmtypes.Head{ +// h9, +// h11, +// h12, +// h13, +// h14Orphaned, +// h14, +// h15, +// } + +// ctx := testutils.Context(t) + +// t.Run("does nothing if all the heads are in database", func(t *testing.T) { +// db := pgtest.NewSqlxDB(t) +// cfg := configtest.NewGeneralConfig(t, nil) +// logger := logger.TestLogger(t) +// orm := headtracker.NewORM(db, logger, cfg.Database(), cltest.FixtureChainID) +// for i := range heads { +// require.NoError(t, orm.IdempotentInsertHead(testutils.Context(t), &heads[i])) +// } + +// ethClient := evmtest.NewEthClientMock(t) +// ethClient.On("ConfiguredChainID", mock.Anything).Return(cfg.DefaultChainID(), nil) +// ht := createHeadTrackerWithNeverSleeper(t, ethClient, cfg, orm) + +// err := ht.Backfill(ctx, &h12, 2) +// require.NoError(t, err) +// }) + +// t.Run("fetches a missing head", func(t *testing.T) { +// db := pgtest.NewSqlxDB(t) +// cfg := configtest.NewGeneralConfig(t, nil) +// logger := logger.TestLogger(t) +// orm := headtracker.NewORM(db, logger, cfg.Database(), cltest.FixtureChainID) +// for i := range heads { +// require.NoError(t, orm.IdempotentInsertHead(testutils.Context(t), &heads[i])) +// } + +// ethClient := evmtest.NewEthClientMock(t) +// ethClient.On("ConfiguredChainID", mock.Anything).Return(cfg.DefaultChainID(), nil) +// ethClient.On("HeadByNumber", mock.Anything, big.NewInt(10)). +// Return(&head10, nil) + +// ht := createHeadTrackerWithNeverSleeper(t, ethClient, cfg, orm) + +// var depth uint = 3 + +// err := ht.Backfill(ctx, &h12, depth) +// require.NoError(t, err) + +// h := ht.headSaver.Chain(h12.Hash) + +// assert.Equal(t, int64(12), h.Number) +// require.NotNil(t, h.Parent) +// assert.Equal(t, int64(11), h.Parent.Number) +// require.NotNil(t, h.Parent) +// assert.Equal(t, int64(10), h.Parent.Parent.Number) +// require.NotNil(t, h.Parent.Parent.Parent) +// assert.Equal(t, int64(9), h.Parent.Parent.Parent.Number) + +// writtenHead, err := orm.HeadByHash(testutils.Context(t), head10.Hash) +// require.NoError(t, err) +// assert.Equal(t, int64(10), writtenHead.Number) +// }) + +// t.Run("fetches only heads that are missing", func(t *testing.T) { +// db := pgtest.NewSqlxDB(t) +// cfg := configtest.NewGeneralConfig(t, nil) +// logger := logger.TestLogger(t) +// orm := headtracker.NewORM(db, logger, cfg.Database(), cltest.FixtureChainID) +// for i := range heads { +// require.NoError(t, orm.IdempotentInsertHead(testutils.Context(t), &heads[i])) +// } + +// ethClient := evmtest.NewEthClientMock(t) +// ethClient.On("ConfiguredChainID", mock.Anything).Return(cfg.DefaultChainID(), nil) + +// ht := createHeadTrackerWithNeverSleeper(t, ethClient, cfg, orm) + +// ethClient.On("HeadByNumber", mock.Anything, big.NewInt(10)). +// Return(&head10, nil) +// ethClient.On("HeadByNumber", mock.Anything, big.NewInt(8)). +// Return(&head8, nil) + +// // Needs to be 8 because there are 8 heads in chain (15,14,13,12,11,10,9,8) +// var depth uint = 8 + +// err := ht.Backfill(ctx, &h15, depth) +// require.NoError(t, err) + +// h := ht.headSaver.Chain(h15.Hash) + +// require.Equal(t, uint32(8), h.ChainLength()) +// earliestInChain := h.EarliestInChain() +// assert.Equal(t, head8.Number, earliestInChain.BlockNumber()) +// assert.Equal(t, head8.Hash, earliestInChain.BlockHash()) +// }) + +// t.Run("does not backfill if chain length is already greater than or equal to depth", func(t *testing.T) { +// db := pgtest.NewSqlxDB(t) +// cfg := configtest.NewGeneralConfig(t, nil) +// logger := logger.TestLogger(t) +// orm := headtracker.NewORM(db, logger, cfg.Database(), cltest.FixtureChainID) +// for i := range heads { +// require.NoError(t, orm.IdempotentInsertHead(testutils.Context(t), &heads[i])) +// } + +// ethClient := evmtest.NewEthClientMock(t) +// ethClient.On("ConfiguredChainID", mock.Anything).Return(cfg.DefaultChainID(), nil) + +// ht := createHeadTrackerWithNeverSleeper(t, ethClient, cfg, orm) + +// err := ht.Backfill(ctx, &h15, 3) +// require.NoError(t, err) + +// err = ht.Backfill(ctx, &h15, 5) +// require.NoError(t, err) +// }) + +// t.Run("only backfills to height 0 if chain length would otherwise cause it to try and fetch a negative head", func(t *testing.T) { +// db := pgtest.NewSqlxDB(t) +// cfg := configtest.NewGeneralConfig(t, nil) +// logger := logger.TestLogger(t) +// orm := headtracker.NewORM(db, logger, cfg.Database(), cltest.FixtureChainID) + +// ethClient := evmtest.NewEthClientMock(t) +// ethClient.On("ConfiguredChainID", mock.Anything).Return(cfg.DefaultChainID(), nil) +// ethClient.On("HeadByNumber", mock.Anything, big.NewInt(0)). +// Return(&head0, nil) + +// require.NoError(t, orm.IdempotentInsertHead(testutils.Context(t), &h1)) + +// ht := createHeadTrackerWithNeverSleeper(t, ethClient, cfg, orm) + +// err := ht.Backfill(ctx, &h1, 400) +// require.NoError(t, err) + +// h := ht.headSaver.Chain(h1.Hash) +// require.NotNil(t, h) + +// require.Equal(t, uint32(2), h.ChainLength()) +// require.Equal(t, int64(0), h.EarliestInChain().BlockNumber()) +// }) + +// t.Run("abandons backfill and returns error if the eth node returns not found", func(t *testing.T) { +// db := pgtest.NewSqlxDB(t) +// cfg := configtest.NewGeneralConfig(t, nil) +// logger := logger.TestLogger(t) +// orm := headtracker.NewORM(db, logger, cfg.Database(), cltest.FixtureChainID) +// for i := range heads { +// require.NoError(t, orm.IdempotentInsertHead(testutils.Context(t), &heads[i])) +// } + +// ethClient := evmtest.NewEthClientMock(t) +// ethClient.On("ConfiguredChainID", mock.Anything).Return(cfg.DefaultChainID(), nil) +// ethClient.On("HeadByNumber", mock.Anything, big.NewInt(10)). +// Return(&head10, nil). +// Once() +// ethClient.On("HeadByNumber", mock.Anything, big.NewInt(8)). +// Return(nil, ethereum.NotFound). +// Once() + +// ht := createHeadTrackerWithNeverSleeper(t, ethClient, cfg, orm) + +// err := ht.Backfill(ctx, &h12, 400) +// require.Error(t, err) +// require.EqualError(t, err, "fetchAndSaveHead failed: not found") + +// h := ht.headSaver.Chain(h12.Hash) + +// // Should contain 12, 11, 10, 9 +// assert.Equal(t, 4, int(h.ChainLength())) +// assert.Equal(t, int64(9), h.EarliestInChain().BlockNumber()) +// }) + +// t.Run("abandons backfill and returns error if the context time budget is exceeded", func(t *testing.T) { +// db := pgtest.NewSqlxDB(t) +// cfg := configtest.NewGeneralConfig(t, nil) +// logger := logger.TestLogger(t) +// orm := headtracker.NewORM(db, logger, cfg.Database(), cltest.FixtureChainID) +// for i := range heads { +// require.NoError(t, orm.IdempotentInsertHead(testutils.Context(t), &heads[i])) +// } + +// ethClient := evmtest.NewEthClientMock(t) +// ethClient.On("ConfiguredChainID", mock.Anything).Return(cfg.DefaultChainID(), nil) +// ethClient.On("HeadByNumber", mock.Anything, big.NewInt(10)). +// Return(&head10, nil) +// ethClient.On("HeadByNumber", mock.Anything, big.NewInt(8)). +// Return(nil, context.DeadlineExceeded) + +// ht := createHeadTrackerWithNeverSleeper(t, ethClient, cfg, orm) + +// err := ht.Backfill(ctx, &h12, 400) +// require.Error(t, err) +// require.EqualError(t, err, "fetchAndSaveHead failed: context deadline exceeded") + +// h := ht.headSaver.Chain(h12.Hash) + +// // Should contain 12, 11, 10, 9 +// assert.Equal(t, 4, int(h.ChainLength())) +// assert.Equal(t, int64(9), h.EarliestInChain().BlockNumber()) +// }) +// } + +// Helper Functions + +func createHeadTracker( + t *testing.T, + config *headtracker.Config, + solanaClient htrktypes.Client[ + *types.Head, + commontypes.Subscription, + types.ChainID, + types.Hash], + hs *headtracker.InMemoryHeadSaver[*types.Head, + types.Hash, + types.ChainID], +) *headTrackerUniverse { + lggr, _ := logger.New() + hb := headtracker.NewBroadcaster(lggr) + mailMon := utils.NewMailboxMonitor(t.Name()) + ht := headtracker.NewTracker(lggr, solanaClient, config, hb, hs, mailMon) + return &headTrackerUniverse{ + mu: new(sync.Mutex), + headTracker: ht, + headBroadcaster: hb, + headSaver: hs, + mailMon: mailMon, + } +} + +// func createHeadTrackerWithNeverSleeper(t *testing.T, solanaClient *client.Client, config *headtracker.Config) *headTrackerUniverse { +// htCfg := headtracker.NewConfig() +// lggr, _ := logger.New() + +// hb := headtracker.NewBroadcaster(lggr) +// hs := headtracker.NewSaver(htCfg, lggr) +// mailMon := utils.NewMailboxMonitor(t.Name()) +// ht := headtracker.NewTracker(lggr, solanaClient, config, hb, hs, mailMon) +// return &headTrackerUniverse{ +// mu: new(sync.Mutex), +// headTracker: ht, +// headBroadcaster: hb, +// headSaver: hs, +// mailMon: mailMon, +// } +// } + +// created with evmcfg := evmtest.NewChainScopedConfig(t, cfg) + +// func createHeadTrackerWithChecker(t *testing.T, ethClient evmclient.Client, config headtracker.Config, htConfig headtracker.HeadTrackerConfig, orm headtracker.ORM, checker httypes.HeadTrackable) *headTrackerUniverse { +// lggr := logger.TestLogger(t) +// hb := headtracker.NewHeadBroadcaster(lggr) +// hs := headtracker.NewSaver(lggr, orm, config, htConfig) +// hb.Subscribe(checker) +// mailMon := utils.NewMailboxMonitor(t.Name()) +// ht := headtracker.NewHeadTracker(lggr, ethClient, config, htConfig, hb, hs, mailMon) +// return &headTrackerUniverse{ +// mu: new(sync.Mutex), +// headTracker: ht, +// headBroadcaster: hb, +// headSaver: hs, +// mailMon: mailMon, +// } +// } + +type headTrackerUniverse struct { + mu *sync.Mutex + stopped bool + headTracker commontypes.HeadTracker[*types.Head, types.Hash] + headBroadcaster commontypes.HeadBroadcaster[*types.Head, types.Hash] + headSaver commontypes.HeadSaver[*types.Head, types.Hash] + mailMon *utils.MailboxMonitor +} + +func (u *headTrackerUniverse) Backfill(ctx context.Context, head *types.Head, depth uint) error { + return u.headTracker.Backfill(ctx, head, depth) +} + +func (u *headTrackerUniverse) Start(t *testing.T) { + u.mu.Lock() + defer u.mu.Unlock() + ctx := testutils.Context(t) + require.NoError(t, u.headBroadcaster.Start(ctx)) + require.NoError(t, u.headTracker.Start(ctx)) + require.NoError(t, u.mailMon.Start(ctx)) + + g := gomega.NewWithT(t) + g.Eventually(func() bool { + report := u.headBroadcaster.HealthReport() + return !slices.ContainsFunc(maps.Values(report), func(e error) bool { return e != nil }) + }, 5*time.Second, testutils.TestInterval).Should(gomega.Equal(true)) + + t.Cleanup(func() { + u.Stop(t) + }) +} + +func (u *headTrackerUniverse) Stop(t *testing.T) { + u.mu.Lock() + defer u.mu.Unlock() + if u.stopped { + return + } + u.stopped = true + require.NoError(t, u.headBroadcaster.Close()) + require.NoError(t, u.headTracker.Close()) + require.NoError(t, u.mailMon.Close()) +} + +func ptr[T any](t T) *T { return &t } From fc4652c95d6ddaca01bef6c1b658483325b9e07a Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Mon, 10 Jul 2023 20:23:18 -0500 Subject: [PATCH 38/47] fixed headsaver --- pkg/solana/headtracker/head_saver_in_mem.go | 19 ++++++++---- .../headtracker/head_saver_in_mem_test.go | 31 ++++++++++--------- 2 files changed, 30 insertions(+), 20 deletions(-) diff --git a/pkg/solana/headtracker/head_saver_in_mem.go b/pkg/solana/headtracker/head_saver_in_mem.go index 37cfe1022..099bf4cbc 100644 --- a/pkg/solana/headtracker/head_saver_in_mem.go +++ b/pkg/solana/headtracker/head_saver_in_mem.go @@ -19,6 +19,7 @@ type InMemoryHeadSaver[H htrktypes.Head[BLOCK_HASH, CHAIN_ID], BLOCK_HASH common HeadsNumber map[int64][]H mu sync.RWMutex getNilHead func() H + getNilHash func() BLOCK_HASH setParent func(H, H) } @@ -33,6 +34,7 @@ func NewInMemoryHeadSaver[ config htrktypes.Config, lggr logger.Logger, getNilHead func() H, + getNilHash func() BLOCK_HASH, setParent func(H, H), ) *InMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID] { return &InMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]{ @@ -41,6 +43,7 @@ func NewInMemoryHeadSaver[ Heads: make(map[BLOCK_HASH]H), HeadsNumber: make(map[int64][]H), getNilHead: getNilHead, + getNilHash: getNilHash, setParent: setParent, } } @@ -51,6 +54,7 @@ func NewSaver(config htrktypes.Config, lggr logger.Logger) *HeadSaver { config, lggr, func() *types.Head { return nil }, + func() types.Hash { return types.Hash{} }, func(head, parent *types.Head) { head.Parent = parent }, ) } @@ -108,16 +112,19 @@ func (hs *InMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) AddHeads(historyDepth int6 for _, head := range newHeads { blockHash := head.BlockHash() blockNumber := head.BlockNumber() + parentHash := head.GetParentHash() if _, exists := hs.Heads[blockHash]; exists { continue } - if parent, exists := hs.Heads[blockHash]; exists { - hs.setParent(head, parent) - } else { - // If parent's head is too old, we should set it to nil - hs.setParent(head, hs.getNilHead()) + if parentHash != hs.getNilHash() { + if parent, exists := hs.Heads[parentHash]; exists { + hs.setParent(head, parent) + } else { + // If parent's head is too old, we should set it to nil + hs.setParent(head, hs.getNilHead()) + } } hs.Heads[blockHash] = head @@ -142,7 +149,7 @@ func (hs *InMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) TrimOldHeads(historyDepth // trimHeads() is an internal function without locking to prevent deadlocks func (hs *InMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) trimHeads(historyDepth int64) { for headNumber, headNumberList := range hs.HeadsNumber { - if hs.latestHead.BlockNumber()-headNumber > historyDepth { + if hs.latestHead.BlockNumber()-headNumber >= historyDepth { for _, head := range headNumberList { delete(hs.Heads, head.BlockHash()) } diff --git a/pkg/solana/headtracker/head_saver_in_mem_test.go b/pkg/solana/headtracker/head_saver_in_mem_test.go index 011d0b93c..98cdd8ae3 100644 --- a/pkg/solana/headtracker/head_saver_in_mem_test.go +++ b/pkg/solana/headtracker/head_saver_in_mem_test.go @@ -48,6 +48,9 @@ func TestInMemoryHeadSaver_Save(t *testing.T) { latest = saver.LatestChain() require.Equal(t, int64(3), latest.BlockNumber()) + + // Check total number of heads + require.Equal(t, 3, len(saver.Heads)) }) t.Run("save invalid head", func(t *testing.T) { @@ -108,11 +111,13 @@ func TestInMemoryHeadSaver_TrimOldHeads(t *testing.T) { require.NoError(t, err) } - // Trim old heads, keeping only the last two (block numbers 3 and 4) + require.Equal(t, 4, len(saver.Heads)) + + // Trim old heads, keeping only the last 3 blocks saver.TrimOldHeads(3) // Check that the correct heads remain - require.Equal(t, 2, len(saver.Heads)) + require.Equal(t, 3, len(saver.Heads)) require.Equal(t, 1, len(saver.HeadByNumber(3))) require.Equal(t, 1, len(saver.HeadByNumber(4))) require.Equal(t, 0, len(saver.HeadByNumber(1))) @@ -122,7 +127,7 @@ func TestInMemoryHeadSaver_TrimOldHeads(t *testing.T) { require.Equal(t, int64(4), latest.BlockNumber()) // Clear All Heads - saver.TrimOldHeads(6) + saver.TrimOldHeads(0) require.Equal(t, 0, len(saver.Heads)) require.Equal(t, 0, len(saver.HeadsNumber)) }) @@ -134,7 +139,7 @@ func TestInMemoryHeadSaver_TrimOldHeads(t *testing.T) { require.NoError(t, err) } - saver.TrimOldHeads(1) + saver.TrimOldHeads(4) // Check that no heads are removed require.Equal(t, 4, len(saver.Heads)) @@ -165,18 +170,16 @@ func TestInMemoryHeadSaver_TrimOldHeads(t *testing.T) { } wg.Wait() - // Concurrently trim old heads with different block numbers - wg.Add(7) - for i := 1; i <= 7; i++ { - go func(num int) { - defer wg.Done() - saver.TrimOldHeads(int64(num)) - }(i) - } + // Concurrently trim old heads of depth 3 + wg.Add(1) + go func() { + defer wg.Done() + saver.TrimOldHeads(3) + }() wg.Wait() // Check that the correct heads remain after concurrent calls to TrimOldHeads - require.Equal(t, 2, len(saver.Heads)) + require.Equal(t, 3, len(saver.Heads)) require.Equal(t, 1, len(saver.HeadByNumber(7))) require.Equal(t, 1, len(saver.HeadByNumber(8))) require.Equal(t, 0, len(saver.HeadByNumber(1))) @@ -214,7 +217,7 @@ func TestInMemoryHeadSaver_Chain(t *testing.T) { err = saver.Save(testutils.Context(t), head2) require.NoError(t, err) - saver.TrimOldHeads(2) + saver.TrimOldHeads(1) invalidBlockHash := head1.BlockHash() retrievedHead := saver.Chain(invalidBlockHash) From eee34aed704eee2cd9a25b3b64638bbb5943d609 Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Mon, 10 Jul 2023 20:50:37 -0500 Subject: [PATCH 39/47] fixed TestHeadTracker_Save_InsertsAndTrimsTable --- pkg/solana/headtracker/head_tracker_test.go | 44 ++++++++++----------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/pkg/solana/headtracker/head_tracker_test.go b/pkg/solana/headtracker/head_tracker_test.go index 4997782ee..e3946e369 100644 --- a/pkg/solana/headtracker/head_tracker_test.go +++ b/pkg/solana/headtracker/head_tracker_test.go @@ -3,6 +3,7 @@ package headtracker_test import ( "context" "fmt" + "math" "math/big" "sync" "testing" @@ -35,9 +36,19 @@ func firstHead( *types.Head, types.Hash, types.ChainID, - ]) (h types.Head) { + ]) (h *types.Head) { + + // Get all the Heads in the HeadSaver and find the one with lowest block number + // Iterate over HeadsNumber + // HeadsNumber is a map[int64][]H + lowestBlockNumber := int64(math.MaxInt64) + for blockNumber := range hs.HeadsNumber { + if blockNumber < lowestBlockNumber { + lowestBlockNumber = blockNumber + } + } - return h + return hs.HeadsNumber[lowestBlockNumber][0] } func TestHeadTracker_New(t *testing.T) { @@ -74,44 +85,31 @@ func TestHeadTracker_Save_InsertsAndTrimsTable(t *testing.T) { client := cltest.NewClientMockWithDefaultChain(t) - var parentHead *types.Head - - for idx := 0; idx < 5; idx++ { + // Generate 200 consecutive heads + for idx := 0; idx < 200; idx++ { idxHead := cltest.Head(idx) + parentHead := headSaver.LatestChain() if parentHead != nil { idxHead.Parent = parentHead } parentHead = idxHead - fmt.Println("parentHead", idxHead.Parent) - assert.Nil(t, headSaver.Save(testutils.Context(t), idxHead)) } ht := createHeadTracker(t, headtracker.NewConfig(), client, headSaver) h := cltest.Head(200) + h.Parent = headSaver.LatestChain() require.NoError(t, ht.headSaver.Save(testutils.Context(t), h)) assert.Equal(t, int64(200), ht.headSaver.LatestChain().BlockNumber()) - // Recursively get the parent of the head until we reach the first head - var firstHead *types.Head - - for { - // print h.Parent - fmt.Println("h.Parent", h.Parent) - if h.Parent == nil { - firstHead = h - break - } - h = h.Parent - } + firstHead := firstHead(t, headSaver) - assert.Equal(t, int64(101), firstHead.BlockNumber()) + assert.Equal(t, int64(100), firstHead.BlockNumber()) - // lastHead, err := orm.LatestHead(testutils.Context(t)) - // require.NoError(t, err) - // assert.Equal(t, int64(200), lastHead.Number) + lastHead := headSaver.LatestChain() + assert.Equal(t, int64(200), lastHead.BlockNumber()) } // func TestHeadTracker_Get(t *testing.T) { From dcadf497b9cda08ac2a3f0d751e399344448f8ad Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Tue, 11 Jul 2023 13:11:54 -0500 Subject: [PATCH 40/47] Added Mock chain for testing --- pkg/internal/cltest/cltest.go | 2 + pkg/internal/testutils/testutils.go | 61 ++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/pkg/internal/cltest/cltest.go b/pkg/internal/cltest/cltest.go index 0a14f5670..16e0718b0 100644 --- a/pkg/internal/cltest/cltest.go +++ b/pkg/internal/cltest/cltest.go @@ -19,6 +19,8 @@ import ( // TODO: write tests for this package +// Chain Specific Test utils + // Head returns a new Head with the given block height func Head(val interface{}) *types.Head { time := solana.UnixTimeSeconds(0) diff --git a/pkg/internal/testutils/testutils.go b/pkg/internal/testutils/testutils.go index 55948e679..86abe1e9a 100644 --- a/pkg/internal/testutils/testutils.go +++ b/pkg/internal/testutils/testutils.go @@ -2,13 +2,23 @@ package testutils import ( "context" + "sync" + "sync/atomic" "testing" "time" + "github.com/stretchr/testify/mock" + + clientmocks "github.com/smartcontractkit/chainlink-relay/pkg/headtracker/types/mocks" + commontypes "github.com/smartcontractkit/chainlink-relay/pkg/types" + commonmocks "github.com/smartcontractkit/chainlink-relay/pkg/types/mocks" "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" ) -// TODO: These can prob refactor to chainlink internal testutils +// TODO: These can refactor to chainlink internal testutils + +// Chain Agnostic Test Utils // Context returns a context with the test's deadline, if available. func Context(tb testing.TB) context.Context { @@ -50,3 +60,52 @@ func NewHeadtrackerConfig(config *headtracker.Config, overrideFn func(*headtrack overrideFn(config) return config } + +type MockChain struct { + Client *clientmocks.Client[ + *types.Head, + commontypes.Subscription, + types.ChainID, + types.Hash] + + CheckFilterLogs func(int64, int64) + subsMu sync.RWMutex + subs []*commonmocks.Subscription + errChs []chan error + subscribeCalls atomic.Int32 + unsubscribeCalls atomic.Int32 +} + +func (m *MockChain) SubscribeCallCount() int32 { + return m.subscribeCalls.Load() +} + +func (m *MockChain) UnsubscribeCallCount() int32 { + return m.unsubscribeCalls.Load() +} + +func (m *MockChain) NewSub(t *testing.T) commontypes.Subscription { + m.subscribeCalls.Add(1) + sub := commonmocks.NewSubscription(t) + errCh := make(chan error) + sub.On("Err"). + Return(func() <-chan error { return errCh }).Maybe() + sub.On("Unsubscribe"). + Run(func(mock.Arguments) { + m.unsubscribeCalls.Add(1) + close(errCh) + }).Return().Maybe() + m.subsMu.Lock() + m.subs = append(m.subs, sub) + m.errChs = append(m.errChs, errCh) + m.subsMu.Unlock() + return sub +} + +func (m *MockChain) SubsErr(err error) { + m.subsMu.Lock() + defer m.subsMu.Unlock() + for _, errCh := range m.errChs { + errCh <- err + } +} From 5f1f859151b1f4855bac89d7167a22b971c2a7b1 Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Tue, 11 Jul 2023 15:43:58 -0500 Subject: [PATCH 41/47] test passing for TestHeadTracker_SwitchesToLongestChainWithHeadSamplingEnabled --- pkg/internal/cltest/cltest.go | 99 +- pkg/internal/testutils/testutils.go | 20 + pkg/internal/utils/hash_helper.go | 11 +- pkg/internal/utils/hash_helper_test.go | 6 +- pkg/solana/client/client.go | 1 - pkg/solana/client/subscription_test.go | 2 - pkg/solana/headtracker/config.go | 4 + .../headtracker/head_broadcaster_test.go | 5 +- pkg/solana/headtracker/head_tracker_test.go | 1041 ++++++++--------- 9 files changed, 647 insertions(+), 542 deletions(-) diff --git a/pkg/internal/cltest/cltest.go b/pkg/internal/cltest/cltest.go index 16e0718b0..affd9f412 100644 --- a/pkg/internal/cltest/cltest.go +++ b/pkg/internal/cltest/cltest.go @@ -56,8 +56,8 @@ func getParentSlot(blockHeight uint64) uint64 { func getBlock(blockHeight, parentSlot uint64, time solana.UnixTimeSeconds) rpc.GetBlockResult { return rpc.GetBlockResult{ - Blockhash: utils.NewHash(), - PreviousBlockhash: utils.NewHash(), + Blockhash: utils.NewSolanaHash(), + PreviousBlockhash: utils.NewSolanaHash(), ParentSlot: parentSlot, Transactions: nil, Rewards: nil, @@ -79,6 +79,97 @@ func createHead(val interface{}, blockHeight uint64, block rpc.GetBlockResult, c } } +// Blocks - a helper logic to construct a range of linked heads +// and an ability to fork and create logs from them +type Blocks struct { + t *testing.T + Hashes []types.Hash + mHashes map[int64]types.Hash + Heads map[int64]*types.Head +} + +func (b *Blocks) Head(number uint64) *types.Head { + return b.Heads[int64(number)] +} + +func NewBlocks(t *testing.T, numHashes int) *Blocks { + hashes := make([]types.Hash, 0) + heads := make(map[int64]*types.Head) + for i := int64(0); i < int64(numHashes); i++ { + hash := utils.NewHash() + hashes = append(hashes, hash) + + heads[i] = Head(i) + if i > 0 { + parent := heads[i-1] + heads[i].Parent = parent + } + } + + hashesMap := make(map[int64]types.Hash) + for i := 0; i < len(hashes); i++ { + hashesMap[int64(i)] = hashes[i] + } + + return &Blocks{ + t: t, + Hashes: hashes, + mHashes: hashesMap, + Heads: heads, + } +} + +func (b *Blocks) NewHead(number uint64) *types.Head { + parentNumber := number - 1 + parent, ok := b.Heads[int64(parentNumber)] + if !ok { + b.t.Fatalf("Can't find parent block at index: %v", parentNumber) + } + + head := Head(number) + head.Parent = parent + + return head +} + +func (b *Blocks) ForkAt(t *testing.T, blockNum int64, numHashes int) *Blocks { + forked := NewBlocks(t, len(b.Heads)+numHashes) + if _, exists := forked.Heads[blockNum]; !exists { + t.Fatalf("Not enough length for block num: %v", blockNum) + } + + for i := int64(0); i < blockNum; i++ { + forked.Heads[i] = b.Heads[i] + } + + forked.Heads[blockNum].Parent = b.Heads[blockNum].Parent + return forked +} + +// HeadBuffer - stores heads in sequence, with increasing timestamps +type HeadBuffer struct { + t *testing.T + Heads []*types.Head +} + +func NewHeadBuffer(t *testing.T) *HeadBuffer { + return &HeadBuffer{ + t: t, + Heads: make([]*types.Head, 0), + } +} + +func (hb *HeadBuffer) Append(head *types.Head) { + // Create a copy of the head, so that we can modify it + cloned := &types.Head{ + Slot: head.Slot, + Block: head.Block, + Parent: head.Parent, + ID: head.ID, + } + hb.Heads = append(hb.Heads, cloned) +} + // MockHeadTrackable allows you to mock HeadTrackable type MockHeadTrackable struct { onNewHeadCount atomic.Int32 @@ -151,8 +242,8 @@ func NewClientMockWithDefaultChain(t *testing.T) *clientmocks.Client[ func ConfigureBlockResult() rpc.GetBlockResult { result := rpc.GetBlockResult{ - Blockhash: utils.NewHash(), - PreviousBlockhash: utils.NewHash(), + Blockhash: utils.NewSolanaHash(), + PreviousBlockhash: utils.NewSolanaHash(), ParentSlot: 0, Transactions: []rpc.TransactionWithMeta{}, Signatures: []solana.Signature{}, diff --git a/pkg/internal/testutils/testutils.go b/pkg/internal/testutils/testutils.go index 86abe1e9a..0025c51a4 100644 --- a/pkg/internal/testutils/testutils.go +++ b/pkg/internal/testutils/testutils.go @@ -109,3 +109,23 @@ func (m *MockChain) SubsErr(err error) { errCh <- err } } + +type RawSub[T any] struct { + ch chan<- T + err <-chan error +} + +func NewRawSub[T any](ch chan<- T, err <-chan error) RawSub[T] { + return RawSub[T]{ch: ch, err: err} +} + +func (r *RawSub[T]) CloseCh() { + close(r.ch) +} + +func (r *RawSub[T]) TrySend(t T) { + select { + case <-r.err: + case r.ch <- t: + } +} diff --git a/pkg/internal/utils/hash_helper.go b/pkg/internal/utils/hash_helper.go index 829454023..efb1f40d0 100644 --- a/pkg/internal/utils/hash_helper.go +++ b/pkg/internal/utils/hash_helper.go @@ -4,10 +4,11 @@ import ( "crypto/rand" "github.com/gagliardetto/solana-go" + "github.com/smartcontractkit/chainlink-solana/pkg/solana/headtracker/types" ) -// NewHash returns a random solana.Hash using SHA-256. -func NewHash() solana.Hash { +// NewSolanaHash returns a random solana.Hash using SHA-256. +func NewSolanaHash() solana.Hash { b := make([]byte, 32) _, err := rand.Read(b) if err != nil { @@ -15,3 +16,9 @@ func NewHash() solana.Hash { } return solana.HashFromBytes(b) } + +func NewHash() types.Hash { + return types.Hash{ + Hash: NewSolanaHash(), + } +} diff --git a/pkg/internal/utils/hash_helper_test.go b/pkg/internal/utils/hash_helper_test.go index 18bc294d7..9f3ebb743 100644 --- a/pkg/internal/utils/hash_helper_test.go +++ b/pkg/internal/utils/hash_helper_test.go @@ -8,11 +8,11 @@ import ( "github.com/stretchr/testify/assert" ) -func TestNewHash(t *testing.T) { +func TestNewSolanaHash(t *testing.T) { t.Parallel() - h1 := utils.NewHash() - h2 := utils.NewHash() + h1 := utils.NewSolanaHash() + h2 := utils.NewSolanaHash() // Check that the two hashes are not the same. assert.NotEqual(t, h1, h2) diff --git a/pkg/solana/client/client.go b/pkg/solana/client/client.go index c9c30500f..451c70d41 100644 --- a/pkg/solana/client/client.go +++ b/pkg/solana/client/client.go @@ -240,7 +240,6 @@ func (c *Client) HeadByNumber(ctx context.Context, number *big.Int) (*types.Head return nil, errors.New("invalid block in HeadByNumber") } chainId := c.ConfiguredChainID() - // TODO: check if parent head can be linked in the headsaver head := &types.Head{ Slot: number.Int64(), Block: *block, diff --git a/pkg/solana/client/subscription_test.go b/pkg/solana/client/subscription_test.go index 1478cb50f..3ab9e3ebc 100644 --- a/pkg/solana/client/subscription_test.go +++ b/pkg/solana/client/subscription_test.go @@ -2,7 +2,6 @@ package client import ( "context" - "fmt" "testing" "time" @@ -67,7 +66,6 @@ func TestSubscription_Unsubscribe(t *testing.T) { select { // Success case <-subscription.ctx.Done(): - fmt.Println("Context is done") return case <-time.After(100 * time.Millisecond): t.Fatal("Expected context to be done") diff --git a/pkg/solana/headtracker/config.go b/pkg/solana/headtracker/config.go index 5dd90bf62..1d0d053d6 100644 --- a/pkg/solana/headtracker/config.go +++ b/pkg/solana/headtracker/config.go @@ -65,3 +65,7 @@ func (c *Config) HeadTrackerSamplingInterval() time.Duration { func (c *Config) PollingInterval() time.Duration { return c.Defaults.PollingInterval } + +func (c *Config) SetHeadTrackerSamplingInterval(d time.Duration) { + c.Defaults.HeadTrackerSamplingInterval = d +} diff --git a/pkg/solana/headtracker/head_broadcaster_test.go b/pkg/solana/headtracker/head_broadcaster_test.go index 54dc85b5f..630e047e4 100644 --- a/pkg/solana/headtracker/head_broadcaster_test.go +++ b/pkg/solana/headtracker/head_broadcaster_test.go @@ -90,9 +90,8 @@ func TestHeadBroadcaster_Subscribe(t *testing.T) { unsubscribe1() headers <- secondHead - // sleep for 3 seconds - time.Sleep(3 * time.Second) - g.Eventually(checker2.OnNewLongestChainCount).Should(gomega.Equal(int32(1))) // TODO: Fix this test + g.SetDefaultEventuallyTimeout(2 * time.Second) + g.Eventually(checker2.OnNewLongestChainCount).Should(gomega.Equal(int32(1))) } func TestHeadBroadcaster_BroadcastNewLongestChain(t *testing.T) { diff --git a/pkg/solana/headtracker/head_tracker_test.go b/pkg/solana/headtracker/head_tracker_test.go index e3946e369..688722712 100644 --- a/pkg/solana/headtracker/head_tracker_test.go +++ b/pkg/solana/headtracker/head_tracker_test.go @@ -2,6 +2,7 @@ package headtracker_test import ( "context" + "errors" "fmt" "math" "math/big" @@ -17,6 +18,7 @@ import ( htrktypes "github.com/smartcontractkit/chainlink-relay/pkg/headtracker/types" "github.com/smartcontractkit/chainlink-relay/pkg/logger" commontypes "github.com/smartcontractkit/chainlink-relay/pkg/types" + commonmocks "github.com/smartcontractkit/chainlink-relay/pkg/types/mocks" "github.com/smartcontractkit/chainlink-relay/pkg/utils" "github.com/smartcontractkit/chainlink-solana/pkg/internal/cltest" "github.com/smartcontractkit/chainlink-solana/pkg/internal/testutils" @@ -58,7 +60,6 @@ func TestHeadTracker_New(t *testing.T) { cfg := headtracker.NewConfig() client := cltest.NewClientMockWithDefaultChain(t) client.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(cltest.Head(0), nil) - headSaver := headtracker.NewSaver(cfg, lggr) assert.Nil(t, headSaver.Save(testutils.Context(t), cltest.Head(1))) last := cltest.Head(16) @@ -70,7 +71,6 @@ func TestHeadTracker_New(t *testing.T) { latest := ht.headSaver.LatestChain() require.NotNil(t, latest) - fmt.Println("latest", latest) assert.Equal(t, last.BlockNumber(), latest.BlockNumber()) } @@ -112,451 +112,425 @@ func TestHeadTracker_Save_InsertsAndTrimsTable(t *testing.T) { assert.Equal(t, int64(200), lastHead.BlockNumber()) } -// func TestHeadTracker_Get(t *testing.T) { -// t.Parallel() - -// start := cltest.Head(5) - -// tests := []struct { -// name string -// initial *evmtypes.Head -// toSave *evmtypes.Head -// want *big.Int -// }{ -// {"greater", start, cltest.Head(6), big.NewInt(6)}, -// {"less than", start, cltest.Head(1), big.NewInt(5)}, -// {"zero", start, cltest.Head(0), big.NewInt(5)}, -// {"nil", start, nil, big.NewInt(5)}, -// {"nil no initial", nil, nil, big.NewInt(0)}, -// } - -// for _, test := range tests { -// t.Run(test.name, func(t *testing.T) { -// db := pgtest.NewSqlxDB(t) -// logger := logger.TestLogger(t) -// config := cltest.NewTestChainScopedConfig(t) -// orm := headtracker.NewORM(db, logger, config.Database(), cltest.FixtureChainID) - -// ethClient := evmtest.NewEthClientMockWithDefaultChain(t) -// chStarted := make(chan struct{}) -// mockEth := &evmtest.MockEth{ -// EthClient: ethClient, -// } -// ethClient.On("SubscribeNewHead", mock.Anything, mock.Anything). -// Maybe(). -// Return( -// func(ctx context.Context, ch chan<- *evmtypes.Head) ethereum.Subscription { -// defer close(chStarted) -// return mockEth.NewSub(t) -// }, -// func(ctx context.Context, ch chan<- *evmtypes.Head) error { return nil }, -// ) -// ethClient.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(cltest.Head(0), nil) - -// fnCall := ethClient.On("HeadByNumber", mock.Anything, mock.Anything) -// fnCall.RunFn = func(args mock.Arguments) { -// num := args.Get(1).(*big.Int) -// fnCall.ReturnArguments = mock.Arguments{cltest.Head(num.Int64()), nil} -// } - -// if test.initial != nil { -// assert.Nil(t, orm.IdempotentInsertHead(testutils.Context(t), test.initial)) -// } - -// ht := createHeadTracker(t, ethClient, config.EVM(), config.EVM().HeadTracker(), orm) -// ht.Start(t) - -// if test.toSave != nil { -// err := ht.headSaver.Save(testutils.Context(t), test.toSave) -// assert.NoError(t, err) -// } - -// assert.Equal(t, test.want, ht.headSaver.LatestChain().ToInt()) -// }) -// } -// } - -// func TestHeadTracker_Start_NewHeads(t *testing.T) { -// t.Parallel() - -// db := pgtest.NewSqlxDB(t) -// logger := logger.TestLogger(t) -// config := cltest.NewTestChainScopedConfig(t) -// orm := headtracker.NewORM(db, logger, config.Database(), cltest.FixtureChainID) - -// ethClient := evmtest.NewEthClientMockWithDefaultChain(t) -// chStarted := make(chan struct{}) -// mockEth := &evmtest.MockEth{EthClient: ethClient} -// sub := mockEth.NewSub(t) -// ethClient.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(cltest.Head(0), nil) -// ethClient.On("SubscribeNewHead", mock.Anything, mock.Anything). -// Run(func(mock.Arguments) { -// close(chStarted) -// }). -// Return(sub, nil) - -// ht := createHeadTracker(t, ethClient, config.EVM(), config.EVM().HeadTracker(), orm) -// ht.Start(t) - -// <-chStarted -// } - -// func TestHeadTracker_Start_CancelContext(t *testing.T) { -// t.Parallel() - -// db := pgtest.NewSqlxDB(t) -// logger := logger.TestLogger(t) -// config := cltest.NewTestChainScopedConfig(t) -// orm := headtracker.NewORM(db, logger, config.Database(), cltest.FixtureChainID) -// ethClient := evmtest.NewEthClientMockWithDefaultChain(t) -// chStarted := make(chan struct{}) -// ethClient.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Run(func(args mock.Arguments) { -// ctx := args.Get(0).(context.Context) -// select { -// case <-ctx.Done(): -// return -// case <-time.After(10 * time.Second): -// assert.FailNow(t, "context was not cancelled within 10s") -// } -// }).Return(cltest.Head(0), nil) -// mockEth := &evmtest.MockEth{EthClient: ethClient} -// sub := mockEth.NewSub(t) -// ethClient.On("SubscribeNewHead", mock.Anything, mock.Anything). -// Run(func(mock.Arguments) { -// close(chStarted) -// }). -// Return(sub, nil). -// Maybe() - -// ht := createHeadTracker(t, ethClient, config.EVM(), config.EVM().HeadTracker(), orm) - -// ctx, cancel := context.WithCancel(testutils.Context(t)) -// go func() { -// time.Sleep(1 * time.Second) -// cancel() -// }() -// err := ht.headTracker.Start(ctx) -// require.NoError(t, err) -// require.NoError(t, ht.headTracker.Close()) -// } - -// func TestHeadTracker_CallsHeadTrackableCallbacks(t *testing.T) { -// t.Parallel() -// g := gomega.NewWithT(t) - -// db := pgtest.NewSqlxDB(t) -// logger := logger.TestLogger(t) -// config := cltest.NewTestChainScopedConfig(t) -// orm := headtracker.NewORM(db, logger, config.Database(), cltest.FixtureChainID) - -// ethClient := evmtest.NewEthClientMockWithDefaultChain(t) - -// chchHeaders := make(chan evmtest.RawSub[*evmtypes.Head], 1) -// mockEth := &evmtest.MockEth{EthClient: ethClient} -// ethClient.On("SubscribeNewHead", mock.Anything, mock.Anything). -// Return( -// func(ctx context.Context, ch chan<- *evmtypes.Head) ethereum.Subscription { -// sub := mockEth.NewSub(t) -// chchHeaders <- evmtest.NewRawSub(ch, sub.Err()) -// return sub -// }, -// func(ctx context.Context, ch chan<- *evmtypes.Head) error { return nil }, -// ) -// ethClient.On("HeadByNumber", mock.Anything, mock.Anything).Return(cltest.Head(0), nil) - -// checker := &cltest.MockHeadTrackable{} -// ht := createHeadTrackerWithChecker(t, ethClient, config.EVM(), config.EVM().HeadTracker(), orm, checker) - -// ht.Start(t) -// assert.Equal(t, int32(0), checker.OnNewLongestChainCount()) - -// headers := <-chchHeaders -// headers.TrySend(&evmtypes.Head{Number: 1, Hash: utils.NewHash(), EVMChainID: utils.NewBig(&cltest.FixtureChainID)}) -// g.Eventually(checker.OnNewLongestChainCount).Should(gomega.Equal(int32(1))) - -// ht.Stop(t) -// assert.Equal(t, int32(1), checker.OnNewLongestChainCount()) -// } - -// func TestHeadTracker_ReconnectOnError(t *testing.T) { -// t.Parallel() -// g := gomega.NewWithT(t) - -// db := pgtest.NewSqlxDB(t) -// logger := logger.TestLogger(t) -// config := cltest.NewTestChainScopedConfig(t) -// orm := headtracker.NewORM(db, logger, config.Database(), cltest.FixtureChainID) - -// ethClient := evmtest.NewEthClientMockWithDefaultChain(t) -// mockEth := &evmtest.MockEth{EthClient: ethClient} -// ethClient.On("SubscribeNewHead", mock.Anything, mock.Anything). -// Return( -// func(ctx context.Context, ch chan<- *evmtypes.Head) ethereum.Subscription { return mockEth.NewSub(t) }, -// func(ctx context.Context, ch chan<- *evmtypes.Head) error { return nil }, -// ) -// ethClient.On("SubscribeNewHead", mock.Anything, mock.Anything).Return(nil, errors.New("cannot reconnect")) -// ethClient.On("SubscribeNewHead", mock.Anything, mock.Anything). -// Return( -// func(ctx context.Context, ch chan<- *evmtypes.Head) ethereum.Subscription { return mockEth.NewSub(t) }, -// func(ctx context.Context, ch chan<- *evmtypes.Head) error { return nil }, -// ) -// ethClient.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(cltest.Head(0), nil) - -// checker := &cltest.MockHeadTrackable{} -// ht := createHeadTrackerWithChecker(t, ethClient, config.EVM(), config.EVM().HeadTracker(), orm, checker) - -// // connect -// ht.Start(t) -// assert.Equal(t, int32(0), checker.OnNewLongestChainCount()) +func TestHeadTracker_Get(t *testing.T) { + t.Parallel() -// // trigger reconnect loop -// mockEth.SubsErr(errors.New("test error to force reconnect")) -// g.Eventually(checker.OnNewLongestChainCount).Should(gomega.Equal(int32(1))) -// } + start := cltest.Head(5) + + tests := []struct { + name string + initial *types.Head + toSave *types.Head + want int64 + }{ + {"greater", start, cltest.Head(6), int64(6)}, + {"less than", start, cltest.Head(1), int64(5)}, + {"zero", start, cltest.Head(0), int64(5)}, + {"nil", start, nil, int64(5)}, + {"nil no initial", nil, nil, int64(0)}, + } -// func TestHeadTracker_ResubscribeOnSubscriptionError(t *testing.T) { -// t.Parallel() -// g := gomega.NewWithT(t) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + headSaver := headtracker.NewSaver(cfg, lggr) + client := cltest.NewClientMockWithDefaultChain(t) + mockChain := &testutils.MockChain{Client: client} + chStarted := make(chan struct{}) + + client.On("SubscribeNewHead", mock.Anything, mock.Anything). + Maybe(). + Return( + func(ctx context.Context, ch chan<- *types.Head) commontypes.Subscription { + defer close(chStarted) + return mockChain.NewSub(t) + }, + func(ctx context.Context, ch chan<- *types.Head) error { return nil }, + ) + client.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(cltest.Head(0), nil) + + fnCall := client.On("HeadByNumber", mock.Anything, mock.Anything) + fnCall.RunFn = func(args mock.Arguments) { + num := args.Get(1).(*big.Int) + fnCall.ReturnArguments = mock.Arguments{cltest.Head(num), nil} + } + + if test.initial != nil { + assert.Nil(t, headSaver.Save(testutils.Context(t), test.initial)) + } + + ht := createHeadTracker(t, cfg, client, headSaver) + ht.Start(t) + + if test.toSave != nil { + err := ht.headSaver.Save(testutils.Context(t), test.toSave) + assert.NoError(t, err) + } + + // Check if that is the correct head that we want + assert.Equal(t, test.want, ht.headSaver.LatestChain().BlockNumber()) + }) + } +} -// db := pgtest.NewSqlxDB(t) -// logger := logger.TestLogger(t) -// config := cltest.NewTestChainScopedConfig(t) -// orm := headtracker.NewORM(db, logger, config.Database(), cltest.FixtureChainID) +func TestHeadTracker_Start_NewHeads(t *testing.T) { + t.Parallel() -// ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + headSaver := headtracker.NewSaver(cfg, lggr) + client := cltest.NewClientMockWithDefaultChain(t) + chStarted := make(chan struct{}) + mockChain := &testutils.MockChain{Client: client} -// chchHeaders := make(chan evmtest.RawSub[*evmtypes.Head], 1) -// mockEth := &evmtest.MockEth{EthClient: ethClient} -// ethClient.On("SubscribeNewHead", mock.Anything, mock.Anything). -// Return( -// func(ctx context.Context, ch chan<- *evmtypes.Head) ethereum.Subscription { -// sub := mockEth.NewSub(t) -// chchHeaders <- evmtest.NewRawSub(ch, sub.Err()) -// return sub -// }, -// func(ctx context.Context, ch chan<- *evmtypes.Head) error { return nil }, -// ) -// ethClient.On("HeadByNumber", mock.Anything, mock.Anything).Return(cltest.Head(0), nil) + sub := mockChain.NewSub(t) + client.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(cltest.Head(0), nil) + client.On("SubscribeNewHead", mock.Anything, mock.Anything). + Run(func(mock.Arguments) { + close(chStarted) + }). + Return(sub, nil) -// checker := &cltest.MockHeadTrackable{} -// ht := createHeadTrackerWithChecker(t, ethClient, config.EVM(), config.EVM().HeadTracker(), orm, checker) + ht := createHeadTracker(t, cfg, client, headSaver) + ht.Start(t) -// ht.Start(t) -// assert.Equal(t, int32(0), checker.OnNewLongestChainCount()) + <-chStarted +} -// headers := <-chchHeaders -// go func() { -// headers.TrySend(cltest.Head(1)) -// }() +func TestHeadTracker_Start_CancelContext(t *testing.T) { + t.Parallel() -// g.Eventually(func() bool { -// report := ht.headTracker.HealthReport() -// return !slices.ContainsFunc(maps.Values(report), func(e error) bool { return e != nil }) -// }, 5*time.Second, testutils.TestInterval).Should(gomega.Equal(true)) + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + headSaver := headtracker.NewSaver(cfg, lggr) + client := cltest.NewClientMockWithDefaultChain(t) + chStarted := make(chan struct{}) + mockChain := &testutils.MockChain{Client: client} + + client.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Run(func(args mock.Arguments) { + ctx := args.Get(0).(context.Context) + select { + case <-ctx.Done(): + return + case <-time.After(10 * time.Second): + assert.FailNow(t, "context was not cancelled within 10s") + } + }).Return(cltest.Head(0), nil) + sub := mockChain.NewSub(t) + client.On("SubscribeNewHead", mock.Anything, mock.Anything). + Run(func(mock.Arguments) { + close(chStarted) + }). + Return(sub, nil). + Maybe() -// // trigger reconnect loop -// headers.CloseCh() + ht := createHeadTracker(t, cfg, client, headSaver) + ctx, cancel := context.WithCancel(testutils.Context(t)) + go func() { + time.Sleep(1 * time.Second) + cancel() + }() + err := ht.headTracker.Start(ctx) + require.NoError(t, err) + require.NoError(t, ht.headTracker.Close()) +} -// // wait for full disconnect and a new subscription -// g.Eventually(checker.OnNewLongestChainCount, 5*time.Second, testutils.TestInterval).Should(gomega.Equal(int32(1))) -// } +func TestHeadTracker_CallsHeadTrackableCallbacks(t *testing.T) { + t.Parallel() + g := gomega.NewWithT(t) -// func TestHeadTracker_Start_LoadsLatestChain(t *testing.T) { -// t.Parallel() + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + headSaver := headtracker.NewSaver(cfg, lggr) + client := cltest.NewClientMockWithDefaultChain(t) + mockChain := &testutils.MockChain{Client: client} + chchHeaders := make(chan testutils.RawSub[*types.Head], 1) + + client.On("SubscribeNewHead", mock.Anything, mock.Anything). + Return( + func(ctx context.Context, ch chan<- *types.Head) commontypes.Subscription { + sub := mockChain.NewSub(t) + chchHeaders <- testutils.NewRawSub(ch, sub.Err()) + return sub + }, + func(ctx context.Context, ch chan<- *types.Head) error { return nil }, + ) + client.On("HeadByNumber", mock.Anything, mock.Anything).Return(cltest.Head(0), nil) + + checker := &cltest.MockHeadTrackable{} + ht := createHeadTrackerWithChecker(t, cfg, client, headSaver, checker) + ht.Start(t) + assert.Equal(t, int32(0), checker.OnNewLongestChainCount()) -// db := pgtest.NewSqlxDB(t) -// logger := logger.TestLogger(t) -// config := cltest.NewTestChainScopedConfig(t) -// ethClient := evmtest.NewEthClientMockWithDefaultChain(t) - -// heads := []*evmtypes.Head{ -// cltest.Head(0), -// cltest.Head(1), -// cltest.Head(2), -// cltest.Head(3), -// } -// var parentHash gethCommon.Hash -// for i := 0; i < len(heads); i++ { -// if parentHash != (gethCommon.Hash{}) { -// heads[i].ParentHash = parentHash -// } -// parentHash = heads[i].Hash -// } -// ethClient.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(heads[3], nil).Maybe() -// ethClient.On("HeadByNumber", mock.Anything, big.NewInt(2)).Return(heads[2], nil).Maybe() -// ethClient.On("HeadByNumber", mock.Anything, big.NewInt(1)).Return(heads[1], nil).Maybe() -// ethClient.On("HeadByNumber", mock.Anything, big.NewInt(0)).Return(heads[0], nil).Maybe() - -// chchHeaders := make(chan evmtest.RawSub[*evmtypes.Head], 1) -// mockEth := &evmtest.MockEth{EthClient: ethClient} -// ethClient.On("SubscribeNewHead", mock.Anything, mock.Anything). -// Return( -// func(ctx context.Context, ch chan<- *evmtypes.Head) ethereum.Subscription { -// sub := mockEth.NewSub(t) -// chchHeaders <- evmtest.NewRawSub(ch, sub.Err()) -// return sub -// }, -// func(ctx context.Context, ch chan<- *evmtypes.Head) error { return nil }, -// ) + headers := <-chchHeaders + headers.TrySend(cltest.Head(1)) -// orm := headtracker.NewORM(db, logger, config.Database(), cltest.FixtureChainID) -// trackable := &cltest.MockHeadTrackable{} -// ht := createHeadTrackerWithChecker(t, ethClient, config.EVM(), config.EVM().HeadTracker(), orm, trackable) + g.Eventually(checker.OnNewLongestChainCount).Should(gomega.Equal(int32(1))) -// require.NoError(t, orm.IdempotentInsertHead(testutils.Context(t), heads[2])) + ht.Stop(t) + assert.Equal(t, int32(1), checker.OnNewLongestChainCount()) +} -// ht.Start(t) +func TestHeadTracker_ReconnectOnError(t *testing.T) { + t.Parallel() + g := gomega.NewWithT(t) -// assert.Equal(t, int32(0), trackable.OnNewLongestChainCount()) + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + headSaver := headtracker.NewSaver(cfg, lggr) + client := cltest.NewClientMockWithDefaultChain(t) + mockChain := &testutils.MockChain{Client: client} + + client.On("SubscribeNewHead", mock.Anything, mock.Anything). + Return( + func(ctx context.Context, ch chan<- *types.Head) commontypes.Subscription { return mockChain.NewSub(t) }, + func(ctx context.Context, ch chan<- *types.Head) error { return nil }, + ) + client.On("SubscribeNewHead", mock.Anything, mock.Anything).Return(nil, errors.New("cannot reconnect")) + client.On("SubscribeNewHead", mock.Anything, mock.Anything). + Return( + func(ctx context.Context, ch chan<- *types.Head) commontypes.Subscription { return mockChain.NewSub(t) }, + func(ctx context.Context, ch chan<- *types.Head) error { return nil }, + ) + client.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(cltest.Head(0), nil) -// headers := <-chchHeaders -// go func() { -// headers.TrySend(cltest.Head(1)) -// }() - -// gomega.NewWithT(t).Eventually(func() bool { -// report := ht.headTracker.HealthReport() -// maps.Copy(report, ht.headBroadcaster.HealthReport()) -// return !slices.ContainsFunc(maps.Values(report), func(e error) bool { return e != nil }) -// }, 5*time.Second, testutils.TestInterval).Should(gomega.Equal(true)) - -// h, err := orm.LatestHead(testutils.Context(t)) -// require.NoError(t, err) -// require.NotNil(t, h) -// assert.Equal(t, h.Number, int64(3)) -// } + checker := &cltest.MockHeadTrackable{} + ht := createHeadTrackerWithChecker(t, cfg, client, headSaver, checker) -// func TestHeadTracker_SwitchesToLongestChainWithHeadSamplingEnabled(t *testing.T) { -// t.Parallel() + // connect + ht.Start(t) + assert.Equal(t, int32(0), checker.OnNewLongestChainCount()) -// db := pgtest.NewSqlxDB(t) -// logger := logger.TestLogger(t) + // trigger reconnect loop + mockChain.SubsErr(errors.New("test error to force reconnect")) + g.SetDefaultEventuallyTimeout(2 * time.Second) + g.Eventually(checker.OnNewLongestChainCount).Should(gomega.Equal(int32(1))) +} -// config := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { -// c.EVM[0].FinalityDepth = ptr[uint32](50) -// // Need to set the buffer to something large since we inject a lot of heads at once and otherwise they will be dropped -// c.EVM[0].HeadTracker.MaxBufferSize = ptr[uint32](100) -// c.EVM[0].HeadTracker.SamplingInterval = models.MustNewDuration(2500 * time.Millisecond) -// }) +func TestHeadTracker_ResubscribeOnSubscriptionError(t *testing.T) { + t.Parallel() + g := gomega.NewWithT(t) -// ethClient := evmtest.NewEthClientMockWithDefaultChain(t) + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + headSaver := headtracker.NewSaver(cfg, lggr) + client := cltest.NewClientMockWithDefaultChain(t) + mockChain := &testutils.MockChain{Client: client} + chchHeaders := make(chan testutils.RawSub[*types.Head], 1) + + client.On("SubscribeNewHead", mock.Anything, mock.Anything). + Return( + func(ctx context.Context, ch chan<- *types.Head) commontypes.Subscription { + sub := mockChain.NewSub(t) + chchHeaders <- testutils.NewRawSub(ch, sub.Err()) + return sub + }, + func(ctx context.Context, ch chan<- *types.Head) error { return nil }, + ) + client.On("HeadByNumber", mock.Anything, mock.Anything).Return(cltest.Head(0), nil) + + checker := &cltest.MockHeadTrackable{} + ht := createHeadTrackerWithChecker(t, cfg, client, headSaver, checker) -// checker := commonmocks.NewHeadTrackable[*evmtypes.Head, gethCommon.Hash](t) -// orm := headtracker.NewORM(db, logger, config.Database(), *config.DefaultChainID()) -// csCfg := evmtest.NewChainScopedConfig(t, config) -// ht := createHeadTrackerWithChecker(t, ethClient, csCfg.EVM(), csCfg.EVM().HeadTracker(), orm, checker) + ht.Start(t) + assert.Equal(t, int32(0), checker.OnNewLongestChainCount()) -// chchHeaders := make(chan evmtest.RawSub[*evmtypes.Head], 1) -// mockEth := &evmtest.MockEth{EthClient: ethClient} -// ethClient.On("SubscribeNewHead", mock.Anything, mock.Anything). -// Return( -// func(ctx context.Context, ch chan<- *evmtypes.Head) ethereum.Subscription { -// sub := mockEth.NewSub(t) -// chchHeaders <- evmtest.NewRawSub(ch, sub.Err()) -// return sub -// }, -// func(ctx context.Context, ch chan<- *evmtypes.Head) error { return nil }, -// ) + headers := <-chchHeaders + go func() { + headers.TrySend(cltest.Head(1)) + }() -// // --------------------- -// blocks := cltest.NewBlocks(t, 10) + g.Eventually(func() bool { + report := ht.headTracker.HealthReport() + return !slices.ContainsFunc(maps.Values(report), func(e error) bool { return e != nil }) + }, 5*time.Second, testutils.TestInterval).Should(gomega.Equal(true)) -// head0 := blocks.Head(0) -// // Initial query -// ethClient.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(head0, nil) -// ht.Start(t) + // trigger reconnect loop + headers.CloseCh() -// headSeq := cltest.NewHeadBuffer(t) -// headSeq.Append(blocks.Head(0)) -// headSeq.Append(blocks.Head(1)) + // wait for full disconnect and a new subscription + g.Eventually(checker.OnNewLongestChainCount, 5*time.Second, testutils.TestInterval).Should(gomega.Equal(int32(1))) +} -// // Blocks 2 and 3 are out of order -// headSeq.Append(blocks.Head(3)) -// headSeq.Append(blocks.Head(2)) +func TestHeadTracker_Start_LoadsLatestChain(t *testing.T) { + t.Parallel() -// // Block 4 comes in -// headSeq.Append(blocks.Head(4)) + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + headSaver := headtracker.NewSaver(cfg, lggr) + client := cltest.NewClientMockWithDefaultChain(t) + mockChain := &testutils.MockChain{Client: client} + chchHeaders := make(chan testutils.RawSub[*types.Head], 1) + + heads := []*types.Head{ + cltest.Head(0), + cltest.Head(1), + cltest.Head(2), + cltest.Head(3), + } -// // Another block at level 4 comes in, that will be uncled -// headSeq.Append(blocks.NewHead(4)) + client.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(heads[3], nil).Maybe() + client.On("HeadByNumber", mock.Anything, big.NewInt(2)).Return(heads[2], nil).Maybe() + client.On("HeadByNumber", mock.Anything, big.NewInt(1)).Return(heads[1], nil).Maybe() + client.On("HeadByNumber", mock.Anything, big.NewInt(0)).Return(heads[0], nil).Maybe() + client.On("SubscribeNewHead", mock.Anything, mock.Anything). + Return( + func(ctx context.Context, ch chan<- *types.Head) commontypes.Subscription { + sub := mockChain.NewSub(t) + chchHeaders <- testutils.NewRawSub(ch, sub.Err()) + return sub + }, + func(ctx context.Context, ch chan<- *types.Head) error { return nil }, + ) + + trackable := &cltest.MockHeadTrackable{} + ht := createHeadTrackerWithChecker(t, cfg, client, headSaver, trackable) + + require.NoError(t, headSaver.Save(context.Background(), heads[2])) -// // Reorg happened forking from block 2 -// blocksForked := blocks.ForkAt(t, 2, 5) -// headSeq.Append(blocksForked.Head(2)) -// headSeq.Append(blocksForked.Head(3)) -// headSeq.Append(blocksForked.Head(4)) -// headSeq.Append(blocksForked.Head(5)) // Now the new chain is longer + ht.Start(t) -// lastLongestChainAwaiter := cltest.NewAwaiter() + assert.Equal(t, int32(0), trackable.OnNewLongestChainCount()) -// // the callback is only called for head number 5 because of head sampling -// checker.On("OnNewLongestChain", mock.Anything, mock.Anything). -// Run(func(args mock.Arguments) { -// h := args.Get(1).(*evmtypes.Head) + headers := <-chchHeaders + go func() { + headers.TrySend(cltest.Head(1)) + }() -// assert.Equal(t, int64(5), h.Number) -// assert.Equal(t, blocksForked.Head(5).Hash, h.Hash) + gomega.NewWithT(t).Eventually(func() bool { + report := ht.headTracker.HealthReport() + maps.Copy(report, ht.headBroadcaster.HealthReport()) + return !slices.ContainsFunc(maps.Values(report), func(e error) bool { return e != nil }) + }, 5*time.Second, testutils.TestInterval).Should(gomega.Equal(true)) -// // This is the new longest chain, check that it came with its parents -// if !assert.NotNil(t, h.Parent) { -// return -// } -// assert.Equal(t, h.Parent.Hash, blocksForked.Head(4).Hash) -// if !assert.NotNil(t, h.Parent.Parent) { -// return -// } -// assert.Equal(t, h.Parent.Parent.Hash, blocksForked.Head(3).Hash) -// if !assert.NotNil(t, h.Parent.Parent.Parent) { -// return -// } -// assert.Equal(t, h.Parent.Parent.Parent.Hash, blocksForked.Head(2).Hash) -// if !assert.NotNil(t, h.Parent.Parent.Parent.Parent) { -// return -// } -// assert.Equal(t, h.Parent.Parent.Parent.Parent.Hash, blocksForked.Head(1).Hash) -// lastLongestChainAwaiter.ItHappened() -// }).Return().Once() + h := headSaver.LatestChain() + require.NotNil(t, h) + assert.Equal(t, h.BlockNumber(), int64(3)) +} -// headers := <-chchHeaders +func TestHeadTracker_SwitchesToLongestChainWithHeadSamplingEnabled(t *testing.T) { + t.Parallel() -// // This grotesque construction is the only way to do dynamic return values using -// // the mock package. We need dynamic returns because we're simulating reorgs. -// latestHeadByNumber := make(map[int64]*evmtypes.Head) -// latestHeadByNumberMu := new(sync.Mutex) + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + headSaver := headtracker.NewSaver(cfg, lggr) + client := cltest.NewClientMockWithDefaultChain(t) + mockChain := &testutils.MockChain{Client: client} + chchHeaders := make(chan testutils.RawSub[*types.Head], 1) + checker := commonmocks.NewHeadTrackable[*types.Head, types.Hash](t) + ht := createHeadTrackerWithChecker(t, cfg, client, headSaver, checker) + cfg.SetHeadTrackerSamplingInterval(2500 * time.Millisecond) + + client.On("SubscribeNewHead", mock.Anything, mock.Anything). + Return( + func(ctx context.Context, ch chan<- *types.Head) commontypes.Subscription { + sub := mockChain.NewSub(t) + chchHeaders <- testutils.NewRawSub(ch, sub.Err()) + return sub + }, + func(ctx context.Context, ch chan<- *types.Head) error { return nil }, + ) + + // --------------------- + blocks := cltest.NewBlocks(t, 10) + + head0 := blocks.Head(0) + // Initial query + client.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(head0, nil) + ht.Start(t) -// fnCall := ethClient.On("HeadByNumber", mock.Anything, mock.Anything) -// fnCall.RunFn = func(args mock.Arguments) { -// latestHeadByNumberMu.Lock() -// defer latestHeadByNumberMu.Unlock() -// num := args.Get(1).(*big.Int) -// head, exists := latestHeadByNumber[num.Int64()] -// if !exists { -// head = cltest.Head(num.Int64()) -// latestHeadByNumber[num.Int64()] = head -// } -// fnCall.ReturnArguments = mock.Arguments{head, nil} -// } + headSeq := cltest.NewHeadBuffer(t) + headSeq.Append(blocks.Head(0)) + headSeq.Append(blocks.Head(1)) + + // Blocks 2 and 3 are out of order + headSeq.Append(blocks.Head(3)) + headSeq.Append(blocks.Head(2)) + + // Block 4 comes in + headSeq.Append(blocks.Head(4)) + + // Another block at level 4 comes in, that will be uncled + headSeq.Append(blocks.NewHead(4)) + + // Reorg happened forking from block 2 + blocksForked := blocks.ForkAt(t, 2, 5) + headSeq.Append(blocksForked.Head(2)) + headSeq.Append(blocksForked.Head(3)) + headSeq.Append(blocksForked.Head(4)) + headSeq.Append(blocksForked.Head(5)) // Now the new chain is longer + + lastLongestChainAwaiter := cltest.NewAwaiter() + + // the callback is only called for head number 5 because of head sampling + checker.On("OnNewLongestChain", mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + h := args.Get(1).(*types.Head) + fmt.Println("OnNewLongestChain", h.BlockNumber()) + + assert.Equal(t, int64(5), h.BlockNumber()) + assert.Equal(t, blocksForked.Head(5).BlockHash(), h.BlockHash()) + + // This is the new longest chain, check that it came with its parents + if !assert.NotNil(t, h.Parent) { + return + } + assert.Equal(t, h.Parent.BlockHash(), blocksForked.Head(4).BlockHash()) + if !assert.NotNil(t, h.Parent.Parent) { + return + } + assert.Equal(t, h.Parent.Parent.BlockHash(), blocksForked.Head(3).BlockHash()) + if !assert.NotNil(t, h.Parent.Parent.Parent) { + return + } + assert.Equal(t, h.Parent.Parent.Parent.BlockHash(), blocksForked.Head(2).BlockHash()) + if !assert.NotNil(t, h.Parent.Parent.Parent.Parent) { + return + } + assert.Equal(t, h.Parent.Parent.Parent.Parent.BlockHash(), blocksForked.Head(1).BlockHash()) + lastLongestChainAwaiter.ItHappened() + }).Return().Once() + + headers := <-chchHeaders + + // This grotesque construction is the only way to do dynamic return values using + // the mock package. We need dynamic returns because we're simulating reorgs. + latestHeadByNumber := make(map[int64]*types.Head) + latestHeadByNumberMu := new(sync.Mutex) + + fnCall := client.On("HeadByNumber", mock.Anything, mock.Anything) + fnCall.RunFn = func(args mock.Arguments) { + latestHeadByNumberMu.Lock() + defer latestHeadByNumberMu.Unlock() + num := args.Get(1).(*big.Int) + head, exists := latestHeadByNumber[num.Int64()] + if !exists { + head = cltest.Head(num.Int64()) + latestHeadByNumber[num.Int64()] = head + } + fnCall.ReturnArguments = mock.Arguments{head, nil} + } -// for _, h := range headSeq.Heads { -// latestHeadByNumberMu.Lock() -// latestHeadByNumber[h.Number] = h -// latestHeadByNumberMu.Unlock() -// headers.TrySend(h) -// } + for _, h := range headSeq.Heads { + latestHeadByNumberMu.Lock() + latestHeadByNumber[h.BlockNumber()] = h + latestHeadByNumberMu.Unlock() + headers.TrySend(h) + } -// // default 10s may not be sufficient, so using testutils.WaitTimeout(t) -// lastLongestChainAwaiter.AwaitOrFail(t, testutils.WaitTimeout(t)) -// ht.Stop(t) -// assert.Equal(t, int64(5), ht.headSaver.LatestChain().Number) + // default 10s may not be sufficient, so using testutils.WaitTimeout(t) + lastLongestChainAwaiter.AwaitOrFail(t, testutils.WaitTimeout(t)) + ht.Stop(t) + assert.Equal(t, int64(5), ht.headSaver.LatestChain().BlockNumber()) -// for _, h := range headSeq.Heads { -// c := ht.headSaver.Chain(h.Hash) -// require.NotNil(t, c) -// assert.Equal(t, c.ParentHash, h.ParentHash) -// assert.Equal(t, c.Timestamp.Unix(), h.Timestamp.UTC().Unix()) -// assert.Equal(t, c.Number, h.Number) -// } -// } + for _, h := range headSeq.Heads { + c := ht.headSaver.Chain(h.BlockHash()) + require.NotNil(t, c) + assert.Equal(t, c.GetParentHash(), h.GetParentHash()) + assert.Equal(t, c.BlockNumber(), h.BlockNumber()) + } +} // func TestHeadTracker_SwitchesToLongestChainWithHeadSamplingDisabled(t *testing.T) { // t.Parallel() @@ -571,31 +545,31 @@ func TestHeadTracker_Save_InsertsAndTrimsTable(t *testing.T) { // c.EVM[0].HeadTracker.SamplingInterval = models.MustNewDuration(0) // }) -// ethClient := evmtest.NewEthClientMockWithDefaultChain(t) +// client := evmtest.NewclientMockWithDefaultChain(t) -// checker := commonmocks.NewHeadTrackable[*evmtypes.Head, gethCommon.Hash](t) +// checker := commonmocks.NewHeadTrackable[*types.Head, types.Hash](t) // orm := headtracker.NewORM(db, logger, config.Database(), cltest.FixtureChainID) // evmcfg := evmtest.NewChainScopedConfig(t, config) -// ht := createHeadTrackerWithChecker(t, ethClient, evmcfg.EVM(), evmcfg.EVM().HeadTracker(), orm, checker) +// ht := createHeadTrackerWithChecker(t, client, evmcfg.EVM(), evmcfg.EVM().HeadTracker(), orm, checker) -// chchHeaders := make(chan evmtest.RawSub[*evmtypes.Head], 1) -// mockEth := &evmtest.MockEth{EthClient: ethClient} -// ethClient.On("SubscribeNewHead", mock.Anything, mock.Anything). +// chchHeaders := make(chan evmtest.RawSub[*types.Head], 1) +// mockChain := &evmtest.mockChain{client: client} +// client.On("SubscribeNewHead", mock.Anything, mock.Anything). // Return( -// func(ctx context.Context, ch chan<- *evmtypes.Head) ethereum.Subscription { -// sub := mockEth.NewSub(t) -// chchHeaders <- evmtest.NewRawSub(ch, sub.Err()) +// func(ctx context.Context, ch chan<- *types.Head) commontypes.Subscription { +// sub := mockChain.NewSub(t) +// chchHeaders <- testutils.NewRawSub(ch, sub.Err()) // return sub // }, -// func(ctx context.Context, ch chan<- *evmtypes.Head) error { return nil }, +// func(ctx context.Context, ch chan<- *types.Head) error { return nil }, // ) // // --------------------- // blocks := cltest.NewBlocks(t, 10) -// head0 := blocks.Head(0) // evmtypes.Head{Number: 0, Hash: utils.NewHash(), ParentHash: utils.NewHash(), Timestamp: time.Unix(0, 0)} +// head0 := blocks.Head(0) // types.Head{Number: 0, Hash: utils.NewSolanaHash(), ParentHash: utils.NewSolanaHash(), Timestamp: time.Unix(0, 0)} // // Initial query -// ethClient.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(head0, nil) +// client.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(head0, nil) // headSeq := cltest.NewHeadBuffer(t) // headSeq.Append(blocks.Head(0)) @@ -622,56 +596,56 @@ func TestHeadTracker_Save_InsertsAndTrimsTable(t *testing.T) { // checker.On("OnNewLongestChain", mock.Anything, mock.Anything). // Run(func(args mock.Arguments) { -// h := args.Get(1).(*evmtypes.Head) -// require.Equal(t, int64(0), h.Number) -// require.Equal(t, blocks.Head(0).Hash, h.Hash) +// h := args.Get(1).(*types.Head) +// require.Equal(t, int64(0), h.BlockNumber()) +// require.Equal(t, blocks.Head(0).BlockHash(), h.BlockHash()) // }).Return().Once() // checker.On("OnNewLongestChain", mock.Anything, mock.Anything). // Run(func(args mock.Arguments) { -// h := args.Get(1).(*evmtypes.Head) -// require.Equal(t, int64(1), h.Number) -// require.Equal(t, blocks.Head(1).Hash, h.Hash) +// h := args.Get(1).(*types.Head) +// require.Equal(t, int64(1), h.BlockNumber()) +// require.Equal(t, blocks.Head(1).BlockHash(), h.BlockHash()) // }).Return().Once() // checker.On("OnNewLongestChain", mock.Anything, mock.Anything). // Run(func(args mock.Arguments) { -// h := args.Get(1).(*evmtypes.Head) -// require.Equal(t, int64(3), h.Number) -// require.Equal(t, blocks.Head(3).Hash, h.Hash) +// h := args.Get(1).(*types.Head) +// require.Equal(t, int64(3), h.BlockNumber()) +// require.Equal(t, blocks.Head(3).BlockHash(), h.BlockHash()) // }).Return().Once() // checker.On("OnNewLongestChain", mock.Anything, mock.Anything). // Run(func(args mock.Arguments) { -// h := args.Get(1).(*evmtypes.Head) -// require.Equal(t, int64(4), h.Number) -// require.Equal(t, blocks.Head(4).Hash, h.Hash) +// h := args.Get(1).(*types.Head) +// require.Equal(t, int64(4), h.BlockNumber()) +// require.Equal(t, blocks.Head(4).BlockHash(), h.BlockHash()) // // Check that the block came with its parents // require.NotNil(t, h.Parent) -// require.Equal(t, h.Parent.Hash, blocks.Head(3).Hash) -// require.NotNil(t, h.Parent.Parent.Hash) -// require.Equal(t, h.Parent.Parent.Hash, blocks.Head(2).Hash) +// require.Equal(t, h.Parent.BlockHash(), blocks.Head(3).BlockHash()) +// require.NotNil(t, h.Parent.Parent.BlockHash()) +// require.Equal(t, h.Parent.Parent.BlockHash(), blocks.Head(2).BlockHash()) // require.NotNil(t, h.Parent.Parent.Parent) -// require.Equal(t, h.Parent.Parent.Parent.Hash, blocks.Head(1).Hash) +// require.Equal(t, h.Parent.Parent.Parent.BlockHash(), blocks.Head(1).BlockHash()) // }).Return().Once() // checker.On("OnNewLongestChain", mock.Anything, mock.Anything). // Run(func(args mock.Arguments) { -// h := args.Get(1).(*evmtypes.Head) +// h := args.Get(1).(*types.Head) -// require.Equal(t, int64(5), h.Number) -// require.Equal(t, blocksForked.Head(5).Hash, h.Hash) +// require.Equal(t, int64(5), h.BlockNumber()) +// require.Equal(t, blocksForked.Head(5).BlockHash(), h.BlockHash()) // // This is the new longest chain, check that it came with its parents // require.NotNil(t, h.Parent) -// require.Equal(t, h.Parent.Hash, blocksForked.Head(4).Hash) +// require.Equal(t, h.Parent.BlockHash(), blocksForked.Head(4).BlockHash()) // require.NotNil(t, h.Parent.Parent) -// require.Equal(t, h.Parent.Parent.Hash, blocksForked.Head(3).Hash) +// require.Equal(t, h.Parent.Parent.BlockHash(), blocksForked.Head(3).BlockHash()) // require.NotNil(t, h.Parent.Parent.Parent) -// require.Equal(t, h.Parent.Parent.Parent.Hash, blocksForked.Head(2).Hash) +// require.Equal(t, h.Parent.Parent.Parent.BlockHash(), blocksForked.Head(2).BlockHash()) // require.NotNil(t, h.Parent.Parent.Parent.Parent) -// require.Equal(t, h.Parent.Parent.Parent.Parent.Hash, blocksForked.Head(1).Hash) +// require.Equal(t, h.Parent.Parent.Parent.Parent.BlockHash(), blocksForked.Head(1).BlockHash()) // lastLongestChainAwaiter.ItHappened() // }).Return().Once() @@ -681,25 +655,25 @@ func TestHeadTracker_Save_InsertsAndTrimsTable(t *testing.T) { // // This grotesque construction is the only way to do dynamic return values using // // the mock package. We need dynamic returns because we're simulating reorgs. -// latestHeadByNumber := make(map[int64]*evmtypes.Head) +// latestHeadByNumber := make(map[int64]*types.Head) // latestHeadByNumberMu := new(sync.Mutex) -// fnCall := ethClient.On("HeadByNumber", mock.Anything, mock.Anything) +// fnCall := client.On("HeadByNumber", mock.Anything, mock.Anything) // fnCall.RunFn = func(args mock.Arguments) { // latestHeadByNumberMu.Lock() // defer latestHeadByNumberMu.Unlock() -// num := args.Get(1).(*big.Int) -// head, exists := latestHeadByNumber[num.Int64()] +// num := args.Get(1).(int64) +// head, exists := latestHeadByNumber[num] // if !exists { -// head = cltest.Head(num.Int64()) -// latestHeadByNumber[num.Int64()] = head +// head = cltest.Head(num) +// latestHeadByNumber[num] = head // } // fnCall.ReturnArguments = mock.Arguments{head, nil} // } // for _, h := range headSeq.Heads { // latestHeadByNumberMu.Lock() -// latestHeadByNumber[h.Number] = h +// latestHeadByNumber[h.BlockNumber()] = h // latestHeadByNumberMu.Unlock() // headers.TrySend(h) // time.Sleep(testutils.TestInterval) @@ -708,14 +682,14 @@ func TestHeadTracker_Save_InsertsAndTrimsTable(t *testing.T) { // // default 10s may not be sufficient, so using testutils.WaitTimeout(t) // lastLongestChainAwaiter.AwaitOrFail(t, testutils.WaitTimeout(t)) // ht.Stop(t) -// assert.Equal(t, int64(5), ht.headSaver.LatestChain().Number) +// assert.Equal(t, int64(5), ht.headSaver.LatestChain().BlockNumber()) // for _, h := range headSeq.Heads { -// c := ht.headSaver.Chain(h.Hash) +// c := ht.headSaver.Chain(h.BlockHash()) // require.NotNil(t, c) // assert.Equal(t, c.ParentHash, h.ParentHash) // assert.Equal(t, c.Timestamp.Unix(), h.Timestamp.UTC().Unix()) -// assert.Equal(t, c.Number, h.Number) +// assert.Equal(t, c.BlockNumber(), h.BlockNumber()) // } // } @@ -735,51 +709,51 @@ func TestHeadTracker_Save_InsertsAndTrimsTable(t *testing.T) { // now := uint64(time.Now().UTC().Unix()) // gethHead0 := &gethTypes.Header{ -// Number: big.NewInt(0), -// ParentHash: gethCommon.BigToHash(big.NewInt(0)), +// Number: int64(0), +// ParentHash: gethCommon.BigToHash(int64(0)), // Time: now, // } -// head0 := evmtypes.NewHead(gethHead0.Number, utils.NewHash(), gethHead0.ParentHash, gethHead0.Time, utils.NewBig(&cltest.FixtureChainID)) +// head0 := evmtypes.NewHead(gethHead0.BlockNumber(), utils.NewSolanaHash(), gethHead0.ParentHash, gethHead0.Time, utils.NewBig(&cltest.FixtureChainID)) // h1 := *cltest.Head(1) -// h1.ParentHash = head0.Hash +// h1.ParentHash = head0.BlockHash() // gethHead8 := &gethTypes.Header{ -// Number: big.NewInt(8), -// ParentHash: utils.NewHash(), +// Number: int64(8), +// ParentHash: utils.NewSolanaHash(), // Time: now, // } -// head8 := evmtypes.NewHead(gethHead8.Number, utils.NewHash(), gethHead8.ParentHash, gethHead8.Time, utils.NewBig(&cltest.FixtureChainID)) +// head8 := evmtypes.NewHead(gethHead8.BlockNumber(), utils.NewSolanaHash(), gethHead8.ParentHash, gethHead8.Time, utils.NewBig(&cltest.FixtureChainID)) // h9 := *cltest.Head(9) -// h9.ParentHash = head8.Hash +// h9.ParentHash = head8.BlockHash() // gethHead10 := &gethTypes.Header{ -// Number: big.NewInt(10), -// ParentHash: h9.Hash, +// Number: int64(10), +// ParentHash: h9.BlockHash(), // Time: now, // } -// head10 := evmtypes.NewHead(gethHead10.Number, utils.NewHash(), gethHead10.ParentHash, gethHead10.Time, utils.NewBig(&cltest.FixtureChainID)) +// head10 := evmtypes.NewHead(gethHead10.BlockNumber(), utils.NewSolanaHash(), gethHead10.ParentHash, gethHead10.Time, utils.NewBig(&cltest.FixtureChainID)) // h11 := *cltest.Head(11) -// h11.ParentHash = head10.Hash +// h11.ParentHash = head10.BlockHash() // h12 := *cltest.Head(12) -// h12.ParentHash = h11.Hash +// h12.ParentHash = h11.BlockHash() // h13 := *cltest.Head(13) -// h13.ParentHash = h12.Hash +// h13.ParentHash = h12.BlockHash() // h14Orphaned := *cltest.Head(14) -// h14Orphaned.ParentHash = h13.Hash +// h14Orphaned.ParentHash = h13.BlockHash() // h14 := *cltest.Head(14) -// h14.ParentHash = h13.Hash +// h14.ParentHash = h13.BlockHash() // h15 := *cltest.Head(15) -// h15.ParentHash = h14.Hash +// h15.ParentHash = h14.BlockHash() -// heads := []evmtypes.Head{ +// heads := []types.Head{ // h9, // h11, // h12, @@ -800,9 +774,9 @@ func TestHeadTracker_Save_InsertsAndTrimsTable(t *testing.T) { // require.NoError(t, orm.IdempotentInsertHead(testutils.Context(t), &heads[i])) // } -// ethClient := evmtest.NewEthClientMock(t) -// ethClient.On("ConfiguredChainID", mock.Anything).Return(cfg.DefaultChainID(), nil) -// ht := createHeadTrackerWithNeverSleeper(t, ethClient, cfg, orm) +// client := evmtest.NewclientMock(t) +// client.On("ConfiguredChainID", mock.Anything).Return(cfg.DefaultChainID(), nil) +// ht := createHeadTrackerWithNeverSleeper(t, client, cfg, orm) // err := ht.Backfill(ctx, &h12, 2) // require.NoError(t, err) @@ -817,31 +791,31 @@ func TestHeadTracker_Save_InsertsAndTrimsTable(t *testing.T) { // require.NoError(t, orm.IdempotentInsertHead(testutils.Context(t), &heads[i])) // } -// ethClient := evmtest.NewEthClientMock(t) -// ethClient.On("ConfiguredChainID", mock.Anything).Return(cfg.DefaultChainID(), nil) -// ethClient.On("HeadByNumber", mock.Anything, big.NewInt(10)). +// client := evmtest.NewclientMock(t) +// client.On("ConfiguredChainID", mock.Anything).Return(cfg.DefaultChainID(), nil) +// client.On("HeadByNumber", mock.Anything, int64(10)). // Return(&head10, nil) -// ht := createHeadTrackerWithNeverSleeper(t, ethClient, cfg, orm) +// ht := createHeadTrackerWithNeverSleeper(t, client, cfg, orm) // var depth uint = 3 // err := ht.Backfill(ctx, &h12, depth) // require.NoError(t, err) -// h := ht.headSaver.Chain(h12.Hash) +// h := ht.headSaver.Chain(h12.BlockHash()) -// assert.Equal(t, int64(12), h.Number) +// assert.Equal(t, int64(12), h.BlockNumber()) // require.NotNil(t, h.Parent) -// assert.Equal(t, int64(11), h.Parent.Number) +// assert.Equal(t, int64(11), h.Parent.BlockNumber()) // require.NotNil(t, h.Parent) -// assert.Equal(t, int64(10), h.Parent.Parent.Number) +// assert.Equal(t, int64(10), h.Parent.Parent.BlockNumber()) // require.NotNil(t, h.Parent.Parent.Parent) -// assert.Equal(t, int64(9), h.Parent.Parent.Parent.Number) +// assert.Equal(t, int64(9), h.Parent.Parent.Parent.BlockNumber()) -// writtenHead, err := orm.HeadByHash(testutils.Context(t), head10.Hash) +// writtenHead, err := orm.HeadByHash(testutils.Context(t), head10.BlockHash()) // require.NoError(t, err) -// assert.Equal(t, int64(10), writtenHead.Number) +// assert.Equal(t, int64(10), writtenHead.BlockNumber()) // }) // t.Run("fetches only heads that are missing", func(t *testing.T) { @@ -853,14 +827,14 @@ func TestHeadTracker_Save_InsertsAndTrimsTable(t *testing.T) { // require.NoError(t, orm.IdempotentInsertHead(testutils.Context(t), &heads[i])) // } -// ethClient := evmtest.NewEthClientMock(t) -// ethClient.On("ConfiguredChainID", mock.Anything).Return(cfg.DefaultChainID(), nil) +// client := evmtest.NewclientMock(t) +// client.On("ConfiguredChainID", mock.Anything).Return(cfg.DefaultChainID(), nil) -// ht := createHeadTrackerWithNeverSleeper(t, ethClient, cfg, orm) +// ht := createHeadTrackerWithNeverSleeper(t, client, cfg, orm) -// ethClient.On("HeadByNumber", mock.Anything, big.NewInt(10)). +// client.On("HeadByNumber", mock.Anything, int64(10)). // Return(&head10, nil) -// ethClient.On("HeadByNumber", mock.Anything, big.NewInt(8)). +// client.On("HeadByNumber", mock.Anything, int64(8)). // Return(&head8, nil) // // Needs to be 8 because there are 8 heads in chain (15,14,13,12,11,10,9,8) @@ -869,12 +843,12 @@ func TestHeadTracker_Save_InsertsAndTrimsTable(t *testing.T) { // err := ht.Backfill(ctx, &h15, depth) // require.NoError(t, err) -// h := ht.headSaver.Chain(h15.Hash) +// h := ht.headSaver.Chain(h15.BlockHash()) // require.Equal(t, uint32(8), h.ChainLength()) // earliestInChain := h.EarliestInChain() -// assert.Equal(t, head8.Number, earliestInChain.BlockNumber()) -// assert.Equal(t, head8.Hash, earliestInChain.BlockHash()) +// assert.Equal(t, head8.BlockNumber(), earliestInChain.BlockNumber()) +// assert.Equal(t, head8.BlockHash(), earliestInChain.BlockHash()) // }) // t.Run("does not backfill if chain length is already greater than or equal to depth", func(t *testing.T) { @@ -886,10 +860,10 @@ func TestHeadTracker_Save_InsertsAndTrimsTable(t *testing.T) { // require.NoError(t, orm.IdempotentInsertHead(testutils.Context(t), &heads[i])) // } -// ethClient := evmtest.NewEthClientMock(t) -// ethClient.On("ConfiguredChainID", mock.Anything).Return(cfg.DefaultChainID(), nil) +// client := evmtest.NewclientMock(t) +// client.On("ConfiguredChainID", mock.Anything).Return(cfg.DefaultChainID(), nil) -// ht := createHeadTrackerWithNeverSleeper(t, ethClient, cfg, orm) +// ht := createHeadTrackerWithNeverSleeper(t, client, cfg, orm) // err := ht.Backfill(ctx, &h15, 3) // require.NoError(t, err) @@ -904,19 +878,19 @@ func TestHeadTracker_Save_InsertsAndTrimsTable(t *testing.T) { // logger := logger.TestLogger(t) // orm := headtracker.NewORM(db, logger, cfg.Database(), cltest.FixtureChainID) -// ethClient := evmtest.NewEthClientMock(t) -// ethClient.On("ConfiguredChainID", mock.Anything).Return(cfg.DefaultChainID(), nil) -// ethClient.On("HeadByNumber", mock.Anything, big.NewInt(0)). +// client := evmtest.NewclientMock(t) +// client.On("ConfiguredChainID", mock.Anything).Return(cfg.DefaultChainID(), nil) +// client.On("HeadByNumber", mock.Anything, int64(0)). // Return(&head0, nil) // require.NoError(t, orm.IdempotentInsertHead(testutils.Context(t), &h1)) -// ht := createHeadTrackerWithNeverSleeper(t, ethClient, cfg, orm) +// ht := createHeadTrackerWithNeverSleeper(t, client, cfg, orm) // err := ht.Backfill(ctx, &h1, 400) // require.NoError(t, err) -// h := ht.headSaver.Chain(h1.Hash) +// h := ht.headSaver.Chain(h1.BlockHash()) // require.NotNil(t, h) // require.Equal(t, uint32(2), h.ChainLength()) @@ -932,22 +906,22 @@ func TestHeadTracker_Save_InsertsAndTrimsTable(t *testing.T) { // require.NoError(t, orm.IdempotentInsertHead(testutils.Context(t), &heads[i])) // } -// ethClient := evmtest.NewEthClientMock(t) -// ethClient.On("ConfiguredChainID", mock.Anything).Return(cfg.DefaultChainID(), nil) -// ethClient.On("HeadByNumber", mock.Anything, big.NewInt(10)). +// client := evmtest.NewclientMock(t) +// client.On("ConfiguredChainID", mock.Anything).Return(cfg.DefaultChainID(), nil) +// client.On("HeadByNumber", mock.Anything, int64(10)). // Return(&head10, nil). // Once() -// ethClient.On("HeadByNumber", mock.Anything, big.NewInt(8)). +// client.On("HeadByNumber", mock.Anything, int64(8)). // Return(nil, ethereum.NotFound). // Once() -// ht := createHeadTrackerWithNeverSleeper(t, ethClient, cfg, orm) +// ht := createHeadTrackerWithNeverSleeper(t, client, cfg, orm) // err := ht.Backfill(ctx, &h12, 400) // require.Error(t, err) // require.EqualError(t, err, "fetchAndSaveHead failed: not found") -// h := ht.headSaver.Chain(h12.Hash) +// h := ht.headSaver.Chain(h12.BlockHash()) // // Should contain 12, 11, 10, 9 // assert.Equal(t, 4, int(h.ChainLength())) @@ -963,20 +937,20 @@ func TestHeadTracker_Save_InsertsAndTrimsTable(t *testing.T) { // require.NoError(t, orm.IdempotentInsertHead(testutils.Context(t), &heads[i])) // } -// ethClient := evmtest.NewEthClientMock(t) -// ethClient.On("ConfiguredChainID", mock.Anything).Return(cfg.DefaultChainID(), nil) -// ethClient.On("HeadByNumber", mock.Anything, big.NewInt(10)). +// client := evmtest.NewclientMock(t) +// client.On("ConfiguredChainID", mock.Anything).Return(cfg.DefaultChainID(), nil) +// client.On("HeadByNumber", mock.Anything, int64(10)). // Return(&head10, nil) -// ethClient.On("HeadByNumber", mock.Anything, big.NewInt(8)). +// client.On("HeadByNumber", mock.Anything, int64(8)). // Return(nil, context.DeadlineExceeded) -// ht := createHeadTrackerWithNeverSleeper(t, ethClient, cfg, orm) +// ht := createHeadTrackerWithNeverSleeper(t, client, cfg, orm) // err := ht.Backfill(ctx, &h12, 400) // require.Error(t, err) // require.EqualError(t, err, "fetchAndSaveHead failed: context deadline exceeded") -// h := ht.headSaver.Chain(h12.Hash) +// h := ht.headSaver.Chain(h12.BlockHash()) // // Should contain 12, 11, 10, 9 // assert.Equal(t, 4, int(h.ChainLength())) @@ -994,7 +968,8 @@ func createHeadTracker( commontypes.Subscription, types.ChainID, types.Hash], - hs *headtracker.InMemoryHeadSaver[*types.Head, + hs *headtracker.InMemoryHeadSaver[ + *types.Head, types.Hash, types.ChainID], ) *headTrackerUniverse { @@ -1030,21 +1005,33 @@ func createHeadTracker( // created with evmcfg := evmtest.NewChainScopedConfig(t, cfg) -// func createHeadTrackerWithChecker(t *testing.T, ethClient evmclient.Client, config headtracker.Config, htConfig headtracker.HeadTrackerConfig, orm headtracker.ORM, checker httypes.HeadTrackable) *headTrackerUniverse { -// lggr := logger.TestLogger(t) -// hb := headtracker.NewHeadBroadcaster(lggr) -// hs := headtracker.NewSaver(lggr, orm, config, htConfig) -// hb.Subscribe(checker) -// mailMon := utils.NewMailboxMonitor(t.Name()) -// ht := headtracker.NewHeadTracker(lggr, ethClient, config, htConfig, hb, hs, mailMon) -// return &headTrackerUniverse{ -// mu: new(sync.Mutex), -// headTracker: ht, -// headBroadcaster: hb, -// headSaver: hs, -// mailMon: mailMon, -// } -// } +func createHeadTrackerWithChecker(t *testing.T, + config *headtracker.Config, + solanaClient htrktypes.Client[ + *types.Head, + commontypes.Subscription, + types.ChainID, + types.Hash], + hs *headtracker.InMemoryHeadSaver[ + *types.Head, + types.Hash, + types.ChainID], + checker commontypes.HeadTrackable[*types.Head, types.Hash], +) *headTrackerUniverse { + lggr, _ := logger.New() + hb := headtracker.NewBroadcaster(lggr) + + hb.Subscribe(checker) + mailMon := utils.NewMailboxMonitor(t.Name()) + ht := headtracker.NewTracker(lggr, solanaClient, config, hb, hs, mailMon) + return &headTrackerUniverse{ + mu: new(sync.Mutex), + headTracker: ht, + headBroadcaster: hb, + headSaver: hs, + mailMon: mailMon, + } +} type headTrackerUniverse struct { mu *sync.Mutex From ca79b23cacad1795e1732b9ada91e7f8deb2f6d8 Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Wed, 12 Jul 2023 16:52:31 -0500 Subject: [PATCH 42/47] Fixed Get Parent Hash method for head --- pkg/solana/headtracker/types/head.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pkg/solana/headtracker/types/head.go b/pkg/solana/headtracker/types/head.go index bde9042c3..b04b822f8 100644 --- a/pkg/solana/headtracker/types/head.go +++ b/pkg/solana/headtracker/types/head.go @@ -79,10 +79,7 @@ func (h *Head) GetParent() commontypes.Head[Hash] { } func (h *Head) GetParentHash() Hash { - if h.Parent == nil { - return Hash{} - } - return h.Parent.BlockHash() + return Hash{Hash: h.Block.PreviousBlockhash} } func (h *Head) HashAtHeight(slotNum int64) Hash { From d4362f1bf941ff9243aabf9b5323c0ec9cefddc6 Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Wed, 12 Jul 2023 16:53:27 -0500 Subject: [PATCH 43/47] backfill fixes to headsaver --- pkg/solana/headtracker/config.go | 4 +++ pkg/solana/headtracker/head_saver_in_mem.go | 28 +++++++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/pkg/solana/headtracker/config.go b/pkg/solana/headtracker/config.go index 1d0d053d6..4d89e5b1c 100644 --- a/pkg/solana/headtracker/config.go +++ b/pkg/solana/headtracker/config.go @@ -69,3 +69,7 @@ func (c *Config) PollingInterval() time.Duration { func (c *Config) SetHeadTrackerSamplingInterval(d time.Duration) { c.Defaults.HeadTrackerSamplingInterval = d } + +func (c *Config) SetHeadTrackerMaxBufferSize(n uint32) { + c.Defaults.HeadTrackerMaxBufferSize = n +} diff --git a/pkg/solana/headtracker/head_saver_in_mem.go b/pkg/solana/headtracker/head_saver_in_mem.go index 099bf4cbc..2660f41a2 100644 --- a/pkg/solana/headtracker/head_saver_in_mem.go +++ b/pkg/solana/headtracker/head_saver_in_mem.go @@ -99,7 +99,22 @@ func (hs *InMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) HeadByNumber(blockNumber i hs.mu.RLock() defer hs.mu.RUnlock() - return hs.HeadsNumber[blockNumber] + if heads, exists := hs.HeadsNumber[blockNumber]; exists { + return heads + } + + return []H{} +} + +func (hs *InMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) HeadByHash(hash BLOCK_HASH) (H, error) { + hs.mu.RLock() + defer hs.mu.RUnlock() + + if head, exists := hs.Heads[hash]; exists { + return head, nil + } + + return hs.getNilHead(), errors.New("head not found") } // Assembles the heads together and populates the Heads Map @@ -108,7 +123,6 @@ func (hs *InMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) AddHeads(historyDepth int6 defer hs.mu.Unlock() hs.trimHeads(historyDepth) - for _, head := range newHeads { blockHash := head.BlockHash() blockNumber := head.BlockNumber() @@ -130,6 +144,16 @@ func (hs *InMemoryHeadSaver[H, BLOCK_HASH, CHAIN_ID]) AddHeads(historyDepth int6 hs.Heads[blockHash] = head hs.HeadsNumber[blockNumber] = append(hs.HeadsNumber[blockNumber], head) + // Set the parent of the existing heads to the new heads added + for _, existingHead := range hs.Heads { + parentHash := existingHead.GetParentHash() + if parentHash != hs.getNilHash() { + if parent, exists := hs.Heads[parentHash]; exists { + hs.setParent(existingHead, parent) + } + } + } + if !hs.latestHead.IsValid() { hs.latestHead = head } else if head.BlockNumber() > hs.latestHead.BlockNumber() { From 3e814e6d1aec149a288f28b1af70bc7fa8290e5e Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Wed, 12 Jul 2023 16:54:06 -0500 Subject: [PATCH 44/47] backfill test passing --- pkg/solana/headtracker/head_tracker_test.go | 896 ++++++++++---------- 1 file changed, 452 insertions(+), 444 deletions(-) diff --git a/pkg/solana/headtracker/head_tracker_test.go b/pkg/solana/headtracker/head_tracker_test.go index 688722712..561397748 100644 --- a/pkg/solana/headtracker/head_tracker_test.go +++ b/pkg/solana/headtracker/head_tracker_test.go @@ -387,7 +387,7 @@ func TestHeadTracker_Start_LoadsLatestChain(t *testing.T) { trackable := &cltest.MockHeadTrackable{} ht := createHeadTrackerWithChecker(t, cfg, client, headSaver, trackable) - require.NoError(t, headSaver.Save(context.Background(), heads[2])) + require.NoError(t, headSaver.Save(testutils.Context(t), heads[2])) ht.Start(t) @@ -532,431 +532,434 @@ func TestHeadTracker_SwitchesToLongestChainWithHeadSamplingEnabled(t *testing.T) } } -// func TestHeadTracker_SwitchesToLongestChainWithHeadSamplingDisabled(t *testing.T) { -// t.Parallel() - -// db := pgtest.NewSqlxDB(t) -// logger := logger.TestLogger(t) - -// config := configtest.NewGeneralConfig(t, func(c *chainlink.Config, s *chainlink.Secrets) { -// c.EVM[0].FinalityDepth = ptr[uint32](50) -// // Need to set the buffer to something large since we inject a lot of heads at once and otherwise they will be dropped -// c.EVM[0].HeadTracker.MaxBufferSize = ptr[uint32](100) -// c.EVM[0].HeadTracker.SamplingInterval = models.MustNewDuration(0) -// }) - -// client := evmtest.NewclientMockWithDefaultChain(t) - -// checker := commonmocks.NewHeadTrackable[*types.Head, types.Hash](t) -// orm := headtracker.NewORM(db, logger, config.Database(), cltest.FixtureChainID) -// evmcfg := evmtest.NewChainScopedConfig(t, config) -// ht := createHeadTrackerWithChecker(t, client, evmcfg.EVM(), evmcfg.EVM().HeadTracker(), orm, checker) - -// chchHeaders := make(chan evmtest.RawSub[*types.Head], 1) -// mockChain := &evmtest.mockChain{client: client} -// client.On("SubscribeNewHead", mock.Anything, mock.Anything). -// Return( -// func(ctx context.Context, ch chan<- *types.Head) commontypes.Subscription { -// sub := mockChain.NewSub(t) -// chchHeaders <- testutils.NewRawSub(ch, sub.Err()) -// return sub -// }, -// func(ctx context.Context, ch chan<- *types.Head) error { return nil }, -// ) - -// // --------------------- -// blocks := cltest.NewBlocks(t, 10) - -// head0 := blocks.Head(0) // types.Head{Number: 0, Hash: utils.NewSolanaHash(), ParentHash: utils.NewSolanaHash(), Timestamp: time.Unix(0, 0)} -// // Initial query -// client.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(head0, nil) - -// headSeq := cltest.NewHeadBuffer(t) -// headSeq.Append(blocks.Head(0)) -// headSeq.Append(blocks.Head(1)) - -// // Blocks 2 and 3 are out of order -// headSeq.Append(blocks.Head(3)) -// headSeq.Append(blocks.Head(2)) - -// // Block 4 comes in -// headSeq.Append(blocks.Head(4)) - -// // Another block at level 4 comes in, that will be uncled -// headSeq.Append(blocks.NewHead(4)) - -// // Reorg happened forking from block 2 -// blocksForked := blocks.ForkAt(t, 2, 5) -// headSeq.Append(blocksForked.Head(2)) -// headSeq.Append(blocksForked.Head(3)) -// headSeq.Append(blocksForked.Head(4)) -// headSeq.Append(blocksForked.Head(5)) // Now the new chain is longer - -// lastLongestChainAwaiter := cltest.NewAwaiter() - -// checker.On("OnNewLongestChain", mock.Anything, mock.Anything). -// Run(func(args mock.Arguments) { -// h := args.Get(1).(*types.Head) -// require.Equal(t, int64(0), h.BlockNumber()) -// require.Equal(t, blocks.Head(0).BlockHash(), h.BlockHash()) -// }).Return().Once() - -// checker.On("OnNewLongestChain", mock.Anything, mock.Anything). -// Run(func(args mock.Arguments) { -// h := args.Get(1).(*types.Head) -// require.Equal(t, int64(1), h.BlockNumber()) -// require.Equal(t, blocks.Head(1).BlockHash(), h.BlockHash()) -// }).Return().Once() - -// checker.On("OnNewLongestChain", mock.Anything, mock.Anything). -// Run(func(args mock.Arguments) { -// h := args.Get(1).(*types.Head) -// require.Equal(t, int64(3), h.BlockNumber()) -// require.Equal(t, blocks.Head(3).BlockHash(), h.BlockHash()) -// }).Return().Once() - -// checker.On("OnNewLongestChain", mock.Anything, mock.Anything). -// Run(func(args mock.Arguments) { -// h := args.Get(1).(*types.Head) -// require.Equal(t, int64(4), h.BlockNumber()) -// require.Equal(t, blocks.Head(4).BlockHash(), h.BlockHash()) - -// // Check that the block came with its parents -// require.NotNil(t, h.Parent) -// require.Equal(t, h.Parent.BlockHash(), blocks.Head(3).BlockHash()) -// require.NotNil(t, h.Parent.Parent.BlockHash()) -// require.Equal(t, h.Parent.Parent.BlockHash(), blocks.Head(2).BlockHash()) -// require.NotNil(t, h.Parent.Parent.Parent) -// require.Equal(t, h.Parent.Parent.Parent.BlockHash(), blocks.Head(1).BlockHash()) -// }).Return().Once() - -// checker.On("OnNewLongestChain", mock.Anything, mock.Anything). -// Run(func(args mock.Arguments) { -// h := args.Get(1).(*types.Head) - -// require.Equal(t, int64(5), h.BlockNumber()) -// require.Equal(t, blocksForked.Head(5).BlockHash(), h.BlockHash()) - -// // This is the new longest chain, check that it came with its parents -// require.NotNil(t, h.Parent) -// require.Equal(t, h.Parent.BlockHash(), blocksForked.Head(4).BlockHash()) -// require.NotNil(t, h.Parent.Parent) -// require.Equal(t, h.Parent.Parent.BlockHash(), blocksForked.Head(3).BlockHash()) -// require.NotNil(t, h.Parent.Parent.Parent) -// require.Equal(t, h.Parent.Parent.Parent.BlockHash(), blocksForked.Head(2).BlockHash()) -// require.NotNil(t, h.Parent.Parent.Parent.Parent) -// require.Equal(t, h.Parent.Parent.Parent.Parent.BlockHash(), blocksForked.Head(1).BlockHash()) -// lastLongestChainAwaiter.ItHappened() -// }).Return().Once() - -// ht.Start(t) - -// headers := <-chchHeaders - -// // This grotesque construction is the only way to do dynamic return values using -// // the mock package. We need dynamic returns because we're simulating reorgs. -// latestHeadByNumber := make(map[int64]*types.Head) -// latestHeadByNumberMu := new(sync.Mutex) - -// fnCall := client.On("HeadByNumber", mock.Anything, mock.Anything) -// fnCall.RunFn = func(args mock.Arguments) { -// latestHeadByNumberMu.Lock() -// defer latestHeadByNumberMu.Unlock() -// num := args.Get(1).(int64) -// head, exists := latestHeadByNumber[num] -// if !exists { -// head = cltest.Head(num) -// latestHeadByNumber[num] = head -// } -// fnCall.ReturnArguments = mock.Arguments{head, nil} -// } - -// for _, h := range headSeq.Heads { -// latestHeadByNumberMu.Lock() -// latestHeadByNumber[h.BlockNumber()] = h -// latestHeadByNumberMu.Unlock() -// headers.TrySend(h) -// time.Sleep(testutils.TestInterval) -// } - -// // default 10s may not be sufficient, so using testutils.WaitTimeout(t) -// lastLongestChainAwaiter.AwaitOrFail(t, testutils.WaitTimeout(t)) -// ht.Stop(t) -// assert.Equal(t, int64(5), ht.headSaver.LatestChain().BlockNumber()) - -// for _, h := range headSeq.Heads { -// c := ht.headSaver.Chain(h.BlockHash()) -// require.NotNil(t, c) -// assert.Equal(t, c.ParentHash, h.ParentHash) -// assert.Equal(t, c.Timestamp.Unix(), h.Timestamp.UTC().Unix()) -// assert.Equal(t, c.BlockNumber(), h.BlockNumber()) -// } -// } - -// func TestHeadTracker_Backfill(t *testing.T) { -// t.Parallel() - -// // Heads are arranged as follows: -// // headN indicates an unpersisted ethereum header -// // hN indicates a persisted head record -// // -// // (1)->(H0) -// // -// // (14Orphaned)-+ -// // +->(13)->(12)->(11)->(H10)->(9)->(H8) -// // (15)->(14)---------+ - -// now := uint64(time.Now().UTC().Unix()) - -// gethHead0 := &gethTypes.Header{ -// Number: int64(0), -// ParentHash: gethCommon.BigToHash(int64(0)), -// Time: now, -// } -// head0 := evmtypes.NewHead(gethHead0.BlockNumber(), utils.NewSolanaHash(), gethHead0.ParentHash, gethHead0.Time, utils.NewBig(&cltest.FixtureChainID)) - -// h1 := *cltest.Head(1) -// h1.ParentHash = head0.BlockHash() - -// gethHead8 := &gethTypes.Header{ -// Number: int64(8), -// ParentHash: utils.NewSolanaHash(), -// Time: now, -// } -// head8 := evmtypes.NewHead(gethHead8.BlockNumber(), utils.NewSolanaHash(), gethHead8.ParentHash, gethHead8.Time, utils.NewBig(&cltest.FixtureChainID)) - -// h9 := *cltest.Head(9) -// h9.ParentHash = head8.BlockHash() - -// gethHead10 := &gethTypes.Header{ -// Number: int64(10), -// ParentHash: h9.BlockHash(), -// Time: now, -// } -// head10 := evmtypes.NewHead(gethHead10.BlockNumber(), utils.NewSolanaHash(), gethHead10.ParentHash, gethHead10.Time, utils.NewBig(&cltest.FixtureChainID)) - -// h11 := *cltest.Head(11) -// h11.ParentHash = head10.BlockHash() - -// h12 := *cltest.Head(12) -// h12.ParentHash = h11.BlockHash() - -// h13 := *cltest.Head(13) -// h13.ParentHash = h12.BlockHash() - -// h14Orphaned := *cltest.Head(14) -// h14Orphaned.ParentHash = h13.BlockHash() - -// h14 := *cltest.Head(14) -// h14.ParentHash = h13.BlockHash() - -// h15 := *cltest.Head(15) -// h15.ParentHash = h14.BlockHash() - -// heads := []types.Head{ -// h9, -// h11, -// h12, -// h13, -// h14Orphaned, -// h14, -// h15, -// } - -// ctx := testutils.Context(t) - -// t.Run("does nothing if all the heads are in database", func(t *testing.T) { -// db := pgtest.NewSqlxDB(t) -// cfg := configtest.NewGeneralConfig(t, nil) -// logger := logger.TestLogger(t) -// orm := headtracker.NewORM(db, logger, cfg.Database(), cltest.FixtureChainID) -// for i := range heads { -// require.NoError(t, orm.IdempotentInsertHead(testutils.Context(t), &heads[i])) -// } - -// client := evmtest.NewclientMock(t) -// client.On("ConfiguredChainID", mock.Anything).Return(cfg.DefaultChainID(), nil) -// ht := createHeadTrackerWithNeverSleeper(t, client, cfg, orm) - -// err := ht.Backfill(ctx, &h12, 2) -// require.NoError(t, err) -// }) - -// t.Run("fetches a missing head", func(t *testing.T) { -// db := pgtest.NewSqlxDB(t) -// cfg := configtest.NewGeneralConfig(t, nil) -// logger := logger.TestLogger(t) -// orm := headtracker.NewORM(db, logger, cfg.Database(), cltest.FixtureChainID) -// for i := range heads { -// require.NoError(t, orm.IdempotentInsertHead(testutils.Context(t), &heads[i])) -// } - -// client := evmtest.NewclientMock(t) -// client.On("ConfiguredChainID", mock.Anything).Return(cfg.DefaultChainID(), nil) -// client.On("HeadByNumber", mock.Anything, int64(10)). -// Return(&head10, nil) - -// ht := createHeadTrackerWithNeverSleeper(t, client, cfg, orm) - -// var depth uint = 3 - -// err := ht.Backfill(ctx, &h12, depth) -// require.NoError(t, err) - -// h := ht.headSaver.Chain(h12.BlockHash()) - -// assert.Equal(t, int64(12), h.BlockNumber()) -// require.NotNil(t, h.Parent) -// assert.Equal(t, int64(11), h.Parent.BlockNumber()) -// require.NotNil(t, h.Parent) -// assert.Equal(t, int64(10), h.Parent.Parent.BlockNumber()) -// require.NotNil(t, h.Parent.Parent.Parent) -// assert.Equal(t, int64(9), h.Parent.Parent.Parent.BlockNumber()) - -// writtenHead, err := orm.HeadByHash(testutils.Context(t), head10.BlockHash()) -// require.NoError(t, err) -// assert.Equal(t, int64(10), writtenHead.BlockNumber()) -// }) - -// t.Run("fetches only heads that are missing", func(t *testing.T) { -// db := pgtest.NewSqlxDB(t) -// cfg := configtest.NewGeneralConfig(t, nil) -// logger := logger.TestLogger(t) -// orm := headtracker.NewORM(db, logger, cfg.Database(), cltest.FixtureChainID) -// for i := range heads { -// require.NoError(t, orm.IdempotentInsertHead(testutils.Context(t), &heads[i])) -// } - -// client := evmtest.NewclientMock(t) -// client.On("ConfiguredChainID", mock.Anything).Return(cfg.DefaultChainID(), nil) - -// ht := createHeadTrackerWithNeverSleeper(t, client, cfg, orm) - -// client.On("HeadByNumber", mock.Anything, int64(10)). -// Return(&head10, nil) -// client.On("HeadByNumber", mock.Anything, int64(8)). -// Return(&head8, nil) - -// // Needs to be 8 because there are 8 heads in chain (15,14,13,12,11,10,9,8) -// var depth uint = 8 - -// err := ht.Backfill(ctx, &h15, depth) -// require.NoError(t, err) - -// h := ht.headSaver.Chain(h15.BlockHash()) - -// require.Equal(t, uint32(8), h.ChainLength()) -// earliestInChain := h.EarliestInChain() -// assert.Equal(t, head8.BlockNumber(), earliestInChain.BlockNumber()) -// assert.Equal(t, head8.BlockHash(), earliestInChain.BlockHash()) -// }) - -// t.Run("does not backfill if chain length is already greater than or equal to depth", func(t *testing.T) { -// db := pgtest.NewSqlxDB(t) -// cfg := configtest.NewGeneralConfig(t, nil) -// logger := logger.TestLogger(t) -// orm := headtracker.NewORM(db, logger, cfg.Database(), cltest.FixtureChainID) -// for i := range heads { -// require.NoError(t, orm.IdempotentInsertHead(testutils.Context(t), &heads[i])) -// } - -// client := evmtest.NewclientMock(t) -// client.On("ConfiguredChainID", mock.Anything).Return(cfg.DefaultChainID(), nil) - -// ht := createHeadTrackerWithNeverSleeper(t, client, cfg, orm) - -// err := ht.Backfill(ctx, &h15, 3) -// require.NoError(t, err) - -// err = ht.Backfill(ctx, &h15, 5) -// require.NoError(t, err) -// }) - -// t.Run("only backfills to height 0 if chain length would otherwise cause it to try and fetch a negative head", func(t *testing.T) { -// db := pgtest.NewSqlxDB(t) -// cfg := configtest.NewGeneralConfig(t, nil) -// logger := logger.TestLogger(t) -// orm := headtracker.NewORM(db, logger, cfg.Database(), cltest.FixtureChainID) - -// client := evmtest.NewclientMock(t) -// client.On("ConfiguredChainID", mock.Anything).Return(cfg.DefaultChainID(), nil) -// client.On("HeadByNumber", mock.Anything, int64(0)). -// Return(&head0, nil) - -// require.NoError(t, orm.IdempotentInsertHead(testutils.Context(t), &h1)) - -// ht := createHeadTrackerWithNeverSleeper(t, client, cfg, orm) - -// err := ht.Backfill(ctx, &h1, 400) -// require.NoError(t, err) - -// h := ht.headSaver.Chain(h1.BlockHash()) -// require.NotNil(t, h) - -// require.Equal(t, uint32(2), h.ChainLength()) -// require.Equal(t, int64(0), h.EarliestInChain().BlockNumber()) -// }) - -// t.Run("abandons backfill and returns error if the eth node returns not found", func(t *testing.T) { -// db := pgtest.NewSqlxDB(t) -// cfg := configtest.NewGeneralConfig(t, nil) -// logger := logger.TestLogger(t) -// orm := headtracker.NewORM(db, logger, cfg.Database(), cltest.FixtureChainID) -// for i := range heads { -// require.NoError(t, orm.IdempotentInsertHead(testutils.Context(t), &heads[i])) -// } - -// client := evmtest.NewclientMock(t) -// client.On("ConfiguredChainID", mock.Anything).Return(cfg.DefaultChainID(), nil) -// client.On("HeadByNumber", mock.Anything, int64(10)). -// Return(&head10, nil). -// Once() -// client.On("HeadByNumber", mock.Anything, int64(8)). -// Return(nil, ethereum.NotFound). -// Once() - -// ht := createHeadTrackerWithNeverSleeper(t, client, cfg, orm) - -// err := ht.Backfill(ctx, &h12, 400) -// require.Error(t, err) -// require.EqualError(t, err, "fetchAndSaveHead failed: not found") - -// h := ht.headSaver.Chain(h12.BlockHash()) - -// // Should contain 12, 11, 10, 9 -// assert.Equal(t, 4, int(h.ChainLength())) -// assert.Equal(t, int64(9), h.EarliestInChain().BlockNumber()) -// }) - -// t.Run("abandons backfill and returns error if the context time budget is exceeded", func(t *testing.T) { -// db := pgtest.NewSqlxDB(t) -// cfg := configtest.NewGeneralConfig(t, nil) -// logger := logger.TestLogger(t) -// orm := headtracker.NewORM(db, logger, cfg.Database(), cltest.FixtureChainID) -// for i := range heads { -// require.NoError(t, orm.IdempotentInsertHead(testutils.Context(t), &heads[i])) -// } - -// client := evmtest.NewclientMock(t) -// client.On("ConfiguredChainID", mock.Anything).Return(cfg.DefaultChainID(), nil) -// client.On("HeadByNumber", mock.Anything, int64(10)). -// Return(&head10, nil) -// client.On("HeadByNumber", mock.Anything, int64(8)). -// Return(nil, context.DeadlineExceeded) - -// ht := createHeadTrackerWithNeverSleeper(t, client, cfg, orm) - -// err := ht.Backfill(ctx, &h12, 400) -// require.Error(t, err) -// require.EqualError(t, err, "fetchAndSaveHead failed: context deadline exceeded") - -// h := ht.headSaver.Chain(h12.BlockHash()) - -// // Should contain 12, 11, 10, 9 -// assert.Equal(t, 4, int(h.ChainLength())) -// assert.Equal(t, int64(9), h.EarliestInChain().BlockNumber()) -// }) -// } +// TODO: Fix this test later +func TestHeadTracker_SwitchesToLongestChainWithHeadSamplingDisabled(t *testing.T) { + t.Parallel() + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + headSaver := headtracker.NewSaver(cfg, lggr) + client := cltest.NewClientMockWithDefaultChain(t) + mockChain := &testutils.MockChain{Client: client} + chchHeaders := make(chan testutils.RawSub[*types.Head], 1) + checker := commonmocks.NewHeadTrackable[*types.Head, types.Hash](t) + ht := createHeadTrackerWithChecker(t, cfg, client, headSaver, checker) + cfg.SetHeadTrackerSamplingInterval(0) + cfg.SetHeadTrackerMaxBufferSize(100) + + client.On("SubscribeNewHead", mock.Anything, mock.Anything). + Return( + func(ctx context.Context, ch chan<- *types.Head) commontypes.Subscription { + sub := mockChain.NewSub(t) + chchHeaders <- testutils.NewRawSub(ch, sub.Err()) + return sub + }, + func(ctx context.Context, ch chan<- *types.Head) error { return nil }, + ) + + // --------------------- + blocks := cltest.NewBlocks(t, 10) + + head0 := blocks.Head(0) // types.Head{Number: 0, Hash: utils.NewSolanaHash(), ParentHash: utils.NewSolanaHash(), Timestamp: time.Unix(0, 0)} + // Initial query + client.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(head0, nil) + + headSeq := cltest.NewHeadBuffer(t) + headSeq.Append(blocks.Head(0)) + headSeq.Append(blocks.Head(1)) + + // Blocks 2 and 3 are out of order + headSeq.Append(blocks.Head(3)) + headSeq.Append(blocks.Head(2)) + + // Block 4 comes in + headSeq.Append(blocks.Head(4)) + + // Another block at level 4 comes in, that will be uncled + headSeq.Append(blocks.NewHead(4)) + + // Reorg happened forking from block 2 + blocksForked := blocks.ForkAt(t, 2, 5) + headSeq.Append(blocksForked.Head(2)) + headSeq.Append(blocksForked.Head(3)) + headSeq.Append(blocksForked.Head(4)) + headSeq.Append(blocksForked.Head(5)) // Now the new chain is longer + + // --------------------- Delete + // Print HeadSequence in a nice format + fmt.Println("headSeq", headSeq.Heads) + + // Iterate over Head sequence, print head.BlockHash() and parent.BlockHash() + // Add new line after each head + for _, h := range headSeq.Heads { + if h.Parent == nil { + fmt.Printf("head %s, parent nil \n\n", h.BlockHash()) + continue + } + fmt.Printf("head hash %s head number %d, parent hash %s parent number %d \n\n ", h.BlockHash(), h.BlockNumber(), h.Parent.BlockHash(), h.Parent.BlockNumber()) + } + // --------------------- Delete + + lastLongestChainAwaiter := cltest.NewAwaiter() + + checker.On("OnNewLongestChain", mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + h := args.Get(1).(*types.Head) + require.Equal(t, int64(0), h.BlockNumber()) + require.Equal(t, blocks.Head(0).BlockHash(), h.BlockHash()) + }).Return().Once() + + checker.On("OnNewLongestChain", mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + h := args.Get(1).(*types.Head) + require.Equal(t, int64(1), h.BlockNumber()) + require.Equal(t, blocks.Head(1).BlockHash(), h.BlockHash()) + + fmt.Println("good h number, hash", h.BlockNumber(), h.BlockHash()) + + }).Return().Once() + + checker.On("OnNewLongestChain", mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + h := args.Get(1).(*types.Head) + require.Equal(t, int64(3), h.BlockNumber()) + require.Equal(t, blocks.Head(3).BlockHash(), h.BlockHash()) + + // Get parent parent parent + fmt.Println("problematic h number, hash", h.BlockNumber(), h.BlockHash()) + + // Get parent + fmt.Println("Parent of problematic h", h.Parent) + }).Return().Once() + + checker.On("OnNewLongestChain", mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + h := args.Get(1).(*types.Head) + require.Equal(t, int64(4), h.BlockNumber()) + require.Equal(t, blocks.Head(4).BlockHash(), h.BlockHash()) + + // Check that the block came with its parents + require.NotNil(t, h.Parent) + require.Equal(t, h.Parent.BlockHash(), blocks.Head(3).BlockHash()) + require.NotNil(t, h.Parent.Parent.BlockHash()) // 2 + require.Equal(t, h.Parent.Parent.BlockHash(), blocks.Head(2).BlockHash()) + require.NotNil(t, h.Parent.Parent.Parent) + require.Equal(t, h.Parent.Parent.Parent.BlockHash(), blocks.Head(1).BlockHash()) + }).Return().Once() + + checker.On("OnNewLongestChain", mock.Anything, mock.Anything). + Run(func(args mock.Arguments) { + h := args.Get(1).(*types.Head) + + require.Equal(t, int64(5), h.BlockNumber()) + require.Equal(t, blocksForked.Head(5).BlockHash(), h.BlockHash()) + + // This is the new longest chain, check that it came with its parents + require.NotNil(t, h.Parent) + require.Equal(t, h.Parent.BlockHash(), blocksForked.Head(4).BlockHash()) + require.NotNil(t, h.Parent.Parent) + require.Equal(t, h.Parent.Parent.BlockHash(), blocksForked.Head(3).BlockHash()) + require.NotNil(t, h.Parent.Parent.Parent) + require.Equal(t, h.Parent.Parent.Parent.BlockHash(), blocksForked.Head(2).BlockHash()) + require.NotNil(t, h.Parent.Parent.Parent.Parent) + require.Equal(t, h.Parent.Parent.Parent.Parent.BlockHash(), blocksForked.Head(1).BlockHash()) + lastLongestChainAwaiter.ItHappened() + }).Return().Once() + + ht.Start(t) + + headers := <-chchHeaders + + // This grotesque construction is the only way to do dynamic return values using + // the mock package. We need dynamic returns because we're simulating reorgs. + latestHeadByNumber := make(map[int64]*types.Head) + latestHeadByNumberMu := new(sync.Mutex) + + fnCall := client.On("HeadByNumber", mock.Anything, mock.Anything) + fnCall.RunFn = func(args mock.Arguments) { + latestHeadByNumberMu.Lock() + defer latestHeadByNumberMu.Unlock() + num := args.Get(1).(*big.Int) + head, exists := latestHeadByNumber[num.Int64()] + if !exists { + head = cltest.Head(num) + latestHeadByNumber[num.Int64()] = head + } + fnCall.ReturnArguments = mock.Arguments{head, nil} + } + + for _, h := range headSeq.Heads { + latestHeadByNumberMu.Lock() + latestHeadByNumber[h.BlockNumber()] = h + latestHeadByNumberMu.Unlock() + headers.TrySend(h) + time.Sleep(testutils.TestInterval) + } + + // default 10s may not be sufficient, so using testutils.WaitTimeout(t) + lastLongestChainAwaiter.AwaitOrFail(t, testutils.WaitTimeout(t)) + ht.Stop(t) + assert.Equal(t, int64(5), ht.headSaver.LatestChain().BlockNumber()) + + for _, h := range headSeq.Heads { + c := ht.headSaver.Chain(h.BlockHash()) + require.NotNil(t, c) + assert.Equal(t, c.GetParentHash(), h.GetParentHash()) + assert.Equal(t, c.BlockNumber(), h.BlockNumber()) + } +} + +func TestHeadTracker_Backfill(t *testing.T) { + t.Parallel() + + // Heads are arranged as follows: + // headN indicates an unpersisted ethereum header + // hN indicates a persisted head record + // + // (1)->(H0) + // + // (14Orphaned)-+ + // +->(13)->(12)->(11)->(H10)->(9)->(H8) + // (15)->(14)---------+ + + head0 := cltest.Head(0) + + h1 := cltest.Head(1) + h1.Block.PreviousBlockhash = head0.BlockHash().Hash + + fmt.Println("\n\n head0 Blockhash \n", head0.BlockHash().Hash) + fmt.Println("\n\n h1 Blockhash \n", h1.GetParentHash()) + fmt.Println("h1 Blockhash \n", h1.Block.PreviousBlockhash) + + h8 := cltest.Head(8) + + h9 := cltest.Head(9) + h9.Block.PreviousBlockhash = h8.BlockHash().Hash + + h10 := cltest.Head(10) + h10.Block.PreviousBlockhash = h9.BlockHash().Hash + + h11 := cltest.Head(11) + h11.Block.PreviousBlockhash = h10.BlockHash().Hash + + h12 := cltest.Head(12) + h12.Block.PreviousBlockhash = h11.BlockHash().Hash + + h13 := cltest.Head(13) + h13.Block.PreviousBlockhash = h12.BlockHash().Hash + + h14Orphaned := cltest.Head(14) + h14Orphaned.Block.PreviousBlockhash = h13.BlockHash().Hash + + h14 := cltest.Head(14) + h14.Block.PreviousBlockhash = h13.BlockHash().Hash + + h15 := cltest.Head(15) + h15.Block.PreviousBlockhash = h14.BlockHash().Hash + + heads := []types.Head{ + *h9, + *h11, + *h12, + *h13, + *h14Orphaned, + *h14, + *h15, + } + + ctx := testutils.Context(t) + + t.Run("does nothing if all the heads are in headsaver", func(t *testing.T) { + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + headSaver := headtracker.NewSaver(cfg, lggr) + + for i := range heads { + require.NoError(t, headSaver.Save(testutils.Context(t), &heads[i])) + } + + client := cltest.NewClientMock(t) + client.On("ConfiguredChainID", mock.Anything).Return(types.Localnet, nil) + ht := createHeadTrackerWithNeverSleeper(t, cfg, client, headSaver) + + err := ht.Backfill(ctx, h12, 2) + require.NoError(t, err) + }) + + t.Run("fetches a missing head", func(t *testing.T) { + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + headSaver := headtracker.NewSaver(cfg, lggr) + + for i := range heads { + require.NoError(t, headSaver.Save(testutils.Context(t), &heads[i])) + } + + client := cltest.NewClientMock(t) + client.On("ConfiguredChainID", mock.Anything).Return(types.Localnet, nil) + client.On("HeadByNumber", mock.Anything, big.NewInt(10)). + Return(h10, nil) + + ht := createHeadTrackerWithNeverSleeper(t, cfg, client, headSaver) + + var depth uint = 3 + + // Should backfill h10 + err := ht.Backfill(ctx, h12, depth) + require.NoError(t, err) + + h := ht.headSaver.Chain(h12.BlockHash()) + + assert.Equal(t, int64(12), h.BlockNumber()) + require.NotNil(t, h.Parent) + assert.Equal(t, int64(11), h.Parent.BlockNumber()) + require.NotNil(t, h.Parent.Parent) + assert.Equal(t, int64(10), h.Parent.Parent.BlockNumber()) + require.NotNil(t, h.Parent.Parent.Parent) + assert.Equal(t, int64(9), h.Parent.Parent.Parent.BlockNumber()) + + writtenHead, err := headSaver.HeadByHash(h10.BlockHash()) + require.NoError(t, err) + assert.Equal(t, int64(10), writtenHead.BlockNumber()) + }) + + t.Run("fetches only heads that are missing", func(t *testing.T) { + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + headSaver := headtracker.NewSaver(cfg, lggr) + + for i := range heads { + require.NoError(t, headSaver.Save(testutils.Context(t), &heads[i])) + } + + client := cltest.NewClientMock(t) + client.On("ConfiguredChainID", mock.Anything).Return(types.Localnet, nil) + + ht := createHeadTrackerWithNeverSleeper(t, cfg, client, headSaver) + + client.On("HeadByNumber", mock.Anything, big.NewInt(10)). + Return(h10, nil) + client.On("HeadByNumber", mock.Anything, big.NewInt(8)). + Return(h8, nil) + + // Needs to be 8 because there are 8 heads in chain (15,14,13,12,11,10,9,8) + var depth uint = 8 + + err := ht.Backfill(ctx, h15, depth) + require.NoError(t, err) + + h := ht.headSaver.Chain(h15.BlockHash()) + + require.Equal(t, uint32(8), h.ChainLength()) + earliestInChain := h.EarliestHeadInChain() + assert.Equal(t, h8.BlockNumber(), earliestInChain.BlockNumber()) + assert.Equal(t, h8.BlockHash(), earliestInChain.BlockHash()) + }) + + t.Run("does not backfill if chain length is already greater than or equal to depth", func(t *testing.T) { + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + headSaver := headtracker.NewSaver(cfg, lggr) + + for i := range heads { + require.NoError(t, headSaver.Save(testutils.Context(t), &heads[i])) + } + + client := cltest.NewClientMock(t) + client.On("ConfiguredChainID", mock.Anything).Return(types.Localnet, nil) + + ht := createHeadTrackerWithNeverSleeper(t, cfg, client, headSaver) + + err := ht.Backfill(ctx, h15, 3) + require.NoError(t, err) + + err = ht.Backfill(ctx, h15, 5) + require.NoError(t, err) + }) + + t.Run("only backfills to height 0 if chain length would otherwise cause it to try and fetch a negative head", func(t *testing.T) { + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + headSaver := headtracker.NewSaver(cfg, lggr) + + client := cltest.NewClientMock(t) + client.On("ConfiguredChainID", mock.Anything).Return(types.Localnet, nil) + client.On("HeadByNumber", mock.Anything, big.NewInt(0)). + Return(head0, nil) + + require.NoError(t, headSaver.Save(testutils.Context(t), h1)) + + ht := createHeadTrackerWithNeverSleeper(t, cfg, client, headSaver) + + err := ht.Backfill(ctx, h1, 400) + require.NoError(t, err) + + h := ht.headSaver.Chain(h1.BlockHash()) + require.NotNil(t, h) + + require.Equal(t, uint32(2), h.ChainLength()) + require.Equal(t, int64(0), h.EarliestHeadInChain().BlockNumber()) + }) + + t.Run("abandons backfill and returns error if the eth node returns not found", func(t *testing.T) { + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + headSaver := headtracker.NewSaver(cfg, lggr) + + for i := range heads { + require.NoError(t, headSaver.Save(testutils.Context(t), &heads[i])) + } + + client := cltest.NewClientMock(t) + client.On("ConfiguredChainID", mock.Anything).Return(types.Localnet, nil) + + client.On("HeadByNumber", mock.Anything, big.NewInt(10)). + Return(h10, nil). + Once() + client.On("HeadByNumber", mock.Anything, big.NewInt(8)). + Return(cltest.Head(0), errors.New("not found")). + Once() + + ht := createHeadTrackerWithNeverSleeper(t, cfg, client, headSaver) + + err := ht.Backfill(ctx, h12, 400) + require.Error(t, err) + require.EqualError(t, err, "fetchAndSaveHead failed: not found") + + h := ht.headSaver.Chain(h12.BlockHash()) + + // Should contain 12, 11, 10, 9 + assert.Equal(t, 4, int(h.ChainLength())) + assert.Equal(t, int64(9), h.EarliestHeadInChain().BlockNumber()) + }) + + t.Run("abandons backfill and returns error if the context time budget is exceeded", func(t *testing.T) { + lggr, _ := logger.New() + cfg := headtracker.NewConfig() + headSaver := headtracker.NewSaver(cfg, lggr) + for i := range heads { + require.NoError(t, headSaver.Save(testutils.Context(t), &heads[i])) + } + + client := cltest.NewClientMock(t) + client.On("ConfiguredChainID", mock.Anything).Return(types.Localnet, nil) + client.On("HeadByNumber", mock.Anything, big.NewInt(10)). + Return(h10, nil) + client.On("HeadByNumber", mock.Anything, big.NewInt(8)). + Return(cltest.Head(0), context.DeadlineExceeded) + + ht := createHeadTrackerWithNeverSleeper(t, cfg, client, headSaver) + + err := ht.Backfill(ctx, h12, 400) + require.Error(t, err) + require.EqualError(t, err, "fetchAndSaveHead failed: context deadline exceeded") + + h := ht.headSaver.Chain(h12.BlockHash()) + + // Should contain 12, 11, 10, 9 + assert.Equal(t, 4, int(h.ChainLength())) + assert.Equal(t, int64(9), h.EarliestHeadInChain().BlockNumber()) + }) +} // Helper Functions @@ -986,24 +989,29 @@ func createHeadTracker( } } -// func createHeadTrackerWithNeverSleeper(t *testing.T, solanaClient *client.Client, config *headtracker.Config) *headTrackerUniverse { -// htCfg := headtracker.NewConfig() -// lggr, _ := logger.New() - -// hb := headtracker.NewBroadcaster(lggr) -// hs := headtracker.NewSaver(htCfg, lggr) -// mailMon := utils.NewMailboxMonitor(t.Name()) -// ht := headtracker.NewTracker(lggr, solanaClient, config, hb, hs, mailMon) -// return &headTrackerUniverse{ -// mu: new(sync.Mutex), -// headTracker: ht, -// headBroadcaster: hb, -// headSaver: hs, -// mailMon: mailMon, -// } -// } - -// created with evmcfg := evmtest.NewChainScopedConfig(t, cfg) +func createHeadTrackerWithNeverSleeper(t *testing.T, + config *headtracker.Config, + solanaClient htrktypes.Client[ + *types.Head, + commontypes.Subscription, + types.ChainID, + types.Hash], + hs *headtracker.InMemoryHeadSaver[ + *types.Head, + types.Hash, + types.ChainID]) *headTrackerUniverse { + lggr, _ := logger.New() + hb := headtracker.NewBroadcaster(lggr) + mailMon := utils.NewMailboxMonitor(t.Name()) + ht := headtracker.NewTracker(lggr, solanaClient, config, hb, hs, mailMon) + return &headTrackerUniverse{ + mu: new(sync.Mutex), + headTracker: ht, + headBroadcaster: hb, + headSaver: hs, + mailMon: mailMon, + } +} func createHeadTrackerWithChecker(t *testing.T, config *headtracker.Config, From 46d01bb78301198cf80f3112b6655cc098dc9978 Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Wed, 12 Jul 2023 17:53:30 -0500 Subject: [PATCH 45/47] All headtracker test passing --- pkg/internal/cltest/cltest.go | 3 +++ pkg/solana/headtracker/head_tracker_test.go | 29 ++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/pkg/internal/cltest/cltest.go b/pkg/internal/cltest/cltest.go index affd9f412..47ad82dcd 100644 --- a/pkg/internal/cltest/cltest.go +++ b/pkg/internal/cltest/cltest.go @@ -103,6 +103,7 @@ func NewBlocks(t *testing.T, numHashes int) *Blocks { if i > 0 { parent := heads[i-1] heads[i].Parent = parent + heads[i].Block.PreviousBlockhash = parent.BlockHash().Hash } } @@ -128,6 +129,7 @@ func (b *Blocks) NewHead(number uint64) *types.Head { head := Head(number) head.Parent = parent + head.Block.PreviousBlockhash = parent.BlockHash().Hash return head } @@ -142,6 +144,7 @@ func (b *Blocks) ForkAt(t *testing.T, blockNum int64, numHashes int) *Blocks { forked.Heads[i] = b.Heads[i] } + forked.Heads[blockNum].Block.PreviousBlockhash = b.Heads[blockNum].Block.PreviousBlockhash forked.Heads[blockNum].Parent = b.Heads[blockNum].Parent return forked } diff --git a/pkg/solana/headtracker/head_tracker_test.go b/pkg/solana/headtracker/head_tracker_test.go index 561397748..1d9e86ccb 100644 --- a/pkg/solana/headtracker/head_tracker_test.go +++ b/pkg/solana/headtracker/head_tracker_test.go @@ -420,7 +420,9 @@ func TestHeadTracker_SwitchesToLongestChainWithHeadSamplingEnabled(t *testing.T) chchHeaders := make(chan testutils.RawSub[*types.Head], 1) checker := commonmocks.NewHeadTrackable[*types.Head, types.Hash](t) ht := createHeadTrackerWithChecker(t, cfg, client, headSaver, checker) + cfg.SetHeadTrackerSamplingInterval(2500 * time.Millisecond) + cfg.SetHeadTrackerMaxBufferSize(100) client.On("SubscribeNewHead", mock.Anything, mock.Anything). Return( @@ -461,6 +463,16 @@ func TestHeadTracker_SwitchesToLongestChainWithHeadSamplingEnabled(t *testing.T) headSeq.Append(blocksForked.Head(4)) headSeq.Append(blocksForked.Head(5)) // Now the new chain is longer + // Print out all the block and the parent block + for i := 0; i < len(headSeq.Heads); i++ { + h := headSeq.Heads[i] + if h.Parent == nil { + fmt.Println("headSeq h.BlockNumber, hash:", h.BlockNumber(), h.BlockHash(), "Parent: nil") + } else { + fmt.Println("headSeq", h.BlockNumber(), h.BlockHash(), "Parent: ", h.Parent.BlockNumber(), h.Parent.BlockHash()) + } + } + lastLongestChainAwaiter := cltest.NewAwaiter() // the callback is only called for head number 5 because of head sampling @@ -472,6 +484,8 @@ func TestHeadTracker_SwitchesToLongestChainWithHeadSamplingEnabled(t *testing.T) assert.Equal(t, int64(5), h.BlockNumber()) assert.Equal(t, blocksForked.Head(5).BlockHash(), h.BlockHash()) + fmt.Println("\n\n H Parent: ", h.Parent) // Why is the parent nil? + // This is the new longest chain, check that it came with its parents if !assert.NotNil(t, h.Parent) { return @@ -488,7 +502,20 @@ func TestHeadTracker_SwitchesToLongestChainWithHeadSamplingEnabled(t *testing.T) if !assert.NotNil(t, h.Parent.Parent.Parent.Parent) { return } - assert.Equal(t, h.Parent.Parent.Parent.Parent.BlockHash(), blocksForked.Head(1).BlockHash()) + // h is 5, h.parent.parent.parent.parent is 1 + fmt.Println("Inconsistency of block number:", h.Parent.Parent.Parent.Parent.BlockNumber()) + // Print out all parent block number + fmt.Println("h Blocknumber", h.BlockNumber()) + fmt.Println("Parent 1 Blocknumber", h.Parent.BlockNumber()) + fmt.Println("Parent 2 Blocknumber", h.Parent.Parent.BlockNumber()) + fmt.Println("Parent 3 Blocknumber", h.Parent.Parent.Parent.BlockNumber(), h.Parent.Parent.Parent.BlockHash()) + fmt.Println("Parent 4 Blocknumber, hash", h.Parent.Parent.Parent.Parent.BlockNumber(), h.Parent.Parent.Parent.Parent.BlockHash()) + + // Why does it have 2 parents? + // 5->4->3->2->2->1 + // Why is 2 he parent of 2 + + assert.Equal(t, blocksForked.Head(1).BlockHash(), h.Parent.Parent.Parent.Parent.BlockHash()) lastLongestChainAwaiter.ItHappened() }).Return().Once() From 9e9aff2f61da98503ab40509a2883be458835212 Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Thu, 13 Jul 2023 11:32:33 -0500 Subject: [PATCH 46/47] cleanup --- pkg/solana/headtracker/head_tracker_test.go | 57 +-------------------- pkg/solana/headtracker/types/chain.go | 2 +- pkg/solana/headtracker/types/head.go | 2 +- pkg/solana/headtracker/types/head_test.go | 26 ++++++++-- 4 files changed, 24 insertions(+), 63 deletions(-) diff --git a/pkg/solana/headtracker/head_tracker_test.go b/pkg/solana/headtracker/head_tracker_test.go index 1d9e86ccb..b6769185e 100644 --- a/pkg/solana/headtracker/head_tracker_test.go +++ b/pkg/solana/headtracker/head_tracker_test.go @@ -3,7 +3,6 @@ package headtracker_test import ( "context" "errors" - "fmt" "math" "math/big" "sync" @@ -463,29 +462,16 @@ func TestHeadTracker_SwitchesToLongestChainWithHeadSamplingEnabled(t *testing.T) headSeq.Append(blocksForked.Head(4)) headSeq.Append(blocksForked.Head(5)) // Now the new chain is longer - // Print out all the block and the parent block - for i := 0; i < len(headSeq.Heads); i++ { - h := headSeq.Heads[i] - if h.Parent == nil { - fmt.Println("headSeq h.BlockNumber, hash:", h.BlockNumber(), h.BlockHash(), "Parent: nil") - } else { - fmt.Println("headSeq", h.BlockNumber(), h.BlockHash(), "Parent: ", h.Parent.BlockNumber(), h.Parent.BlockHash()) - } - } - lastLongestChainAwaiter := cltest.NewAwaiter() // the callback is only called for head number 5 because of head sampling checker.On("OnNewLongestChain", mock.Anything, mock.Anything). Run(func(args mock.Arguments) { h := args.Get(1).(*types.Head) - fmt.Println("OnNewLongestChain", h.BlockNumber()) assert.Equal(t, int64(5), h.BlockNumber()) assert.Equal(t, blocksForked.Head(5).BlockHash(), h.BlockHash()) - fmt.Println("\n\n H Parent: ", h.Parent) // Why is the parent nil? - // This is the new longest chain, check that it came with its parents if !assert.NotNil(t, h.Parent) { return @@ -502,18 +488,6 @@ func TestHeadTracker_SwitchesToLongestChainWithHeadSamplingEnabled(t *testing.T) if !assert.NotNil(t, h.Parent.Parent.Parent.Parent) { return } - // h is 5, h.parent.parent.parent.parent is 1 - fmt.Println("Inconsistency of block number:", h.Parent.Parent.Parent.Parent.BlockNumber()) - // Print out all parent block number - fmt.Println("h Blocknumber", h.BlockNumber()) - fmt.Println("Parent 1 Blocknumber", h.Parent.BlockNumber()) - fmt.Println("Parent 2 Blocknumber", h.Parent.Parent.BlockNumber()) - fmt.Println("Parent 3 Blocknumber", h.Parent.Parent.Parent.BlockNumber(), h.Parent.Parent.Parent.BlockHash()) - fmt.Println("Parent 4 Blocknumber, hash", h.Parent.Parent.Parent.Parent.BlockNumber(), h.Parent.Parent.Parent.Parent.BlockHash()) - - // Why does it have 2 parents? - // 5->4->3->2->2->1 - // Why is 2 he parent of 2 assert.Equal(t, blocksForked.Head(1).BlockHash(), h.Parent.Parent.Parent.Parent.BlockHash()) lastLongestChainAwaiter.ItHappened() @@ -559,7 +533,6 @@ func TestHeadTracker_SwitchesToLongestChainWithHeadSamplingEnabled(t *testing.T) } } -// TODO: Fix this test later func TestHeadTracker_SwitchesToLongestChainWithHeadSamplingDisabled(t *testing.T) { t.Parallel() lggr, _ := logger.New() @@ -586,7 +559,7 @@ func TestHeadTracker_SwitchesToLongestChainWithHeadSamplingDisabled(t *testing.T // --------------------- blocks := cltest.NewBlocks(t, 10) - head0 := blocks.Head(0) // types.Head{Number: 0, Hash: utils.NewSolanaHash(), ParentHash: utils.NewSolanaHash(), Timestamp: time.Unix(0, 0)} + head0 := blocks.Head(0) // Initial query client.On("HeadByNumber", mock.Anything, (*big.Int)(nil)).Return(head0, nil) @@ -611,21 +584,6 @@ func TestHeadTracker_SwitchesToLongestChainWithHeadSamplingDisabled(t *testing.T headSeq.Append(blocksForked.Head(4)) headSeq.Append(blocksForked.Head(5)) // Now the new chain is longer - // --------------------- Delete - // Print HeadSequence in a nice format - fmt.Println("headSeq", headSeq.Heads) - - // Iterate over Head sequence, print head.BlockHash() and parent.BlockHash() - // Add new line after each head - for _, h := range headSeq.Heads { - if h.Parent == nil { - fmt.Printf("head %s, parent nil \n\n", h.BlockHash()) - continue - } - fmt.Printf("head hash %s head number %d, parent hash %s parent number %d \n\n ", h.BlockHash(), h.BlockNumber(), h.Parent.BlockHash(), h.Parent.BlockNumber()) - } - // --------------------- Delete - lastLongestChainAwaiter := cltest.NewAwaiter() checker.On("OnNewLongestChain", mock.Anything, mock.Anything). @@ -640,9 +598,6 @@ func TestHeadTracker_SwitchesToLongestChainWithHeadSamplingDisabled(t *testing.T h := args.Get(1).(*types.Head) require.Equal(t, int64(1), h.BlockNumber()) require.Equal(t, blocks.Head(1).BlockHash(), h.BlockHash()) - - fmt.Println("good h number, hash", h.BlockNumber(), h.BlockHash()) - }).Return().Once() checker.On("OnNewLongestChain", mock.Anything, mock.Anything). @@ -650,12 +605,6 @@ func TestHeadTracker_SwitchesToLongestChainWithHeadSamplingDisabled(t *testing.T h := args.Get(1).(*types.Head) require.Equal(t, int64(3), h.BlockNumber()) require.Equal(t, blocks.Head(3).BlockHash(), h.BlockHash()) - - // Get parent parent parent - fmt.Println("problematic h number, hash", h.BlockNumber(), h.BlockHash()) - - // Get parent - fmt.Println("Parent of problematic h", h.Parent) }).Return().Once() checker.On("OnNewLongestChain", mock.Anything, mock.Anything). @@ -753,10 +702,6 @@ func TestHeadTracker_Backfill(t *testing.T) { h1 := cltest.Head(1) h1.Block.PreviousBlockhash = head0.BlockHash().Hash - fmt.Println("\n\n head0 Blockhash \n", head0.BlockHash().Hash) - fmt.Println("\n\n h1 Blockhash \n", h1.GetParentHash()) - fmt.Println("h1 Blockhash \n", h1.Block.PreviousBlockhash) - h8 := cltest.Head(8) h9 := cltest.Head(9) diff --git a/pkg/solana/headtracker/types/chain.go b/pkg/solana/headtracker/types/chain.go index 1728273c9..cf952b7b9 100644 --- a/pkg/solana/headtracker/types/chain.go +++ b/pkg/solana/headtracker/types/chain.go @@ -21,7 +21,7 @@ func (id ChainID) String() string { case Localnet: return "localnet" default: - return "localnet" + return "unknown" } } diff --git a/pkg/solana/headtracker/types/head.go b/pkg/solana/headtracker/types/head.go index b04b822f8..5e629b4ae 100644 --- a/pkg/solana/headtracker/types/head.go +++ b/pkg/solana/headtracker/types/head.go @@ -104,7 +104,7 @@ func (h *Head) HasChainID() bool { if h == nil { return false } - return h.ChainID().String() != "unknown" // TODO: Refactor this into a more coherent check + return h.ChainID().String() != "unknown" } func (h *Head) IsValid() bool { diff --git a/pkg/solana/headtracker/types/head_test.go b/pkg/solana/headtracker/types/head_test.go index 9d9686735..2efc6697c 100644 --- a/pkg/solana/headtracker/types/head_test.go +++ b/pkg/solana/headtracker/types/head_test.go @@ -73,14 +73,30 @@ func TestHead_EarliestHeadInChain(t *testing.T) { } func TestHead_GetParentHash(t *testing.T) { - blockResult := cltest.ConfigureBlockResult() id := types.Mainnet - head := types.NewHead(3, blockResult, - types.NewHead(2, blockResult, - types.NewHead(1, blockResult, nil, id), id), id) + blockResult0 := cltest.ConfigureBlockResult() + h0 := types.NewHead(0, blockResult0, nil, id) + + blockResult1 := cltest.ConfigureBlockResult() + blockResult1.ParentSlot = 0 + blockResult1.PreviousBlockhash = blockResult0.Blockhash + h1 := types.NewHead(1, blockResult1, h0, id) + + blockResult2 := cltest.ConfigureBlockResult() + blockResult2.ParentSlot = 1 + blockResult2.PreviousBlockhash = blockResult1.Blockhash + h2 := types.NewHead(2, blockResult2, h1, id) + + blockResult3 := cltest.ConfigureBlockResult() + blockResult3.ParentSlot = 2 + blockResult3.PreviousBlockhash = blockResult2.Blockhash + h3 := types.NewHead(3, blockResult3, h2, id) - assert.Equal(t, head.Parent.BlockHash(), head.GetParentHash()) + // h3 -> h2 -> h1 -> h0 + assert.Equal(t, h2.BlockHash(), h3.GetParentHash()) + assert.Equal(t, h1.BlockHash(), h2.GetParentHash()) + assert.Equal(t, h0.BlockHash(), h1.GetParentHash()) } func TestHead_GetParent(t *testing.T) { From b66e00e157d7a0bff5626042eece301c1b710e11 Mon Sep 17 00:00:00 2001 From: Yong Kang Date: Thu, 13 Jul 2023 16:53:27 -0500 Subject: [PATCH 47/47] cleanup of TODO --- pkg/solana/client/client.go | 19 ++++++++++++------- pkg/solana/headtracker/config.go | 3 +-- pkg/solana/headtracker/types/chain.go | 3 ++- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/pkg/solana/client/client.go b/pkg/solana/client/client.go index 451c70d41..c5444905d 100644 --- a/pkg/solana/client/client.go +++ b/pkg/solana/client/client.go @@ -63,6 +63,7 @@ type Client struct { contextDuration time.Duration log logger.Logger pollingInterval time.Duration + chainId types.ChainID // provides a duplicate function call suppression mechanism requestGroup *singleflight.Group @@ -148,14 +149,19 @@ func (c *Client) ChainID() (string, error) { return network, nil } -// TODO: requires refactor. Do we want to store chainID? how do we want to cache ChainID? func (c *Client) ConfiguredChainID() types.ChainID { + if c.chainId != types.Unknown { + return c.chainId + } + chainID, err := c.ChainID() if err != nil { - c.log.Warnf("unable to determine configured chain ID: %v", err) - return types.ChainID(types.Localnet) + c.log.Warnf("Unable to determine configured chain ID: %v", err) + return types.Unknown } - return types.StringToChainID(chainID) + + c.chainId = types.StringToChainID(chainID) + return c.chainId } func (c *Client) GetFeeForMessage(msg string) (uint64, error) { @@ -262,10 +268,10 @@ func (c *Client) SubscribeNewHead(ctx context.Context, ch chan<- *types.Head) (* return case <-ticker.C: block, slot, err := c.getLatestBlock(ctx) - // TODO: Improve error handling + if err != nil { subscription.errChan <- err - continue + return } // Create a new Head object and send to channel @@ -318,7 +324,6 @@ func (c *Client) GetBlock(ctx context.Context, slot uint64) (out *rpc.GetBlockRe return res.(*rpc.GetBlockResult), err } -// TODO: confirm commitment for RPC again. Public RPC nodes cannot handle CommitmentProcessed due to requests being too frequent. func (c *Client) GetLatestSlot(ctx context.Context) (uint64, error) { ctx, cancel := context.WithTimeout(ctx, c.contextDuration) defer cancel() diff --git a/pkg/solana/headtracker/config.go b/pkg/solana/headtracker/config.go index 4d89e5b1c..1b36ffb59 100644 --- a/pkg/solana/headtracker/config.go +++ b/pkg/solana/headtracker/config.go @@ -10,7 +10,6 @@ import ( // It should be replaced with a more robust Config // such as the one in pkg/solana/Config -// TODO: replace this Config with a more robust Config. Requires research type Config struct { Defaults configSet } @@ -26,7 +25,7 @@ type configSet struct { var defaultConfigSet = configSet{ // headtracker - BlockEmissionIdleWarningThreshold: 30 * time.Second, // TODO: Check this Config value again + BlockEmissionIdleWarningThreshold: 30 * time.Second, FinalityDepth: 50, HeadTrackerHistoryDepth: 100, HeadTrackerMaxBufferSize: 3, diff --git a/pkg/solana/headtracker/types/chain.go b/pkg/solana/headtracker/types/chain.go index cf952b7b9..998fc6b3f 100644 --- a/pkg/solana/headtracker/types/chain.go +++ b/pkg/solana/headtracker/types/chain.go @@ -7,6 +7,7 @@ const ( Testnet Devnet Localnet + Unknown ) // String returns the string representation of the Network value. @@ -36,6 +37,6 @@ func StringToChainID(id string) ChainID { case "localnet": return Localnet default: - return Localnet + return Unknown } }