Skip to content

Commit cb9e98a

Browse files
committed
feat(evmreader): fast sync application inputs and outputs
This is an optimization to decrease the number of blocks the node has to scan for an new application. By retrieving zero from GetNumberOfInputs call, we can skip scanning the blocks previous to this, thus reducing the total block range. The same strategy is used for outputs.
1 parent 85ee681 commit cb9e98a

8 files changed

Lines changed: 158 additions & 17 deletions

File tree

internal/evmreader/application_adapter.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,7 @@ func (a *ApplicationContractAdapterImpl) RetrieveOutputExecutionEvents(
9595
}
9696
return events, nil
9797
}
98+
99+
func (a *ApplicationContractAdapterImpl) GetDeploymentBlockNumber(opts *bind.CallOpts) (*big.Int, error) {
100+
return a.application.GetDeploymentBlockNumber(opts)
101+
}

internal/evmreader/evmreader.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ type ApplicationContractAdapter interface {
5757
RetrieveOutputExecutionEvents(
5858
opts *bind.FilterOpts,
5959
) ([]*iapplication.IApplicationOutputExecuted, error)
60+
GetDeploymentBlockNumber(opts *bind.CallOpts) (*big.Int, error)
6061
}
6162

6263
// Interface for Input reading
@@ -65,6 +66,7 @@ type InputSourceAdapter interface {
6566
// by go-ethereum and cannot be used for testing
6667
RetrieveInputs(opts *bind.FilterOpts, appAddresses []common.Address, index []*big.Int,
6768
) ([]iinputbox.IInputBoxInputAdded, error)
69+
GetNumberOfInputs(opts *bind.CallOpts, appContract common.Address) (*big.Int, error)
6870
}
6971

