@@ -5,10 +5,14 @@ import (
55 "encoding/json"
66 "fmt"
77 "math/big"
8+ "sync/atomic"
89 "testing"
910 "time"
1011
1112 "github.com/0xsequence/ethkit/ethmonitor/internal/mocks"
13+ "github.com/0xsequence/ethkit/go-ethereum"
14+ "github.com/0xsequence/ethkit/go-ethereum/common"
15+ "github.com/0xsequence/ethkit/go-ethereum/core/types"
1216 "github.com/stretchr/testify/require"
1317 "go.uber.org/mock/gomock"
1418)
@@ -35,6 +39,137 @@ func TestPrefetchRejectsInvalidBlockBeforeCaching(t *testing.T) {
3539 }
3640}
3741
42+ func TestMonitorRecoversInvalidPrefetchedLogs (t * testing.T ) {
43+ for _ , payload := range []string {`{}` , `[{}]` } {
44+ for _ , cached := range []bool {false , true } {
45+ t .Run (fmt .Sprintf ("payload=%s/cached=%v" , payload , cached ), func (t * testing.T ) {
46+ const first , target , last = uint64 (1000 ), uint64 (1002 ), uint64 (1004 )
47+ targetHash := common .BigToHash (new (big.Int ).SetUint64 (target ))
48+ expectedLogs := []types.Log {{
49+ Address : common .HexToAddress ("0x1234" ),
50+ Topics : []common.Hash {common .HexToHash ("0xabcd" )},
51+ Data : []byte {1 , 2 },
52+ TxHash : common .HexToHash ("0x5678" ),
53+ BlockHash : targetHash ,
54+ BlockNumber : target ,
55+ }}
56+ validPayload , err := json .Marshal (expectedLogs )
57+ require .NoError (t , err )
58+
59+ provider := mocks .NewMockRawInterface (gomock .NewController (t ))
60+ provider .EXPECT ().ChainID (gomock .Any ()).Return (big .NewInt (1 ), nil ).AnyTimes ()
61+ provider .EXPECT ().IsStreamingEnabled ().Return (false ).AnyTimes ()
62+ provider .EXPECT ().BlockNumber (gomock .Any ()).Return (last , nil ).AnyTimes ()
63+ provider .EXPECT ().RawBlockByNumber (gomock .Any (), gomock .Any ()).DoAndReturn (
64+ func (ctx context.Context , number * big.Int ) (json.RawMessage , error ) {
65+ if err := ctx .Err (); err != nil {
66+ return nil , err
67+ }
68+ if number .Uint64 () > last {
69+ return nil , ethereum .NotFound
70+ }
71+ var bloom types.Bloom
72+ if number .Uint64 () == target {
73+ bloom = types .BytesToBloom ([]byte {1 })
74+ }
75+ header := & types.Header {
76+ Number : number ,
77+ ParentHash : common .BigToHash (new (big.Int ).Sub (number , big .NewInt (1 ))),
78+ Difficulty : big .NewInt (0 ),
79+ GasLimit : 30_000_000 ,
80+ Bloom : bloom ,
81+ }
82+ header .SetHash (common .BigToHash (number ))
83+ return json .Marshal (header )
84+ },
85+ ).AnyTimes ()
86+ var originCalls atomic.Int64
87+ provider .EXPECT ().RawFilterLogs (gomock .Any (), gomock .Any ()).DoAndReturn (
88+ func (ctx context.Context , query ethereum.FilterQuery ) (json.RawMessage , error ) {
89+ if err := ctx .Err (); err != nil {
90+ return nil , err
91+ }
92+ if * query .BlockHash != targetHash {
93+ return json .RawMessage (`[]` ), nil
94+ }
95+ if originCalls .Add (1 ) == 1 && ! cached {
96+ return json .RawMessage (payload ), nil
97+ }
98+ return json .RawMessage (validPayload ), nil
99+ },
100+ ).AnyTimes ()
101+
102+ opts := DefaultOptions
103+ opts .WithLogs = true
104+ opts .PrefetchConcurrency = 1
105+ opts .StartBlockNumber = new (big.Int ).SetUint64 (first )
106+ opts .PollingInterval = 10 * time .Millisecond
107+ opts .CacheExpiry = time .Hour
108+ // Leave CacheBackend unset to exercise automatic memory caching.
109+ monitor , err := NewMonitor (provider , opts )
110+ require .NoError (t , err )
111+ monitor .chainID = big .NewInt (1 )
112+ key := CacheKeyBlockLogs (monitor .chainID , targetHash , monitor .logTopics ())
113+ if cached {
114+ require .NoError (t , monitor .cache .SetEx (context .Background (), key , []byte (payload ), time .Hour ))
115+ } else {
116+ // Complete the speculative fetch before starting the serial loop,
117+ // so the worker deterministically receives the malformed response.
118+ monitor .prefetch .fetch (context .Background (), prefetchJob {num : target })
119+ require .Equal (t , int64 (1 ), originCalls .Load ())
120+ _ , found , err := monitor .cache .Get (context .Background (), key )
121+ require .NoError (t , err )
122+ require .False (t , found , "failed prefetch must not cache malformed logs" )
123+ }
124+
125+ sub := monitor .Subscribe ("TestMonitorRecoversInvalidPrefetchedLogs" )
126+ defer sub .Unsubscribe ()
127+ ctx , cancel := context .WithCancel (context .Background ())
128+ done := make (chan error , 1 )
129+ go func () { done <- monitor .Run (ctx ) }()
130+ defer func () {
131+ cancel ()
132+ select {
133+ case err := <- done :
134+ require .NoError (t , err )
135+ case <- time .After (5 * time .Second ):
136+ t .Error ("monitor did not stop" )
137+ }
138+ }()
139+
140+ timer := time .NewTimer (3 * time .Second )
141+ defer timer .Stop ()
142+ next := first
143+ for next <= last {
144+ select {
145+ case blocks := <- sub .Blocks ():
146+ for _ , block := range blocks {
147+ require .Equal (t , Added , block .Event )
148+ require .True (t , block .OK )
149+ require .Equal (t , next , block .NumberU64 (), "blocks must publish in order" )
150+ if next == target {
151+ require .Equal (t , expectedLogs , block .Logs )
152+ }
153+ next ++
154+ }
155+ case <- timer .C :
156+ t .Fatalf ("publication stalled at block %d; origin calls: %d" , next , originCalls .Load ())
157+ }
158+ }
159+ if ! cached {
160+ require .GreaterOrEqual (t , originCalls .Load (), int64 (2 ), "malformed prefetch must allow an origin retry" )
161+ } else {
162+ require .Positive (t , originCalls .Load (), "invalid cache entry must allow an origin retry" )
163+ }
164+ stored , found , err := monitor .cache .Get (context .Background (), key )
165+ require .NoError (t , err )
166+ require .True (t , found )
167+ require .JSONEq (t , string (validPayload ), string (stored ))
168+ })
169+ }
170+ }
171+ }
172+
38173func TestPrefetchPanicStopsWorkers (t * testing.T ) {
39174 provider := mocks .NewMockRawInterface (gomock .NewController (t ))
40175 provider .EXPECT ().BlockNumber (gomock .Any ()).DoAndReturn (func (context.Context ) (uint64 , error ) {
0 commit comments