Skip to content

Commit bba7722

Browse files
committed
feat(evm-reader): Read output execution statuses
1 parent 43932d3 commit bba7722

14 files changed

+1022
-129
lines changed
+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// (c) Cartesi and individual authors (see AUTHORS)
2+
// SPDX-License-Identifier: Apache-2.0 (see LICENSE)
3+
4+
package evmreader
5+
6+
import (
7+
appcontract "github.com/cartesi/rollups-node/pkg/contracts/application"
8+
"github.com/ethereum/go-ethereum/accounts/abi/bind"
9+
"github.com/ethereum/go-ethereum/common"
10+
"github.com/ethereum/go-ethereum/ethclient"
11+
)
12+
13+
// IConsensus Wrapper
14+
type ApplicationContractAdapter struct {
15+
application *appcontract.Application
16+
}
17+
18+
func NewApplicationContractAdapter(
19+
appAddress common.Address,
20+
client *ethclient.Client,
21+
) (*ApplicationContractAdapter, error) {
22+
applicationContract, err := appcontract.NewApplication(appAddress, client)
23+
if err != nil {
24+
return nil, err
25+
}
26+
return &ApplicationContractAdapter{
27+
application: applicationContract,
28+
}, nil
29+
}
30+
31+
func (a *ApplicationContractAdapter) GetConsensus(opts *bind.CallOpts) (common.Address, error) {
32+
return a.application.GetConsensus(opts)
33+
}
34+
35+
func (a *ApplicationContractAdapter) RetrieveOutputExecutionEvents(
36+
opts *bind.FilterOpts,
37+
) ([]*appcontract.ApplicationOutputExecuted, error) {
38+
39+
itr, err := a.application.FilterOutputExecuted(opts)
40+
if err != nil {
41+
return nil, err
42+
}
43+
defer itr.Close()
44+
45+
var events []*appcontract.ApplicationOutputExecuted
46+
for itr.Next() {
47+
outputExecutedEvent := itr.Event
48+
events = append(events, outputExecutedEvent)
49+
}
50+
if err = itr.Error(); err != nil {
51+
return nil, err
52+
}
53+
return events, nil
54+
}

internal/evmreader/evmreader.go

+14-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"math/big"
1212

1313
. "github.com/cartesi/rollups-node/internal/node/model"
14+
appcontract "github.com/cartesi/rollups-node/pkg/contracts/application"
1415
"github.com/cartesi/rollups-node/pkg/contracts/iconsensus"
1516
"github.com/cartesi/rollups-node/pkg/contracts/inputbox"
1617
"github.com/ethereum/go-ethereum"
@@ -47,6 +48,12 @@ type EvmReaderRepository interface {
4748
claims []*Epoch,
4849
mostRecentBlockNumber uint64,
4950
) error
51+
GetOutput(
52+
ctx context.Context, indexKey uint64, appAddressKey Address,
53+
) (*Output, error)
54+
UpdateOutputExecutionTransaction(
55+
ctx context.Context, app Address, executedOutputs []*Output, blockNumber uint64,
56+
) error
5057
}
5158

5259
// EthClient mimics part of ethclient.Client functions to narrow down the
@@ -71,6 +78,9 @@ type ConsensusContract interface {
7178

7279
type ApplicationContract interface {
7380
GetConsensus(opts *bind.CallOpts) (Address, error)
81+
RetrieveOutputExecutionEvents(
82+
opts *bind.FilterOpts,
83+
) ([]*appcontract.ApplicationOutputExecuted, error)
7484
}
7585

7686
type ContractFactory interface {
@@ -93,7 +103,8 @@ type application struct {
93103
consensusContract ConsensusContract
94104
}
95105

96-
// EvmReader reads Input Added and Claim Submitted events from the blockchain
106+
// EvmReader reads Input Added, Claim Submitted and
107+
// Output Executed events from the blockchain
97108
type EvmReader struct {
98109
client EthClient
99110
wsClient EthWsClient
@@ -213,6 +224,8 @@ func (r *EvmReader) watchForNewBlocks(ctx context.Context, ready chan<- struct{}
213224

214225
r.checkForClaimStatus(ctx, apps, mostRecentBlockNumber)
215226

227+
r.checkForOutputExecution(ctx, apps, mostRecentBlockNumber)
228+
216229
}
217230
}
218231
}

internal/evmreader/evmreader_test.go

+51
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"time"
1414

1515
. "github.com/cartesi/rollups-node/internal/node/model"
16+
appcontract "github.com/cartesi/rollups-node/pkg/contracts/application"
1617
"github.com/cartesi/rollups-node/pkg/contracts/iconsensus"
1718
"github.com/cartesi/rollups-node/pkg/contracts/inputbox"
1819
"github.com/ethereum/go-ethereum"
@@ -449,6 +450,28 @@ func newMockRepository() *MockRepository {
449450
mock.Anything,
450451
).Return(nil)
451452

453+
repo.On("UpdateOutputExecutionTransaction",
454+
mock.Anything,
455+
mock.Anything,
456+
mock.Anything,
457+
mock.Anything).Return(nil)
458+
459+
outputHash := common.HexToHash("0xAABBCCDDEE")
460+
repo.On("GetOutput",
461+
mock.Anything,
462+
0,
463+
common.HexToAddress("0x2E663fe9aE92275242406A185AA4fC8174339D3E")).Return(
464+
&Output{
465+
Id: 1,
466+
Index: 0,
467+
RawData: common.Hex2Bytes("0xdeadbeef"),
468+
Hash: &outputHash,
469+
InputId: 1,
470+
OutputHashesSiblings: nil,
471+
TransactionHash: nil,
472+
},
473+
)
474+
452475
return repo
453476

454477
}
@@ -617,6 +640,24 @@ func (m *MockRepository) UpdateEpochs(ctx context.Context,
617640
return args.Error(0)
618641
}
619642

