Skip to content

Commit

Permalink
EVM-356 Update checkpoint out of fsm (#1113)
Browse files Browse the repository at this point in the history
* Move logic from fsm to checkpoint manager

* PostBlock and PostEpoch in checkpointManager

* Function comment fix

* Call PostEpoch on checkpointManager

* Test fix

* e2e tests fix

* Comments fix

* Comments fix

* Change test

* Comments fix
  • Loading branch information
goran-ethernal authored Jan 13, 2023
1 parent 217beb4 commit 3006cb3
Show file tree
Hide file tree
Showing 10 changed files with 298 additions and 339 deletions.
111 changes: 98 additions & 13 deletions consensus/polybft/checkpoint_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package polybft
import (
"fmt"
"math/big"
"sort"
"strconv"

bls "github.com/0xPolygon/polygon-edge/consensus/polybft/signer"
"github.com/0xPolygon/polygon-edge/contracts"
"github.com/0xPolygon/polygon-edge/txrelayer"
"github.com/0xPolygon/polygon-edge/types"
metrics "github.com/armon/go-metrics"
Expand Down Expand Up @@ -33,6 +35,28 @@ var (
defaultCheckpointsOffset = uint64(900)
)

type CheckpointManager interface {
PostBlock(req *PostBlockRequest) error
IsCheckpointBlock(blockNumber uint64, isEpochEndingBlock bool) bool
SubmitCheckpoint(header *types.Header, isEndOfEpoch bool) error
SetLastSentBlock(blockNumber uint64)
}

var _ CheckpointManager = (*dummyCheckpointManager)(nil)

type dummyCheckpointManager struct{}

func (d *dummyCheckpointManager) PostBlock(req *PostBlockRequest) error { return nil }
func (d *dummyCheckpointManager) IsCheckpointBlock(blockNumber uint64, isEpochEndingBlock bool) bool {
return false
}
func (d *dummyCheckpointManager) SubmitCheckpoint(header *types.Header, isEndOfEpoch bool) error {
return nil
}
func (d *dummyCheckpointManager) SetLastSentBlock(blockNumber uint64) {}

var _ CheckpointManager = (*checkpointManager)(nil)

// checkpointManager encapsulates logic for checkpoint data submission
type checkpointManager struct {
// key is the identity of the node submitting a checkpoint
Expand All @@ -47,16 +71,19 @@ type checkpointManager struct {
checkpointsOffset uint64
// checkpointManagerAddr is address of CheckpointManager smart contract
checkpointManagerAddr types.Address
// latestCheckpointID represents last checkpointed block number
latestCheckpointID uint64
// lastSentBlock represents the last block on which a checkpoint transaction was sent
lastSentBlock uint64
// logger instance
logger hclog.Logger
// state boltDb instance
state *State
}

// newCheckpointManager creates a new instance of checkpointManager
func newCheckpointManager(key ethgo.Key, checkpointOffset uint64,
checkpointManagerSC types.Address, txRelayer txrelayer.TxRelayer,
blockchain blockchainBackend, backend polybftBackend, logger hclog.Logger) *checkpointManager {
blockchain blockchainBackend, backend polybftBackend, logger hclog.Logger,
state *State) *checkpointManager {
return &checkpointManager{
key: key,
blockchain: blockchain,
Expand All @@ -65,6 +92,7 @@ func newCheckpointManager(key ethgo.Key, checkpointOffset uint64,
checkpointsOffset: checkpointOffset,
checkpointManagerAddr: checkpointManagerSC,
logger: logger,
state: state,
}
}

Expand Down Expand Up @@ -92,8 +120,13 @@ func (c *checkpointManager) getLatestCheckpointBlock() (uint64, error) {
return latestCheckpointBlockNum, nil
}

// submitCheckpoint sends a transaction with checkpoint data to the rootchain
func (c *checkpointManager) submitCheckpoint(latestHeader types.Header, isEndOfEpoch bool) error {
// SetLastSentBlock sets the lastSentBlock field
func (c *checkpointManager) SetLastSentBlock(blockNumber uint64) {
c.lastSentBlock = blockNumber
}

// SubmitCheckpoint sends a transaction with checkpoint data to the rootchain
func (c *checkpointManager) SubmitCheckpoint(latestHeader *types.Header, isEndOfEpoch bool) error {
lastCheckpointBlockNumber, err := c.getLatestCheckpointBlock()
if err != nil {
return err
Expand Down Expand Up @@ -152,7 +185,7 @@ func (c *checkpointManager) submitCheckpoint(latestHeader types.Header, isEndOfE
continue
}

if err = c.encodeAndSendCheckpoint(txn, *parentHeader, *parentExtra, true); err != nil {
if err = c.encodeAndSendCheckpoint(txn, parentHeader, parentExtra, true); err != nil {
return err
}

Expand All @@ -170,13 +203,13 @@ func (c *checkpointManager) submitCheckpoint(latestHeader types.Header, isEndOfE
}
}

return c.encodeAndSendCheckpoint(txn, latestHeader, *currentExtra, isEndOfEpoch)
return c.encodeAndSendCheckpoint(txn, latestHeader, currentExtra, isEndOfEpoch)
}

// encodeAndSendCheckpoint encodes checkpoint data for the given block and
// sends a transaction to the CheckpointManager rootchain contract
func (c *checkpointManager) encodeAndSendCheckpoint(txn *ethgo.Transaction,
header types.Header, extra Extra, isEndOfEpoch bool) error {
header *types.Header, extra *Extra, isEndOfEpoch bool) error {
c.logger.Debug("send checkpoint txn...", "block number", header.Number)

nextEpochValidators := AccountSet{}
Expand Down Expand Up @@ -214,7 +247,7 @@ func (c *checkpointManager) encodeAndSendCheckpoint(txn *ethgo.Transaction,
}

// abiEncodeCheckpointBlock encodes checkpoint data into ABI format for a given header
func (c *checkpointManager) abiEncodeCheckpointBlock(blockNumber uint64, blockHash types.Hash, extra Extra,
func (c *checkpointManager) abiEncodeCheckpointBlock(blockNumber uint64, blockHash types.Hash, extra *Extra,
nextValidators AccountSet) ([]byte, error) {
aggs, err := bls.UnmarshalSignature(extra.Committed.AggregatedSignature)
if err != nil {
Expand Down Expand Up @@ -246,8 +279,60 @@ func (c *checkpointManager) abiEncodeCheckpointBlock(blockNumber uint64, blockHa
return submitCheckpointMethod.Encode(params)
}

// isCheckpointBlock returns true for blocks in the middle of the epoch
// which are offseted by predefined count of blocks
func (c *checkpointManager) isCheckpointBlock(blockNumber uint64) bool {
return blockNumber == c.latestCheckpointID+c.checkpointsOffset
// IsCheckpointBlock returns true for blocks in the middle of the epoch
// which are offset by predefined count of blocks
// or if given block is an epoch ending block
func (c *checkpointManager) IsCheckpointBlock(blockNumber uint64, isEpochEndingBlock bool) bool {
return isEpochEndingBlock || blockNumber == c.lastSentBlock+c.checkpointsOffset
}

// PostBlock is called on every insert of finalized block (either from consensus or syncer)
// It will read any exit event that happened in block and insert it to state boltDb
func (c *checkpointManager) PostBlock(req *PostBlockRequest) error {
epoch := req.Epoch
if req.IsEpochEndingBlock {
// exit events that happened in epoch ending blocks,
// should be added to the tree of the next epoch
epoch++
}

// commit exit events only when we finalize a block
events, err := getExitEventsFromReceipts(epoch, req.FullBlock.Block.Number(), req.FullBlock.Receipts)
if err != nil {
return err
}

return c.state.insertExitEvents(events)
}

// getExitEventsFromReceipts parses logs from receipts to find exit events
func getExitEventsFromReceipts(epoch, block uint64, receipts []*types.Receipt) ([]*ExitEvent, error) {
events := make([]*ExitEvent, 0)

for i := 0; i < len(receipts); i++ {
for _, log := range receipts[i].Logs {
if log.Address != contracts.L2StateSenderContract {
continue
}

event, err := decodeExitEvent(convertLog(log), epoch, block)
if err != nil {
return nil, err
}

if event == nil {
// valid case, not an exit event
continue
}

events = append(events, event)
}
}

// enforce sequential order
sort.Slice(events, func(i, j int) bool {
return events[i].ID < events[j].ID
})

return events, nil
}
123 changes: 104 additions & 19 deletions consensus/polybft/checkpoint_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import (
"github.com/0xPolygon/polygon-edge/types"
)

func TestCheckpointManager_submitCheckpoint(t *testing.T) {
func TestCheckpointManager_SubmitCheckpoint(t *testing.T) {
t.Parallel()

const (
Expand Down Expand Up @@ -91,7 +91,7 @@ func TestCheckpointManager_submitCheckpoint(t *testing.T) {
logger: hclog.NewNullLogger(),
}

err := c.submitCheckpoint(*headersMap.getHeader(blocksCount), false)
err := c.SubmitCheckpoint(headersMap.getHeader(blocksCount), false)
require.NoError(t, err)
txRelayerMock.AssertExpectations(t)

Expand Down Expand Up @@ -148,7 +148,7 @@ func TestCheckpointManager_abiEncodeCheckpointBlock(t *testing.T) {
consensusBackend: backendMock,
logger: hclog.NewNullLogger(),
}
checkpointDataEncoded, err := c.abiEncodeCheckpointBlock(header.Number, header.Hash, *extra, nextValidators.getPublicIdentities())
checkpointDataEncoded, err := c.abiEncodeCheckpointBlock(header.Number, header.Hash, extra, nextValidators.getPublicIdentities())
require.NoError(t, err)

decodedCheckpointData, err := submitCheckpointMethod.Inputs.Decode(checkpointDataEncoded[4:])
Expand Down Expand Up @@ -239,26 +239,43 @@ func TestCheckpointManager_getCurrentCheckpointID(t *testing.T) {
}
}

func TestCheckpointManager_isCheckpointBlock(t *testing.T) {
func TestCheckpointManager_IsCheckpointBlock(t *testing.T) {
t.Parallel()

cases := []struct {
name string
blockNumber uint64
checkpointsOffset uint64
isCheckpointBlock bool
name string
blockNumber uint64
checkpointsOffset uint64
isEpochEndingBlock bool
isCheckpointBlock bool
}{
{
name: "Not checkpoint block",
blockNumber: 3,
checkpointsOffset: 6,
isCheckpointBlock: false,
name: "Not checkpoint block",
blockNumber: 3,
checkpointsOffset: 6,
isEpochEndingBlock: false,
isCheckpointBlock: false,
},
{
name: "Checkpoint block",
blockNumber: 6,
checkpointsOffset: 6,
isCheckpointBlock: true,
name: "Checkpoint block",
blockNumber: 6,
checkpointsOffset: 6,
isEpochEndingBlock: false,
isCheckpointBlock: true,
},
{
name: "Epoch ending block - Fixed epoch size met",
blockNumber: 10,
checkpointsOffset: 5,
isEpochEndingBlock: true,
isCheckpointBlock: true,
},
{
name: "Epoch ending block - Epoch ended before fix size was met",
blockNumber: 9,
checkpointsOffset: 5,
isEpochEndingBlock: true,
isCheckpointBlock: true,
},
}

Expand All @@ -267,12 +284,61 @@ func TestCheckpointManager_isCheckpointBlock(t *testing.T) {
t.Run(c.name, func(t *testing.T) {
t.Parallel()

checkpointMgr := newCheckpointManager(wallet.NewEcdsaSigner(createTestKey(t)), c.checkpointsOffset, types.ZeroAddress, nil, nil, nil, hclog.NewNullLogger())
require.Equal(t, c.isCheckpointBlock, checkpointMgr.isCheckpointBlock(c.blockNumber))
checkpointMgr := newCheckpointManager(wallet.NewEcdsaSigner(createTestKey(t)), c.checkpointsOffset, types.ZeroAddress, nil, nil, nil, hclog.NewNullLogger(), nil)
require.Equal(t, c.isCheckpointBlock, checkpointMgr.IsCheckpointBlock(c.blockNumber, c.isEpochEndingBlock))
})
}
}

func TestCheckpointManager_PostBlock(t *testing.T) {
const (
numOfReceipts = 5
block = 5
epoch = 1
)

state := newTestState(t)

receipts := make([]*types.Receipt, numOfReceipts)
for i := 0; i < numOfReceipts; i++ {
receipts[i] = &types.Receipt{Logs: []*types.Log{
createTestLogForExitEvent(t, uint64(i)),
}}
}

req := &PostBlockRequest{FullBlock: &types.FullBlock{Block: &types.Block{Header: &types.Header{Number: block}}, Receipts: receipts},
Epoch: epoch}

checkpointManager := newCheckpointManager(wallet.NewEcdsaSigner(createTestKey(t)), 5, types.ZeroAddress,
nil, nil, nil, hclog.NewNullLogger(), state)

t.Run("PostBlock - not epoch ending block", func(t *testing.T) {
req.IsEpochEndingBlock = false
require.NoError(t, checkpointManager.PostBlock(req))

exitEvents, err := state.getExitEvents(epoch, func(exitEvent *ExitEvent) bool {
return exitEvent.BlockNumber == block
})

require.NoError(t, err)
require.Len(t, exitEvents, numOfReceipts)
require.Equal(t, uint64(epoch), exitEvents[0].EpochNumber)
})

t.Run("PostBlock - epoch ending block (exit events are saved to the next epoch)", func(t *testing.T) {
req.IsEpochEndingBlock = true
require.NoError(t, checkpointManager.PostBlock(req))

exitEvents, err := state.getExitEvents(epoch+1, func(exitEvent *ExitEvent) bool {
return exitEvent.BlockNumber == block
})

require.NoError(t, err)
require.Len(t, exitEvents, numOfReceipts)
require.Equal(t, uint64(epoch+1), exitEvents[0].EpochNumber)
})
}

func TestPerformExit(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -374,7 +440,7 @@ func TestPerformExit(t *testing.T) {
aggSignature, err := signature.Marshal()
require.NoError(t, err)

extra := Extra{
extra := &Extra{
Checkpoint: &checkpointData,
}
extra.Committed = &Signature{
Expand Down Expand Up @@ -534,3 +600,22 @@ func getBlockNumberCheckpointSubmitInput(t *testing.T, input []byte) uint64 {

return blockNumber.Uint64()
}

func createTestLogForExitEvent(t *testing.T, exitEventID uint64) *types.Log {
t.Helper()

topics := make([]types.Hash, 4)
topics[0] = types.Hash(exitEventABI.ID())
topics[1] = types.BytesToHash(itob(exitEventID))
topics[2] = types.BytesToHash(types.StringToAddress("0x1111").Bytes())
topics[3] = types.BytesToHash(types.StringToAddress("0x2222").Bytes())
someType := abi.MustNewType("tuple(string firstName, string lastName)")
encodedData, err := someType.Encode(map[string]string{"firstName": "John", "lastName": "Doe"})
require.NoError(t, err)

return &types.Log{
Address: contracts.L2StateSenderContract,
Topics: topics,
Data: encodedData,
}
}
Loading

0 comments on commit 3006cb3

Please sign in to comment.