diff --git a/consensus/consensus.go b/consensus/consensus.go index 09b296816a..2e403d1452 100644 --- a/consensus/consensus.go +++ b/consensus/consensus.go @@ -34,6 +34,9 @@ type Consensus interface { // GetSyncProgression retrieves the current sync progression, if any GetSyncProgression() *progress.Progression + // GetBridgeProvider returns an instance of BridgeDataProvider + GetBridgeProvider() BridgeDataProvider + // Initialize initializes the consensus (e.g. setup data) Initialize() error @@ -75,3 +78,9 @@ type Params struct { // Factory is the factory function to create a discovery consensus type Factory func(*Params) (Consensus, error) + +// BridgeDataProvider is an interface providing bridge related functions +type BridgeDataProvider interface { + // GenerateExit proof generates proof of exit for given exit event + GenerateExitProof(exitID, epoch, checkpointBlock uint64) ([]types.Hash, error) +} diff --git a/consensus/dev/dev.go b/consensus/dev/dev.go index 63b571e06e..bc04dbd26b 100644 --- a/consensus/dev/dev.go +++ b/consensus/dev/dev.go @@ -242,3 +242,7 @@ func (d *Dev) Close() error { return nil } + +func (d *Dev) GetBridgeProvider() consensus.BridgeDataProvider { + return nil +} diff --git a/consensus/dummy/dummy.go b/consensus/dummy/dummy.go index b8a8b8cd66..fafc2dc6d6 100644 --- a/consensus/dummy/dummy.go +++ b/consensus/dummy/dummy.go @@ -75,6 +75,10 @@ func (d *Dummy) Close() error { return nil } +func (d *Dummy) GetBridgeProvider() consensus.BridgeDataProvider { + return nil +} + func (d *Dummy) run() { d.logger.Info("started") // do nothing diff --git a/consensus/ibft/ibft.go b/consensus/ibft/ibft.go index 23a3e1ea92..ab80f457de 100644 --- a/consensus/ibft/ibft.go +++ b/consensus/ibft/ibft.go @@ -532,6 +532,11 @@ func (i *backendIBFT) SetHeaderHash() { } } +// GetBridgeProvider returns an instance of BridgeDataProvider +func (i *backendIBFT) GetBridgeProvider() consensus.BridgeDataProvider { + return nil +} + // updateCurrentModules updates Signer, Hooks, and Validators // that are used at specified height // by fetching from ForkManager diff --git a/consensus/polybft/consensus_runtime.go b/consensus/polybft/consensus_runtime.go index 7923706eca..07f536c461 100644 --- a/consensus/polybft/consensus_runtime.go +++ b/consensus/polybft/consensus_runtime.go @@ -150,7 +150,7 @@ func (c *consensusRuntime) AddLog(eventLog *ethgo.Log) { "index", eventLog.LogIndex, ) - event, err := decodeEvent(eventLog) + event, err := decodeStateSyncEvent(eventLog) if err != nil { c.logger.Error("failed to decode state sync event", "hash", eventLog.TransactionHash, "err", err) @@ -281,15 +281,16 @@ func (c *consensusRuntime) FSM() (*fsm, error) { isEndOfEpoch := c.isEndOfEpoch(pendingBlockNumber) ff := &fsm{ - config: c.config.PolyBFTConfig, - parent: parent, - backend: c.config.blockchain, - polybftBackend: c.config.polybftBackend, - blockBuilder: blockBuilder, - validators: newValidatorSet(types.BytesToAddress(parent.Miner), epoch.Validators), - isEndOfEpoch: isEndOfEpoch, - isEndOfSprint: isEndOfSprint, - logger: c.logger.Named("fsm"), + config: c.config.PolyBFTConfig, + parent: parent, + backend: c.config.blockchain, + polybftBackend: c.config.polybftBackend, + blockBuilder: blockBuilder, + validators: newValidatorSet(types.BytesToAddress(parent.Miner), epoch.Validators), + isEndOfEpoch: isEndOfEpoch, + isEndOfSprint: isEndOfSprint, + generateEventRoot: c.getExitEventRootHash, + logger: c.logger.Named("fsm"), } if c.IsBridgeEnabled() { @@ -760,6 +761,50 @@ func (c *consensusRuntime) calculateUptime(currentBlock *types.Header) (*CommitE return commitEpoch, nil } +// getExitEventRootHash returns the exit event root hash from gathered exit events in given epoch +func (c *consensusRuntime) getExitEventRootHash(epoch uint64) (types.Hash, error) { + exitEvents, err := c.state.getExitEventsByEpoch(epoch) + if err != nil { + return types.ZeroHash, err + } + + if len(exitEvents) == 0 { + return types.ZeroHash, nil + } + + tree, err := createExitTree(exitEvents) + if err != nil { + return types.ZeroHash, err + } + + return tree.Hash(), nil +} + +// GenerateExitProof generates proof of exit +func (c *consensusRuntime) GenerateExitProof(exitID, epoch, checkpointBlock uint64) ([]types.Hash, error) { + exitEvent, err := c.state.getExitEvent(exitID, epoch, checkpointBlock) + if err != nil { + return nil, err + } + + e, err := exitEventABIType.Encode(exitEvent) + if err != nil { + return nil, err + } + + exitEvents, err := c.state.getExitEventsForProof(epoch, checkpointBlock) + if err != nil { + return nil, err + } + + tree, err := createExitTree(exitEvents) + if err != nil { + return nil, err + } + + return tree.GenerateProofForLeaf(e, 0) +} + // setIsActiveValidator updates the activeValidatorFlag field func (c *consensusRuntime) setIsActiveValidator(isActiveValidator bool) { if isActiveValidator { @@ -889,3 +934,20 @@ func validateVote(vote *MessageSignature, epoch *epochMetadata) error { return nil } + +// createExitTree creates an exit event merkle tree from provided exit events +func createExitTree(exitEvents []*ExitEvent) (*MerkleTree, error) { + numOfEvents := len(exitEvents) + data := make([][]byte, numOfEvents) + + for i := 0; i < numOfEvents; i++ { + b, err := exitEventABIType.Encode(exitEvents[i]) + if err != nil { + return nil, err + } + + data[i] = b + } + + return NewMerkleTree(data) +} diff --git a/consensus/polybft/consensus_runtime_test.go b/consensus/polybft/consensus_runtime_test.go index fcf91e7caf..557a631803 100644 --- a/consensus/polybft/consensus_runtime_test.go +++ b/consensus/polybft/consensus_runtime_test.go @@ -150,7 +150,7 @@ func TestConsensusRuntime_AddLog(t *testing.T) { config: &runtimeConfig{Key: createTestKey(t)}, } topics := make([]ethgo.Hash, 4) - topics[0] = stateTransferEvent.ID() + topics[0] = stateTransferEventABI.ID() topics[1] = ethgo.BytesToHash([]byte{0x1}) topics[2] = ethgo.BytesToHash(runtime.config.Key.Address().Bytes()) topics[3] = ethgo.BytesToHash(contracts.NativeTokenContract[:]) @@ -167,7 +167,7 @@ func TestConsensusRuntime_AddLog(t *testing.T) { Topics: topics, Data: encodedData, } - event, err := decodeEvent(log) + event, err := decodeStateSyncEvent(log) require.NoError(t, err) runtime.AddLog(log) @@ -534,6 +534,8 @@ func TestConsensusRuntime_FSM_NotInValidatorSet(t *testing.T) { func TestConsensusRuntime_FSM_NotEndOfEpoch_NotEndOfSprint(t *testing.T) { t.Parallel() + state := newTestState(t) + lastBlock := &types.Header{Number: 1} validators := newTestValidators(3) blockchainMock := new(blockchainMock) @@ -555,6 +557,7 @@ func TestConsensusRuntime_FSM_NotEndOfEpoch_NotEndOfSprint(t *testing.T) { Validators: validators.getPublicIdentities(), }, lastBuiltBlock: lastBlock, + state: state, } fsm, err := runtime.FSM() @@ -1639,6 +1642,100 @@ func TestConsensusRuntime_FSM_EndOfEpoch_PostHook(t *testing.T) { blockchainMock.AssertExpectations(t) } +func TestConsensusRuntime_getExitEventRootHash(t *testing.T) { + const ( + numOfBlocks = 10 + numOfEventsPerBlock = 2 + ) + + state := newTestState(t) + runtime := &consensusRuntime{ + state: state, + } + + encodedEvents := setupExitEventsForProofVerification(t, state, numOfBlocks, numOfEventsPerBlock) + + t.Run("Get exit event root hash", func(t *testing.T) { + tree, err := NewMerkleTree(encodedEvents) + require.NoError(t, err) + + hash, err := runtime.getExitEventRootHash(1) + require.NoError(t, err) + require.Equal(t, tree.Hash(), hash) + }) + + t.Run("Get exit event root hash - no events", func(t *testing.T) { + hash, err := runtime.getExitEventRootHash(2) + require.NoError(t, err) + require.Equal(t, types.Hash{}, hash) + }) +} + +func TestConsensusRuntime_GenerateExitProof(t *testing.T) { + const ( + numOfBlocks = 10 + numOfEventsPerBlock = 2 + ) + + state := newTestState(t) + runtime := &consensusRuntime{ + state: state, + } + + encodedEvents := setupExitEventsForProofVerification(t, state, numOfBlocks, numOfEventsPerBlock) + checkpointEvents := encodedEvents[:numOfEventsPerBlock] + + // manually create merkle tree for a desired checkpoint to verify the generated proof + tree, err := NewMerkleTree(checkpointEvents) + require.NoError(t, err) + + proof, err := runtime.GenerateExitProof(1, 1, 1) + require.NoError(t, err) + require.NotNil(t, proof) + + t.Run("Generate and validate exit proof", func(t *testing.T) { + // verify generated proof on desired tree + require.NoError(t, VerifyProof(1, encodedEvents[1], proof, tree.Hash())) + }) + + t.Run("Generate and validate exit proof - invalid proof", func(t *testing.T) { + invalidProof := proof + invalidProof[0][0]++ + + // verify generated proof on desired tree + require.ErrorContains(t, VerifyProof(1, encodedEvents[1], invalidProof, tree.Hash()), "not a member of merkle tree") + }) + + t.Run("Generate exit proof - no event", func(t *testing.T) { + _, err := runtime.GenerateExitProof(21, 1, 1) + require.ErrorContains(t, err, "could not find any exit event that has an id") + }) +} + +func setupExitEventsForProofVerification(t *testing.T, state *State, + numOfBlocks, numOfEventsPerBlock uint64) [][]byte { + t.Helper() + + encodedEvents := make([][]byte, numOfBlocks*numOfEventsPerBlock) + index := uint64(0) + + for i := uint64(1); i <= numOfBlocks; i++ { + for j := uint64(1); j <= numOfEventsPerBlock; j++ { + e := &ExitEvent{index, ethgo.ZeroAddress, ethgo.ZeroAddress, []byte{0, 1}, 1, i} + require.NoError(t, state.insertExitEvent(e)) + + b, err := exitEventABIType.Encode(e) + + require.NoError(t, err) + + encodedEvents[index] = b + index++ + } + } + + return encodedEvents +} + func createTestTransportMessage(t *testing.T, hash []byte, epochNumber uint64, key *wallet.Key) *TransportMessage { t.Helper() diff --git a/consensus/polybft/fsm.go b/consensus/polybft/fsm.go index 9007c3ad6c..39105c6e1e 100644 --- a/consensus/polybft/fsm.go +++ b/consensus/polybft/fsm.go @@ -85,6 +85,9 @@ type fsm struct { // stateSyncExecutionIndex is the next state sync execution index in smart contract stateSyncExecutionIndex uint64 + // generateEventRoot returns the root hash of exit event tree + generateEventRoot func(epoch uint64) (types.Hash, error) + logger hcf.Logger // The logger object } @@ -158,6 +161,8 @@ func (f *fsm) BuildProposal() (*pbft.Proposal, error) { headerTime = time.Now() } + // TODO - Here we will call f.generateEventRootFunc(epoch) to get the exit root after adding transactions + stateBlock, err := f.blockBuilder.Build(func(h *types.Header) { h.Timestamp = uint64(headerTime.Unix()) h.ExtraData = append(make([]byte, signer.IstanbulExtraVanity), extra.MarshalRLPTo(nil)...) diff --git a/consensus/polybft/fsm_test.go b/consensus/polybft/fsm_test.go index cb1b4dcb0e..3632ed82fc 100644 --- a/consensus/polybft/fsm_test.go +++ b/consensus/polybft/fsm_test.go @@ -1559,7 +1559,7 @@ func createTestCommitment(t *testing.T, accounts []*wallet.Account) *CommitmentM uint64(i), accounts[i].Ecdsa.Address(), accounts[0].Ecdsa.Address(), - []byte{}, nil, + []byte{}, ) bitmap.Set(uint64(i)) diff --git a/consensus/polybft/merkle_tree.go b/consensus/polybft/merkle_tree.go index ce76a73f9c..b70e279eb4 100644 --- a/consensus/polybft/merkle_tree.go +++ b/consensus/polybft/merkle_tree.go @@ -12,6 +12,8 @@ import ( "github.com/0xPolygon/polygon-edge/types" ) +var errLeafNotFound = errors.New("leaf not found") + // MerkleTree is the structure for the Merkle tree. type MerkleTree struct { // hasher is a pointer to the hashing struct (e.g., Keccak256) @@ -65,6 +67,17 @@ func NewMerkleTreeWithHashing(data [][]byte, hash hash.Hash) (*MerkleTree, error return tree, nil } +// LeafIndex returns the index of given leaf if found in tree +func (t *MerkleTree) LeafIndex(leaf []byte) (uint64, error) { + for i, d := range t.data { + if bytes.Equal(d, leaf) { + return uint64(i), nil + } + } + + return 0, errLeafNotFound +} + // Hash is the Merkle Tree root hash func (t *MerkleTree) Hash() types.Hash { return types.BytesToHash(t.nodes[1]) @@ -76,7 +89,6 @@ func (t *MerkleTree) String() string { } // GenerateProof generates the proof of membership for a piece of data in the Merkle tree. -// If the data is not present in the tree this will return an error. func (t *MerkleTree) GenerateProof(index uint64, height int) []types.Hash { proofLen := int(math.Ceil(math.Log2(float64(len(t.data))))) - height proofHashes := make([]types.Hash, proofLen) @@ -92,6 +104,17 @@ func (t *MerkleTree) GenerateProof(index uint64, height int) []types.Hash { return proofHashes } +// GenerateProofForLeaf generates the proof of membership for a piece of data in the Merkle tree. +// If the data is not present in the tree this will return an error +func (t *MerkleTree) GenerateProofForLeaf(leaf []byte, height int) ([]types.Hash, error) { + leafIndex, err := t.LeafIndex(leaf) + if err != nil { + return nil, err + } + + return t.GenerateProof(leafIndex, height), nil +} + // VerifyProof verifies a Merkle tree proof of membership for provided data using the default hash type (Keccak256) func VerifyProof(index uint64, leaf []byte, proof []types.Hash, root types.Hash) error { return VerifyProofUsing(index, leaf, proof, root, crypto.NewKeccakState()) diff --git a/consensus/polybft/polybft.go b/consensus/polybft/polybft.go index d2b7267a8a..f791b81f56 100644 --- a/consensus/polybft/polybft.go +++ b/consensus/polybft/polybft.go @@ -618,6 +618,11 @@ func (p *Polybft) PreCommitState(_ *types.Header, _ *state.Transition) error { return nil } +// GetBridgeProvider returns an instance of BridgeDataProvider +func (p *Polybft) GetBridgeProvider() consensus.BridgeDataProvider { + return p.runtime +} + type pbftTransportWrapper struct { topic *network.Topic } diff --git a/consensus/polybft/state.go b/consensus/polybft/state.go index 816726f820..ff285a858a 100644 --- a/consensus/polybft/state.go +++ b/consensus/polybft/state.go @@ -1,6 +1,7 @@ package polybft import ( + "bytes" "encoding/binary" "encoding/json" "errors" @@ -19,7 +20,7 @@ import ( /* The client has a boltDB backed state store. The schema as of looks as follows: -events/ +state sync events/ |--> stateSyncEvent.Id -> *StateSyncEvent (json marshalled) commitments/ @@ -34,11 +35,16 @@ epochs/ validatorSnapshots/ |--> epochNumber -> *AccountSet (json marshalled) + +exit events/ +|--> (id+epoch+blockNumber) -> *ExitEvent (json marshalled) */ var ( // ABI - stateTransferEvent = abi.MustNewEvent("event StateSynced(uint256 indexed id, address indexed sender, address indexed receiver, bytes data)") //nolint:lll + stateTransferEventABI = abi.MustNewEvent("event StateSynced(uint256 indexed id, address indexed sender, address indexed receiver, bytes data)") //nolint:lll + exitEventABI = abi.MustNewEvent("event L2StateSynced(uint256 indexed id, address indexed sender, address indexed receiver, bytes data)") //nolint:lll + exitEventABIType = abi.MustNewType("tuple(uint256 id, address sender, address receiver, bytes data)") ) const ( @@ -55,6 +61,17 @@ const ( stateSyncBundleSize = 5 ) +type exitEventNotFoundError struct { + exitID uint64 + epoch uint64 + checkpointBlock uint64 +} + +func (e *exitEventNotFoundError) Error() string { + return fmt.Sprintf("could not find any exit event that has an id: %v, added in block: %v and epoch: %v", + e.exitID, e.checkpointBlock, e.epoch) +} + // StateSyncEvent is a bridge event from the rootchain type StateSyncEvent struct { // ID is the decoded 'index' field from the event @@ -67,8 +84,6 @@ type StateSyncEvent struct { Data []byte // Skip is the decoded 'skip' field from the event Skip bool - // Log contains raw data about smart contract event execution - Log *ethgo.Log } // newStateSyncEvent creates an instance of pending state sync event. @@ -76,48 +91,110 @@ func newStateSyncEvent( id uint64, sender ethgo.Address, target ethgo.Address, - data []byte, log *ethgo.Log, + data []byte, ) *StateSyncEvent { return &StateSyncEvent{ ID: id, Sender: sender, Receiver: target, Data: data, - Log: log, } } -func decodeEvent(log *ethgo.Log) (*StateSyncEvent, error) { - raw, err := stateTransferEvent.ParseLog(log) +func (s *StateSyncEvent) String() string { + return fmt.Sprintf("Id=%d, Sender=%v, Target=%v", s.ID, s.Sender, s.Receiver) +} + +func decodeStateSyncEvent(log *ethgo.Log) (*StateSyncEvent, error) { + raw, err := stateTransferEventABI.ParseLog(log) if err != nil { return nil, err } - id, ok := raw["id"].(*big.Int) + eventGeneric, err := decodeEventData(raw, log, + func(id *big.Int, sender, receiver ethgo.Address, data []byte) interface{} { + return newStateSyncEvent(id.Uint64(), sender, receiver, data) + }) + if err != nil { + return nil, err + } + + stateSyncEvent, ok := eventGeneric.(*StateSyncEvent) + if !ok { + return nil, errors.New("failed to convert event to StateSyncEvent instance") + } + + return stateSyncEvent, nil +} + +func decodeExitEvent(log *ethgo.Log, epoch, block uint64) (*ExitEvent, error) { + raw, err := exitEventABI.ParseLog(log) + if err != nil { + return nil, err + } + + eventGeneric, err := decodeEventData(raw, log, + func(id *big.Int, sender, receiver ethgo.Address, data []byte) interface{} { + return &ExitEvent{ID: id.Uint64(), + Sender: sender, + Receiver: receiver, + Data: data, + EpochNumber: epoch, + BlockNumber: block} + }) + if err != nil { + return nil, err + } + + exitEvent, ok := eventGeneric.(*ExitEvent) + if !ok { + return nil, errors.New("failed to convert event to ExitEvent instance") + } + + return exitEvent, err +} + +// decodeEventData decodes provided map of event metadata and +// creates a generic instance which is returned by eventCreator callback +func decodeEventData(eventDataMap map[string]interface{}, log *ethgo.Log, + eventCreator func(*big.Int, ethgo.Address, ethgo.Address, []byte) interface{}) (interface{}, error) { + id, ok := eventDataMap["id"].(*big.Int) if !ok { return nil, fmt.Errorf("failed to decode id field of log: %+v", log) } - sender, ok := raw["sender"].(ethgo.Address) + sender, ok := eventDataMap["sender"].(ethgo.Address) if !ok { return nil, fmt.Errorf("failed to decode sender field of log: %+v", log) } - target, ok := raw["receiver"].(ethgo.Address) + receiver, ok := eventDataMap["receiver"].(ethgo.Address) if !ok { - return nil, fmt.Errorf("failed to decode target field of log: %+v", log) + return nil, fmt.Errorf("failed to decode receiver field of log: %+v", log) } - data, ok := raw["data"].([]byte) + data, ok := eventDataMap["data"].([]byte) if !ok { return nil, fmt.Errorf("failed to decode data field of log: %+v", log) } - return newStateSyncEvent(id.Uint64(), sender, target, data, log), nil + return eventCreator(id, sender, receiver, data), nil } -func (s *StateSyncEvent) String() string { - return fmt.Sprintf("Id=%d, Sender=%v, Target=%v", s.ID, s.Sender, s.Receiver) +// ExitEvent is an event emitted by Exit contract +type ExitEvent struct { + // ID is the decoded 'index' field from the event + ID uint64 `abi:"id"` + // Sender is the decoded 'sender' field from the event + Sender ethgo.Address `abi:"sender"` + // Receiver is the decoded 'receiver' field from the event + Receiver ethgo.Address `abi:"receiver"` + // Data is the decoded 'data' field from the event + Data []byte `abi:"data"` + // EpochNumber is the epoch number in which exit event was added + EpochNumber uint64 `abi:"-"` + // BlockNumber is the block in which exit event was added + BlockNumber uint64 `abi:"-"` } // MessageSignature encapsulates sender identifier and its signature @@ -142,8 +219,10 @@ type TransportMessage struct { var ( // bucket to store rootchain bridge events - syncStateEventsBucket = []byte("events") - //bucket to store commitments + syncStateEventsBucket = []byte("stateSyncEvents") + // bucket to store exit contract events + exitEventsBucket = []byte("exitEvent") + // bucket to store commitments commitmentsBucket = []byte("commitments") // bucket to store bundles bundlesBucket = []byte("bundles") @@ -154,7 +233,7 @@ var ( // bucket to store validator snapshots validatorSnapshotsBucket = []byte("validatorSnapshots") // array of all parent buckets - parentBuckets = [][]byte{syncStateEventsBucket, commitmentsBucket, bundlesBucket, + parentBuckets = [][]byte{syncStateEventsBucket, exitEventsBucket, commitmentsBucket, bundlesBucket, epochsBucket, validatorSnapshotsBucket} // ErrNotEnoughStateSyncs error message ErrNotEnoughStateSyncs = errors.New("there is either a gap or not enough sync events") @@ -256,6 +335,83 @@ func (s *State) list() ([]*StateSyncEvent, error) { return events, nil } +// insertStateSyncEvent inserts a new state sync event to state event bucket in db +func (s *State) insertExitEvent(event *ExitEvent) error { + return s.db.Update(func(tx *bolt.Tx) error { + raw, err := json.Marshal(event) + if err != nil { + return err + } + + bucket := tx.Bucket(exitEventsBucket) + + return bucket.Put(bytes.Join([][]byte{itob(event.EpochNumber), itob(event.ID), itob(event.BlockNumber)}, nil), raw) + }) +} + +// getExitEvent returns exit event with given id, which happened in given epoch and given block number +func (s *State) getExitEvent(exitEventID, epoch, checkpointBlockNumber uint64) (*ExitEvent, error) { + var exitEvent *ExitEvent + + err := s.db.View(func(tx *bolt.Tx) error { + bucket := tx.Bucket(exitEventsBucket) + + key := bytes.Join([][]byte{itob(epoch), itob(exitEventID), itob(checkpointBlockNumber)}, nil) + v := bucket.Get(key) + if v == nil { + return &exitEventNotFoundError{ + exitID: exitEventID, + checkpointBlock: checkpointBlockNumber, + epoch: epoch, + } + } + + return json.Unmarshal(v, &exitEvent) + }) + + return exitEvent, err +} + +// getExitEventsByEpoch returns all exit events that happened in the given epoch +func (s *State) getExitEventsByEpoch(epoch uint64) ([]*ExitEvent, error) { + return s.getExitEvents(epoch, func(exitEvent *ExitEvent) bool { + return exitEvent.EpochNumber == epoch + }) +} + +// getExitEventsForProof returns all exit events that happened in and prior to the given checkpoint block number +// with respect to the epoch in which block is added +func (s *State) getExitEventsForProof(epoch, checkpointBlock uint64) ([]*ExitEvent, error) { + return s.getExitEvents(epoch, func(exitEvent *ExitEvent) bool { + return exitEvent.EpochNumber == epoch && exitEvent.BlockNumber <= checkpointBlock + }) +} + +// getExitEvents returns exit events for given epoch and provided filter +func (s *State) getExitEvents(epoch uint64, filter func(exitEvent *ExitEvent) bool) ([]*ExitEvent, error) { + var events []*ExitEvent + + err := s.db.View(func(tx *bolt.Tx) error { + c := tx.Bucket(exitEventsBucket).Cursor() + prefix := itob(epoch) + + for k, v := c.Seek(prefix); bytes.HasPrefix(k, prefix); k, v = c.Next() { + var event *ExitEvent + if err := json.Unmarshal(v, &event); err != nil { + return err + } + + if filter(event) { + events = append(events, event) + } + } + + return nil + }) + + return events, err +} + // insertStateSyncEvent inserts a new state sync event to state event bucket in db func (s *State) insertStateSyncEvent(event *StateSyncEvent) error { return s.db.Update(func(tx *bolt.Tx) error { diff --git a/consensus/polybft/state_test.go b/consensus/polybft/state_test.go index bc1ab2538e..fece42b7c6 100644 --- a/consensus/polybft/state_test.go +++ b/consensus/polybft/state_test.go @@ -46,7 +46,7 @@ func TestState_InsertEvent(t *testing.T) { t.Parallel() state := newTestState(t) - evnt1 := newStateSyncEvent(0, ethgo.Address{}, ethgo.Address{}, nil, nil) + evnt1 := newStateSyncEvent(0, ethgo.Address{}, ethgo.Address{}, nil) err := state.insertStateSyncEvent(evnt1) assert.NoError(t, err) @@ -352,6 +352,93 @@ func TestState_insertAndGetBundles(t *testing.T) { assert.NotNil(t, bundlesFromDB[0].Proof) } +func TestState_Insert_And_Get_ExitEvents_PerEpoch(t *testing.T) { + const ( + numOfEpochs = 11 + numOfBlocksPerEpoch = 10 + numOfEventsPerBlock = 11 + ) + + state := newTestState(t) + insertTestExitEvents(t, state, numOfEpochs, numOfBlocksPerEpoch, numOfEventsPerBlock) + + t.Run("Get events for existing epoch", func(t *testing.T) { + events, err := state.getExitEventsByEpoch(1) + + assert.NoError(t, err) + assert.Len(t, events, numOfBlocksPerEpoch*numOfEventsPerBlock) + }) + + t.Run("Get events for non-existing epoch", func(t *testing.T) { + events, err := state.getExitEventsByEpoch(12) + + assert.NoError(t, err) + assert.Len(t, events, 0) + }) +} + +func TestState_Insert_And_Get_ExitEvents_ForProof(t *testing.T) { + const ( + numOfEpochs = 11 + numOfBlocksPerEpoch = 10 + numOfEventsPerBlock = 10 + ) + + state := newTestState(t) + insertTestExitEvents(t, state, numOfEpochs, numOfBlocksPerEpoch, numOfEventsPerBlock) + + var cases = []struct { + epoch uint64 + checkpointBlockNumber uint64 + expectedNumberOfEvents int + }{ + {1, 1, 10}, + {1, 2, 20}, + {1, 8, 80}, + {2, 12, 20}, + {2, 14, 40}, + {3, 26, 60}, + {4, 38, 80}, + {11, 105, 50}, + } + + for _, c := range cases { + events, err := state.getExitEventsForProof(c.epoch, c.checkpointBlockNumber) + + assert.NoError(t, err) + assert.Len(t, events, c.expectedNumberOfEvents) + } +} + +func TestState_Insert_And_Get_ExitEvents_ForProof_NoEvents(t *testing.T) { + state := newTestState(t) + insertTestExitEvents(t, state, 1, 10, 1) + + events, err := state.getExitEventsForProof(2, 11) + + assert.NoError(t, err) + assert.Nil(t, events) +} + +func insertTestExitEvents(t *testing.T, state *State, + numOfEpochs, numOfBlocksPerEpoch, numOfEventsPerBlock int) { + t.Helper() + + index, block := uint64(1), uint64(1) + + for i := uint64(1); i <= uint64(numOfEpochs); i++ { + for j := 1; j <= numOfBlocksPerEpoch; j++ { + for k := 1; k <= numOfEventsPerBlock; k++ { + event := &ExitEvent{index, ethgo.HexToAddress("0x101"), ethgo.HexToAddress("0x102"), []byte{11, 22}, i, block} + assert.NoError(t, state.insertExitEvent(event)) + + index++ + } + block++ + } + } +} + func insertTestCommitments(t *testing.T, state *State, epoch, numberOfCommitments uint64) { t.Helper() diff --git a/jsonrpc/bridge_endpoint.go b/jsonrpc/bridge_endpoint.go new file mode 100644 index 0000000000..a7b9e50a48 --- /dev/null +++ b/jsonrpc/bridge_endpoint.go @@ -0,0 +1,20 @@ +package jsonrpc + +import ( + "github.com/0xPolygon/polygon-edge/types" +) + +// bridgeStore interface provides access to the methods needed by bridge endpoint +type bridgeStore interface { + GenerateExitProof(exitID, epoch, checkpointBlock uint64) ([]types.Hash, error) +} + +// Bridge is the bridge jsonrpc endpoint +type Bridge struct { + store bridgeStore +} + +// GenerateExitProof generates exit proof for given exit event +func (b *Bridge) GenerateExitProof(exitID, epoch, checkpointBlock argUint64) (interface{}, error) { + return b.store.GenerateExitProof(uint64(exitID), uint64(epoch), uint64(checkpointBlock)) +} diff --git a/jsonrpc/bridge_endpoint_test.go b/jsonrpc/bridge_endpoint_test.go new file mode 100644 index 0000000000..779a953d89 --- /dev/null +++ b/jsonrpc/bridge_endpoint_test.go @@ -0,0 +1,40 @@ +package jsonrpc + +import ( + "encoding/json" + "testing" + + "github.com/hashicorp/go-hclog" + "github.com/stretchr/testify/require" +) + +func TestBridgeEndpoint(t *testing.T) { + store := newMockStore() + + dispatcher := newDispatcher( + hclog.NewNullLogger(), + store, + &dispatcherParams{ + chainID: 0, + priceLimit: 0, + jsonRPCBatchLengthLimit: 20, + blockRangeLimit: 1000, + }, + ) + + mockConnection, _ := newMockWsConnWithMsgCh() + + msg := []byte(`{ + "method": "bridge_generateExitProof", + "params": ["0x0001", "0x0001", "0x0002"], + "id": 1 + }`) + + data, err := dispatcher.HandleWs(msg, mockConnection) + require.NoError(t, err) + + resp := new(SuccessResponse) + require.NoError(t, json.Unmarshal(data, resp)) + require.Nil(t, resp.Error) + require.NotNil(t, resp.Result) +} diff --git a/jsonrpc/dispatcher.go b/jsonrpc/dispatcher.go index 342fd99b55..764b62bd8d 100644 --- a/jsonrpc/dispatcher.go +++ b/jsonrpc/dispatcher.go @@ -34,6 +34,7 @@ type endpoints struct { Web3 *Web3 Net *Net TxPool *TxPool + Bridge *Bridge } // Dispatcher handles all json rpc requests by delegating @@ -93,11 +94,13 @@ func (d *Dispatcher) registerEndpoints(store JSONRPCStore) { d.params.chainName, } d.endpoints.TxPool = &TxPool{store} + d.endpoints.Bridge = &Bridge{store} d.registerService("eth", d.endpoints.Eth) d.registerService("net", d.endpoints.Net) d.registerService("web3", d.endpoints.Web3) d.registerService("txpool", d.endpoints.TxPool) + d.registerService("bridge", d.endpoints.Bridge) } func (d *Dispatcher) getFnHandler(req Request) (*serviceData, *funcData, Error) { diff --git a/jsonrpc/jsonrpc.go b/jsonrpc/jsonrpc.go index 25a8b13738..8863d4bdf0 100644 --- a/jsonrpc/jsonrpc.go +++ b/jsonrpc/jsonrpc.go @@ -55,6 +55,7 @@ type JSONRPCStore interface { networkStore txPoolStore filterManagerStore + bridgeStore } type Config struct { diff --git a/jsonrpc/mocks_test.go b/jsonrpc/mocks_test.go index 704b197449..6da6d455ac 100644 --- a/jsonrpc/mocks_test.go +++ b/jsonrpc/mocks_test.go @@ -133,3 +133,9 @@ func (m *mockStore) GetTxs(inclQueued bool) ( func (m *mockStore) GetCapacity() (uint64, uint64) { return 0, 0 } + +func (m *mockStore) GenerateExitProof(exitID, epoch, checkpointNumber uint64) ([]types.Hash, error) { + hash := types.BytesToHash([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) + + return []types.Hash{hash}, nil +} diff --git a/server/server.go b/server/server.go index f4b0a2b69f..dc435921e5 100644 --- a/server/server.go +++ b/server/server.go @@ -446,6 +446,7 @@ type jsonRPCHub struct { *state.Executor *network.Server consensus.Consensus + consensus.BridgeDataProvider } // HELPER + WRAPPER METHODS // @@ -563,6 +564,7 @@ func (s *Server) setupJSONRPC() error { Executor: s.executor, Consensus: s.consensus, Server: s.network, + BridgeDataProvider: s.consensus.GetBridgeProvider(), } conf := &jsonrpc.Config{