Skip to content

Commit e59b130

Browse files
committed
refactor(evmreader): consolidate input fetch into unified fetchInputs
1 parent c197f76 commit e59b130

7 files changed

Lines changed: 288 additions & 219 deletions

File tree

internal/evmreader/edge_cases_test.go

Lines changed: 133 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package evmreader
66
import (
77
"context"
88
"errors"
9+
"math"
910
"math/big"
1011
"time"
1112

@@ -58,7 +59,8 @@ func (s *EvmReaderSuite) TestFetchApplicationInputsDuplicateAndOutOfOrderEvents(
5859
makeInputEvent(addr, 1, 100), // duplicate
5960
}, nil)
6061

61-
inputs, err := s.evmReader.fetchApplicationInputs(s.ctx, app, 10, 200)
62+
prevValue := new(big.Int).SetUint64(0) // repo returns 0 inputs
63+
inputs, err := s.evmReader.fetchInputs(s.ctx, app, 10, 200, prevValue, 0, math.MaxUint64)
6264
s.Require().NoError(err)
6365

6466
// 3 unique inputs, sorted by index
@@ -68,6 +70,130 @@ func (s *EvmReaderSuite) TestFetchApplicationInputsDuplicateAndOutOfOrderEvents(
6870
s.Require().Equal(uint64(2), inputs[2].Index)
6971
}
7072