643+
func (m *MockRepository) GetOutput(
644+
ctx context.Context, indexKey uint64, appAddressKey Address,
645+
) (*Output, error) {
646+
args := m.Called(ctx, indexKey, appAddressKey)
647+
obj := args.Get(0)
648+
if obj == nil {
649+
return nil, args.Error(1)
650+
}
651+
return obj.(*Output), args.Error(1)
652+
}
653+
654+
func (m *MockRepository) UpdateOutputExecutionTransaction(
655+
ctx context.Context, app Address, executedOutputs []*Output, blockNumber uint64,
656+
) error {
657+
args := m.Called(ctx, app, executedOutputs, blockNumber)
658+
return args.Error(0)
659+
}
660+
620661
type MockApplicationContract struct {
621662
mock.Mock
622663
}
@@ -636,6 +677,13 @@ func (m *MockApplicationContract) GetConsensus(
636677
return args.Get(0).(common.Address), args.Error(1)
637678
}
638679

680+
func (m *MockApplicationContract) RetrieveOutputExecutionEvents(
681+
opts *bind.FilterOpts,
682+
) ([]*appcontract.ApplicationOutputExecuted, error) {
683+
args := m.Called(opts)
684+
return args.Get(0).([]*appcontract.ApplicationOutputExecuted), args.Error(1)
685+
}
686+
639687
type MockIConsensusContract struct {
640688
mock.Mock
641689
}
@@ -688,6 +736,9 @@ func newEmvReaderContractFactory() *MockEvmReaderContractFactory {
688736
mock.Anything,
689737
).Return(common.HexToAddress("0xdeadbeef"), nil)
690738

739+
applicationContract.On("RetrieveOutputExecutionEvents",
740+
mock.Anything).Return([]*appcontract.ApplicationOutputExecuted{}, nil)
741+
691742
consensusContract := &MockIConsensusContract{}
692743

693744
consensusContract.On("GetEpochLength",

internal/evmreader/input_test.go

-78
Original file line numberDiff line numberDiff line change
@@ -121,84 +121,6 @@ func (s *EvmReaderSuite) TestItReadsInputsFromNewBlocks() {
121121
)
122122
}
123123