7072
type SubscriptionError struct {

internal/evmreader/evmreader_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,11 @@ func (m *MockInputBox) RetrieveInputs(
377377
return args.Get(0).([]iinputbox.IInputBoxInputAdded), args.Error(1)
378378
}
379379

380+
func (m *MockInputBox) GetNumberOfInputs(opts *bind.CallOpts, appContract common.Address) (*big.Int, error) {
381+
args := m.Called(opts, appContract)
382+
return args.Get(0).(*big.Int), args.Error(1)
383+
}
384+
380385
// Mock InputReaderRepository
381386
type MockRepository struct {
382387
mock.Mock
@@ -567,6 +572,11 @@ func (m *MockApplicationContract) RetrieveOutputExecutionEvents(
567572
return args.Get(0).([]*iapplication.IApplicationOutputExecuted), args.Error(1)
568573
}
569574

575+
func (m *MockApplicationContract) GetDeploymentBlockNumber(opts *bind.CallOpts) (*big.Int, error) {
576+
args := m.Called(opts)
577+
return args.Get(0).(*big.Int), args.Error(1)
578+
}
579+
570580
type MockAdapterFactory struct {
571581
mock.Mock
572582
}

internal/evmreader/input.go

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,54 @@ import (
77
"context"
88
"errors"
99
"fmt"
10+
"math/big"
1011

1112
. "github.com/cartesi/rollups-node/internal/model"
1213
"github.com/ethereum/go-ethereum/accounts/abi/bind"
1314
"github.com/ethereum/go-ethereum/common"
1415
)
1516

17+
func findSafeFirstInputBlockToScan(app *appContracts, mostRecentBlockNumber uint64, mostRecentBlockNumberCallOpts *bind.CallOpts) uint64 {
18+
var noiBig *big.Int
19+
var err error
20+
21+
// find if the application has ever received any input. sync to present if not
22+
noiBig, err = app.inputSource.GetNumberOfInputs(
23+
mostRecentBlockNumberCallOpts,
24+
app.application.IApplicationAddress,
25+
)
26+
if err != nil {
27+
return app.application.LastInputCheckBlock
28+
}
29+
if noiBig.Uint64() == 0 {
30+
return mostRecentBlockNumber
31+
}
32+
33+
// find if the application has received an input since its deployment. sync to that block if not
34+
// we'll need its deployment block number to do that
35+
deploymentBlockNumberBig, err := app.applicationContract.GetDeploymentBlockNumber(mostRecentBlockNumberCallOpts)
36+
if err != nil {
37+
return app.application.LastInputCheckBlock
38+
}
39+
40+
noiBig, err = app.inputSource.GetNumberOfInputs(&bind.CallOpts{
41+
BlockNumber: deploymentBlockNumberBig,
42+
},
43+
app.application.IApplicationAddress,
44+
)
45+
if err != nil {
46+
return app.application.LastInputCheckBlock
47+
}
48+
if noiBig.Uint64() == 0 {
49+
return deploymentBlockNumberBig.Uint64()
50+
}
51+
52+
// TODO(mpolitzer): Applicaiton has inputs previous to its deployment. We can reduce the number of blocks to scan by
53+
// doing a binary search over GetNumberOfInputs and finding the block where 0 -> 1 transition happens. As a simpler,
54+
// also correct implementation. We return the first possible block an input could appear on.
55+
return app.application.IInputBoxBlock
56+
}
57+
1658
// checkForNewInputs checks if is there new Inputs for all running Applications
1759
func (r *Service) checkForNewInputs(
1860
ctx context.Context,
@@ -25,6 +67,10 @@ func (r *Service) checkForNewInputs(
2567

2668
r.Logger.Debug("Checking for new inputs")
2769

70+
mostRecentBlockNumberCallOpts := &bind.CallOpts{
71+
BlockNumber: new(big.Int).SetUint64(mostRecentBlockNumber),
72+
}
73+
2874
appsByInputBox := map[common.Address][]appContracts{}
2975
for _, app := range applications {
3076
if !app.application.HasDataAvailabilitySelector(DataAvailability_InputBox) {
@@ -39,18 +85,27 @@ func (r *Service) checkForNewInputs(
3985
"inputbox_address", inputBoxAddress,
4086
"most recent block", mostRecentBlockNumber,
4187
)
42-
appsByLastInputCheckBlock := indexApps(byLastInputCheckBlock, inputBoxApps)
88+
89+
appsByLastInputCheckBlock := make(map[uint64][]appContracts)
90+
for _, app := range inputBoxApps {
91+
lastInputCheckBlock := app.application.LastInputCheckBlock
92+
if lastInputCheckBlock == 0 { // New application. Find a safe start block to scan for inputs
93+
lastInputCheckBlock = findSafeFirstInputBlockToScan(&app,
94+
mostRecentBlockNumber,
95+
mostRecentBlockNumberCallOpts,
96+
) - 1
97+
r.Logger.Info("Fast sync application inputs",
98+
"application", app.application.Name,
99+
"start_block", lastInputCheckBlock,
100+
)
101+
app.application.LastInputCheckBlock = lastInputCheckBlock
102+
}
103+
appsByLastInputCheckBlock[lastInputCheckBlock] = append(appsByLastInputCheckBlock[lastInputCheckBlock], app)
104+
}
43105

44106
for lastProcessedBlock, apps := range appsByLastInputCheckBlock {
45107
appAddresses := appsToAddresses(apps)
46108

47-
// Only check blocks starting from the block where the InputBox
48-
// contract was deployed as Inputs can be added to that same block
49-
inputBoxDeploymentBlock := apps[0].application.IInputBoxBlock
50-
if lastProcessedBlock < inputBoxDeploymentBlock {
51-
lastProcessedBlock = inputBoxDeploymentBlock - 1
52-
}
53-
54109
if mostRecentBlockNumber > lastProcessedBlock {
55110

56111
r.Logger.Debug("Checking inputs for applications",
@@ -340,8 +395,3 @@ func (r *Service) readInputsFromBlockchain(
340395
}
341396
return appInputsMap, nil
342397
}
343-
344-
// byLastInputCheckBlock key extractor function intended to be used with `indexApps` function
345-
func byLastInputCheckBlock(app appContracts) uint64 {
346-
return app.application.LastInputCheckBlock
347-
}

internal/evmreader/input_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package evmreader
55

66
import (
7+
"math/big"
78
"time"
89

910
. "github.com/cartesi/rollups-node/internal/model"
@@ -129,6 +130,24 @@ func (s *EvmReaderSuite) TestItReadsInputsFromNewBlocksFilteredByDA() {
129130
mock.Anything,
130131
).Return(events_1, nil)
131132

133+
inputBox.Unset("GetNumberOfInputs")
134+
inputBox.On(
135+
"GetNumberOfInputs",
136+
mock.Anything,
137+
mock.Anything,
138+
).Return(new(big.Int).SetUint64(2), nil)
139+
140+
applicationContract.On(
141+
"GetDeploymentBlockNumber",
142+
mock.Anything,
143+
).Return(new(big.Int).SetUint64(10), nil)
144+
145+
inputBox.On(
146+
"GetNumberOfInputs",
147+
mock.Anything,
148+
mock.Anything,
149+
).Return(new(big.Int).SetUint64(0), nil)
150+
132151
// Start service
133152
ready := make(chan struct{}, 1)
134153
errChannel := make(chan error, 1)
@@ -267,6 +286,24 @@ func (s *EvmReaderSuite) TestItUpdatesLastInputCheckBlockWhenThereIsNoInputs() {
267286
mock.Anything,
268287
).Return(events_0, nil)
269288

289+
inputBox.Unset("GetNumberOfInputs")
290+
inputBox.On(
291+
"GetNumberOfInputs",
292+
mock.Anything,
293+
mock.Anything,
294+
).Return(new(big.Int).SetUint64(1), nil)
295+
296+
applicationContract.On(
297+
"GetDeploymentBlockNumber",
298+
mock.Anything,
299+
).Return(new(big.Int).SetUint64(10), nil)
300+
301+
inputBox.On(
302+
"GetNumberOfInputs",
303+
mock.Anything,
304+
mock.Anything,
305+
).Return(new(big.Int).SetUint64(0), nil)
306+
270307
events_1 := []iinputbox.IInputBoxInputAdded{}
271308
mostRecentBlockNumber_1 := uint64(0x12)
272309
retrieveInputsOpts_1 := bind.FilterOpts{
@@ -350,6 +387,18 @@ func (s *EvmReaderSuite) TestItReadsMultipleInputsFromSingleNewBlock() {
350387
mock.Anything,
351388
).Return(events_2, nil)
352389

390+
inputBox.Unset("GetNumberOfInputs")
391+
inputBox.On(
392+
"GetNumberOfInputs",
393+
mock.Anything,
394+
mock.Anything,
395+
).Return(new(big.Int).SetUint64(2), nil)
396+
397+
applicationContract.On(
398+
"GetDeploymentBlockNumber",
399+
mock.Anything,
400+
).Return(new(big.Int).SetUint64(10), nil)
401+
353402
// Prepare Repo
354403
s.repository.Unset("ListApplications")
355404
s.repository.On(
@@ -450,6 +499,7 @@ func (s *EvmReaderSuite) TestItStartsWhenLasProcessedBlockIsTheMostRecentBlock()
450499
IInputBoxBlock: 0x10,
451500
EpochLength: 10,
452501
LastInputCheckBlock: 0x13,
502+
LastOutputCheckBlock:0x13,
453503
}}, uint64(1), nil).Once()
454504

455505
s.repository.Unset("UpdateEventLastCheckBlock")

internal/evmreader/inputsource_adapter.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,7 @@ func (i *InputSourceAdapterImpl) RetrieveInputs(
110110
}
111111
return events, nil
112112
}
113+
114+
func (i *InputSourceAdapterImpl) GetNumberOfInputs(opts *bind.CallOpts, addr common.Address) (*big.Int, error) {
115+
return i.inputbox.GetNumberOfInputs(opts, addr)
116+
}

internal/evmreader/output.go

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,22 @@ package evmreader
66
import (
77
"bytes"
88
"context"
9+
"math/big"
910

1011
. "github.com/cartesi/rollups-node/internal/model"
1112
"github.com/ethereum/go-ethereum/accounts/abi/bind"
1213
)
1314

15+
// Find the deployment block number of the application. There can be no OutputExecuted events prior to this.
16+
// Use input box deployment block as a fallback.
17+
func findSafeFirstOutputBlockToScan(app *appContracts, mostRecentBlockNumberCallOpts *bind.CallOpts) uint64 {
18+
deploymentBlockNumberBig, err := app.applicationContract.GetDeploymentBlockNumber(mostRecentBlockNumberCallOpts)
19+
if err != nil {
20+
return app.application.IInputBoxBlock
21+
}
22+
return deploymentBlockNumberBig.Uint64()
23+
}
24+
1425
func (r *Service) checkForOutputExecution(
1526
ctx context.Context,
1627
apps []appContracts,
@@ -21,11 +32,20 @@ func (r *Service) checkForOutputExecution(
2132

2233
r.Logger.Debug("Checking for new Output Executed Events", "apps", appAddresses)
2334

24-
for _, app := range apps {
35+
mostRecentBlockNumberCallOpts := &bind.CallOpts{
36+
BlockNumber: new(big.Int).SetUint64(mostRecentBlockNumber),
37+
}
2538

26-
// Safeguard: Only check blocks starting from the block where the InputBox
27-
// contract was deployed as Inputs can be added to that same block
28-
lastOutputCheck := max(app.application.LastOutputCheckBlock, app.application.IInputBoxBlock)
39+
for _, app := range apps {
40+
lastOutputCheck := app.application.LastOutputCheckBlock
41+
if lastOutputCheck == 0 { // New application. Find a safe start block to scan for outputs
42+
lastOutputCheck = findSafeFirstOutputBlockToScan(&app, mostRecentBlockNumberCallOpts) - 1
43+
r.Logger.Info("Fast sync application outputs",
44+
"application", app.application.Name,
45+
"start_block", lastOutputCheck,
46+
)
47+
app.application.LastOutputCheckBlock = lastOutputCheck
48+
}
2949

3050
if mostRecentBlockNumber > lastOutputCheck {
3151

internal/evmreader/output_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ func (s *EvmReaderSuite) TestOutputExecution() {
3939
DataAvailability: DataAvailability_InputBox[:],
4040
IInputBoxBlock: 0x10,
4141
EpochLength: 10,
42+
LastInputCheckBlock: 0x01, // don't fast sync inputs
4243
LastOutputCheckBlock: 0x10,
4344
}}, uint64(1), nil).Once()
4445
s.repository.On(

0 commit comments

Comments
 (0)