wormhole/node/pkg/watchers/evm/connectors/poller_test.go

351 lines
11 KiB
Go

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()
}