Skip to content

Commit 12bc110

Browse files
committed
feat: improve blockchainstate
1 parent e598c02 commit 12bc110

File tree

3 files changed

+211
-45
lines changed

3 files changed

+211
-45
lines changed
Lines changed: 119 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,158 @@
11
package blockchainstate
22

3+
//go:generate go tool mockgen -package=mock_blockchainstate -source=blockchainstate.go -destination=mock/blockchainstate.go
4+
35
import (
46
"context"
7+
"errors"
8+
"math"
9+
"strconv"
510
"sync"
611
"time"
712

13+
"github.com/ethereum/go-ethereum/accounts/abi/bind"
14+
"github.com/status-im/go-wallet-sdk/pkg/contracts/multicall3"
15+
"github.com/status-im/status-go/rpc/chain"
816
"github.com/status-im/status-go/services/wallet/common"
917
)
1018

19+
type EthClientGetter interface {
20+
EthClient(chainID uint64) (chain.ClientInterface, error)
21+
}
22+
1123
type LatestBlockData struct {
1224
blockNumber uint64
1325
timestamp time.Time
1426
blockDuration time.Duration
1527
}
1628

1729
type BlockChainState struct {
18-
blkMu sync.RWMutex
30+
ethClientGetter EthClientGetter
31+
mu sync.RWMutex
1932
latestBlockNumbers map[uint64]LatestBlockData
2033
sinceFn func(time.Time) time.Duration
2134
}
2235