73+
// --- Bounds filtering: inputs outside [lowerBound, upperBound) are excluded ---
74+
// When RetrieveInputs returns events with indices outside the requested bounds,
75+
// fetchInputs must exclude them. This exercises the bounds filtering added in the
76+
// unified fetchInputs (C2 consolidation).
77+
func (s *EvmReaderSuite) TestFetchInputsBoundsFiltering() {
78+
addr := common.HexToAddress("0x3333333333333333333333333333333333333333")
79+
80+
inputSrc := &MockInputBox{}
81+
app := appContracts{
82+
application: &Application{
83+
Name: "test-app",
84+
IApplicationAddress: addr,
85+
IInputBoxAddress: inputBoxAddr,
86+
IInputBoxBlock: 10,
87+
},
88+
inputSource: inputSrc,
89+
}
90+
91+
// On-chain: 0 inputs before block 100, 5 from block 100
92+
inputSrc.On("GetNumberOfInputs", blockRange(0, 100), mock.Anything).
93+
Return(new(big.Int).SetUint64(0), nil)
94+
inputSrc.On("GetNumberOfInputs", blockFrom(100), mock.Anything).
95+
Return(new(big.Int).SetUint64(5), nil)
96+
97+
// RetrieveInputs returns indices 0..4, but we only want [2, 4)
98+
inputSrc.On("RetrieveInputs",
99+
mock.MatchedBy(func(opts *bind.FilterOpts) bool { return opts.Start == 100 }),
100+
mock.Anything, mock.Anything,
101+
).Return([]iinputbox.IInputBoxInputAdded{
102+
makeInputEvent(addr, 0, 100), // below lowerBound → excluded
103+
makeInputEvent(addr, 1, 100), // below lowerBound → excluded
104+
makeInputEvent(addr, 2, 100), // in bounds → included
105+
makeInputEvent(addr, 3, 100), // in bounds → included
106+
makeInputEvent(addr, 4, 100), // >= upperBound → excluded
107+
}, nil)
108+
109+
prevValue := new(big.Int).SetUint64(0)
110+
inputs, err := s.evmReader.fetchInputs(s.ctx, app, 10, 200, prevValue, 2, 4)
111+
s.Require().NoError(err)
112+
113+
// Only indices 2 and 3 should be included
114+
s.Require().Len(inputs, 2)
115+
s.Require().Equal(uint64(2), inputs[0].Index)
116+
s.Require().Equal(uint64(3), inputs[1].Index)
117+
}
118+
119+
// --- Bounds filtering: upperBound == lowerBound yields zero inputs ---
120+
// When lowerBound == upperBound, the half-open range [lb, ub) is empty,
121+
// so no inputs should be returned even if the chain has matching events.
122+
func (s *EvmReaderSuite) TestFetchInputsEmptyBoundsRange() {
123+
addr := common.HexToAddress("0x3333333333333333333333333333333333333333")
124+
125+
inputSrc := &MockInputBox{}
126+
app := appContracts{
127+
application: &Application{
128+
Name: "test-app",
129+
IApplicationAddress: addr,
130+
IInputBoxAddress: inputBoxAddr,
131+
IInputBoxBlock: 10,
132+
},
133+
inputSource: inputSrc,
134+
}
135+
136+
// On-chain: 0 inputs before block 100, 2 from block 100
137+
inputSrc.On("GetNumberOfInputs", blockRange(0, 100), mock.Anything).
138+
Return(new(big.Int).SetUint64(0), nil)
139+
inputSrc.On("GetNumberOfInputs", blockFrom(100), mock.Anything).
140+
Return(new(big.Int).SetUint64(2), nil)
141+
142+
inputSrc.On("RetrieveInputs",
143+
mock.MatchedBy(func(opts *bind.FilterOpts) bool { return opts.Start == 100 }),
144+
mock.Anything, mock.Anything,
145+
).Return([]iinputbox.IInputBoxInputAdded{
146+
makeInputEvent(addr, 0, 100),
147+
makeInputEvent(addr, 1, 100),
148+
}, nil)
149+
150+
// lowerBound == upperBound == 5 → empty range
151+
prevValue := new(big.Int).SetUint64(0)
152+
inputs, err := s.evmReader.fetchInputs(s.ctx, app, 10, 200, prevValue, 5, 5)
153+
s.Require().NoError(err)
154+
s.Require().Empty(inputs)
155+
}
156+
157+
// --- Bounds filtering: boundary-exact inclusion/exclusion ---
158+
// Verifies the half-open [lowerBound, upperBound) semantics precisely:
159+
// lowerBound is inclusive, upperBound is exclusive.
160+
func (s *EvmReaderSuite) TestFetchInputsBoundaryExactness() {
161+
addr := common.HexToAddress("0x3333333333333333333333333333333333333333")
162+
163+
inputSrc := &MockInputBox{}
164+
app := appContracts{
165+
application: &Application{
166+
Name: "test-app",
167+
IApplicationAddress: addr,
168+
IInputBoxAddress: inputBoxAddr,
169+
IInputBoxBlock: 10,
170+
},
171+
inputSource: inputSrc,
172+
}
173+
174+
// On-chain: 0 inputs before block 100, 3 from block 100
175+
inputSrc.On("GetNumberOfInputs", blockRange(0, 100), mock.Anything).
176+
Return(new(big.Int).SetUint64(0), nil)
177+
inputSrc.On("GetNumberOfInputs", blockFrom(100), mock.Anything).
178+
Return(new(big.Int).SetUint64(3), nil)
179+
180+
inputSrc.On("RetrieveInputs",
181+
mock.MatchedBy(func(opts *bind.FilterOpts) bool { return opts.Start == 100 }),
182+
mock.Anything, mock.Anything,
183+
).Return([]iinputbox.IInputBoxInputAdded{
184+
makeInputEvent(addr, 0, 100),
185+
makeInputEvent(addr, 1, 100),
186+
makeInputEvent(addr, 2, 100),
187+
}, nil)
188+
189+
// Range [1, 2): only index 1 should be included
190+
prevValue := new(big.Int).SetUint64(0)
191+
inputs, err := s.evmReader.fetchInputs(s.ctx, app, 10, 200, prevValue, 1, 2)
192+
s.Require().NoError(err)
193+
s.Require().Len(inputs, 1)
194+
s.Require().Equal(uint64(1), inputs[0].Index)
195+
}
196+
71197
// --- #18: Adversarial EpochSealed data (LowerBound > UpperBound) ---
72198
// When a sealed event has InputIndexLowerBound > InputIndexUpperBound,
73199
// the code skips input fetching and stores the epoch without crashing.
@@ -139,7 +265,7 @@ func (s *SealedEpochsSuite) TestSealedEpochLowerBoundGreaterThanUpperBound() {
139265
}
140266

