Skip to content

Commit ff9a253

Browse files
committed
feat(evmreader): add a fast inputs sync
Previously the node had to scan the blockchain logs from input box deployment up to the latest block to find all inputs of a new application. This new feature implements an optimization that skips this scanning when the application has no inputs and does a binary search to find the exact blocks where the inputs happened. The objective is to reduce provider usage and reduce the time it takes to bring new applications online. On the implementation side, we've added a binary search utility function that caches the partial search ranges from one input to the next to further reduce provider usage instead of multiple individual searches. The function accepts any f, x, count such that f(x) -> count. MBSearch finds the xs where count has changed. Concretely, x is a block number, count is the number of inputs and f is GetNumberOfInputs.
1 parent 85ee681 commit ff9a253

9 files changed

Lines changed: 454 additions & 16 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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ type ApplicationContractAdapter interface {
5757
RetrieveOutputExecutionEvents(
5858
opts *bind.FilterOpts,
5959
) ([]*iapplication.IApplicationOutputExecuted, error)
60+
61+
GetDeploymentBlockNumber(opts *bind.CallOpts) (*big.Int, error)
6062
}
6163

6264
// Interface for Input reading
@@ -65,6 +67,8 @@ type InputSourceAdapter interface {
6567
// by go-ethereum and cannot be used for testing
6668
RetrieveInputs(opts *bind.FilterOpts, appAddresses []common.Address, index []*big.Int,
6769
) ([]iinputbox.IInputBoxInputAdded, error)
70+
71+
GetNumberOfInputs(opts *bind.CallOpts, appContract common.Address) (*big.Int, error)
6872
}
6973

