Skip to content

Commit

Permalink
L1 Synchronization process check some L2Blocks from TrustedNode (#3445)
Browse files Browse the repository at this point in the history
* L1 Synchronization process check some L2Blocks from TrustedNode
  • Loading branch information
joanestebanr committed Mar 12, 2024
1 parent b184e2b commit 393d47f
Show file tree
Hide file tree
Showing 19 changed files with 879 additions and 35 deletions.
1 change: 0 additions & 1 deletion cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,6 @@ func runSynchronizer(cfg config.Config, etherman *etherman.Client, ethTxManagerS
log.Info("trustedSequencerURL ", trustedSequencerURL)
}
zkEVMClient := client.NewClient(trustedSequencerURL)

etherManForL1 := []syncinterfaces.EthermanFullInterface{}
// If synchronizer are using sequential mode, we only need one etherman client
if cfg.Synchronizer.L1SynchronizationMode == synchronizer.ParallelMode {
Expand Down
2 changes: 2 additions & 0 deletions config/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ SyncInterval = "1s"
SyncChunkSize = 100
TrustedSequencerURL = "" # If it is empty or not specified, then the value is read from the smc
L1SynchronizationMode = "sequential"
L1SyncCheckL2BlockHash = true
L1SyncCheckL2BlockNumberhModulus = 30
[Synchronizer.L1ParallelSynchronization]
MaxClients = 10
MaxPendingNoProcessedBlocks = 25
Expand Down
2 changes: 1 addition & 1 deletion docs/config-file/node-config-doc.html

Large diffs are not rendered by default.

83 changes: 57 additions & 26 deletions docs/config-file/node-config-doc.md

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions docs/config-file/node-config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,16 @@
"description": "TrustedSequencerURL is the rpc url to connect and sync the trusted state",
"default": ""
},
"L1SyncCheckL2BlockHash": {
"type": "boolean",
"description": "L1SyncCheckL2BlockHash if is true when a batch is closed is force to check L2Block hash against trustedNode (only apply for permissionless)",
"default": true
},
"L1SyncCheckL2BlockNumberhModulus": {
"type": "integer",
"description": "L1SyncCheckL2BlockNumberhModulus is the modulus used to choose the l2block to check\na modules 5, for instance, means check all l2block multiples of 5 (10,15,20,...)",
"default": 30
},
"L1SynchronizationMode": {
"type": "string",
"enum": [
Expand Down
139 changes: 139 additions & 0 deletions synchronizer/actions/check_l2block.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package actions

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

"github.com/0xPolygonHermez/zkevm-node/jsonrpc/types"
"github.com/0xPolygonHermez/zkevm-node/log"
"github.com/0xPolygonHermez/zkevm-node/state"
"github.com/jackc/pgx/v4"
)

// Implements PostClosedBatchChecker

type stateGetL2Block interface {
GetL2BlockByNumber(ctx context.Context, blockNumber uint64, dbTx pgx.Tx) (*state.L2Block, error)
GetLastL2BlockNumber(ctx context.Context, dbTx pgx.Tx) (uint64, error)
}

type trustedRPCGetL2Block interface {
BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error)
}

// CheckL2BlockHash is a struct that implements a checker of L2Block hash
type CheckL2BlockHash struct {
state stateGetL2Block
trustedClient trustedRPCGetL2Block
lastL2BlockChecked uint64
// Is a modulus used to choose the l2block to check
modulusL2BlockToCheck uint64
}

// NewCheckL2BlockHash creates a new CheckL2BlockHash
func NewCheckL2BlockHash(state stateGetL2Block,
trustedClient trustedRPCGetL2Block,
initialL2BlockNumber uint64,
modulusBlockNumber uint64) *CheckL2BlockHash {
return &CheckL2BlockHash{
state: state,
trustedClient: trustedClient,
lastL2BlockChecked: initialL2BlockNumber,
modulusL2BlockToCheck: modulusBlockNumber,
}
}

// CheckL2Block checks the L2Block hash between the local and the trusted
func (p *CheckL2BlockHash) CheckL2Block(ctx context.Context, dbTx pgx.Tx) error {
lastLocalL2BlockNumber, err := p.state.GetLastL2BlockNumber(ctx, dbTx)
if errors.Is(err, state.ErrNotFound) || errors.Is(err, state.ErrStateNotSynchronized) {
log.Debugf("checkL2block:No L2Block in database. err: %s", err.Error())
return nil
}
if err != nil {
log.Errorf("checkL2block: Error getting last L2Block from the database. err: %s", err.Error())
return err
}
shouldCheck, l2BlockNumber := p.GetNextL2BlockToCheck(lastLocalL2BlockNumber, p.GetMinimumL2BlockToCheck())
if !shouldCheck {
return nil
}
err = p.iterationCheckL2Block(ctx, l2BlockNumber, dbTx)
if err != nil {
return err
}
return nil
}

// GetNextL2BlockToCheck returns true is need to check and the blocknumber
func (p *CheckL2BlockHash) GetNextL2BlockToCheck(lastLocalL2BlockNumber, minL2BlockNumberToCheck uint64) (bool, uint64) {
l2BlockNumber := max(minL2BlockNumberToCheck, lastLocalL2BlockNumber)
if l2BlockNumber > lastLocalL2BlockNumber {
log.Infof("checkL2block: skip check L2block (next to check: %d) currently LastL2BlockNumber: %d", minL2BlockNumberToCheck, lastLocalL2BlockNumber)
return false, 0
}
return true, l2BlockNumber
}

// GetMinimumL2BlockToCheck returns the minimum L2Block to check
func (p *CheckL2BlockHash) GetMinimumL2BlockToCheck() uint64 {
if p.modulusL2BlockToCheck == 0 {
return p.lastL2BlockChecked + 1
}
return ((p.lastL2BlockChecked / p.modulusL2BlockToCheck) + 1) * p.modulusL2BlockToCheck
}

// GetL2Blocks returns localL2Block and trustedL2Block
func (p *CheckL2BlockHash) GetL2Blocks(ctx context.Context, blockNumber uint64, dbTx pgx.Tx) (*state.L2Block, *types.Block, error) {
localL2Block, err := p.state.GetL2BlockByNumber(ctx, blockNumber, dbTx)
if err != nil {
log.Debugf("checkL2block: Error getting L2Block %d from the database. err: %s", blockNumber, err.Error())
return nil, nil, err
}
trustedL2Block, err := p.trustedClient.BlockByNumber(ctx, big.NewInt(int64(blockNumber)))
if err != nil {
log.Errorf("checkL2block: Error getting L2Block %d from the Trusted RPC. err:%s", blockNumber, err.Error())
return nil, nil, err
}
return localL2Block, trustedL2Block, nil
}

// CheckPostClosedBatch checks the last L2Block hash on close batch
func (p *CheckL2BlockHash) iterationCheckL2Block(ctx context.Context, l2BlockNumber uint64, dbTx pgx.Tx) error {
prefixLogs := fmt.Sprintf("checkL2block: L2BlockNumber: %d ", l2BlockNumber)
localL2Block, trustedL2Block, err := p.GetL2Blocks(ctx, l2BlockNumber, dbTx)
if errors.Is(err, state.ErrNotFound) || errors.Is(err, state.ErrStateNotSynchronized) {
log.Debugf("%s not found in the database", prefixLogs, l2BlockNumber)
return nil
}
if err != nil {
log.Errorf("%s Error getting from the database and trusted. err: %s", prefixLogs, err.Error())
return err
}
if localL2Block == nil || trustedL2Block == nil {
log.Errorf("%s localL2Block or trustedL2Block is nil", prefixLogs, l2BlockNumber)
return nil
}

if err := compareL2Blocks(prefixLogs, localL2Block, trustedL2Block); err != nil {
log.Errorf("%s Error comparing L2Blocks from the database and trusted. err: %s", prefixLogs, err.Error())
return err
}

log.Infof("%s checked L2Block in the database and the trusted batch are the same %s", prefixLogs, localL2Block.Hash().String())
// Compare the two blocks
p.lastL2BlockChecked = l2BlockNumber
return nil
}

func compareL2Blocks(prefixLogs string, localL2Block *state.L2Block, trustedL2Block *types.Block) error {
if localL2Block == nil || trustedL2Block == nil || trustedL2Block.Hash == nil {
return fmt.Errorf("%s localL2Block or trustedL2Block or trustedHash are nil", prefixLogs)
}
if localL2Block.Hash() != *trustedL2Block.Hash {
return fmt.Errorf("%s localL2Block.Hash %s and trustedL2Block.Hash %s are different", prefixLogs, localL2Block.Hash().String(), (*trustedL2Block.Hash).String())
}
return nil
}
34 changes: 34 additions & 0 deletions synchronizer/actions/check_l2block_processor_decorator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package actions

import (
"context"

"github.com/0xPolygonHermez/zkevm-node/etherman"
"github.com/jackc/pgx/v4"
)

// CheckL2BlockProcessorDecorator This class is just a decorator to call CheckL2Block
type CheckL2BlockProcessorDecorator struct {
L1EventProcessor
l2blockChecker *CheckL2BlockHash
}

// NewCheckL2BlockDecorator creates a new CheckL2BlockDecorator
func NewCheckL2BlockDecorator(l1EventProcessor L1EventProcessor, l2blockChecker *CheckL2BlockHash) *CheckL2BlockProcessorDecorator {
return &CheckL2BlockProcessorDecorator{
L1EventProcessor: l1EventProcessor,
l2blockChecker: l2blockChecker,
}
}

// Process wraps the real Process and after check the L2Blocks
func (p *CheckL2BlockProcessorDecorator) Process(ctx context.Context, order etherman.Order, l1Block *etherman.Block, dbTx pgx.Tx) error {
res := p.L1EventProcessor.Process(ctx, order, l1Block, dbTx)
if res != nil {
return res
}
if p.l2blockChecker == nil {
return nil
}
return p.l2blockChecker.CheckL2Block(ctx, dbTx)
}
131 changes: 131 additions & 0 deletions synchronizer/actions/check_l2block_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package actions_test

import (
"context"
"math/big"
"testing"

rpctypes "github.com/0xPolygonHermez/zkevm-node/jsonrpc/types"
"github.com/0xPolygonHermez/zkevm-node/state"
"github.com/0xPolygonHermez/zkevm-node/synchronizer/actions"
mock_syncinterfaces "github.com/0xPolygonHermez/zkevm-node/synchronizer/common/syncinterfaces/mocks"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)

type CheckL2BlocksTestData struct {
sut *actions.CheckL2BlockHash
mockState *mock_syncinterfaces.StateFullInterface
zKEVMClient *mock_syncinterfaces.ZKEVMClientInterface
}

func TestCheckL2BlockHash_GetMinimumL2BlockToCheck(t *testing.T) {
// Create an instance of CheckL2BlockHash
values := []struct {
initial uint64
modulus uint64
expected uint64
}{
{0, 10, 10},
{1, 10, 10},
{9, 10, 10},
{10, 10, 20},
{0, 0, 1},
{1, 0, 2},
}
for _, data := range values {
// Call the GetNextL2BlockToCheck method
checkL2Block := actions.NewCheckL2BlockHash(nil, nil, data.initial, data.modulus)
nextL2Block := checkL2Block.GetMinimumL2BlockToCheck()

// Assert the expected result
assert.Equal(t, data.expected, nextL2Block)
}
}

func TestCheckL2BlockHashNotEnoughBlocksToCheck(t *testing.T) {
data := newCheckL2BlocksTestData(t, 0, 10)
// Call the CheckL2Block method
data.mockState.EXPECT().GetLastL2BlockNumber(mock.Anything, mock.Anything).Return(uint64(0), nil)
err := data.sut.CheckL2Block(context.Background(), nil)
require.NoError(t, err)
}

func newCheckL2BlocksTestData(t *testing.T, initialL2Block, modulus uint64) CheckL2BlocksTestData {
res := CheckL2BlocksTestData{
mockState: mock_syncinterfaces.NewStateFullInterface(t),
zKEVMClient: mock_syncinterfaces.NewZKEVMClientInterface(t),
}
res.sut = actions.NewCheckL2BlockHash(res.mockState, res.zKEVMClient, initialL2Block, modulus)
return res
}
func TestCheckL2BlockHash_GetNextL2BlockToCheck(t *testing.T) {
values := []struct {
lastLocalL2BlockNumber uint64
minL2BlockNumberToCheck uint64
expectedShouldCheck bool
expectedNextL2BlockNumber uint64
}{
{0, 10, false, 0},
{10, 10, true, 10},
{9, 10, false, 0},
{10, 10, true, 10},
{0, 0, true, 0},
{1, 0, true, 1},
}

for _, data := range values {
checkL2Block := actions.NewCheckL2BlockHash(nil, nil, 0, 0)
shouldCheck, nextL2Block := checkL2Block.GetNextL2BlockToCheck(data.lastLocalL2BlockNumber, data.minL2BlockNumberToCheck)

assert.Equal(t, data.expectedShouldCheck, shouldCheck, data)
assert.Equal(t, data.expectedNextL2BlockNumber, nextL2Block, data)
}
}

func TestCheckL2BlockHashMatch(t *testing.T) {
data := newCheckL2BlocksTestData(t, 1, 10)
lastL2Block := uint64(14)
lastL2BlockBigInt := big.NewInt(int64(lastL2Block))
gethHeader := types.Header{
Number: big.NewInt(int64(lastL2Block)),
}
stateBlock := state.NewL2Block(state.NewL2Header(&gethHeader), nil, nil, nil, nil)

data.mockState.EXPECT().GetLastL2BlockNumber(mock.Anything, mock.Anything).Return(lastL2Block, nil)
data.mockState.EXPECT().GetL2BlockByNumber(mock.Anything, lastL2Block, mock.Anything).Return(stateBlock, nil)
l2blockHash := stateBlock.Hash()
rpcL2Block := rpctypes.Block{
Hash: &l2blockHash,
Number: rpctypes.ArgUint64(lastL2Block),
}

data.zKEVMClient.EXPECT().BlockByNumber(mock.Anything, lastL2BlockBigInt).Return(&rpcL2Block, nil)
err := data.sut.CheckL2Block(context.Background(), nil)
require.NoError(t, err)
}

func TestCheckL2BlockHashMissmatch(t *testing.T) {
data := newCheckL2BlocksTestData(t, 1, 10)
lastL2Block := uint64(14)
lastL2BlockBigInt := big.NewInt(int64(lastL2Block))
gethHeader := types.Header{
Number: big.NewInt(int64(lastL2Block)),
}
stateBlock := state.NewL2Block(state.NewL2Header(&gethHeader), nil, nil, nil, nil)

data.mockState.EXPECT().GetLastL2BlockNumber(mock.Anything, mock.Anything).Return(lastL2Block, nil)
data.mockState.EXPECT().GetL2BlockByNumber(mock.Anything, lastL2Block, mock.Anything).Return(stateBlock, nil)
l2blockHash := common.HexToHash("0x1234")
rpcL2Block := rpctypes.Block{
Hash: &l2blockHash,
Number: rpctypes.ArgUint64(lastL2Block),
}

data.zKEVMClient.EXPECT().BlockByNumber(mock.Anything, lastL2BlockBigInt).Return(&rpcL2Block, nil)
err := data.sut.CheckL2Block(context.Background(), nil)
require.Error(t, err)
}
5 changes: 5 additions & 0 deletions synchronizer/actions/etrog/processor_l1_sequence_batches.go
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,11 @@ func (p *ProcessorL1SequenceBatchesEtrog) checkTrustedState(ctx context.Context,
log.Warnf(errMsg)
reorgReasons.WriteString(errMsg)
}
if tBatch.WIP {
errMsg := batchNumStr + "Trusted batch is WIP\n"
log.Warnf(errMsg)
reorgReasons.WriteString(errMsg)
}

if reorgReasons.Len() > 0 {
reason := reorgReasons.String()
Expand Down
Loading

0 comments on commit 393d47f

Please sign in to comment.