141267
// --- #10: RetrieveInputs failure at a specific block ---
142-
// When RetrieveInputs fails during fetchSealedEpochInputs, the error must
268+
// When RetrieveInputs fails during fetchInputs, the error must
143269
// propagate through FindTransitions back to the caller.
144270
func (s *SealedEpochsSuite) TestFetchSealedEpochInputsRetrieveFailure() {
145271
app := appContracts{
@@ -173,7 +299,11 @@ func (s *SealedEpochsSuite) TestFetchSealedEpochInputsRetrieveFailure() {
173299
mock.Anything, mock.Anything,
174300
).Return(([]iinputbox.IInputBoxInputAdded)(nil), errors.New("RPC timeout"))
175301

176-
_, err := s.evmReader.fetchSealedEpochInputs(s.ctx, app, epoch)
302+
prevValue := new(big.Int).SetUint64(epoch.InputIndexLowerBound)
303+
_, err := s.evmReader.fetchInputs(s.ctx, app,
304+
epoch.FirstBlock, epoch.LastBlock,
305+
prevValue,
306+
epoch.InputIndexLowerBound, epoch.InputIndexUpperBound)
177307
s.Require().Error(err)
178308
s.Require().ErrorContains(err, "RPC timeout")
179309
s.Require().ErrorContains(err, "failed to walk input transitions")

internal/evmreader/error_paths_test.go

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func (s *EvmReaderSuite) TestCreateEpochsAndInputsErrorDoesNotAdvanceCheckpoint(
7272
}
7373

7474
// --- Priority 2: Sealed epoch input count mismatch → error, no epoch stored ---
75-
// When fetchSealedEpochInputs returns fewer inputs than the sealed event
75+
// When fetchInputs returns fewer inputs than the sealed event
7676
// declares, processSealedEpochEvent must return an error and NOT store the epoch.
7777
func (s *SealedEpochsSuite) TestSealedEpochInputCountMismatchReturnsError() {
7878
const sealBlock uint64 = 200
@@ -384,14 +384,63 @@ func (s *EvmReaderSuite) TestOutputBlockRegressionDoesNotWriteToDb() {
384384
repo.AssertNumberOfCalls(s.T(), "GetNumberOfExecutedOutputs", 0)
385385
}
386386

387+
// --- Input count mismatch in IConsensus path → app skipped, no checkpoint advance ---
388+
// When the on-chain counter delta at endBlock disagrees with the number of inputs
389+
// returned by RetrieveInputs (e.g., missing event), the app must be skipped
390+
// (not stored) and its checkpoint must NOT advance.
391+
func (s *EvmReaderSuite) TestIConsensusInputCountMismatchSkipsApp() {
392+
addr := common.HexToAddress("0x6666666666666666666666666666666666666666")
393+
394+
inputSrc := &MockInputBox{}
395+
app := &Application{
396+
ID: 1,
397+
Name: "test-app",
398+
IApplicationAddress: addr,
399+
IInputBoxAddress: inputBoxAddr,
400+
DataAvailability: DataAvailability_InputBox[:],
401+
EpochLength: 10,
402+
LastInputCheckBlock: 100,
403+
}
404+
405+
// On-chain counter says 2 new inputs, but RetrieveInputs only returns 1
406+
inputSrc.On("GetNumberOfInputs", blockRange(0, 105), mock.Anything).
407+
Return(new(big.Int).SetUint64(0), nil)
408+
inputSrc.On("GetNumberOfInputs", blockFrom(105), mock.Anything).
409+
Return(new(big.Int).SetUint64(2), nil)
410+
411+
// RetrieveInputs at 105 only returns 1 event (missing second input)
412+
inputSrc.On("RetrieveInputs",
413+
mock.MatchedBy(func(opts *bind.FilterOpts) bool { return opts.Start == 105 }),
414+
mock.Anything, mock.Anything,
415+
).Return([]iinputbox.IInputBoxInputAdded{
416+
makeInputEvent(addr, 0, 105),
417+
}, nil)
418+
419+
apps := []appContracts{
420+
{application: app, inputSource: inputSrc},
421+
}
422+
423+
repo := newMockRepository()
424+
repo.On("GetNumberOfInputs", mock.Anything, mock.Anything).
425+
Return(uint64(0), nil)
426+
s.evmReader.repository = repo
427+
428+
err := s.evmReader.readAndStoreInputs(s.ctx, 100, 110, apps)
429+
s.Require().NoError(err) // per-app failure doesn't abort
430+
431+
// App was skipped: counter says 2 new, but only 1 fetched → no DB writes
432+
repo.AssertNumberOfCalls(s.T(), "CreateEpochsAndInputs", 0)
433+
repo.AssertNumberOfCalls(s.T(), "UpdateEventLastCheckBlock", 0)
434+
}
435+
387436
// --- EpochLength=0 sets app inoperable ---
388437
// When an application with EpochLength=0 reaches the epoch indexing logic,
389438
// it must be set inoperable to prevent division-by-zero in calculateEpochIndex.
390439
func (s *EvmReaderSuite) TestEpochLengthZeroSetsAppInoperable() {
391440
addr := common.HexToAddress("0x5555555555555555555555555555555555555555")
392441

393442
inputSrc := &MockInputBox{}
394-
// On-chain: constant 0 inputs (no transitions, fetchApplicationInputs succeeds)
443+
// On-chain: constant 0 inputs (no transitions, fetchInputs succeeds)
395444
inputSrc.On("GetNumberOfInputs", mock.Anything, mock.Anything).
396445
Return(new(big.Int).SetUint64(0), nil)
397446

0 commit comments

Comments
 (0)