124-
func (s *EvmReaderSuite) TestItReadsInputsFromNewBlocksWrongIConsensus() {
125-
126-
wsClient := FakeWSEhtClient{}
127-
128-
evmReader := NewEvmReader(
129-
s.client,
130-
&wsClient,
131-
s.inputBox,
132-
s.repository,
133-
0x10,
134-
DefaultBlockStatusLatest,
135-
s.contractFactory,
136-
)
137-
138-
// Prepare repository
139-
s.repository.Unset("GetAllRunningApplications")
140-
s.repository.On(
141-
"GetAllRunningApplications",
142-
mock.Anything,
143-
).Return([]Application{{
144-
ContractAddress: common.HexToAddress("0x2E663fe9aE92275242406A185AA4fC8174339D3E"),
145-
IConsensusAddress: common.HexToAddress("0xFFFFFFFF"),
146-
LastProcessedBlock: 0x00,
147-
}}, nil).Once()
148-
s.repository.On(
149-
"GetAllRunningApplications",
150-
mock.Anything,
151-
).Return([]Application{{
152-
ContractAddress: common.HexToAddress("0x2E663fe9aE92275242406A185AA4fC8174339D3E"),
153-
IConsensusAddress: common.HexToAddress("0xFFFFFFFF"),
154-
LastProcessedBlock: 0x11,
155-
}}, nil).Once()
156-
157-
// Prepare Client
158-
s.client.Unset("HeaderByNumber")
159-
s.client.On(
160-
"HeaderByNumber",
161-
mock.Anything,
162-
mock.Anything,
163-
).Return(&header0, nil).Once()
164-
s.client.On(
165-
"HeaderByNumber",
166-
mock.Anything,
167-
mock.Anything,
168-
).Return(&header1, nil).Once()
169-
s.client.On(
170-
"HeaderByNumber",
171-
mock.Anything,
172-
mock.Anything,
173-
).Return(&header2, nil).Once()
174-
175-
// Start service
176-
ready := make(chan struct{}, 1)
177-
errChannel := make(chan error, 1)
178-
179-
go func() {
180-
errChannel <- evmReader.Run(s.ctx, ready)
181-
}()
182-
183-
select {
184-
case <-ready:
185-
break
186-
case err := <-errChannel:
187-
s.FailNow("unexpected error signal", err)
188-
}
189-
190-
wsClient.fireNewHead(&header0)
191-
wsClient.fireNewHead(&header1)
192-
time.Sleep(time.Second)
193-
194-
s.inputBox.AssertNumberOfCalls(s.T(), "RetrieveInputs", 0)
195-
s.repository.AssertNumberOfCalls(
196-
s.T(),
197-
"StoreEpochAndInputsTransaction",
198-
0,
199-
)
200-
}
201-
202124
func (s *EvmReaderSuite) TestItUpdatesLastProcessedBlockWhenThereIsNoInputs() {
203125

204126
wsClient := FakeWSEhtClient{}

internal/evmreader/output.go

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
// (c) Cartesi and individual authors (see AUTHORS)
2+
// SPDX-License-Identifier: Apache-2.0 (see LICENSE)
3+
4+
package evmreader
5+
6+
import (
7+
"bytes"
8+
"context"
9+
"log/slog"
10+
11+
. "github.com/cartesi/rollups-node/internal/node/model"
12+
"github.com/ethereum/go-ethereum/accounts/abi/bind"
13+
)
14+
15+
func (r *EvmReader) checkForOutputExecution(
16+
ctx context.Context,
17+
apps []application,
18+
mostRecentBlockNumber uint64,
19+
) {
20+
21+
appAddresses := appsToAddresses(apps)
22+
23+
slog.Debug("Checking for new Output Executed Events", "apps", appAddresses)
24+
25+
for _, app := range apps {
26+
27+
LastOutputCheck := app.LastOutputCheckBlock
28+
29+
// Safeguard: Only check blocks starting from the block where the InputBox
30+
// contract was deployed as Inputs can be added to that same block
31+
if LastOutputCheck < r.inputBoxDeploymentBlock {
32+
LastOutputCheck = r.inputBoxDeploymentBlock
33+
}
34+
35+
if mostRecentBlockNumber > LastOutputCheck {
36+
37+
slog.Info("Checking output execution for application",
38+
"app", app.ContractAddress,
39+
"last output check block", LastOutputCheck,
40+
"most recent block", mostRecentBlockNumber)
41+
42+
r.readAndUpdateOutputs(ctx, app, LastOutputCheck, mostRecentBlockNumber)
43+
44+
} else if mostRecentBlockNumber < LastOutputCheck {
45+
slog.Warn(
46+
"Not reading output execution: most recent block is lower than the last processed one", //nolint:lll
47+
"app", app.ContractAddress,
48+
"last output check block", LastOutputCheck,
49+
"most recent block", mostRecentBlockNumber,
50+
)
51+
} else {
52+
slog.Info("Not reading output execution: already checked the most recent blocks",
53+
"app", app.ContractAddress,
54+
"last output check block", LastOutputCheck,
55+
"most recent block", mostRecentBlockNumber,
56+
)
57+
}
58+
}
59+
60+
}
61+
62+
func (r *EvmReader) readAndUpdateOutputs(
63+
ctx context.Context, app application, lastOutputCheck, mostRecentBlockNumber uint64) {
64+
65+
contract := app.applicationContract
66+
67+
opts := &bind.FilterOpts{
68+
Start: lastOutputCheck + 1,
69+
End: &mostRecentBlockNumber,
70+
}
71+
72+
outputExecutedEvents, err := contract.RetrieveOutputExecutionEvents(opts)
73+
if err != nil {
74+
slog.Error("Error reading output events", "app", app.ContractAddress, "error", err)
75+
return
76+
}
77+
78+
// Should we check the output hash??
79+
var executedOutputs []*Output
80+
for _, event := range outputExecutedEvents {
81+
82+
// Compare output to check it is the correct one
83+
output, err := r.repository.GetOutput(ctx, event.OutputIndex, app.ContractAddress)
84+
if err != nil {
85+
slog.Error("Error retrieving output",
86+
"app", app.ContractAddress, "index", event.OutputIndex, "error", err)
87+
return
88+
}
89+
90+
if !bytes.Equal(output.RawData, event.Output) {
91+
slog.Debug("Output mismatch",
92+
"app", app.ContractAddress, "index", event.OutputIndex,
93+
"actual", output.RawData, "event's", event.Output)
94+
95+
slog.Error("Output mismatch. Application is in an invalid state",
96+
"app", app.ContractAddress,
97+
"index", event.OutputIndex)
98+
99+
return
100+
}
101+
102+
slog.Info("Output executed", "app", app.ContractAddress, "index", event.OutputIndex)
103+
output.TransactionHash = &event.Raw.TxHash
104+
executedOutputs = append(executedOutputs, output)
105+
}
106+
107+
err = r.repository.UpdateOutputExecutionTransaction(
108+
ctx, app.ContractAddress, executedOutputs, mostRecentBlockNumber)
109+
if err != nil {
110+
slog.Error("Error storing output execution statuses", "app", app, "error", err)
111+
}
112+
113+
}

0 commit comments

Comments
 (0)