|
@@ -0,0 +1,350 @@
|
|
|
|
|
+package connectors
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "context"
|
|
|
|
|
+ "encoding/json"
|
|
|
|
|
+ "fmt"
|
|
|
|
|
+ "sync"
|
|
|
|
|
+ "testing"
|
|
|
|
|
+ "time"
|
|
|
|
|
+
|
|
|
|
|
+ "github.com/stretchr/testify/assert"
|
|
|
|
|
+ "github.com/stretchr/testify/require"
|
|
|
|
|
+
|
|
|
|
|
+ "go.uber.org/zap"
|
|
|
|
|
+
|
|
|
|
|
+ ethAbi "github.com/certusone/wormhole/node/pkg/watchers/evm/connectors/ethabi"
|
|
|
|
|
+
|
|
|
|
|
+ ethereum "github.com/ethereum/go-ethereum"
|
|
|
|
|
+ ethCommon "github.com/ethereum/go-ethereum/common"
|
|
|
|
|
+ ethTypes "github.com/ethereum/go-ethereum/core/types"
|
|
|
|
|
+ ethClient "github.com/ethereum/go-ethereum/ethclient"
|
|
|
|
|
+ ethEvent "github.com/ethereum/go-ethereum/event"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+// mockConnectorForPoller implements the connector interface for testing purposes.
|
|
|
|
|
+type mockConnectorForPoller struct {
|
|
|
|
|
+ address ethCommon.Address
|
|
|
|
|
+ client *ethClient.Client
|
|
|
|
|
+ mutex sync.Mutex
|
|
|
|
|
+ err error
|
|
|
|
|
+ persistentError bool
|
|
|
|
|
+ blockNumber uint64
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// setError takes an error which will be returned on the next RPC call. The error will persist until cleared.
|
|
|
|
|
+func (m *mockConnectorForPoller) setError(err error) {
|
|
|
|
|
+ m.mutex.Lock()
|
|
|
|
|
+ m.err = err
|
|
|
|
|
+ m.persistentError = true
|
|
|
|
|
+ m.mutex.Unlock()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// setSingleError takes an error which will be returned on the next RPC call. After that, the error is reset to nil.
|
|
|
|
|
+func (m *mockConnectorForPoller) setSingleError(err error) {
|
|
|
|
|
+ m.mutex.Lock()
|
|
|
|
|
+ m.err = err
|
|
|
|
|
+ m.persistentError = false
|
|
|
|
|
+ m.mutex.Unlock()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (e *mockConnectorForPoller) NetworkName() string {
|
|
|
|
|
+ return "mockConnectorForPoller"
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (e *mockConnectorForPoller) ContractAddress() ethCommon.Address {
|
|
|
|
|
+ return e.address
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (e *mockConnectorForPoller) GetCurrentGuardianSetIndex(ctx context.Context) (uint32, error) {
|
|
|
|
|
+ return 0, fmt.Errorf("not implemented")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (e *mockConnectorForPoller) GetGuardianSet(ctx context.Context, index uint32) (ethAbi.StructsGuardianSet, error) {
|
|
|
|
|
+ return ethAbi.StructsGuardianSet{}, fmt.Errorf("not implemented")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (e *mockConnectorForPoller) WatchLogMessagePublished(ctx context.Context, sink chan<- *ethAbi.AbiLogMessagePublished) (ethEvent.Subscription, error) {
|
|
|
|
|
+ var s ethEvent.Subscription
|
|
|
|
|
+ return s, fmt.Errorf("not implemented")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (e *mockConnectorForPoller) TransactionReceipt(ctx context.Context, txHash ethCommon.Hash) (*ethTypes.Receipt, error) {
|
|
|
|
|
+ return nil, fmt.Errorf("not implemented")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (e *mockConnectorForPoller) TimeOfBlockByHash(ctx context.Context, hash ethCommon.Hash) (uint64, error) {
|
|
|
|
|
+ return 0, fmt.Errorf("not implemented")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (e *mockConnectorForPoller) ParseLogMessagePublished(log ethTypes.Log) (*ethAbi.AbiLogMessagePublished, error) {
|
|
|
|
|
+ return nil, fmt.Errorf("not implemented")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (e *mockConnectorForPoller) SubscribeForBlocks(ctx context.Context, sink chan<- *NewBlock) (ethereum.Subscription, error) {
|
|
|
|
|
+ var s ethEvent.Subscription
|
|
|
|
|
+ return s, fmt.Errorf("not implemented")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (e *mockConnectorForPoller) RawCallContext(ctx context.Context, result interface{}, method string, args ...interface{}) (err error) {
|
|
|
|
|
+ if method != "eth_getBlockByNumber" {
|
|
|
|
|
+ panic("method not implemented by mockConnectorForPoller")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ e.mutex.Lock()
|
|
|
|
|
+ // If they set the error, return that immediately.
|
|
|
|
|
+ if e.err != nil {
|
|
|
|
|
+ err = e.err
|
|
|
|
|
+ if !e.persistentError {
|
|
|
|
|
+ e.err = nil
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ str := fmt.Sprintf(`{"author":"0x24c275f0719fdaec6356c4eb9f39ecb9c4d37ce1","baseFeePerGas":"0x3b9aca00","difficulty":"0x0","extraData":"0x","gasLimit":"0xe4e1c0","gasUsed":"0x0","hash":"0xfc8b62a31110121c57cfcccfaf2b147cc2c13b6d01bde4737846cefd29f045cf","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","miner":"0x24c275f0719fdaec6356c4eb9f39ecb9c4d37ce1","nonce":"0x0000000000000000","number":"0x%x","parentHash":"0x09d6d33a658b712f41db7fb9f775f94911ae0132123116aa4f8cf3da9f774e89","receiptsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","sha3Uncles":"0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347","size":"0x201","stateRoot":"0x0409ed10e03fd49424ae1489c6fbc6ff1897f45d0e214655ebdb8df94eedc3c0","timestamp":"0x6373ec24","totalDifficulty":"0x0","transactions":[],"transactionsRoot":"0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421","uncles":[]}`, e.blockNumber)
|
|
|
|
|
+ err = json.Unmarshal([]byte(str), &result)
|
|
|
|
|
+ }
|
|
|
|
|
+ e.mutex.Unlock()
|
|
|
|
|
+
|
|
|
|
|
+ return
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (e *mockConnectorForPoller) setBlockNumber(blockNumber uint64) {
|
|
|
|
|
+ e.mutex.Lock()
|
|
|
|
|
+ e.blockNumber = blockNumber
|
|
|
|
|
+ e.mutex.Unlock()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (e *mockConnectorForPoller) expectedHash() ethCommon.Hash {
|
|
|
|
|
+ return ethCommon.HexToHash("0xfc8b62a31110121c57cfcccfaf2b147cc2c13b6d01bde4737846cefd29f045cf")
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (e *mockConnectorForPoller) Client() *ethClient.Client {
|
|
|
|
|
+ return e.client
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+type mockFinalizerForPoller struct {
|
|
|
|
|
+ mutex sync.Mutex
|
|
|
|
|
+ finalized bool
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func newMockFinalizerForPoller(initialState bool) *mockFinalizerForPoller {
|
|
|
|
|
+ return &mockFinalizerForPoller{finalized: initialState}
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (f *mockFinalizerForPoller) setFinalized(finalized bool) {
|
|
|
|
|
+ f.mutex.Lock()
|
|
|
|
|
+ defer f.mutex.Unlock()
|
|
|
|
|
+ f.finalized = finalized
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func (f *mockFinalizerForPoller) IsBlockFinalized(ctx context.Context, block *NewBlock) (bool, error) {
|
|
|
|
|
+ f.mutex.Lock()
|
|
|
|
|
+ defer f.mutex.Unlock()
|
|
|
|
|
+ return f.finalized, nil
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestBlockPoller is one big, ugly test because of all the set up required.
|
|
|
|
|
+func TestBlockPoller(t *testing.T) {
|
|
|
|
|
+ ctx := context.Background()
|
|
|
|
|
+ logger := zap.NewNop()
|
|
|
|
|
+ baseConnector := mockConnectorForPoller{}
|
|
|
|
|
+
|
|
|
|
|
+ finalizer := newMockFinalizerForPoller(true) // Start by assuming blocks are finalized.
|
|
|
|
|
+ assert.NotNil(t, finalizer)
|
|
|
|
|
+
|
|
|
|
|
+ poller := &BlockPollConnector{
|
|
|
|
|
+ Connector: &baseConnector,
|
|
|
|
|
+ Delay: 1 * time.Millisecond,
|
|
|
|
|
+ useFinalized: false,
|
|
|
|
|
+ finalizer: finalizer,
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Set the starting block.
|
|
|
|
|
+ baseConnector.setBlockNumber(0x309a0c)
|
|
|
|
|
+
|
|
|
|
|
+ // The go routines will post results here.
|
|
|
|
|
+ var mutex sync.Mutex
|
|
|
|
|
+ var block *NewBlock
|
|
|
|
|
+ var err error
|
|
|
|
|
+ var pollerStatus int
|
|
|
|
|
+
|
|
|
|
|
+ // Start the poller running.
|
|
|
|
|
+ go func() {
|
|
|
|
|
+ mutex.Lock()
|
|
|
|
|
+ pollerStatus = 1
|
|
|
|
|
+ mutex.Unlock()
|
|
|
|
|
+ err := poller.run(ctx, logger)
|
|
|
|
|
+ require.NoError(t, err)
|
|
|
|
|
+ mutex.Lock()
|
|
|
|
|
+ pollerStatus = 2
|
|
|
|
|
+ mutex.Unlock()
|
|
|
|
|
+ }()
|
|
|
|
|
+
|
|
|
|
|
+ // Subscribe for events to be processed by our go routine.
|
|
|
|
|
+ headSink := make(chan *NewBlock, 2)
|
|
|
|
|
+ headerSubscription, suberr := poller.SubscribeForBlocks(ctx, headSink)
|
|
|
|
|
+ require.NoError(t, suberr)
|
|
|
|
|
+
|
|
|
|
|
+ go func() {
|
|
|
|
|
+ for {
|
|
|
|
|
+ select {
|
|
|
|
|
+ case <-ctx.Done():
|
|
|
|
|
+ return
|
|
|
|
|
+ case thisErr := <-headerSubscription.Err():
|
|
|
|
|
+ mutex.Lock()
|
|
|
|
|
+ err = thisErr
|
|
|
|
|
+ mutex.Unlock()
|
|
|
|
|
+ case thisBlock := <-headSink:
|
|
|
|
|
+ require.NotNil(t, thisBlock)
|
|
|
|
|
+ mutex.Lock()
|
|
|
|
|
+ block = thisBlock
|
|
|
|
|
+ mutex.Unlock()
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }()
|
|
|
|
|
+
|
|
|
|
|
+ // First sleep a bit and make sure there were no start up errors.
|
|
|
|
|
+ time.Sleep(10 * time.Millisecond)
|
|
|
|
|
+ mutex.Lock()
|
|
|
|
|
+ require.Equal(t, 1, pollerStatus)
|
|
|
|
|
+ require.NoError(t, err)
|
|
|
|
|
+ assert.Nil(t, block)
|
|
|
|
|
+ mutex.Unlock()
|
|
|
|
|
+
|
|
|
|
|
+ // Post the first new block and verify we get it.
|
|
|
|
|
+ baseConnector.setBlockNumber(0x309a0d)
|
|
|
|
|
+
|
|
|
|
|
+ time.Sleep(10 * time.Millisecond)
|
|
|
|
|
+ mutex.Lock()
|
|
|
|
|
+ require.Equal(t, 1, pollerStatus)
|
|
|
|
|
+ require.NoError(t, err)
|
|
|
|
|
+ require.NotNil(t, block)
|
|
|
|
|
+ assert.Equal(t, uint64(0x309a0d), block.Number.Uint64())
|
|
|
|
|
+ assert.Equal(t, baseConnector.expectedHash(), block.Hash)
|
|
|
|
|
+ block = nil
|
|
|
|
|
+ mutex.Unlock()
|
|
|
|
|
+
|
|
|
|
|
+ // Sleep some more and verify we don't see any more blocks, since we haven't posted a new one.
|
|
|
|
|
+ baseConnector.setBlockNumber(0x309a0d)
|
|
|
|
|
+ time.Sleep(10 * time.Millisecond)
|
|
|
|
|
+ mutex.Lock()
|
|
|
|
|
+ require.Equal(t, 1, pollerStatus)
|
|
|
|
|
+ require.NoError(t, err)
|
|
|
|
|
+ require.Nil(t, block)
|
|
|
|
|
+ mutex.Unlock()
|
|
|
|
|
+
|
|
|
|
|
+ // Post the next block and verify we get it.
|
|
|
|
|
+ baseConnector.setBlockNumber(0x309a0e)
|
|
|
|
|
+ time.Sleep(10 * time.Millisecond)
|
|
|
|
|
+ mutex.Lock()
|
|
|
|
|
+ require.Equal(t, 1, pollerStatus)
|
|
|
|
|
+ require.NoError(t, err)
|
|
|
|
|
+ require.NotNil(t, block)
|
|
|
|
|
+ assert.Equal(t, uint64(0x309a0e), block.Number.Uint64())
|
|
|
|
|
+ assert.Equal(t, baseConnector.expectedHash(), block.Hash)
|
|
|
|
|
+ block = nil
|
|
|
|
|
+ mutex.Unlock()
|
|
|
|
|
+
|
|
|
|
|
+ // Post the next block but mark it as not finalized, so we shouldn't see it yet.
|
|
|
|
|
+ mutex.Lock()
|
|
|
|
|
+ finalizer.setFinalized(false)
|
|
|
|
|
+ baseConnector.setBlockNumber(0x309a0f)
|
|
|
|
|
+ mutex.Unlock()
|
|
|
|
|
+
|
|
|
|
|
+ time.Sleep(10 * time.Millisecond)
|
|
|
|
|
+ mutex.Lock()
|
|
|
|
|
+ require.Equal(t, 1, pollerStatus)
|
|
|
|
|
+ require.NoError(t, err)
|
|
|
|
|
+ require.Nil(t, block)
|
|
|
|
|
+ mutex.Unlock()
|
|
|
|
|
+
|
|
|
|
|
+ // Once it goes finalized we should see it.
|
|
|
|
|
+ mutex.Lock()
|
|
|
|
|
+ finalizer.setFinalized(true)
|
|
|
|
|
+ mutex.Unlock()
|
|
|
|
|
+
|
|
|
|
|
+ time.Sleep(10 * time.Millisecond)
|
|
|
|
|
+ mutex.Lock()
|
|
|
|
|
+ require.Equal(t, 1, pollerStatus)
|
|
|
|
|
+ require.NoError(t, err)
|
|
|
|
|
+ require.NotNil(t, block)
|
|
|
|
|
+ assert.Equal(t, uint64(0x309a0f), block.Number.Uint64())
|
|
|
|
|
+ assert.Equal(t, baseConnector.expectedHash(), block.Hash)
|
|
|
|
|
+ block = nil
|
|
|
|
|
+ mutex.Unlock()
|
|
|
|
|
+
|
|
|
|
|
+ // An RPC error should be returned to us.
|
|
|
|
|
+ err = nil
|
|
|
|
|
+ baseConnector.setError(fmt.Errorf("RPC failed"))
|
|
|
|
|
+
|
|
|
|
|
+ time.Sleep(10 * time.Millisecond)
|
|
|
|
|
+ mutex.Lock()
|
|
|
|
|
+ require.Equal(t, 1, pollerStatus)
|
|
|
|
|
+ assert.Error(t, err)
|
|
|
|
|
+ assert.Nil(t, block)
|
|
|
|
|
+ baseConnector.setError(nil)
|
|
|
|
|
+ err = nil
|
|
|
|
|
+ mutex.Unlock()
|
|
|
|
|
+
|
|
|
|
|
+ // Post the next block and verify we get it (so we survived the RPC error).
|
|
|
|
|
+ baseConnector.setBlockNumber(0x309a10)
|
|
|
|
|
+
|
|
|
|
|
+ time.Sleep(10 * time.Millisecond)
|
|
|
|
|
+ mutex.Lock()
|
|
|
|
|
+ require.Equal(t, 1, pollerStatus)
|
|
|
|
|
+ require.NoError(t, err)
|
|
|
|
|
+ require.NotNil(t, block)
|
|
|
|
|
+ assert.Equal(t, uint64(0x309a10), block.Number.Uint64())
|
|
|
|
|
+ assert.Equal(t, baseConnector.expectedHash(), block.Hash)
|
|
|
|
|
+ block = nil
|
|
|
|
|
+ mutex.Unlock()
|
|
|
|
|
+
|
|
|
|
|
+ // Post an old block and we should not hear about it.
|
|
|
|
|
+ baseConnector.setBlockNumber(0x309a0c)
|
|
|
|
|
+
|
|
|
|
|
+ time.Sleep(10 * time.Millisecond)
|
|
|
|
|
+ mutex.Lock()
|
|
|
|
|
+ require.Equal(t, 1, pollerStatus)
|
|
|
|
|
+ require.NoError(t, err)
|
|
|
|
|
+ require.Nil(t, block)
|
|
|
|
|
+ mutex.Unlock()
|
|
|
|
|
+
|
|
|
|
|
+ // But we should keep going when we get a new one.
|
|
|
|
|
+ baseConnector.setBlockNumber(0x309a11)
|
|
|
|
|
+
|
|
|
|
|
+ time.Sleep(10 * time.Millisecond)
|
|
|
|
|
+ mutex.Lock()
|
|
|
|
|
+ require.Equal(t, 1, pollerStatus)
|
|
|
|
|
+ require.NoError(t, err)
|
|
|
|
|
+ require.NotNil(t, block)
|
|
|
|
|
+ assert.Equal(t, uint64(0x309a11), block.Number.Uint64())
|
|
|
|
|
+ assert.Equal(t, baseConnector.expectedHash(), block.Hash)
|
|
|
|
|
+ block = nil
|
|
|
|
|
+ mutex.Unlock()
|
|
|
|
|
+
|
|
|
|
|
+ // If there's a gap in the blocks, we should keep going.
|
|
|
|
|
+ baseConnector.setBlockNumber(0x309a13)
|
|
|
|
|
+
|
|
|
|
|
+ time.Sleep(10 * time.Millisecond)
|
|
|
|
|
+ mutex.Lock()
|
|
|
|
|
+ require.Equal(t, 1, pollerStatus)
|
|
|
|
|
+ require.NoError(t, err)
|
|
|
|
|
+ require.NotNil(t, block)
|
|
|
|
|
+ assert.Equal(t, uint64(0x309a13), block.Number.Uint64())
|
|
|
|
|
+ assert.Equal(t, baseConnector.expectedHash(), block.Hash)
|
|
|
|
|
+ block = nil
|
|
|
|
|
+ mutex.Unlock()
|
|
|
|
|
+
|
|
|
|
|
+ // Should retry on a transient error and be able to continue.
|
|
|
|
|
+ baseConnector.setSingleError(fmt.Errorf("RPC failed"))
|
|
|
|
|
+ baseConnector.setBlockNumber(0x309a14)
|
|
|
|
|
+
|
|
|
|
|
+ time.Sleep(10 * time.Millisecond)
|
|
|
|
|
+ mutex.Lock()
|
|
|
|
|
+ require.Equal(t, 1, pollerStatus)
|
|
|
|
|
+ require.NoError(t, err)
|
|
|
|
|
+ require.NotNil(t, block)
|
|
|
|
|
+ assert.Equal(t, uint64(0x309a14), block.Number.Uint64())
|
|
|
|
|
+ assert.Equal(t, baseConnector.expectedHash(), block.Hash)
|
|
|
|
|
+ block = nil
|
|
|
|
|
+ mutex.Unlock()
|
|
|
|
|
+}
|