23-
func NewBlockChainState() *BlockChainState {
36+
func NewBlockChainState(ethClientGetter EthClientGetter) *BlockChainState {
2437
return &BlockChainState{
25-
blkMu: sync.RWMutex{},
38+
ethClientGetter: ethClientGetter,
39+
mu: sync.RWMutex{},
2640
latestBlockNumbers: make(map[uint64]LatestBlockData),
2741
sinceFn: time.Since,
2842
}
2943
}
3044

45+
// Get the estimated latest block number for a chain
3146
func (s *BlockChainState) GetEstimatedLatestBlockNumber(ctx context.Context, chainID uint64) (uint64, error) {
32-
blockNumber, _ := s.estimateLatestBlockNumber(chainID)
33-
return blockNumber, nil
47+
s.mu.RLock()
48+
defer s.mu.RUnlock()
49+
50+
return s.estimateLatestBlockNumber(ctx, chainID)
3451
}
3552

36-
func (s *BlockChainState) SetLastBlockNumber(chainID uint64, blockNumber uint64) {
37-
blockDuration, found := common.AverageBlockDurationForChain[common.ChainID(chainID)]
38-
if !found {
39-
blockDuration = common.AverageBlockDurationForChain[common.ChainID(common.UnknownChainID)]
53+
// Get the estimated block time for a given block number and a chain
54+
func (s *BlockChainState) GetEstimatedBlockTime(ctx context.Context, chainID uint64, blockNumber uint64) (time.Time, error) {
55+
s.mu.RLock()
56+
defer s.mu.RUnlock()
57+
58+
return s.estimateBlockTime(ctx, chainID, blockNumber)
59+
}
60+
61+
// Set the latest known block number for a chain
62+
func (s *BlockChainState) SetLatestBlockNumber(chainID uint64, blockNumber uint64) {
63+
s.mu.RLock()
64+
defer s.mu.RUnlock()
65+
66+
s.setLatestBlockNumber(chainID, blockNumber)
67+
}
68+
69+
// Estimate the latest block number based on the last real value and the average block duration.
70+
// Must be called with a locked mutex.
71+
func (s *BlockChainState) estimateLatestBlockNumber(ctx context.Context, chainID uint64) (uint64, error) {
72+
blockData, err := s.getOrInitializeLatestBlockData(ctx, chainID)
73+
if err != nil {
74+
return 0, err
4075
}
41-
s.setLatestBlockDataForChain(chainID, LatestBlockData{
42-
blockNumber: blockNumber,
43-
timestamp: time.Now(),
44-
blockDuration: blockDuration,
45-
})
76+
77+
timeDiff := s.sinceFn(blockData.timestamp)
78+
blockDiff := uint64(math.Floor(float64(timeDiff) / float64(blockData.blockDuration)))
79+
return blockData.blockNumber + blockDiff, nil
4680
}
4781

48-
func (s *BlockChainState) setLatestBlockDataForChain(chainID uint64, latestBlockData LatestBlockData) {
49-
s.blkMu.Lock()
50-
defer s.blkMu.Unlock()
51-
s.latestBlockNumbers[chainID] = latestBlockData
82+
// Estimate the timestamp for a given block number based on the last real value and the average block duration.
83+
// Must be called with a locked mutex.
84+
func (s *BlockChainState) estimateBlockTime(ctx context.Context, chainID uint64, blockNumber uint64) (time.Time, error) {
85+
blockData, err := s.getOrInitializeLatestBlockData(ctx, chainID)
86+
if err != nil {
87+
return time.Time{}, err
88+
}
89+
blockDiff := int64(blockNumber) - int64(blockData.blockNumber)
90+
timeDiff := time.Duration(blockDiff * int64(blockData.blockDuration))
91+
blockTime := blockData.timestamp.Add(timeDiff)
92+
return blockTime, nil
5293
}
5394

54-
func (s *BlockChainState) estimateLatestBlockNumber(chainID uint64) (uint64, bool) {
55-
s.blkMu.RLock()
56-
defer s.blkMu.RUnlock()
95+
func (s *BlockChainState) getOrInitializeLatestBlockData(ctx context.Context, chainID uint64) (LatestBlockData, error) {
5796
blockData, ok := s.latestBlockNumbers[chainID]
5897
if !ok {
59-
return 0, false
98+
err := s.initializeLatestBlockNumber(ctx, chainID)
99+
if err != nil {
100+
return LatestBlockData{}, err
101+
}
102+
blockData, ok = s.latestBlockNumbers[chainID]
103+
if !ok {
104+
return LatestBlockData{}, errors.New("Failed to initialize latest block number for chain ID " + strconv.Itoa(int(chainID)))
105+
}
106+
}
107+
return blockData, nil
108+
}
109+
110+
// Initialize the latest block number for a chain
111+
func (s *BlockChainState) initializeLatestBlockNumber(ctx context.Context, chainID uint64) error {
112+
ethClient, err := s.ethClientGetter.EthClient(chainID)
113+
if err != nil {
114+
return err
115+
}
116+
117+
// Get multicall3 contract address
118+
multicallAddr, exists := multicall3.GetMulticall3Address(int64(chainID))
119+
if !exists {
120+
return errors.New("Multicall3 not supported on chain ID " + strconv.Itoa(int(chainID)))
121+
}
122+
123+
// Create multicall3 contract instance for the caller interface
124+
multicallContract, err := multicall3.NewMulticall3(multicallAddr, ethClient)
125+
if err != nil {
126+
return err
127+
}
128+
129+
blockNumber, err := multicallContract.GetBlockNumber(&bind.CallOpts{
130+
Context: ctx,
131+
})
132+
if err != nil {
133+
return err
134+
}
135+
136+
s.setLatestBlockNumber(chainID, blockNumber.Uint64())
137+
return nil
138+
}
139+
140+
func (s *BlockChainState) setLatestBlockNumber(chainID uint64, blockNumber uint64) {
141+
blockDuration, found := common.AverageBlockDurationForChain[common.ChainID(chainID)]
142+
if !found {
143+
blockDuration = common.AverageBlockDurationForChain[common.ChainID(common.UnknownChainID)]
144+
}
145+
146+
if data, ok := s.latestBlockNumbers[chainID]; ok {
147+
// Only update if the new block number is greater than the current one
148+
if data.blockNumber >= blockNumber {
149+
return
150+
}
151+
}
152+
153+
s.latestBlockNumbers[chainID] = LatestBlockData{
154+
blockNumber: blockNumber,
155+
timestamp: time.Now(),
156+
blockDuration: blockDuration,
60157
}
61-
timeDiff := s.sinceFn(blockData.timestamp)
62-
return blockData.blockNumber + uint64((timeDiff / blockData.blockDuration)), true
63158
}
Lines changed: 87 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11
package blockchainstate
22

33
import (
4+
"context"
5+
"errors"
46
"testing"
57
"time"
68

9+
mock_blockchainstate "github.com/status-im/status-go/services/wallet/blockchainstate/mock"
710
"github.com/stretchr/testify/require"
11+
12+
"go.uber.org/mock/gomock"
813
)
914

1015
var mockupTime = time.Unix(946724400, 0) // 2000-01-01 12:00:00
@@ -13,34 +18,95 @@ func mockupSince(t time.Time) time.Duration {
1318
return mockupTime.Sub(t)
1419
}
1520

16-
func setupTestState(t *testing.T) (s *BlockChainState) {
17-
state := NewBlockChainState()
21+
func setupTestState(t *testing.T) (*BlockChainState, *mock_blockchainstate.MockEthClientGetter) {
22+
ctrl := gomock.NewController(t)
23+
ethClientGetter := mock_blockchainstate.NewMockEthClientGetter(ctrl)
24+
25+
state := NewBlockChainState(ethClientGetter)
1826
state.sinceFn = mockupSince
19-
return state
27+
return state, ethClientGetter
2028
}
2129

22-
func TestEstimateLatestBlockNumber(t *testing.T) {
23-
state := setupTestState(t)
30+
func TestGetEstimatedLatestBlockNumber_WithExistingData(t *testing.T) {
31+
state, _ := setupTestState(t)
2432

25-
state.setLatestBlockDataForChain(1, LatestBlockData{
26-
blockNumber: uint64(100),
33+
// Manually set block data (simulating what would happen after initialization)
34+
state.latestBlockNumbers[1] = LatestBlockData{
35+
blockNumber: 100,
2736
timestamp: mockupTime.Add(-31 * time.Second),
2837
blockDuration: 10 * time.Second,
29-
})
38+
}
3039

31-
state.setLatestBlockDataForChain(2, LatestBlockData{
32-
blockNumber: uint64(200),
40+
state.latestBlockNumbers[2] = LatestBlockData{
41+
blockNumber: 200,
3342
timestamp: mockupTime.Add(-5 * time.Second),
3443
blockDuration: 12 * time.Second,
35-
})
36-
37-
val, ok := state.estimateLatestBlockNumber(1)
38-
require.True(t, ok)
39-
require.Equal(t, uint64(103), val)
40-
val, ok = state.estimateLatestBlockNumber(2)
41-
require.True(t, ok)
42-
require.Equal(t, uint64(200), val)
43-
val, ok = state.estimateLatestBlockNumber(3)
44-
require.False(t, ok)
45-
require.Equal(t, uint64(0), val)
44+
}
45+
46+
// Test chain 1: should estimate 3 blocks ahead (31 seconds / 10 seconds per block)
47+
blockNumber, err := state.GetEstimatedLatestBlockNumber(context.Background(), 1)
48+
require.NoError(t, err)
49+
require.Equal(t, uint64(103), blockNumber)
50+
51+
// Test chain 2: should not estimate ahead (5 seconds < 12 seconds per block)
52+
blockNumber, err = state.GetEstimatedLatestBlockNumber(context.Background(), 2)
53+
require.NoError(t, err)
54+
require.Equal(t, uint64(200), blockNumber)
55+
}
56+
57+
func TestGetEstimatedLatestBlockNumber_WithInitialization(t *testing.T) {
58+
state, ethClientGetter := setupTestState(t)
59+
60+
// Mock eth client to return an error (simulating network failure)
61+
ethClientGetter.EXPECT().EthClient(uint64(1)).Return(nil, errors.New("network error")).AnyTimes()
62+
63+
// This should trigger initialization but fail
64+
blockNumber, err := state.GetEstimatedLatestBlockNumber(context.Background(), 1)
65+
require.Error(t, err)
66+
require.Equal(t, uint64(0), blockNumber)
67+
require.Contains(t, err.Error(), "network error")
68+
}
69+
70+
func TestGetEstimatedBlockTime(t *testing.T) {
71+
state, _ := setupTestState(t)
72+
73+
// Set up test data
74+
state.latestBlockNumbers[1] = LatestBlockData{
75+
blockNumber: 100,
76+
timestamp: mockupTime.Add(-10 * time.Second),
77+
blockDuration: 2 * time.Second,
78+
}
79+
80+
// Test block time estimation
81+
blockTime, err := state.GetEstimatedBlockTime(context.Background(), 1, 105)
82+
require.NoError(t, err)
83+
84+
// Block 105 is 5 blocks ahead of 100, so 5 * 2 seconds = 10 seconds ahead
85+
expectedTime := mockupTime
86+
require.Equal(t, expectedTime, blockTime)
87+
}
88+
89+
func TestSetLatestBlockNumber(t *testing.T) {
90+
state, _ := setupTestState(t)
91+
92+
// Test setting block number
93+
state.SetLatestBlockNumber(1, 100)
94+
95+
// Verify it was set
96+
blockData, exists := state.latestBlockNumbers[1]
97+
require.True(t, exists)
98+
require.Equal(t, uint64(100), blockData.blockNumber)
99+
require.True(t, blockData.timestamp.After(mockupTime.Add(-time.Minute))) // Should be recent
100+
101+
// Test setting a smaller block number (should not update)
102+
state.SetLatestBlockNumber(1, 50)
103+
blockData, exists = state.latestBlockNumbers[1]
104+
require.True(t, exists)
105+
require.Equal(t, uint64(100), blockData.blockNumber) // Should still be 100
106+
107+
// Test setting a larger block number (should update)
108+
state.SetLatestBlockNumber(1, 150)
109+
blockData, exists = state.latestBlockNumbers[1]
110+
require.True(t, exists)
111+
require.Equal(t, uint64(150), blockData.blockNumber) // Should be updated to 150
46112
}

services/wallet/common/const.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,15 @@ func IsSupportedChainID(chainID uint64) bool {
129129
var AverageBlockDurationForChain = map[ChainID]time.Duration{
130130
ChainID(UnknownChainID): time.Duration(12000) * time.Millisecond,
131131
ChainID(EthereumMainnet): time.Duration(12000) * time.Millisecond,
132+
ChainID(EthereumSepolia): time.Duration(12000) * time.Millisecond,
132133
ChainID(OptimismMainnet): time.Duration(2000) * time.Millisecond,
134+
ChainID(OptimismSepolia): time.Duration(2000) * time.Millisecond,
133135
ChainID(ArbitrumMainnet): time.Duration(250) * time.Millisecond,
136+
ChainID(ArbitrumSepolia): time.Duration(250) * time.Millisecond,
134137
ChainID(BaseMainnet): time.Duration(2000) * time.Millisecond,
138+
ChainID(BaseSepolia): time.Duration(2000) * time.Millisecond,
135139
ChainID(BSCMainnet): time.Duration(3000) * time.Millisecond,
140+
ChainID(BSCTestnet): time.Duration(3000) * time.Millisecond,
136141
ChainID(StatusNetworkSepolia): time.Duration(2000) * time.Millisecond,
137142
}
138143

0 commit comments

Comments
 (0)