7074
type SubscriptionError struct {

internal/evmreader/evmreader_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,11 @@ func newMockInputBox() *MockInputBox {
360360
return inputSource
361361
}
362362

363+
func (m *MockInputBox) GetNumberOfInputs(opts *bind.CallOpts, appContract common.Address) (*big.Int, error) {
364+
args := m.Called(opts, appContract)
365+
return args.Get(0).(*big.Int), args.Error(1)
366+
}
367+
363368
func (m *MockInputBox) Unset(methodName string) {
364369
for _, call := range m.ExpectedCalls {
365370
if call.Method == methodName {
@@ -560,6 +565,11 @@ func (m *MockApplicationContract) Unset(methodName string) {
560565
}
561566
}
562567

568+
func (m *MockApplicationContract) GetDeploymentBlockNumber(opts *bind.CallOpts) (*big.Int, error) {
569+
args := m.Called(opts)
570+
return args.Get(0).(*big.Int), args.Error(1)
571+
}
572+
563573
func (m *MockApplicationContract) RetrieveOutputExecutionEvents(
564574
opts *bind.FilterOpts,
565575
) ([]*iapplication.IApplicationOutputExecuted, error) {

internal/evmreader/input.go

Lines changed: 173 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,152 @@ import (
77
"context"
88
"errors"
99
"fmt"
10+
"math/big"
11+
"sort"
1012

1113
. "github.com/cartesi/rollups-node/internal/model"
1214
"github.com/ethereum/go-ethereum/accounts/abi/bind"
1315
"github.com/ethereum/go-ethereum/common"
1416
)
1517

18+
// readInputsOnMachingBlocks fetches the inputAdded events from matching the blocks
19+
// on the blockchain they appear instead of searching for a range.
20+
func (r *Service) readInputsOnMachingBlocks(
21+
ctx context.Context,
22+
app *appContracts,
23+
blocks []uint64,
24+
inputSource InputSourceAdapter,
25+
) ([]*Input, error) {
26+
inputs := []*Input{}
27+
28+
for i, block := range blocks {
29+
if (i > 0) && (blocks[i-1] == block) { // skip repetitions
30+
continue
31+
}
32+
opts := bind.FilterOpts{
33+
Context: ctx,
34+
Start: block,
35+
End: &block,
36+
}
37+
inputsEvents, err := inputSource.RetrieveInputs(
38+
&opts,
39+
[]common.Address{app.application.IApplicationAddress},
40+
nil,
41+
)
42+
if err != nil {
43+
return nil, fmt.Errorf("failed to retrieve inputs: %w", err)
44+
}
45+
46+
// NOTE: there may be more than one input in the same block
47+
for _, event := range inputsEvents {
48+
r.Logger.Debug("Received input",
49+
"address", event.AppContract,
50+
"index", event.Index,
51+
"block", event.Raw.BlockNumber)
52+
input := &Input{
53+
Index: event.Index.Uint64(),
54+
Status: InputCompletionStatus_None,
55+
RawData: event.Input,
56+
BlockNumber: event.Raw.BlockNumber,
57+
TransactionReference: common.BigToHash(event.Index),
58+
}
59+
inputs = append(inputs, input)
60+
}
61+
}
62+
63+
sort.Slice(inputs, func(i, j int) bool {
64+
return inputs[i].Index < inputs[j].Index
65+
})
66+
return inputs, nil
67+
}
68+
69+
// fastSyncInputs finds inputs via getNumberOfInputs instead of reading the logs
70+
// this is cheaper when the logs span many blocks, especially when the application
71+
// has no inputs. In case there are inputs, search for the block in which they
72+
// appear with a binary search.
73+
func (r *Service) fastSyncInputs(
74+
ctx context.Context,
75+
lastProcessedBlock uint64,
76+
mostRecentBlockNumber uint64,
77+
app *appContracts,
78+
) ([]*Input, error) {
79+
r.Logger.Debug("Fast sync inputs",
80+
"application", app.application.Name,
81+
)
82+
83+
getNumberOfInputs := func(nr uint64) (uint64, error) {
84+
n, err := app.inputSource.GetNumberOfInputs(&bind.CallOpts{
85+
BlockNumber: new(big.Int).SetUint64(nr),
86+
}, app.application.IApplicationAddress)
87+
if err != nil {
88+
return 0, fmt.Errorf("call to GetNumberOfInputs failed: %w", err)
89+
}
90+
return n.Uint64(), nil
91+
}
92+
93+
noi, err := getNumberOfInputs(mostRecentBlockNumber)
94+
if err != nil {
95+
r.Logger.Debug("Fast sync failed, application will do a regular sync.",
96+
"application", app.application.IApplicationAddress,
97+
"error", err,
98+
)
99+
return nil, err
100+
}
101+
102+
// application has no inputs, sync is done
103+
if noi == 0 {
104+
r.Logger.Info("No inputs, fast sync done",
105+
"application", app.application.Name,
106+
)
107+
return nil, nil
108+
}
109+
110+
// application has inputs, find their blocks and read them in
111+
deployedAtBig, err := app.applicationContract.GetDeploymentBlockNumber(&bind.CallOpts{
112+
BlockNumber: new(big.Int).SetUint64(mostRecentBlockNumber),
113+
})
114+
if err != nil {
115+
r.Logger.Info("Fast sync failed, application will do a regular sync.",
116+
"application", app.application.IApplicationAddress,
117+
"error", err,
118+
)
119+
return nil, err
120+
}
121+
122+
// assume that most inputs happen after this application deployment.
123+
// Do the first range split according to it. Fallback to searching the whole
124+
// range if the assumption is false.
125+
applicationDeploymentBlock := deployedAtBig.Uint64()
126+
noiOld, err := getNumberOfInputs(applicationDeploymentBlock)
127+
if err != nil {
128+
r.Logger.Debug("Fast sync failed, application will do a regular sync.",
129+
"application", app.application.IApplicationAddress,
130+
"error", err,
131+
)
132+
return nil, err
133+
}
134+
135+
startSearchBlock := applicationDeploymentBlock
136+
endSearchBlock := mostRecentBlockNumber
137+
if noiOld == 0 { // all inputs are newer than application deployment
138+
// use current values
139+
} else if noi == noiOld { // all inputs are older than application deployment
140+
startSearchBlock = app.application.IInputBoxBlock
141+
endSearchBlock = applicationDeploymentBlock
142+
} else { // there are both old and new inputs
143+
startSearchBlock = app.application.IInputBoxBlock
144+
}
145+
146+
inputsBlockNumbers, err := MBSearch(startSearchBlock, endSearchBlock, noi, getNumberOfInputs)
147+
r.Logger.Info("Fast sync found inputs on the following blocks",
148+
"application", app.application.Name,
149+
"inputBlockNumbers", inputsBlockNumbers,
150+
)
151+
152+
inputs, err := r.readInputsOnMachingBlocks(ctx, app, inputsBlockNumbers, app.inputSource)
153+
return inputs, err
154+
}
155+
16156
// checkForNewInputs checks if is there new Inputs for all running Applications
17157
func (r *Service) checkForNewInputs(
18158
ctx context.Context,
@@ -44,15 +184,7 @@ func (r *Service) checkForNewInputs(
44184
for lastProcessedBlock, apps := range appsByLastInputCheckBlock {
45185
appAddresses := appsToAddresses(apps)
46186

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-
54187
if mostRecentBlockNumber > lastProcessedBlock {
55-
56188
r.Logger.Debug("Checking inputs for applications",
57189
"apps", appAddresses,
58190
"last_processed_block", lastProcessedBlock,
@@ -107,12 +239,39 @@ func (r *Service) readAndStoreInputs(
107239

108240
// Retrieve Inputs from blockchain
109241
nextSearchBlock := lastProcessedBlock + 1
110-
appInputsMap, err := r.readInputsFromBlockchain(ctx, apps, nextSearchBlock, mostRecentBlockNumber)
111-
if err != nil {
112-
return fmt.Errorf("failed to read inputs from block %v to block %v. %w",
113-
nextSearchBlock,
114-
mostRecentBlockNumber,
115-
err)
242+
var appInputsMap = make(map[common.Address][]*Input)
243+
244+
// try to fast sync
245+
if lastProcessedBlock == 0 {
246+
for _, app := range apps {
247+
inputs, err := r.fastSyncInputs(ctx, lastProcessedBlock, mostRecentBlockNumber, &app)
248+
if err != nil {
249+
return fmt.Errorf("failed to read inputs of application %v: %w",
250+
app.application.IApplicationAddress,
251+
err,
252+
)
253+
}
254+
appInputsMap[app.application.IApplicationAddress] = inputs
255+
}
256+
lastProcessedBlock = mostRecentBlockNumber
257+
nextSearchBlock = mostRecentBlockNumber + 1
258+
} else {
259+
var err error
260+
// Only check blocks starting from the block where the InputBox
261+
// contract was deployed as Inputs can be added to that same block
262+
inputBoxDeploymentBlock := apps[0].application.IInputBoxBlock
263+
if lastProcessedBlock < inputBoxDeploymentBlock {
264+
lastProcessedBlock = inputBoxDeploymentBlock - 1
265+
}
266+
nextSearchBlock = lastProcessedBlock + 1 // update because we changed lastProcessedBlock
267+
268+
appInputsMap, err = r.readInputsFromBlockchain(ctx, apps, nextSearchBlock, mostRecentBlockNumber)
269+
if err != nil {
270+
return fmt.Errorf("failed to read inputs from block %v to block %v. %w",
271+
nextSearchBlock,
272+
mostRecentBlockNumber,
273+
err)
274+
}
116275
}
117276

118277
addrToApp := mapAddressToApp(apps)

0 commit comments

Comments
 (0)