Skip to content

Commit

Permalink
Feature/agg oracle (#23)
Browse files Browse the repository at this point in the history
* implementation completed, missing tests

* WIP

* WIP

* WIP

* sync refactor

* decouple sync processors from EVM

* Add CLI for aggOracle

* WIP

* wip

* WIP

* start reorg detector

* fix docker

* pass test with docker

* add TODO

* go mod tidy

* Add PR review suggestions from Stefan-Ethernal

* fix UTs

* Add PR review suggestions from joanestebanr

---------

Co-authored-by: Stefan Negovanović <[email protected]>
  • Loading branch information
arnaubennassar and Stefan-Ethernal committed Sep 17, 2024
1 parent 28cd7af commit a97d944
Show file tree
Hide file tree
Showing 41 changed files with 2,503 additions and 782 deletions.
7 changes: 4 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# CONTAINER FOR BUILDING BINARY
FROM golang:1.22.4 AS build
FROM golang:1.22.5-alpine3.20 AS build

WORKDIR $GOPATH/src/github.com/0xPolygon/cdk

RUN apk update && apk add --no-cache make build-base git
# INSTALL DEPENDENCIES
COPY go.mod go.sum /src/
RUN cd /src && go mod download
Expand All @@ -12,9 +13,9 @@ COPY . /src
RUN cd /src && make build

# CONTAINER FOR RUNNING BINARY
FROM alpine:3.18.4
FROM alpine:3.20
COPY --from=build /src/dist/cdk /app/cdk
RUN mkdir /app/data && apk update && apk add postgresql15-client
EXPOSE 8123

CMD ["/bin/sh", "-c", "/app/cdk run"]
CMD ["/bin/sh", "-c", "/app/cdk run"]
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ else
endif
GOBASE := $(shell pwd)
GOBIN := $(GOBASE)/dist
GOENVVARS := GOBIN=$(GOBIN) CGO_ENABLED=0 GOOS=linux GOARCH=$(ARCH)
GOENVVARS := GOBIN=$(GOBIN) CGO_ENABLED=1 GOOS=linux GOARCH=$(ARCH)
GOBINARY := cdk
GOCMD := $(GOBASE)/cmd

Expand Down
117 changes: 117 additions & 0 deletions aggoracle/chaingersender/evm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package chaingersender

import (
"context"
"fmt"
"math/big"
"time"

"github.com/0xPolygon/cdk-contracts-tooling/contracts/manual/pessimisticglobalexitroot"
cfgTypes "github.com/0xPolygon/cdk/config/types"
"github.com/0xPolygon/cdk/log"
"github.com/0xPolygonHermez/zkevm-ethtx-manager/ethtxmanager"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)

type EthClienter interface {
ethereum.LogFilterer
ethereum.BlockNumberReader
ethereum.ChainReader
bind.ContractBackend
}

type EthTxManager interface {
Remove(ctx context.Context, id common.Hash) error
ResultsByStatus(ctx context.Context, statuses []ethtxmanager.MonitoredTxStatus) ([]ethtxmanager.MonitoredTxResult, error)
Result(ctx context.Context, id common.Hash) (ethtxmanager.MonitoredTxResult, error)
Add(ctx context.Context, to *common.Address, forcedNonce *uint64, value *big.Int, data []byte, gasOffset uint64, sidecar *types.BlobTxSidecar) (common.Hash, error)
}

type EVMChainGERSender struct {
gerContract *pessimisticglobalexitroot.Pessimisticglobalexitroot
gerAddr common.Address
sender common.Address
client EthClienter
ethTxMan EthTxManager
gasOffset uint64
waitPeriodMonitorTx time.Duration
}

type EVMConfig struct {
GlobalExitRootL2Addr common.Address `mapstructure:"GlobalExitRootL2"`
URLRPCL2 string `mapstructure:"URLRPCL2"`
ChainIDL2 uint64 `mapstructure:"ChainIDL2"`
GasOffset uint64 `mapstructure:"GasOffset"`
WaitPeriodMonitorTx cfgTypes.Duration `mapstructure:"WaitPeriodMonitorTx"`
SenderAddr common.Address `mapstructure:"SenderAddr"`
EthTxManager ethtxmanager.Config `mapstructure:"EthTxManager"`
}

func NewEVMChainGERSender(
l2GlobalExitRoot, sender common.Address,
l2Client EthClienter,
ethTxMan EthTxManager,
gasOffset uint64,
waitPeriodMonitorTx time.Duration,
) (*EVMChainGERSender, error) {
gerContract, err := pessimisticglobalexitroot.NewPessimisticglobalexitroot(l2GlobalExitRoot, l2Client)
if err != nil {
return nil, err
}
return &EVMChainGERSender{
gerContract: gerContract,
gerAddr: l2GlobalExitRoot,
sender: sender,
client: l2Client,
ethTxMan: ethTxMan,
gasOffset: gasOffset,
waitPeriodMonitorTx: waitPeriodMonitorTx,
}, nil
}

func (c *EVMChainGERSender) IsGERAlreadyInjected(ger common.Hash) (bool, error) {
timestamp, err := c.gerContract.GlobalExitRootMap(&bind.CallOpts{Pending: false}, ger)
if err != nil {
return false, fmt.Errorf("error calling gerContract.GlobalExitRootMap: %w", err)
}
return timestamp.Cmp(big.NewInt(0)) != 0, nil
}

func (c *EVMChainGERSender) UpdateGERWaitUntilMined(ctx context.Context, ger common.Hash) error {
abi, err := pessimisticglobalexitroot.PessimisticglobalexitrootMetaData.GetAbi()
if err != nil {
return err
}
data, err := abi.Pack("updateGlobalExitRoot", ger)
if err != nil {
return err
}
id, err := c.ethTxMan.Add(ctx, &c.gerAddr, nil, big.NewInt(0), data, c.gasOffset, nil)
if err != nil {
return err
}
for {
time.Sleep(c.waitPeriodMonitorTx)
log.Debugf("waiting for tx %s to be mined", id.Hex())
res, err := c.ethTxMan.Result(ctx, id)
if err != nil {
log.Error("error calling ethTxMan.Result: ", err)
}
switch res.Status {
case ethtxmanager.MonitoredTxStatusCreated,
ethtxmanager.MonitoredTxStatusSent:
continue
case ethtxmanager.MonitoredTxStatusFailed:
return fmt.Errorf("tx %s failed", res.ID)
case ethtxmanager.MonitoredTxStatusMined,
ethtxmanager.MonitoredTxStatusSafe,
ethtxmanager.MonitoredTxStatusFinalized:
return nil
default:
log.Error("unexpected tx status: ", res.Status)
}
}
}
25 changes: 25 additions & 0 deletions aggoracle/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package aggoracle

import (
"github.com/0xPolygon/cdk/aggoracle/chaingersender"
"github.com/0xPolygon/cdk/config/types"
)

type TargetChainType string

const (
EVMChain TargetChainType = "EVM"
)

var (
SupportedChainTypes = []TargetChainType{EVMChain}
)

type Config struct {
TargetChainType TargetChainType `mapstructure:"TargetChainType"`
URLRPCL1 string `mapstructure:"URLRPCL1"`
// TODO: BlockFinality doesnt work as per the jsonschema
BlockFinality string `jsonschema:"enum=latest,enum=safe, enum=pending, enum=finalized" mapstructure:"BlockFinality"`
WaitPeriodNextGER types.Duration `mapstructure:"WaitPeriodNextGER"`
EVMSender chaingersender.EVMConfig `mapstructure:"EVMSender"`
}
214 changes: 214 additions & 0 deletions aggoracle/e2e_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
package aggoracle_test

import (
"context"
"errors"
"fmt"
"math/big"
"strconv"
"testing"
"time"

gerContractL1 "github.com/0xPolygon/cdk-contracts-tooling/contracts/manual/globalexitrootnopush0"
gerContractEVMChain "github.com/0xPolygon/cdk-contracts-tooling/contracts/manual/pessimisticglobalexitrootnopush0"
"github.com/0xPolygon/cdk/aggoracle"
"github.com/0xPolygon/cdk/aggoracle/chaingersender"
"github.com/0xPolygon/cdk/etherman"
"github.com/0xPolygon/cdk/l1infotreesync"
"github.com/0xPolygon/cdk/log"
"github.com/0xPolygon/cdk/reorgdetector"
ethtxmanager "github.com/0xPolygonHermez/zkevm-ethtx-manager/ethtxmanager"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient/simulated"
mock "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)

func TestEVM(t *testing.T) {
ctx := context.Background()
l1Client, syncer, gerL1Contract, authL1 := commonSetup(t)
sender := evmSetup(t)
oracle, err := aggoracle.New(sender, l1Client.Client(), syncer, etherman.LatestBlock, time.Millisecond)
require.NoError(t, err)
go oracle.Start(ctx)

runTest(t, gerL1Contract, sender, l1Client, authL1)
}

func commonSetup(t *testing.T) (
*simulated.Backend,
*l1infotreesync.L1InfoTreeSync,
*gerContractL1.Globalexitrootnopush0,
*bind.TransactOpts,
) {
// Config and spin up
ctx := context.Background()
// Simulated L1
privateKeyL1, err := crypto.GenerateKey()
require.NoError(t, err)
authL1, err := bind.NewKeyedTransactorWithChainID(privateKeyL1, big.NewInt(1337))
require.NoError(t, err)
l1Client, gerL1Addr, gerL1Contract, err := newSimulatedL1(authL1)
require.NoError(t, err)
// Reorg detector
dbPathReorgDetector := t.TempDir()
reorg, err := reorgdetector.New(ctx, l1Client.Client(), dbPathReorgDetector)
require.NoError(t, err)
// Syncer
dbPathSyncer := t.TempDir()
syncer, err := l1infotreesync.New(ctx, dbPathSyncer, gerL1Addr, 10, etherman.LatestBlock, reorg, l1Client.Client(), time.Millisecond, 0, 100*time.Millisecond, 3)
require.NoError(t, err)
go syncer.Start(ctx)

return l1Client, syncer, gerL1Contract, authL1
}

func evmSetup(t *testing.T) aggoracle.ChainSender {
privateKeyL2, err := crypto.GenerateKey()
require.NoError(t, err)
authL2, err := bind.NewKeyedTransactorWithChainID(privateKeyL2, big.NewInt(1337))
require.NoError(t, err)
l2Client, gerL2Addr, _, err := newSimulatedEVMAggSovereignChain(authL2)
require.NoError(t, err)
ethTxManMock := aggoracle.NewEthTxManagerMock(t)
// id, err := c.ethTxMan.Add(ctx, &c.gerAddr, nil, big.NewInt(0), tx.Data(), c.gasOffset, nil)
ethTxManMock.On("Add", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
Run(func(args mock.Arguments) {
ctx := context.Background()
nonce, err := l2Client.Client().PendingNonceAt(ctx, authL2.From)
if err != nil {
log.Error(err)
return
}
gas, err := l2Client.Client().EstimateGas(ctx, ethereum.CallMsg{
From: authL2.From,
To: args.Get(1).(*common.Address),
Value: big.NewInt(0),
Data: args.Get(4).([]byte),
})
if err != nil {
log.Error(err)
res, err := l2Client.Client().CallContract(ctx, ethereum.CallMsg{
From: authL2.From,
To: args.Get(1).(*common.Address),
Value: big.NewInt(0),
Data: args.Get(4).([]byte),
}, nil)
log.Debugf("contract call: %s", res)
if err != nil {
log.Error(err)
}
return
}
price, err := l2Client.Client().SuggestGasPrice(ctx)
if err != nil {
log.Error(err)
}
tx := types.NewTx(&types.LegacyTx{
To: args.Get(1).(*common.Address),
Nonce: nonce,
Value: big.NewInt(0),
Data: args.Get(4).([]byte),
Gas: gas,
GasPrice: price,
})
tx.Gas()
signedTx, err := authL2.Signer(authL2.From, tx)
if err != nil {
log.Error(err)
return
}
err = l2Client.Client().SendTransaction(ctx, signedTx)
if err != nil {
log.Error(err)
return
}
l2Client.Commit()
}).
Return(common.Hash{}, nil)
// res, err := c.ethTxMan.Result(ctx, id)
ethTxManMock.On("Result", mock.Anything, mock.Anything).
Return(ethtxmanager.MonitoredTxResult{Status: ethtxmanager.MonitoredTxStatusMined}, nil)
sender, err := chaingersender.NewEVMChainGERSender(gerL2Addr, authL2.From, l2Client.Client(), ethTxManMock, 0, time.Millisecond*50)
require.NoError(t, err)

return sender
}

func newSimulatedL1(auth *bind.TransactOpts) (
client *simulated.Backend,
gerAddr common.Address,
gerContract *gerContractL1.Globalexitrootnopush0,
err error,
) {
balance, _ := new(big.Int).SetString("10000000000000000000000000", 10) //nolint:gomnd
address := auth.From
genesisAlloc := map[common.Address]types.Account{
address: {
Balance: balance,
},
}
blockGasLimit := uint64(999999999999999999) //nolint:gomnd
client = simulated.NewBackend(genesisAlloc, simulated.WithBlockGasLimit(blockGasLimit))

gerAddr, _, gerContract, err = gerContractL1.DeployGlobalexitrootnopush0(auth, client.Client(), auth.From, auth.From)

client.Commit()
return
}

func newSimulatedEVMAggSovereignChain(auth *bind.TransactOpts) (
client *simulated.Backend,
gerAddr common.Address,
gerContract *gerContractEVMChain.Pessimisticglobalexitrootnopush0,
err error,
) {
balance, _ := new(big.Int).SetString("10000000000000000000000000", 10) //nolint:gomnd
address := auth.From
genesisAlloc := map[common.Address]types.Account{
address: {
Balance: balance,
},
}
blockGasLimit := uint64(999999999999999999) //nolint:gomnd
client = simulated.NewBackend(genesisAlloc, simulated.WithBlockGasLimit(blockGasLimit))

gerAddr, _, gerContract, err = gerContractEVMChain.DeployPessimisticglobalexitrootnopush0(auth, client.Client(), auth.From)
if err != nil {
return
}
client.Commit()

_GLOBAL_EXIT_ROOT_SETTER_ROLE := common.HexToHash("0x7b95520991dfda409891be0afa2635b63540f92ee996fda0bf695a166e5c5176")
_, err = gerContract.GrantRole(auth, _GLOBAL_EXIT_ROOT_SETTER_ROLE, auth.From)
client.Commit()
hasRole, _ := gerContract.HasRole(&bind.CallOpts{Pending: false}, _GLOBAL_EXIT_ROOT_SETTER_ROLE, auth.From)
if !hasRole {
err = errors.New("failed to set role")
}
return
}

func runTest(
t *testing.T,
gerL1Contract *gerContractL1.Globalexitrootnopush0,
sender aggoracle.ChainSender,
l1Client *simulated.Backend,
authL1 *bind.TransactOpts,
) {
for i := 0; i < 10; i++ {
_, err := gerL1Contract.UpdateExitRoot(authL1, common.HexToHash(strconv.Itoa(i)))
require.NoError(t, err)
l1Client.Commit()
time.Sleep(time.Millisecond * 50)
expectedGER, err := gerL1Contract.GetLastGlobalExitRoot(&bind.CallOpts{Pending: false})
require.NoError(t, err)
isInjected, err := sender.IsGERAlreadyInjected(expectedGER)
require.NoError(t, err)
require.True(t, isInjected, fmt.Sprintf("iteration %d, GER: %s", i, common.Bytes2Hex(expectedGER[:])))
}
}
Loading

0 comments on commit a97d944

Please sign in to comment.