From 14f9ab12814035955b775fb88d6108b624a88bbf Mon Sep 17 00:00:00 2001 From: begmaroman Date: Wed, 7 Aug 2024 10:59:53 +0100 Subject: [PATCH 01/43] Added e2e test for reorg detection --- reorgdetector/e2e_test.go | 96 ++++++++++++++++++++++++++++++++++ reorgdetector/reorgdetector.go | 10 ++-- 2 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 reorgdetector/e2e_test.go diff --git a/reorgdetector/e2e_test.go b/reorgdetector/e2e_test.go new file mode 100644 index 00000000..56f40ecb --- /dev/null +++ b/reorgdetector/e2e_test.go @@ -0,0 +1,96 @@ +package reorgdetector_test + +import ( + context "context" + "fmt" + "math/big" + "testing" + "time" + + "github.com/0xPolygon/cdk/reorgdetector" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/require" + + "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/ethclient/simulated" +) + +func newSimulatedL1(t *testing.T, auth *bind.TransactOpts) *simulated.Backend { + t.Helper() + + balance, _ := new(big.Int).SetString("10000000000000000000000000", 10) //nolint:gomnd + + blockGasLimit := uint64(999999999999999999) //nolint:gomnd + client := simulated.NewBackend(map[common.Address]types.Account{ + auth.From: { + Balance: balance, + }, + }, simulated.WithBlockGasLimit(blockGasLimit)) + client.Commit() + + return client +} + +func TestE2E(t *testing.T) { + 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) + clientL1 := newSimulatedL1(t, authL1) + require.NoError(t, err) + + // Reorg detector + dbPathReorgDetector := t.TempDir() + reorgDetector, err := reorgdetector.New(ctx, clientL1.Client(), dbPathReorgDetector) + require.NoError(t, err) + + /*ch := make(chan *types.Header, 10) + sub, err := clientL1.Client().SubscribeNewHead(ctx, ch) + require.NoError(t, err) + go func() { + for { + select { + case <-ctx.Done(): + return + case <-sub.Err(): + return + case header := <-ch: + err := reorgDetector.AddBlockToTrack(ctx, "test", header.Number.Uint64(), header.Hash()) + require.NoError(t, err) + } + } + }()*/ + + reorgDetector.Start(ctx) + + reorgSubscription, err := reorgDetector.Subscribe("test") + require.NoError(t, err) + + go func() { + firstReorgedBlock := <-reorgSubscription.FirstReorgedBlock + fmt.Println("firstReorgedBlock", firstReorgedBlock) + }() + + for i := 0; i < 20; i++ { + block := clientL1.Commit() + header, err := clientL1.Client().HeaderByHash(ctx, block) + require.NoError(t, err) + err = reorgDetector.AddBlockToTrack(ctx, "test", header.Number.Uint64(), header.Hash()) + require.NoError(t, err) + + // Reorg every 4 blocks with 2 blocks depth + if i%4 == 0 { + reorgBlock, err := clientL1.Client().BlockByNumber(ctx, big.NewInt(int64(i-2))) + require.NoError(t, err) + err = clientL1.Fork(reorgBlock.Hash()) + require.NoError(t, err) + } + + time.Sleep(time.Second) + } +} diff --git a/reorgdetector/reorgdetector.go b/reorgdetector/reorgdetector.go index c0b0caa2..a53edad0 100644 --- a/reorgdetector/reorgdetector.go +++ b/reorgdetector/reorgdetector.go @@ -112,7 +112,7 @@ func (bm blockMap) getClosestHigherBlock(blockNum uint64) (block, bool) { return sorted[0], true } -// removeRange removes blocks from from to to +// removeRange removes blocks from "from" to "to" func (bm blockMap) removeRange(from, to uint64) { for i := from; i <= to; i++ { delete(bm, i) @@ -391,8 +391,11 @@ func (r *ReorgDetector) addUnfinalisedBlocks(ctx context.Context) { } if previousBlock.Hash == lastBlockFromClient.ParentHash { - unfinalisedBlocksMap[i] = block{Num: lastBlockFromClient.Number.Uint64(), Hash: lastBlockFromClient.Hash()} - } else if previousBlock.Hash != lastBlockFromClient.ParentHash { + unfinalisedBlocksMap[i] = block{ + Num: lastBlockFromClient.Number.Uint64(), + Hash: lastBlockFromClient.Hash(), + } + } else { // reorg happened, we will find out from where exactly and report this to subscribers reorgBlock = i } @@ -534,7 +537,6 @@ func (r *ReorgDetector) saveTrackedBlock(ctx context.Context, id string, b block raw, err := json.Marshal(subscriberBlockMap.getSorted()) if err != nil { - return err } From af97f030d215d40a98f2403acba8db3dc4fdc947 Mon Sep 17 00:00:00 2001 From: begmaroman Date: Tue, 13 Aug 2024 11:48:19 +0100 Subject: [PATCH 02/43] Implemented reorg monitor --- reorgmonitor/monitor.go | 265 +++++++++++++++++++++++++ reorgmonitor/monitor_test.go | 110 +++++++++++ reorgmonitor/types.go | 367 +++++++++++++++++++++++++++++++++++ 3 files changed, 742 insertions(+) create mode 100644 reorgmonitor/monitor.go create mode 100644 reorgmonitor/monitor_test.go create mode 100644 reorgmonitor/types.go diff --git a/reorgmonitor/monitor.go b/reorgmonitor/monitor.go new file mode 100644 index 00000000..c6cef858 --- /dev/null +++ b/reorgmonitor/monitor.go @@ -0,0 +1,265 @@ +package reorgmonitor + +import ( + "context" + "fmt" + "log" + "math/big" + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/pkg/errors" +) + +type EthClient interface { + BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) + HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) + BlockNumber(ctx context.Context) (uint64, error) +} + +type ReorgMonitor struct { + lock sync.Mutex + + client EthClient + + maxBlocksInCache int + lastBlockHeight uint64 + + newReorgChan chan<- *Reorg + knownReorgs map[string]uint64 // key: reorgId, value: endBlockNumber + + blockByHash map[common.Hash]*Block + blocksByHeight map[uint64]map[common.Hash]*Block + + EarliestBlockNumber uint64 + LatestBlockNumber uint64 +} + +func NewReorgMonitor(client EthClient, reorgChan chan<- *Reorg, maxBlocks int) *ReorgMonitor { + return &ReorgMonitor{ + client: client, + maxBlocksInCache: maxBlocks, + blockByHash: make(map[common.Hash]*Block), + blocksByHeight: make(map[uint64]map[common.Hash]*Block), + newReorgChan: reorgChan, + knownReorgs: make(map[string]uint64), + } +} + +// AddBlockToTrack adds a block to the monitor, and sends it to the monitor's channel. +// After adding a new block, a reorg check takes place. If a new completed reorg is detected, it is sent to the channel. +func (mon *ReorgMonitor) AddBlockToTrack(block *Block) error { + mon.lock.Lock() + defer mon.lock.Unlock() + + mon.addBlock(block) + + // Do nothing if block is at previous height + if block.Number == mon.lastBlockHeight { + return nil + } + + if len(mon.blocksByHeight) < 3 { + return nil + } + + // Analyze blocks once a new height has been reached + mon.lastBlockHeight = block.Number + + anal, err := mon.AnalyzeTree(0, 2) + if err != nil { + return err + } + + for _, reorg := range anal.Reorgs { + if !reorg.IsFinished { // don't care about unfinished reorgs + continue + } + + // Send new finished reorgs to channel + if _, isKnownReorg := mon.knownReorgs[reorg.Id()]; !isKnownReorg { + mon.knownReorgs[reorg.Id()] = reorg.EndBlockHeight + mon.newReorgChan <- reorg + } + } + + return nil +} + +// addBlock adds a block to history if it hasn't been seen before, and download unknown referenced blocks (parent, uncles). +func (mon *ReorgMonitor) addBlock(block *Block) bool { + defer mon.trimCache() + + // If known, then only overwrite if known was by uncle + knownBlock, isKnown := mon.blockByHash[block.Hash] + if isKnown && knownBlock.Origin != OriginUncle { + return false + } + + // Only accept blocks that are after the earliest known (some nodes might be further back) + if block.Number < mon.EarliestBlockNumber { + return false + } + + // Print + blockInfo := fmt.Sprintf("Add%s \t %-12s \t %s", block.String(), block.Origin, mon) + log.Println(blockInfo) + + // Add for access by hash + mon.blockByHash[block.Hash] = block + + // Create array of blocks at this height, if necessary + if _, found := mon.blocksByHeight[block.Number]; !found { + mon.blocksByHeight[block.Number] = make(map[common.Hash]*Block) + } + + // Add to map of blocks at this height + mon.blocksByHeight[block.Number][block.Hash] = block + + // Set earliest block + if mon.EarliestBlockNumber == 0 || block.Number < mon.EarliestBlockNumber { + mon.EarliestBlockNumber = block.Number + } + + // Set latest block + if block.Number > mon.LatestBlockNumber { + mon.LatestBlockNumber = block.Number + } + + // Check if further blocks can be downloaded from this one + if block.Number > mon.EarliestBlockNumber { // check backhistory only if we are past the earliest block + err := mon.checkBlockForReferences(block) + if err != nil { + log.Println(err) + } + } + + return true +} + +func (mon *ReorgMonitor) trimCache() { + // Trim reorg history + for reorgId, reorgEndBlockheight := range mon.knownReorgs { + if reorgEndBlockheight < mon.EarliestBlockNumber { + delete(mon.knownReorgs, reorgId) + } + } + + for currentHeight := mon.EarliestBlockNumber; currentHeight < mon.LatestBlockNumber; currentHeight++ { + blocks, heightExists := mon.blocksByHeight[currentHeight] + if !heightExists { + continue + } + + // Set new lowest block number + mon.EarliestBlockNumber = currentHeight + + // Stop if trimmed enough + if len(mon.blockByHash) <= mon.maxBlocksInCache { + return + } + + // Trim + for hash := range blocks { + delete(mon.blocksByHeight[currentHeight], hash) + delete(mon.blockByHash, hash) + } + delete(mon.blocksByHeight, currentHeight) + } +} + +func (mon *ReorgMonitor) checkBlockForReferences(block *Block) error { + // Check parent + _, found := mon.blockByHash[block.ParentHash] + if !found { + // fmt.Printf("- parent of %d %s not found (%s), downloading...\n", block.Number, block.Hash, block.ParentHash) + _, _, err := mon.ensureBlock(block.ParentHash, OriginGetParent) + if err != nil { + return errors.Wrap(err, "get-parent error") + } + } + + // Check uncles + for _, uncleHeader := range block.Block.Uncles() { + // fmt.Printf("- block %d %s has uncle: %s\n", block.Number, block.Hash, uncleHeader.Hash()) + _, _, err := mon.ensureBlock(uncleHeader.Hash(), OriginUncle) + if err != nil { + return errors.Wrap(err, "get-uncle error") + } + } + + // ro.DebugPrintln(fmt.Sprintf("- added block %d %s", block.NumberU64(), block.Hash())) + return nil +} + +func (mon *ReorgMonitor) ensureBlock(blockHash common.Hash, origin BlockOrigin) (block *Block, alreadyExisted bool, err error) { + // Check and potentially download block + var found bool + block, found = mon.blockByHash[blockHash] + if found { + return block, true, nil + } + + fmt.Printf("- block %s (%s) not found, downloading from...\n", blockHash, origin) + ethBlock, err := mon.client.BlockByHash(context.Background(), blockHash) + if err != nil { + fmt.Println("- err block not found:", blockHash, err) // todo: try other clients + msg := fmt.Sprintf("EnsureBlock error for hash %s", blockHash) + return nil, false, errors.Wrap(err, msg) + } + + block = NewBlock(ethBlock, origin) + + // Add a new block without sending to channel, because that makes reorg.AddBlock() asynchronous, + // but we want reorg.AddBlock() to wait until all references are added. + mon.addBlock(block) + + return block, false, nil +} + +func (mon *ReorgMonitor) AnalyzeTree(maxBlocks, distanceToLastBlockHeight uint64) (*TreeAnalysis, error) { + // Set end height of search + endBlockNumber := mon.LatestBlockNumber - distanceToLastBlockHeight + + // Set start height of search + startBlockNumber := mon.EarliestBlockNumber + if maxBlocks > 0 && endBlockNumber-maxBlocks > mon.EarliestBlockNumber { + startBlockNumber = endBlockNumber - maxBlocks + } + + // Build tree datastructure + tree := NewBlockTree() + for height := startBlockNumber; height <= endBlockNumber; height++ { + numBlocksAtHeight := len(mon.blocksByHeight[height]) + if numBlocksAtHeight == 0 { + err := fmt.Errorf("error in monitor.AnalyzeTree: no blocks at height %d", height) + return nil, err + } + + // Start tree only when 1 block at this height. If more blocks then skip. + if tree.FirstNode == nil && numBlocksAtHeight > 1 { + continue + } + + // Add all blocks at this height to the tree + for _, currentBlock := range mon.blocksByHeight[height] { + err := tree.AddBlock(currentBlock) + if err != nil { + return nil, errors.Wrap(err, "monitor.AnalyzeTree->tree.AddBlock error") + } + } + } + + // Get analysis of tree + anal, err := NewTreeAnalysis(tree) + if err != nil { + return nil, errors.Wrap(err, "monitor.AnalyzeTree->NewTreeAnalysis error") + } + + return anal, nil +} + +func (mon *ReorgMonitor) String() string { + return fmt.Sprintf("ReorgMonitor: %d - %d, %d / %d blocks, %d reorgcache", mon.EarliestBlockNumber, mon.LatestBlockNumber, len(mon.blockByHash), len(mon.blocksByHeight), len(mon.knownReorgs)) +} diff --git a/reorgmonitor/monitor_test.go b/reorgmonitor/monitor_test.go new file mode 100644 index 00000000..183076ea --- /dev/null +++ b/reorgmonitor/monitor_test.go @@ -0,0 +1,110 @@ +package reorgmonitor + +import ( + "context" + "fmt" + "math/big" + "testing" + "time" + + "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" + "github.com/stretchr/testify/require" +) + +func newSimulatedL1(t *testing.T, auth *bind.TransactOpts) *simulated.Backend { + t.Helper() + + balance, _ := new(big.Int).SetString("10000000000000000000000000", 10) //nolint:gomnd + + blockGasLimit := uint64(999999999999999999) //nolint:gomnd + client := simulated.NewBackend(map[common.Address]types.Account{ + auth.From: { + Balance: balance, + }, + }, simulated.WithBlockGasLimit(blockGasLimit)) + client.Commit() + + return client +} + +func Test_ReorgMonitor(t *testing.T) { + const produceBlocks = 29 + const reorgPeriod = 5 + const reorgDepth = 2 + + 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) + clientL1 := newSimulatedL1(t, authL1) + require.NoError(t, err) + + reorgChan := make(chan *Reorg, 100) + mon := NewReorgMonitor(clientL1.Client(), reorgChan, 100) + + // Add head tracker + ch := make(chan *types.Header, 100) + sub, err := clientL1.Client().SubscribeNewHead(ctx, ch) + require.NoError(t, err) + go func() { + for { + select { + case <-ctx.Done(): + return + case <-sub.Err(): + return + case header := <-ch: + block, err := clientL1.Client().BlockByNumber(ctx, header.Number) + require.NoError(t, err) + + err = mon.AddBlockToTrack(NewBlock(block, OriginSubscription)) + require.NoError(t, err) + } + } + }() + + expectedReorgBlocks := make(map[uint64]struct{}) + lastReorgOn := int64(0) + for i := 1; lastReorgOn <= produceBlocks; i++ { + block := clientL1.Commit() + time.Sleep(time.Millisecond) + + header, err := clientL1.Client().HeaderByHash(ctx, block) + require.NoError(t, err) + headerNumber := header.Number.Int64() + + // Reorg every "reorgPeriod" blocks with "reorgDepth" blocks depth + if headerNumber > lastReorgOn && headerNumber%reorgPeriod == 0 { + lastReorgOn = headerNumber + + reorgBlock, err := clientL1.Client().BlockByNumber(ctx, big.NewInt(headerNumber-reorgDepth)) + require.NoError(t, err) + + fmt.Println("Forking from block", reorgBlock.Number(), "on block", headerNumber) + expectedReorgBlocks[reorgBlock.NumberU64()] = struct{}{} + + err = clientL1.Fork(reorgBlock.Hash()) + require.NoError(t, err) + } + } + + // Commit some blocks to ensure reorgs are detected + for i := 0; i < reorgPeriod; i++ { + clientL1.Commit() + } + + fmt.Println("Expected reorg blocks", expectedReorgBlocks) + + for range expectedReorgBlocks { + reorg := <-reorgChan + _, ok := expectedReorgBlocks[reorg.StartBlockHeight-1] + require.True(t, ok, "unexpected reorg starting from", reorg.StartBlockHeight-1) + } +} diff --git a/reorgmonitor/types.go b/reorgmonitor/types.go new file mode 100644 index 00000000..fe6ad782 --- /dev/null +++ b/reorgmonitor/types.go @@ -0,0 +1,367 @@ +package reorgmonitor + +import ( + "fmt" + "sort" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +type BlockOrigin string + +const ( + OriginSubscription BlockOrigin = "Subscription" + OriginGetParent BlockOrigin = "GetParent" + OriginUncle BlockOrigin = "Uncle" +) + +// Reorg is the analysis Summary of a specific reorg +type Reorg struct { + IsFinished bool + SeenLive bool + + StartBlockHeight uint64 // first block in a reorg (block number after common parent) + EndBlockHeight uint64 // last block in a reorg + + Chains map[common.Hash][]*Block + + Depth int + + BlocksInvolved map[common.Hash]*Block + MainChainHash common.Hash + MainChainBlocks map[common.Hash]*Block + NumReplacedBlocks int + + CommonParent *Block + FirstBlockAfterReorg *Block +} + +func NewReorg(parentNode *TreeNode) (*Reorg, error) { + if len(parentNode.Children) < 2 { + return nil, fmt.Errorf("cannot create reorg because parent node with < 2 children") + } + + reorg := Reorg{ + CommonParent: parentNode.Block, + StartBlockHeight: parentNode.Block.Number + 1, + + Chains: make(map[common.Hash][]*Block), + BlocksInvolved: make(map[common.Hash]*Block), + MainChainBlocks: make(map[common.Hash]*Block), + + SeenLive: true, // will be set to false if any of the added blocks was received via uncle-info + } + + // Build the individual chains until the enc, by iterating over children recursively + for _, chainRootNode := range parentNode.Children { + chain := make([]*Block, 0) + + var addChildToChainRecursive func(node *TreeNode) + addChildToChainRecursive = func(node *TreeNode) { + chain = append(chain, node.Block) + + for _, childNode := range node.Children { + addChildToChainRecursive(childNode) + } + } + addChildToChainRecursive(chainRootNode) + reorg.Chains[chainRootNode.Block.Hash] = chain + } + + // Find depth of chains + chainLengths := []int{} + for _, chain := range reorg.Chains { + chainLengths = append(chainLengths, len(chain)) + } + sort.Sort(sort.Reverse(sort.IntSlice(chainLengths))) + + // Depth is number of blocks in second chain + reorg.Depth = chainLengths[1] + + // Truncate the longest chain to the second, which is when the reorg actually stopped + for key, chain := range reorg.Chains { + if len(chain) > reorg.Depth { + reorg.FirstBlockAfterReorg = chain[reorg.Depth] // first block that will be truncated + reorg.Chains[key] = chain[:reorg.Depth] + reorg.MainChainHash = key + } + } + + // If two chains with same height, then the reorg isn't yet finalized + if chainLengths[0] == chainLengths[1] { + reorg.IsFinished = false + } else { + reorg.IsFinished = true + } + + // Build final list of involved blocks, and get end blockheight + for chainHash, chain := range reorg.Chains { + for _, block := range chain { + reorg.BlocksInvolved[block.Hash] = block + + if block.Origin != OriginSubscription && block.Origin != OriginGetParent { + reorg.SeenLive = false + } + + if chainHash == reorg.MainChainHash { + reorg.MainChainBlocks[block.Hash] = block + reorg.EndBlockHeight = block.Number + } + } + } + + reorg.NumReplacedBlocks = len(reorg.BlocksInvolved) - reorg.Depth + + return &reorg, nil +} + +func (r *Reorg) Id() string { + id := fmt.Sprintf("%d_%d_d%d_b%d", r.StartBlockHeight, r.EndBlockHeight, r.Depth, len(r.BlocksInvolved)) + if r.SeenLive { + id += "_l" + } + return id +} + +func (r *Reorg) String() string { + return fmt.Sprintf("Reorg %s: live=%-5v chains=%d, depth=%d, replaced=%d", r.Id(), r.SeenLive, len(r.Chains), r.Depth, r.NumReplacedBlocks) +} + +func (r *Reorg) MermaidSyntax() string { + ret := "stateDiagram-v2\n" + + for _, block := range r.BlocksInvolved { + ret += fmt.Sprintf(" %s --> %s\n", block.ParentHash, block.Hash) + } + + // Add first block after reorg + ret += fmt.Sprintf(" %s --> %s", r.FirstBlockAfterReorg.ParentHash, r.FirstBlockAfterReorg.Hash) + return ret +} + +type TreeNode struct { + Block *Block + Parent *TreeNode + Children []*TreeNode + + IsFirst bool + IsMainChain bool +} + +func NewTreeNode(block *Block, parent *TreeNode) *TreeNode { + return &TreeNode{ + Block: block, + Parent: parent, + Children: []*TreeNode{}, + IsFirst: parent == nil, + } +} + +func (tn *TreeNode) String() string { + return fmt.Sprintf("TreeNode %d %s main=%5v \t first=%5v, %d children", tn.Block.Number, tn.Block.Hash, tn.IsMainChain, tn.IsFirst, len(tn.Children)) +} + +func (tn *TreeNode) AddChild(node *TreeNode) { + tn.Children = append(tn.Children, node) +} + +// TreeAnalysis takes in a BlockTree and collects information about reorgs +type TreeAnalysis struct { + Tree *BlockTree + + StartBlockHeight uint64 // first block number with siblings + EndBlockHeight uint64 + IsSplitOngoing bool + + NumBlocks int + NumBlocksMainChain int + + Reorgs map[string]*Reorg +} + +func NewTreeAnalysis(t *BlockTree) (*TreeAnalysis, error) { + analysis := TreeAnalysis{ + Tree: t, + Reorgs: make(map[string]*Reorg), + } + + if t.FirstNode == nil { // empty analysis for empty tree + return &analysis, nil + } + + analysis.StartBlockHeight = t.FirstNode.Block.Number + analysis.EndBlockHeight = t.LatestNodes[0].Block.Number + + if len(t.LatestNodes) > 1 { + analysis.IsSplitOngoing = true + } + + analysis.NumBlocks = len(t.NodeByHash) + analysis.NumBlocksMainChain = len(t.MainChainNodeByHash) + + // Find reorgs + for _, node := range t.NodeByHash { + if len(node.Children) > 1 { + reorg, err := NewReorg(node) + if err != nil { + return nil, err + } + analysis.Reorgs[reorg.Id()] = reorg + } + } + + return &analysis, nil +} + +func (a *TreeAnalysis) Print() { + fmt.Printf("TreeAnalysis %d - %d, nodes: %d, mainchain: %d, reorgs: %d\n", a.StartBlockHeight, a.EndBlockHeight, a.NumBlocks, a.NumBlocksMainChain, len(a.Reorgs)) + if a.IsSplitOngoing { + fmt.Println("- split ongoing") + } + + for _, reorg := range a.Reorgs { + fmt.Println("") + fmt.Println(reorg.String()) + fmt.Printf("- common parent: %d %s, first block after: %d %s\n", reorg.CommonParent.Number, reorg.CommonParent.Hash, reorg.FirstBlockAfterReorg.Number, reorg.FirstBlockAfterReorg.Hash) + + for chainKey, chain := range reorg.Chains { + if chainKey == reorg.MainChainHash { + fmt.Printf("- mainchain l=%d: ", len(chain)) + } else { + fmt.Printf("- sidechain l=%d: ", len(chain)) + } + for _, block := range chain { + fmt.Printf("%s ", block.Hash) + } + fmt.Print("\n") + } + } +} + +// BlockTree is the tree of blocks, used to traverse up (from children to parents) and down (from parents to children). +// Reorgs start on each node with more than one child. +type BlockTree struct { + FirstNode *TreeNode + LatestNodes []*TreeNode // Nodes at latest blockheight (can be more than 1) + NodeByHash map[common.Hash]*TreeNode + MainChainNodeByHash map[common.Hash]*TreeNode +} + +func NewBlockTree() *BlockTree { + return &BlockTree{ + LatestNodes: []*TreeNode{}, + NodeByHash: make(map[common.Hash]*TreeNode), + MainChainNodeByHash: make(map[common.Hash]*TreeNode), + } +} + +func (t *BlockTree) AddBlock(block *Block) error { + // First block is a special case + if t.FirstNode == nil { + node := NewTreeNode(block, nil) + t.FirstNode = node + t.LatestNodes = []*TreeNode{node} + t.NodeByHash[block.Hash] = node + return nil + } + + // All other blocks are inserted as child of it's parent parent + parent, parentFound := t.NodeByHash[block.ParentHash] + if !parentFound { + err := fmt.Errorf("error in BlockTree.AddBlock(): parent not found. block: %d %s, parent: %s", block.Number, block.Hash, block.ParentHash) + return err + } + + node := NewTreeNode(block, parent) + t.NodeByHash[block.Hash] = node + parent.AddChild(node) + + // Remember nodes at latest block height + if len(t.LatestNodes) == 0 { + t.LatestNodes = []*TreeNode{node} + } else { + if block.Number == t.LatestNodes[0].Block.Number { // add to list of latest nodes! + t.LatestNodes = append(t.LatestNodes, node) + } else if block.Number > t.LatestNodes[0].Block.Number { // replace + t.LatestNodes = []*TreeNode{node} + } + } + + // Mark main-chain nodes as such. Step 1: reset all nodes to non-main-chain + t.MainChainNodeByHash = make(map[common.Hash]*TreeNode) + for _, n := range t.NodeByHash { + n.IsMainChain = false + } + + // Step 2: Traverse backwards and mark main chain. If there's more than 1 nodes at latest height, then we don't yet know which chain will be the main-chain + if len(t.LatestNodes) == 1 { + var TraverseMainChainFromLatestToEarliest func(node *TreeNode) + TraverseMainChainFromLatestToEarliest = func(node *TreeNode) { + if node == nil { + return + } + node.IsMainChain = true + t.MainChainNodeByHash[node.Block.Hash] = node + TraverseMainChainFromLatestToEarliest(node.Parent) + } + TraverseMainChainFromLatestToEarliest(t.LatestNodes[0]) + } + + return nil +} + +func (t *BlockTree) Print() { + fmt.Printf("BlockTree: nodes=%d\n", len(t.NodeByHash)) + + if t.FirstNode == nil { + return + } + + // Print tree by traversing from parent to all children + PrintNodeAndChildren(t.FirstNode, 1) + + // Print latest nodes + fmt.Printf("Latest nodes:\n") + for _, n := range t.LatestNodes { + fmt.Println("-", n.String()) + } +} + +// Block is a geth Block and information about where it came from +type Block struct { + Block *types.Block + Origin BlockOrigin + ObservedUnixTimestamp int64 + + // some helpers + Number uint64 + Hash common.Hash + ParentHash common.Hash +} + +func NewBlock(block *types.Block, origin BlockOrigin) *Block { + return &Block{ + Block: block, + Origin: origin, + ObservedUnixTimestamp: time.Now().UnixNano(), + + Number: block.NumberU64(), + Hash: block.Hash(), + ParentHash: block.ParentHash(), + } +} + +func (block *Block) String() string { + t := time.Unix(int64(block.Block.Time()), 0).UTC() + return fmt.Sprintf("Block %d %s / %s / tx: %4d, uncles: %d", block.Number, block.Hash, t, len(block.Block.Transactions()), len(block.Block.Uncles())) +} + +func PrintNodeAndChildren(node *TreeNode, depth int) { + indent := "-" + fmt.Printf("%s %s\n", indent, node.String()) + for _, childNode := range node.Children { + PrintNodeAndChildren(childNode, depth+1) + } +} From 947b95cde6f7b3355d68f8c01c2cec2477ae216c Mon Sep 17 00:00:00 2001 From: begmaroman Date: Tue, 13 Aug 2024 11:50:57 +0100 Subject: [PATCH 03/43] Removed useless test --- reorgdetector/e2e_test.go | 96 --------------------------------------- 1 file changed, 96 deletions(-) delete mode 100644 reorgdetector/e2e_test.go diff --git a/reorgdetector/e2e_test.go b/reorgdetector/e2e_test.go deleted file mode 100644 index 56f40ecb..00000000 --- a/reorgdetector/e2e_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package reorgdetector_test - -import ( - context "context" - "fmt" - "math/big" - "testing" - "time" - - "github.com/0xPolygon/cdk/reorgdetector" - "github.com/ethereum/go-ethereum/crypto" - "github.com/stretchr/testify/require" - - "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/ethclient/simulated" -) - -func newSimulatedL1(t *testing.T, auth *bind.TransactOpts) *simulated.Backend { - t.Helper() - - balance, _ := new(big.Int).SetString("10000000000000000000000000", 10) //nolint:gomnd - - blockGasLimit := uint64(999999999999999999) //nolint:gomnd - client := simulated.NewBackend(map[common.Address]types.Account{ - auth.From: { - Balance: balance, - }, - }, simulated.WithBlockGasLimit(blockGasLimit)) - client.Commit() - - return client -} - -func TestE2E(t *testing.T) { - 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) - clientL1 := newSimulatedL1(t, authL1) - require.NoError(t, err) - - // Reorg detector - dbPathReorgDetector := t.TempDir() - reorgDetector, err := reorgdetector.New(ctx, clientL1.Client(), dbPathReorgDetector) - require.NoError(t, err) - - /*ch := make(chan *types.Header, 10) - sub, err := clientL1.Client().SubscribeNewHead(ctx, ch) - require.NoError(t, err) - go func() { - for { - select { - case <-ctx.Done(): - return - case <-sub.Err(): - return - case header := <-ch: - err := reorgDetector.AddBlockToTrack(ctx, "test", header.Number.Uint64(), header.Hash()) - require.NoError(t, err) - } - } - }()*/ - - reorgDetector.Start(ctx) - - reorgSubscription, err := reorgDetector.Subscribe("test") - require.NoError(t, err) - - go func() { - firstReorgedBlock := <-reorgSubscription.FirstReorgedBlock - fmt.Println("firstReorgedBlock", firstReorgedBlock) - }() - - for i := 0; i < 20; i++ { - block := clientL1.Commit() - header, err := clientL1.Client().HeaderByHash(ctx, block) - require.NoError(t, err) - err = reorgDetector.AddBlockToTrack(ctx, "test", header.Number.Uint64(), header.Hash()) - require.NoError(t, err) - - // Reorg every 4 blocks with 2 blocks depth - if i%4 == 0 { - reorgBlock, err := clientL1.Client().BlockByNumber(ctx, big.NewInt(int64(i-2))) - require.NoError(t, err) - err = clientL1.Fork(reorgBlock.Hash()) - require.NoError(t, err) - } - - time.Sleep(time.Second) - } -} From ff2cf6d72a5c10f3ba0f6fa4e7aea894d22d4e10 Mon Sep 17 00:00:00 2001 From: begmaroman Date: Tue, 13 Aug 2024 11:51:49 +0100 Subject: [PATCH 04/43] Revert changes --- reorgdetector/reorgdetector.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/reorgdetector/reorgdetector.go b/reorgdetector/reorgdetector.go index a53edad0..c0b0caa2 100644 --- a/reorgdetector/reorgdetector.go +++ b/reorgdetector/reorgdetector.go @@ -112,7 +112,7 @@ func (bm blockMap) getClosestHigherBlock(blockNum uint64) (block, bool) { return sorted[0], true } -// removeRange removes blocks from "from" to "to" +// removeRange removes blocks from from to to func (bm blockMap) removeRange(from, to uint64) { for i := from; i <= to; i++ { delete(bm, i) @@ -391,11 +391,8 @@ func (r *ReorgDetector) addUnfinalisedBlocks(ctx context.Context) { } if previousBlock.Hash == lastBlockFromClient.ParentHash { - unfinalisedBlocksMap[i] = block{ - Num: lastBlockFromClient.Number.Uint64(), - Hash: lastBlockFromClient.Hash(), - } - } else { + unfinalisedBlocksMap[i] = block{Num: lastBlockFromClient.Number.Uint64(), Hash: lastBlockFromClient.Hash()} + } else if previousBlock.Hash != lastBlockFromClient.ParentHash { // reorg happened, we will find out from where exactly and report this to subscribers reorgBlock = i } @@ -537,6 +534,7 @@ func (r *ReorgDetector) saveTrackedBlock(ctx context.Context, id string, b block raw, err := json.Marshal(subscriberBlockMap.getSorted()) if err != nil { + return err } From aa2fd26cc4cd84b870709117a1977da1de3647cb Mon Sep 17 00:00:00 2001 From: begmaroman Date: Tue, 20 Aug 2024 17:40:41 +0100 Subject: [PATCH 05/43] Implementation --- reorgdetector/mock_eth_client.go | 126 +++++++++++++++++- reorgdetector/reorgdetector.go | 8 ++ .../reorgdetectorv2.go | 126 ++++++++++++------ .../reorgdetectorv2_test.go | 44 +++--- {reorgmonitor => reorgdetector}/types.go | 67 ++++------ 5 files changed, 261 insertions(+), 110 deletions(-) rename reorgmonitor/monitor.go => reorgdetector/reorgdetectorv2.go (66%) rename reorgmonitor/monitor_test.go => reorgdetector/reorgdetectorv2_test.go (71%) rename {reorgmonitor => reorgdetector}/types.go (81%) diff --git a/reorgdetector/mock_eth_client.go b/reorgdetector/mock_eth_client.go index e0eef607..a73b1cea 100644 --- a/reorgdetector/mock_eth_client.go +++ b/reorgdetector/mock_eth_client.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.39.0. DO NOT EDIT. +// Code generated by mockery v2.40.1. DO NOT EDIT. package reorgdetector @@ -6,6 +6,10 @@ import ( context "context" big "math/big" + common "github.com/ethereum/go-ethereum/common" + + ethereum "github.com/ethereum/go-ethereum" + mock "github.com/stretchr/testify/mock" types "github.com/ethereum/go-ethereum/core/types" @@ -16,6 +20,66 @@ type EthClientMock struct { mock.Mock } +// BlockByHash provides a mock function with given fields: ctx, hash +func (_m *EthClientMock) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) { + ret := _m.Called(ctx, hash) + + if len(ret) == 0 { + panic("no return value specified for BlockByHash") + } + + var r0 *types.Block + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, common.Hash) (*types.Block, error)); ok { + return rf(ctx, hash) + } + if rf, ok := ret.Get(0).(func(context.Context, common.Hash) *types.Block); ok { + r0 = rf(ctx, hash) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.Block) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, common.Hash) error); ok { + r1 = rf(ctx, hash) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// BlockByNumber provides a mock function with given fields: ctx, number +func (_m *EthClientMock) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) { + ret := _m.Called(ctx, number) + + if len(ret) == 0 { + panic("no return value specified for BlockByNumber") + } + + var r0 *types.Block + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *big.Int) (*types.Block, error)); ok { + return rf(ctx, number) + } + if rf, ok := ret.Get(0).(func(context.Context, *big.Int) *types.Block); ok { + r0 = rf(ctx, number) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.Block) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *big.Int) error); ok { + r1 = rf(ctx, number) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // BlockNumber provides a mock function with given fields: ctx func (_m *EthClientMock) BlockNumber(ctx context.Context) (uint64, error) { ret := _m.Called(ctx) @@ -44,6 +108,36 @@ func (_m *EthClientMock) BlockNumber(ctx context.Context) (uint64, error) { return r0, r1 } +// HeaderByHash provides a mock function with given fields: ctx, hash +func (_m *EthClientMock) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) { + ret := _m.Called(ctx, hash) + + if len(ret) == 0 { + panic("no return value specified for HeaderByHash") + } + + var r0 *types.Header + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, common.Hash) (*types.Header, error)); ok { + return rf(ctx, hash) + } + if rf, ok := ret.Get(0).(func(context.Context, common.Hash) *types.Header); ok { + r0 = rf(ctx, hash) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.Header) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, common.Hash) error); ok { + r1 = rf(ctx, hash) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // HeaderByNumber provides a mock function with given fields: ctx, number func (_m *EthClientMock) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) { ret := _m.Called(ctx, number) @@ -74,6 +168,36 @@ func (_m *EthClientMock) HeaderByNumber(ctx context.Context, number *big.Int) (* return r0, r1 } +// SubscribeNewHead provides a mock function with given fields: ctx, ch +func (_m *EthClientMock) SubscribeNewHead(ctx context.Context, ch chan<- *types.Header) (ethereum.Subscription, error) { + ret := _m.Called(ctx, ch) + + if len(ret) == 0 { + panic("no return value specified for SubscribeNewHead") + } + + var r0 ethereum.Subscription + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, chan<- *types.Header) (ethereum.Subscription, error)); ok { + return rf(ctx, ch) + } + if rf, ok := ret.Get(0).(func(context.Context, chan<- *types.Header) ethereum.Subscription); ok { + r0 = rf(ctx, ch) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(ethereum.Subscription) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, chan<- *types.Header) error); ok { + r1 = rf(ctx, ch) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // NewEthClientMock creates a new instance of EthClientMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. // The first argument is typically a *testing.T value. func NewEthClientMock(t interface { diff --git a/reorgdetector/reorgdetector.go b/reorgdetector/reorgdetector.go index c0b0caa2..9564cb6a 100644 --- a/reorgdetector/reorgdetector.go +++ b/reorgdetector/reorgdetector.go @@ -9,6 +9,8 @@ import ( "sync" "time" + "github.com/ethereum/go-ethereum" + "github.com/0xPolygon/cdk/log" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" @@ -42,6 +44,12 @@ func tableCfgFunc(defaultBuckets kv.TableCfg) kv.TableCfg { } type EthClient interface { + // V2 + SubscribeNewHead(ctx context.Context, ch chan<- *types.Header) (ethereum.Subscription, error) + BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) + BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) + HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) + HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) BlockNumber(ctx context.Context) (uint64, error) } diff --git a/reorgmonitor/monitor.go b/reorgdetector/reorgdetectorv2.go similarity index 66% rename from reorgmonitor/monitor.go rename to reorgdetector/reorgdetectorv2.go index c6cef858..01aa5bba 100644 --- a/reorgmonitor/monitor.go +++ b/reorgdetector/reorgdetectorv2.go @@ -1,23 +1,16 @@ -package reorgmonitor +package reorgdetector import ( "context" "fmt" "log" - "math/big" "sync" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" + types "github.com/ethereum/go-ethereum/core/types" "github.com/pkg/errors" ) -type EthClient interface { - BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) - HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) - BlockNumber(ctx context.Context) (uint64, error) -} - type ReorgMonitor struct { lock sync.Mutex @@ -26,8 +19,9 @@ type ReorgMonitor struct { maxBlocksInCache int lastBlockHeight uint64 - newReorgChan chan<- *Reorg - knownReorgs map[string]uint64 // key: reorgId, value: endBlockNumber + knownReorgs map[string]uint64 // key: reorgId, value: endBlockNumber + subscriptionsLock sync.RWMutex + subscriptions map[string]*Subscription blockByHash map[common.Hash]*Block blocksByHeight map[uint64]map[common.Hash]*Block @@ -36,27 +30,83 @@ type ReorgMonitor struct { LatestBlockNumber uint64 } -func NewReorgMonitor(client EthClient, reorgChan chan<- *Reorg, maxBlocks int) *ReorgMonitor { +func NewReorgMonitor(client EthClient, maxBlocks int) *ReorgMonitor { return &ReorgMonitor{ client: client, maxBlocksInCache: maxBlocks, blockByHash: make(map[common.Hash]*Block), blocksByHeight: make(map[uint64]map[common.Hash]*Block), - newReorgChan: reorgChan, knownReorgs: make(map[string]uint64), + subscriptions: make(map[string]*Subscription), + } +} + +func (mon *ReorgMonitor) Start(ctx context.Context) error { + // Add head tracker + ch := make(chan *types.Header, 100) + sub, err := mon.client.SubscribeNewHead(ctx, ch) + if err != nil { + return err + } + + go func() { + for { + select { + case <-ctx.Done(): + return + case <-sub.Err(): + return + case header := <-ch: + if err = mon.onNewHeader(header); err != nil { + log.Println(err) + continue + } + } + } + }() + + return nil +} + +func (mon *ReorgMonitor) Subscribe(id string) (*Subscription, error) { + mon.subscriptionsLock.Lock() + defer mon.subscriptionsLock.Unlock() + + if sub, ok := mon.subscriptions[id]; ok { + return sub, nil + } + + sub := &Subscription{ + FirstReorgedBlock: make(chan uint64), + ReorgProcessed: make(chan bool), + } + mon.subscriptions[id] = sub + + return sub, nil +} + +func (mon *ReorgMonitor) AddBlockToTrack(ctx context.Context, id string, blockNum uint64, blockHash common.Hash) error { + return nil +} + +func (mon *ReorgMonitor) notifySubscribers(reorg *Reorg) { + // TODO: Add wg group + mon.subscriptionsLock.Lock() + for _, sub := range mon.subscriptions { + sub.FirstReorgedBlock <- reorg.StartBlockHeight + <-sub.ReorgProcessed } + mon.subscriptionsLock.Unlock() } -// AddBlockToTrack adds a block to the monitor, and sends it to the monitor's channel. -// After adding a new block, a reorg check takes place. If a new completed reorg is detected, it is sent to the channel. -func (mon *ReorgMonitor) AddBlockToTrack(block *Block) error { +func (mon *ReorgMonitor) onNewHeader(header *types.Header) error { mon.lock.Lock() defer mon.lock.Unlock() - mon.addBlock(block) + mon.addBlock(NewBlock(header, OriginSubscription)) // Do nothing if block is at previous height - if block.Number == mon.lastBlockHeight { + if header.Number.Uint64() == mon.lastBlockHeight { return nil } @@ -65,7 +115,7 @@ func (mon *ReorgMonitor) AddBlockToTrack(block *Block) error { } // Analyze blocks once a new height has been reached - mon.lastBlockHeight = block.Number + mon.lastBlockHeight = header.Number.Uint64() anal, err := mon.AnalyzeTree(0, 2) if err != nil { @@ -80,7 +130,7 @@ func (mon *ReorgMonitor) AddBlockToTrack(block *Block) error { // Send new finished reorgs to channel if _, isKnownReorg := mon.knownReorgs[reorg.Id()]; !isKnownReorg { mon.knownReorgs[reorg.Id()] = reorg.EndBlockHeight - mon.newReorgChan <- reorg + go mon.notifySubscribers(reorg) } } @@ -92,43 +142,39 @@ func (mon *ReorgMonitor) addBlock(block *Block) bool { defer mon.trimCache() // If known, then only overwrite if known was by uncle - knownBlock, isKnown := mon.blockByHash[block.Hash] + knownBlock, isKnown := mon.blockByHash[block.Header.Hash()] if isKnown && knownBlock.Origin != OriginUncle { return false } // Only accept blocks that are after the earliest known (some nodes might be further back) - if block.Number < mon.EarliestBlockNumber { + if block.Header.Number.Uint64() < mon.EarliestBlockNumber { return false } - // Print - blockInfo := fmt.Sprintf("Add%s \t %-12s \t %s", block.String(), block.Origin, mon) - log.Println(blockInfo) - // Add for access by hash - mon.blockByHash[block.Hash] = block + mon.blockByHash[block.Header.Hash()] = block // Create array of blocks at this height, if necessary - if _, found := mon.blocksByHeight[block.Number]; !found { - mon.blocksByHeight[block.Number] = make(map[common.Hash]*Block) + if _, found := mon.blocksByHeight[block.Header.Number.Uint64()]; !found { + mon.blocksByHeight[block.Header.Number.Uint64()] = make(map[common.Hash]*Block) } // Add to map of blocks at this height - mon.blocksByHeight[block.Number][block.Hash] = block + mon.blocksByHeight[block.Header.Number.Uint64()][block.Header.Hash()] = block // Set earliest block - if mon.EarliestBlockNumber == 0 || block.Number < mon.EarliestBlockNumber { - mon.EarliestBlockNumber = block.Number + if mon.EarliestBlockNumber == 0 || block.Header.Number.Uint64() < mon.EarliestBlockNumber { + mon.EarliestBlockNumber = block.Header.Number.Uint64() } // Set latest block - if block.Number > mon.LatestBlockNumber { - mon.LatestBlockNumber = block.Number + if block.Header.Number.Uint64() > mon.LatestBlockNumber { + mon.LatestBlockNumber = block.Header.Number.Uint64() } // Check if further blocks can be downloaded from this one - if block.Number > mon.EarliestBlockNumber { // check backhistory only if we are past the earliest block + if block.Header.Number.Uint64() > mon.EarliestBlockNumber { // check backhistory only if we are past the earliest block err := mon.checkBlockForReferences(block) if err != nil { log.Println(err) @@ -171,23 +217,23 @@ func (mon *ReorgMonitor) trimCache() { func (mon *ReorgMonitor) checkBlockForReferences(block *Block) error { // Check parent - _, found := mon.blockByHash[block.ParentHash] + _, found := mon.blockByHash[block.Header.ParentHash] if !found { // fmt.Printf("- parent of %d %s not found (%s), downloading...\n", block.Number, block.Hash, block.ParentHash) - _, _, err := mon.ensureBlock(block.ParentHash, OriginGetParent) + _, _, err := mon.ensureBlock(block.Header.ParentHash, OriginGetParent) if err != nil { return errors.Wrap(err, "get-parent error") } } // Check uncles - for _, uncleHeader := range block.Block.Uncles() { + /*for _, uncleHeader := range block.Block.Uncles() { // fmt.Printf("- block %d %s has uncle: %s\n", block.Number, block.Hash, uncleHeader.Hash()) _, _, err := mon.ensureBlock(uncleHeader.Hash(), OriginUncle) if err != nil { return errors.Wrap(err, "get-uncle error") } - } + }*/ // ro.DebugPrintln(fmt.Sprintf("- added block %d %s", block.NumberU64(), block.Hash())) return nil @@ -202,7 +248,7 @@ func (mon *ReorgMonitor) ensureBlock(blockHash common.Hash, origin BlockOrigin) } fmt.Printf("- block %s (%s) not found, downloading from...\n", blockHash, origin) - ethBlock, err := mon.client.BlockByHash(context.Background(), blockHash) + ethBlock, err := mon.client.HeaderByHash(context.Background(), blockHash) if err != nil { fmt.Println("- err block not found:", blockHash, err) // todo: try other clients msg := fmt.Sprintf("EnsureBlock error for hash %s", blockHash) diff --git a/reorgmonitor/monitor_test.go b/reorgdetector/reorgdetectorv2_test.go similarity index 71% rename from reorgmonitor/monitor_test.go rename to reorgdetector/reorgdetectorv2_test.go index 183076ea..35436062 100644 --- a/reorgmonitor/monitor_test.go +++ b/reorgdetector/reorgdetectorv2_test.go @@ -1,4 +1,4 @@ -package reorgmonitor +package reorgdetector import ( "context" @@ -31,7 +31,7 @@ func newSimulatedL1(t *testing.T, auth *bind.TransactOpts) *simulated.Backend { return client } -func Test_ReorgMonitor(t *testing.T) { +func Test_ReorgDetectorV2(t *testing.T) { const produceBlocks = 29 const reorgPeriod = 5 const reorgDepth = 2 @@ -46,29 +46,13 @@ func Test_ReorgMonitor(t *testing.T) { clientL1 := newSimulatedL1(t, authL1) require.NoError(t, err) - reorgChan := make(chan *Reorg, 100) - mon := NewReorgMonitor(clientL1.Client(), reorgChan, 100) + mon := NewReorgMonitor(clientL1.Client(), 100) - // Add head tracker - ch := make(chan *types.Header, 100) - sub, err := clientL1.Client().SubscribeNewHead(ctx, ch) + sub, err := mon.Subscribe("test") + require.NoError(t, err) + + err = mon.Start(context.Background()) require.NoError(t, err) - go func() { - for { - select { - case <-ctx.Done(): - return - case <-sub.Err(): - return - case header := <-ch: - block, err := clientL1.Client().BlockByNumber(ctx, header.Number) - require.NoError(t, err) - - err = mon.AddBlockToTrack(NewBlock(block, OriginSubscription)) - require.NoError(t, err) - } - } - }() expectedReorgBlocks := make(map[uint64]struct{}) lastReorgOn := int64(0) @@ -87,7 +71,6 @@ func Test_ReorgMonitor(t *testing.T) { reorgBlock, err := clientL1.Client().BlockByNumber(ctx, big.NewInt(headerNumber-reorgDepth)) require.NoError(t, err) - fmt.Println("Forking from block", reorgBlock.Number(), "on block", headerNumber) expectedReorgBlocks[reorgBlock.NumberU64()] = struct{}{} err = clientL1.Fork(reorgBlock.Hash()) @@ -102,9 +85,16 @@ func Test_ReorgMonitor(t *testing.T) { fmt.Println("Expected reorg blocks", expectedReorgBlocks) + for firstReorgedBlock := range sub.FirstReorgedBlock { + sub.ReorgProcessed <- true + fmt.Println("reorg", firstReorgedBlock) + } + for range expectedReorgBlocks { - reorg := <-reorgChan - _, ok := expectedReorgBlocks[reorg.StartBlockHeight-1] - require.True(t, ok, "unexpected reorg starting from", reorg.StartBlockHeight-1) + //reorg := <-sub.FirstReorgedBlock + //sub.ReorgProcessed <- true + //fmt.Println("reorg", reorg) + //_, ok := expectedReorgBlocks[reorg.StartBlockHeight-1] + //require.True(t, ok, "unexpected reorg starting from", reorg.StartBlockHeight-1) } } diff --git a/reorgmonitor/types.go b/reorgdetector/types.go similarity index 81% rename from reorgmonitor/types.go rename to reorgdetector/types.go index fe6ad782..0d7b6179 100644 --- a/reorgmonitor/types.go +++ b/reorgdetector/types.go @@ -1,9 +1,8 @@ -package reorgmonitor +package reorgdetector import ( "fmt" "sort" - "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" @@ -45,7 +44,7 @@ func NewReorg(parentNode *TreeNode) (*Reorg, error) { reorg := Reorg{ CommonParent: parentNode.Block, - StartBlockHeight: parentNode.Block.Number + 1, + StartBlockHeight: parentNode.Block.Header.Number.Uint64() + 1, Chains: make(map[common.Hash][]*Block), BlocksInvolved: make(map[common.Hash]*Block), @@ -67,7 +66,7 @@ func NewReorg(parentNode *TreeNode) (*Reorg, error) { } } addChildToChainRecursive(chainRootNode) - reorg.Chains[chainRootNode.Block.Hash] = chain + reorg.Chains[chainRootNode.Block.Header.Hash()] = chain } // Find depth of chains @@ -99,15 +98,15 @@ func NewReorg(parentNode *TreeNode) (*Reorg, error) { // Build final list of involved blocks, and get end blockheight for chainHash, chain := range reorg.Chains { for _, block := range chain { - reorg.BlocksInvolved[block.Hash] = block + reorg.BlocksInvolved[block.Header.Hash()] = block if block.Origin != OriginSubscription && block.Origin != OriginGetParent { reorg.SeenLive = false } if chainHash == reorg.MainChainHash { - reorg.MainChainBlocks[block.Hash] = block - reorg.EndBlockHeight = block.Number + reorg.MainChainBlocks[block.Header.Hash()] = block + reorg.EndBlockHeight = block.Header.Number.Uint64() } } } @@ -133,11 +132,11 @@ func (r *Reorg) MermaidSyntax() string { ret := "stateDiagram-v2\n" for _, block := range r.BlocksInvolved { - ret += fmt.Sprintf(" %s --> %s\n", block.ParentHash, block.Hash) + ret += fmt.Sprintf(" %s --> %s\n", block.Header.ParentHash, block.Header.Hash()) } // Add first block after reorg - ret += fmt.Sprintf(" %s --> %s", r.FirstBlockAfterReorg.ParentHash, r.FirstBlockAfterReorg.Hash) + ret += fmt.Sprintf(" %s --> %s", r.FirstBlockAfterReorg.Header.ParentHash, r.FirstBlockAfterReorg.Header.Hash()) return ret } @@ -160,7 +159,7 @@ func NewTreeNode(block *Block, parent *TreeNode) *TreeNode { } func (tn *TreeNode) String() string { - return fmt.Sprintf("TreeNode %d %s main=%5v \t first=%5v, %d children", tn.Block.Number, tn.Block.Hash, tn.IsMainChain, tn.IsFirst, len(tn.Children)) + return fmt.Sprintf("TreeNode %d %s main=%5v \t first=%5v, %d children", tn.Block.Header.Number.Uint64(), tn.Block.Header.Hash(), tn.IsMainChain, tn.IsFirst, len(tn.Children)) } func (tn *TreeNode) AddChild(node *TreeNode) { @@ -191,8 +190,8 @@ func NewTreeAnalysis(t *BlockTree) (*TreeAnalysis, error) { return &analysis, nil } - analysis.StartBlockHeight = t.FirstNode.Block.Number - analysis.EndBlockHeight = t.LatestNodes[0].Block.Number + analysis.StartBlockHeight = t.FirstNode.Block.Header.Number.Uint64() + analysis.EndBlockHeight = t.LatestNodes[0].Block.Header.Number.Uint64() if len(t.LatestNodes) > 1 { analysis.IsSplitOngoing = true @@ -224,7 +223,7 @@ func (a *TreeAnalysis) Print() { for _, reorg := range a.Reorgs { fmt.Println("") fmt.Println(reorg.String()) - fmt.Printf("- common parent: %d %s, first block after: %d %s\n", reorg.CommonParent.Number, reorg.CommonParent.Hash, reorg.FirstBlockAfterReorg.Number, reorg.FirstBlockAfterReorg.Hash) + fmt.Printf("- common parent: %d %s, first block after: %d %s\n", reorg.CommonParent.Header.Number.Uint64(), reorg.CommonParent.Header.Hash(), reorg.FirstBlockAfterReorg.Header.Number.Uint64(), reorg.FirstBlockAfterReorg.Header.Hash()) for chainKey, chain := range reorg.Chains { if chainKey == reorg.MainChainHash { @@ -233,7 +232,7 @@ func (a *TreeAnalysis) Print() { fmt.Printf("- sidechain l=%d: ", len(chain)) } for _, block := range chain { - fmt.Printf("%s ", block.Hash) + fmt.Printf("%s ", block.Header.Hash()) } fmt.Print("\n") } @@ -263,28 +262,28 @@ func (t *BlockTree) AddBlock(block *Block) error { node := NewTreeNode(block, nil) t.FirstNode = node t.LatestNodes = []*TreeNode{node} - t.NodeByHash[block.Hash] = node + t.NodeByHash[block.Header.Hash()] = node return nil } // All other blocks are inserted as child of it's parent parent - parent, parentFound := t.NodeByHash[block.ParentHash] + parent, parentFound := t.NodeByHash[block.Header.ParentHash] if !parentFound { - err := fmt.Errorf("error in BlockTree.AddBlock(): parent not found. block: %d %s, parent: %s", block.Number, block.Hash, block.ParentHash) + err := fmt.Errorf("error in BlockTree.AddBlock(): parent not found. block: %d %s, parent: %s", block.Header.Number.Uint64(), block.Header.Hash(), block.Header.ParentHash) return err } node := NewTreeNode(block, parent) - t.NodeByHash[block.Hash] = node + t.NodeByHash[block.Header.Hash()] = node parent.AddChild(node) // Remember nodes at latest block height if len(t.LatestNodes) == 0 { t.LatestNodes = []*TreeNode{node} } else { - if block.Number == t.LatestNodes[0].Block.Number { // add to list of latest nodes! + if block.Header.Number.Uint64() == t.LatestNodes[0].Block.Header.Number.Uint64() { // add to list of latest nodes! t.LatestNodes = append(t.LatestNodes, node) - } else if block.Number > t.LatestNodes[0].Block.Number { // replace + } else if block.Header.Number.Uint64() > t.LatestNodes[0].Block.Header.Number.Uint64() { // replace t.LatestNodes = []*TreeNode{node} } } @@ -303,7 +302,7 @@ func (t *BlockTree) AddBlock(block *Block) error { return } node.IsMainChain = true - t.MainChainNodeByHash[node.Block.Hash] = node + t.MainChainNodeByHash[node.Block.Header.Hash()] = node TraverseMainChainFromLatestToEarliest(node.Parent) } TraverseMainChainFromLatestToEarliest(t.LatestNodes[0]) @@ -331,33 +330,17 @@ func (t *BlockTree) Print() { // Block is a geth Block and information about where it came from type Block struct { - Block *types.Block - Origin BlockOrigin - ObservedUnixTimestamp int64 - - // some helpers - Number uint64 - Hash common.Hash - ParentHash common.Hash + Header *types.Header + Origin BlockOrigin } -func NewBlock(block *types.Block, origin BlockOrigin) *Block { +func NewBlock(header *types.Header, origin BlockOrigin) *Block { return &Block{ - Block: block, - Origin: origin, - ObservedUnixTimestamp: time.Now().UnixNano(), - - Number: block.NumberU64(), - Hash: block.Hash(), - ParentHash: block.ParentHash(), + Header: header, + Origin: origin, } } -func (block *Block) String() string { - t := time.Unix(int64(block.Block.Time()), 0).UTC() - return fmt.Sprintf("Block %d %s / %s / tx: %4d, uncles: %d", block.Number, block.Hash, t, len(block.Block.Transactions()), len(block.Block.Uncles())) -} - func PrintNodeAndChildren(node *TreeNode, depth int) { indent := "-" fmt.Printf("%s %s\n", indent, node.String()) From ac4cbe417c70aa0979c655849aefd4415f963650 Mon Sep 17 00:00:00 2001 From: begmaroman Date: Tue, 20 Aug 2024 17:45:38 +0100 Subject: [PATCH 06/43] Implementation --- reorgdetector/reorgdetectorv2.go | 22 +++++++++++----------- reorgdetector/types.go | 21 ++++++++++++--------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/reorgdetector/reorgdetectorv2.go b/reorgdetector/reorgdetectorv2.go index 01aa5bba..2b9879d4 100644 --- a/reorgdetector/reorgdetectorv2.go +++ b/reorgdetector/reorgdetectorv2.go @@ -142,13 +142,13 @@ func (mon *ReorgMonitor) addBlock(block *Block) bool { defer mon.trimCache() // If known, then only overwrite if known was by uncle - knownBlock, isKnown := mon.blockByHash[block.Header.Hash()] - if isKnown && knownBlock.Origin != OriginUncle { + _, isKnown := mon.blockByHash[block.Header.Hash()] + if isKnown { return false } // Only accept blocks that are after the earliest known (some nodes might be further back) - if block.Header.Number.Uint64() < mon.EarliestBlockNumber { + if block.Number() < mon.EarliestBlockNumber { return false } @@ -156,25 +156,25 @@ func (mon *ReorgMonitor) addBlock(block *Block) bool { mon.blockByHash[block.Header.Hash()] = block // Create array of blocks at this height, if necessary - if _, found := mon.blocksByHeight[block.Header.Number.Uint64()]; !found { - mon.blocksByHeight[block.Header.Number.Uint64()] = make(map[common.Hash]*Block) + if _, found := mon.blocksByHeight[block.Number()]; !found { + mon.blocksByHeight[block.Number()] = make(map[common.Hash]*Block) } // Add to map of blocks at this height - mon.blocksByHeight[block.Header.Number.Uint64()][block.Header.Hash()] = block + mon.blocksByHeight[block.Number()][block.Header.Hash()] = block // Set earliest block - if mon.EarliestBlockNumber == 0 || block.Header.Number.Uint64() < mon.EarliestBlockNumber { - mon.EarliestBlockNumber = block.Header.Number.Uint64() + if mon.EarliestBlockNumber == 0 || block.Number() < mon.EarliestBlockNumber { + mon.EarliestBlockNumber = block.Number() } // Set latest block - if block.Header.Number.Uint64() > mon.LatestBlockNumber { - mon.LatestBlockNumber = block.Header.Number.Uint64() + if block.Number() > mon.LatestBlockNumber { + mon.LatestBlockNumber = block.Number() } // Check if further blocks can be downloaded from this one - if block.Header.Number.Uint64() > mon.EarliestBlockNumber { // check backhistory only if we are past the earliest block + if block.Number() > mon.EarliestBlockNumber { // check backhistory only if we are past the earliest block err := mon.checkBlockForReferences(block) if err != nil { log.Println(err) diff --git a/reorgdetector/types.go b/reorgdetector/types.go index 0d7b6179..ab2c2fa3 100644 --- a/reorgdetector/types.go +++ b/reorgdetector/types.go @@ -13,7 +13,6 @@ type BlockOrigin string const ( OriginSubscription BlockOrigin = "Subscription" OriginGetParent BlockOrigin = "GetParent" - OriginUncle BlockOrigin = "Uncle" ) // Reorg is the analysis Summary of a specific reorg @@ -44,7 +43,7 @@ func NewReorg(parentNode *TreeNode) (*Reorg, error) { reorg := Reorg{ CommonParent: parentNode.Block, - StartBlockHeight: parentNode.Block.Header.Number.Uint64() + 1, + StartBlockHeight: parentNode.Block.Number() + 1, Chains: make(map[common.Hash][]*Block), BlocksInvolved: make(map[common.Hash]*Block), @@ -106,7 +105,7 @@ func NewReorg(parentNode *TreeNode) (*Reorg, error) { if chainHash == reorg.MainChainHash { reorg.MainChainBlocks[block.Header.Hash()] = block - reorg.EndBlockHeight = block.Header.Number.Uint64() + reorg.EndBlockHeight = block.Number() } } } @@ -159,7 +158,7 @@ func NewTreeNode(block *Block, parent *TreeNode) *TreeNode { } func (tn *TreeNode) String() string { - return fmt.Sprintf("TreeNode %d %s main=%5v \t first=%5v, %d children", tn.Block.Header.Number.Uint64(), tn.Block.Header.Hash(), tn.IsMainChain, tn.IsFirst, len(tn.Children)) + return fmt.Sprintf("TreeNode %d %s main=%5v \t first=%5v, %d children", tn.Block.Number(), tn.Block.Header.Hash(), tn.IsMainChain, tn.IsFirst, len(tn.Children)) } func (tn *TreeNode) AddChild(node *TreeNode) { @@ -190,8 +189,8 @@ func NewTreeAnalysis(t *BlockTree) (*TreeAnalysis, error) { return &analysis, nil } - analysis.StartBlockHeight = t.FirstNode.Block.Header.Number.Uint64() - analysis.EndBlockHeight = t.LatestNodes[0].Block.Header.Number.Uint64() + analysis.StartBlockHeight = t.FirstNode.Block.Number() + analysis.EndBlockHeight = t.LatestNodes[0].Block.Number() if len(t.LatestNodes) > 1 { analysis.IsSplitOngoing = true @@ -269,7 +268,7 @@ func (t *BlockTree) AddBlock(block *Block) error { // All other blocks are inserted as child of it's parent parent parent, parentFound := t.NodeByHash[block.Header.ParentHash] if !parentFound { - err := fmt.Errorf("error in BlockTree.AddBlock(): parent not found. block: %d %s, parent: %s", block.Header.Number.Uint64(), block.Header.Hash(), block.Header.ParentHash) + err := fmt.Errorf("error in BlockTree.AddBlock(): parent not found. block: %d %s, parent: %s", block.Number(), block.Header.Hash(), block.Header.ParentHash) return err } @@ -281,9 +280,9 @@ func (t *BlockTree) AddBlock(block *Block) error { if len(t.LatestNodes) == 0 { t.LatestNodes = []*TreeNode{node} } else { - if block.Header.Number.Uint64() == t.LatestNodes[0].Block.Header.Number.Uint64() { // add to list of latest nodes! + if block.Number() == t.LatestNodes[0].Block.Number() { // add to list of latest nodes! t.LatestNodes = append(t.LatestNodes, node) - } else if block.Header.Number.Uint64() > t.LatestNodes[0].Block.Header.Number.Uint64() { // replace + } else if block.Number() > t.LatestNodes[0].Block.Number() { // replace t.LatestNodes = []*TreeNode{node} } } @@ -341,6 +340,10 @@ func NewBlock(header *types.Header, origin BlockOrigin) *Block { } } +func (b *Block) Number() uint64 { + return b.Header.Number.Uint64() +} + func PrintNodeAndChildren(node *TreeNode, depth int) { indent := "-" fmt.Printf("%s %s\n", indent, node.String()) From 9517b4b9f2b36e8124364a450d95ca18a3b0463f Mon Sep 17 00:00:00 2001 From: begmaroman Date: Tue, 20 Aug 2024 18:00:03 +0100 Subject: [PATCH 07/43] Implementation --- cmd/run.go | 6 +- reorgdetector/mock_eth_client.go | 88 --------------------------- reorgdetector/reorgdetector.go | 5 -- reorgdetector/reorgdetectorv2.go | 27 +++++--- reorgdetector/reorgdetectorv2_test.go | 25 ++++---- 5 files changed, 34 insertions(+), 117 deletions(-) diff --git a/cmd/run.go b/cmd/run.go index 7f9ed6ed..f2147760 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -27,6 +27,7 @@ import ( "github.com/0xPolygon/cdk/sequencesender/txbuilder" "github.com/0xPolygon/cdk/state" "github.com/0xPolygon/cdk/state/pgstatestorage" + "github.com/0xPolygon/cdk/sync" "github.com/0xPolygon/cdk/translator" ethtxman "github.com/0xPolygonHermez/zkevm-ethtx-manager/etherman" "github.com/0xPolygonHermez/zkevm-ethtx-manager/etherman/etherscan" @@ -75,7 +76,8 @@ func start(cliCtx *cli.Context) error { if err != nil { log.Fatal(err) } - reorgDetector := newReorgDetectorL1(cliCtx.Context, *c, l1Client) + //reorgDetector := newReorgDetectorL1(cliCtx.Context, *c, l1Client) + reorgDetector := reorgdetector.NewReorgMonitor(l1Client, 100) go reorgDetector.Start(cliCtx.Context) syncer := newL1InfoTreeSyncer(cliCtx.Context, *c, l1Client, reorgDetector) go syncer.Start(cliCtx.Context) @@ -376,7 +378,7 @@ func newL1InfoTreeSyncer( ctx context.Context, cfg config.Config, l1Client *ethclient.Client, - reorgDetector *reorgdetector.ReorgDetector, + reorgDetector sync.ReorgDetector, ) *l1infotreesync.L1InfoTreeSync { syncer, err := l1infotreesync.New( ctx, diff --git a/reorgdetector/mock_eth_client.go b/reorgdetector/mock_eth_client.go index a73b1cea..a76c62f9 100644 --- a/reorgdetector/mock_eth_client.go +++ b/reorgdetector/mock_eth_client.go @@ -20,94 +20,6 @@ type EthClientMock struct { mock.Mock } -// BlockByHash provides a mock function with given fields: ctx, hash -func (_m *EthClientMock) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) { - ret := _m.Called(ctx, hash) - - if len(ret) == 0 { - panic("no return value specified for BlockByHash") - } - - var r0 *types.Block - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, common.Hash) (*types.Block, error)); ok { - return rf(ctx, hash) - } - if rf, ok := ret.Get(0).(func(context.Context, common.Hash) *types.Block); ok { - r0 = rf(ctx, hash) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*types.Block) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, common.Hash) error); ok { - r1 = rf(ctx, hash) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// BlockByNumber provides a mock function with given fields: ctx, number -func (_m *EthClientMock) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) { - ret := _m.Called(ctx, number) - - if len(ret) == 0 { - panic("no return value specified for BlockByNumber") - } - - var r0 *types.Block - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, *big.Int) (*types.Block, error)); ok { - return rf(ctx, number) - } - if rf, ok := ret.Get(0).(func(context.Context, *big.Int) *types.Block); ok { - r0 = rf(ctx, number) - } else { - if ret.Get(0) != nil { - r0 = ret.Get(0).(*types.Block) - } - } - - if rf, ok := ret.Get(1).(func(context.Context, *big.Int) error); ok { - r1 = rf(ctx, number) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - -// BlockNumber provides a mock function with given fields: ctx -func (_m *EthClientMock) BlockNumber(ctx context.Context) (uint64, error) { - ret := _m.Called(ctx) - - if len(ret) == 0 { - panic("no return value specified for BlockNumber") - } - - var r0 uint64 - var r1 error - if rf, ok := ret.Get(0).(func(context.Context) (uint64, error)); ok { - return rf(ctx) - } - if rf, ok := ret.Get(0).(func(context.Context) uint64); ok { - r0 = rf(ctx) - } else { - r0 = ret.Get(0).(uint64) - } - - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // HeaderByHash provides a mock function with given fields: ctx, hash func (_m *EthClientMock) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) { ret := _m.Called(ctx, hash) diff --git a/reorgdetector/reorgdetector.go b/reorgdetector/reorgdetector.go index 9564cb6a..d1737ef3 100644 --- a/reorgdetector/reorgdetector.go +++ b/reorgdetector/reorgdetector.go @@ -44,14 +44,9 @@ func tableCfgFunc(defaultBuckets kv.TableCfg) kv.TableCfg { } type EthClient interface { - // V2 SubscribeNewHead(ctx context.Context, ch chan<- *types.Header) (ethereum.Subscription, error) - BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) - BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) - HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) - BlockNumber(ctx context.Context) (uint64, error) } type block struct { diff --git a/reorgdetector/reorgdetectorv2.go b/reorgdetector/reorgdetectorv2.go index 2b9879d4..456614cb 100644 --- a/reorgdetector/reorgdetectorv2.go +++ b/reorgdetector/reorgdetectorv2.go @@ -7,7 +7,7 @@ import ( "sync" "github.com/ethereum/go-ethereum/common" - types "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/types" "github.com/pkg/errors" ) @@ -86,17 +86,29 @@ func (mon *ReorgMonitor) Subscribe(id string) (*Subscription, error) { } func (mon *ReorgMonitor) AddBlockToTrack(ctx context.Context, id string, blockNum uint64, blockHash common.Hash) error { + mon.subscriptionsLock.RLock() + if sub, ok := mon.subscriptions[id]; !ok { + mon.subscriptionsLock.RUnlock() + return ErrNotSubscribed + } else { + // In case there are reorgs being processed, wait + // Note that this also makes any addition to trackedBlocks[id] safe + sub.pendingReorgsToBeProcessed.Wait() + } + mon.subscriptionsLock.RUnlock() + return nil } func (mon *ReorgMonitor) notifySubscribers(reorg *Reorg) { - // TODO: Add wg group - mon.subscriptionsLock.Lock() + mon.subscriptionsLock.RLock() for _, sub := range mon.subscriptions { + sub.pendingReorgsToBeProcessed.Add(1) sub.FirstReorgedBlock <- reorg.StartBlockHeight <-sub.ReorgProcessed + sub.pendingReorgsToBeProcessed.Done() } - mon.subscriptionsLock.Unlock() + mon.subscriptionsLock.RUnlock() } func (mon *ReorgMonitor) onNewHeader(header *types.Header) error { @@ -250,9 +262,7 @@ func (mon *ReorgMonitor) ensureBlock(blockHash common.Hash, origin BlockOrigin) fmt.Printf("- block %s (%s) not found, downloading from...\n", blockHash, origin) ethBlock, err := mon.client.HeaderByHash(context.Background(), blockHash) if err != nil { - fmt.Println("- err block not found:", blockHash, err) // todo: try other clients - msg := fmt.Sprintf("EnsureBlock error for hash %s", blockHash) - return nil, false, errors.Wrap(err, msg) + return nil, false, errors.Wrapf(err, "EnsureBlock error for hash %s", blockHash) } block = NewBlock(ethBlock, origin) @@ -279,8 +289,7 @@ func (mon *ReorgMonitor) AnalyzeTree(maxBlocks, distanceToLastBlockHeight uint64 for height := startBlockNumber; height <= endBlockNumber; height++ { numBlocksAtHeight := len(mon.blocksByHeight[height]) if numBlocksAtHeight == 0 { - err := fmt.Errorf("error in monitor.AnalyzeTree: no blocks at height %d", height) - return nil, err + return nil, fmt.Errorf("error in monitor.AnalyzeTree: no blocks at height %d", height) } // Start tree only when 1 block at this height. If more blocks then skip. diff --git a/reorgdetector/reorgdetectorv2_test.go b/reorgdetector/reorgdetectorv2_test.go index 35436062..712b156a 100644 --- a/reorgdetector/reorgdetectorv2_test.go +++ b/reorgdetector/reorgdetectorv2_test.go @@ -2,7 +2,6 @@ package reorgdetector import ( "context" - "fmt" "math/big" "testing" "time" @@ -54,7 +53,7 @@ func Test_ReorgDetectorV2(t *testing.T) { err = mon.Start(context.Background()) require.NoError(t, err) - expectedReorgBlocks := make(map[uint64]struct{}) + expectedReorgBlocks := make(map[uint64]bool) lastReorgOn := int64(0) for i := 1; lastReorgOn <= produceBlocks; i++ { block := clientL1.Commit() @@ -71,7 +70,7 @@ func Test_ReorgDetectorV2(t *testing.T) { reorgBlock, err := clientL1.Client().BlockByNumber(ctx, big.NewInt(headerNumber-reorgDepth)) require.NoError(t, err) - expectedReorgBlocks[reorgBlock.NumberU64()] = struct{}{} + expectedReorgBlocks[reorgBlock.NumberU64()] = false err = clientL1.Fork(reorgBlock.Hash()) require.NoError(t, err) @@ -83,18 +82,18 @@ func Test_ReorgDetectorV2(t *testing.T) { clientL1.Commit() } - fmt.Println("Expected reorg blocks", expectedReorgBlocks) - - for firstReorgedBlock := range sub.FirstReorgedBlock { + for range expectedReorgBlocks { + firstReorgedBlock := <-sub.FirstReorgedBlock sub.ReorgProcessed <- true - fmt.Println("reorg", firstReorgedBlock) + + processed, ok := expectedReorgBlocks[firstReorgedBlock-1] + require.True(t, ok) + require.False(t, processed) + + expectedReorgBlocks[firstReorgedBlock-1] = true } - for range expectedReorgBlocks { - //reorg := <-sub.FirstReorgedBlock - //sub.ReorgProcessed <- true - //fmt.Println("reorg", reorg) - //_, ok := expectedReorgBlocks[reorg.StartBlockHeight-1] - //require.True(t, ok, "unexpected reorg starting from", reorg.StartBlockHeight-1) + for _, processed := range expectedReorgBlocks { + require.True(t, processed) } } From 52557ef97d11d0aaa42b4b9553b82b8dfc2ef75b Mon Sep 17 00:00:00 2001 From: begmaroman Date: Tue, 20 Aug 2024 18:01:55 +0100 Subject: [PATCH 08/43] Updated tests --- aggoracle/e2e_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/aggoracle/e2e_test.go b/aggoracle/e2e_test.go index ce081c35..6b92f12e 100644 --- a/aggoracle/e2e_test.go +++ b/aggoracle/e2e_test.go @@ -55,8 +55,9 @@ func commonSetup(t *testing.T) ( l1Client, gerL1Addr, gerL1Contract, err := newSimulatedL1(authL1) require.NoError(t, err) // Reorg detector - dbPathReorgDetector := t.TempDir() - reorg, err := reorgdetector.New(ctx, l1Client.Client(), dbPathReorgDetector) + // dbPathReorgDetector := t.TempDir() + //reorg, err := reorgdetector.New(ctx, l1Client.Client(), dbPathReorgDetector) + reorg := reorgdetector.NewReorgMonitor(l1Client.Client(), 100) require.NoError(t, err) // Syncer dbPathSyncer := t.TempDir() From ac5f7384a22ad37e8dbe34d32ec52d3710f054bd Mon Sep 17 00:00:00 2001 From: begmaroman Date: Tue, 20 Aug 2024 18:07:19 +0100 Subject: [PATCH 09/43] Updated tests --- cmd/run.go | 54 +++++------------------ reorgdetector/mock_eth_client.go | 75 +++++++++++++++++++++++++------- 2 files changed, 70 insertions(+), 59 deletions(-) diff --git a/cmd/run.go b/cmd/run.go index 9bfb6e68..f0b71608 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -9,6 +9,8 @@ import ( "os/signal" "runtime" + "github.com/0xPolygon/cdk/sync" + zkevm "github.com/0xPolygon/cdk" dataCommitteeClient "github.com/0xPolygon/cdk-data-availability/client" "github.com/0xPolygon/cdk/aggoracle" @@ -28,7 +30,6 @@ import ( "github.com/0xPolygon/cdk/sequencesender/txbuilder" "github.com/0xPolygon/cdk/state" "github.com/0xPolygon/cdk/state/pgstatestorage" - "github.com/0xPolygon/cdk/sync" "github.com/0xPolygon/cdk/translator" ethtxman "github.com/0xPolygonHermez/zkevm-ethtx-manager/etherman" "github.com/0xPolygonHermez/zkevm-ethtx-manager/etherman/etherscan" @@ -382,44 +383,6 @@ func newState(c *config.Config, l2ChainID uint64, sqlDB *pgxpool.Pool) *state.St return st } -func newReorgDetectorL1( - ctx context.Context, - cfg config.Config, - l1Client *ethclient.Client, -) *reorgdetector.ReorgDetector { - rd, err := reorgdetector.New(ctx, l1Client, cfg.ReorgDetectorL1.DBPath) - if err != nil { - log.Fatal(err) - } - return rd -} - -func newL1InfoTreeSyncer( - ctx context.Context, - cfg config.Config, - l1Client *ethclient.Client, - reorgDetector sync.ReorgDetector, -) *l1infotreesync.L1InfoTreeSync { - syncer, err := l1infotreesync.New( - ctx, - cfg.L1InfoTreeSync.DBPath, - cfg.L1InfoTreeSync.GlobalExitRootAddr, - cfg.L1InfoTreeSync.RollupManagerAddr, - cfg.L1InfoTreeSync.SyncBlockChunkSize, - etherman.BlockNumberFinality(cfg.L1InfoTreeSync.BlockFinality), - reorgDetector, - l1Client, - cfg.L1InfoTreeSync.WaitForNewBlocksPeriod.Duration, - cfg.L1InfoTreeSync.InitialBlock, - cfg.L1InfoTreeSync.RetryAfterErrorPeriod.Duration, - cfg.L1InfoTreeSync.MaxRetryAttemptsAfterError, - ) - if err != nil { - log.Fatal(err) - } - return syncer -} - func isNeeded(casesWhereNeeded, actualCases []string) bool { for _, actaulCase := range actualCases { for _, caseWhereNeeded := range casesWhereNeeded { @@ -436,7 +399,7 @@ func runL1InfoTreeSyncerIfNeeded( components []string, cfg config.Config, l1Client *ethclient.Client, - reorgDetector *reorgdetector.ReorgDetector, + reorgDetector sync.ReorgDetector, ) *l1infotreesync.L1InfoTreeSync { if !isNeeded([]string{AGGORACLE, SEQUENCE_SENDER}, components) { return nil @@ -474,12 +437,17 @@ func runL1ClientIfNeeded(components []string, urlRPCL1 string) *ethclient.Client return l1CLient } -func runReorgDetectorL1IfNeeded(ctx context.Context, components []string, l1Client *ethclient.Client, dbPath string) *reorgdetector.ReorgDetector { +func runReorgDetectorL1IfNeeded(ctx context.Context, components []string, l1Client *ethclient.Client, dbPath string) sync.ReorgDetector { if !isNeeded([]string{SEQUENCE_SENDER, AGGREGATOR, AGGORACLE}, components) { return nil } - rd := newReorgDetector(ctx, dbPath, l1Client) - go rd.Start(ctx) + // rd := newReorgDetector(ctx, dbPath, l1Client) + rd := reorgdetector.NewReorgMonitor(l1Client, 100) + go func() { + if err := rd.Start(ctx); err != nil { + log.Fatal(err) + } + }() return rd } diff --git a/reorgdetector/mock_eth_client.go b/reorgdetector/mock_eth_client.go index 85376cc4..a76c62f9 100644 --- a/reorgdetector/mock_eth_client.go +++ b/reorgdetector/mock_eth_client.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.22.1. DO NOT EDIT. +// Code generated by mockery v2.40.1. DO NOT EDIT. package reorgdetector @@ -6,6 +6,10 @@ import ( context "context" big "math/big" + common "github.com/ethereum/go-ethereum/common" + + ethereum "github.com/ethereum/go-ethereum" + mock "github.com/stretchr/testify/mock" types "github.com/ethereum/go-ethereum/core/types" @@ -16,23 +20,29 @@ type EthClientMock struct { mock.Mock } -// BlockNumber provides a mock function with given fields: ctx -func (_m *EthClientMock) BlockNumber(ctx context.Context) (uint64, error) { - ret := _m.Called(ctx) +// HeaderByHash provides a mock function with given fields: ctx, hash +func (_m *EthClientMock) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) { + ret := _m.Called(ctx, hash) + + if len(ret) == 0 { + panic("no return value specified for HeaderByHash") + } - var r0 uint64 + var r0 *types.Header var r1 error - if rf, ok := ret.Get(0).(func(context.Context) (uint64, error)); ok { - return rf(ctx) + if rf, ok := ret.Get(0).(func(context.Context, common.Hash) (*types.Header, error)); ok { + return rf(ctx, hash) } - if rf, ok := ret.Get(0).(func(context.Context) uint64); ok { - r0 = rf(ctx) + if rf, ok := ret.Get(0).(func(context.Context, common.Hash) *types.Header); ok { + r0 = rf(ctx, hash) } else { - r0 = ret.Get(0).(uint64) + if ret.Get(0) != nil { + r0 = ret.Get(0).(*types.Header) + } } - if rf, ok := ret.Get(1).(func(context.Context) error); ok { - r1 = rf(ctx) + if rf, ok := ret.Get(1).(func(context.Context, common.Hash) error); ok { + r1 = rf(ctx, hash) } else { r1 = ret.Error(1) } @@ -44,6 +54,10 @@ func (_m *EthClientMock) BlockNumber(ctx context.Context) (uint64, error) { func (_m *EthClientMock) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) { ret := _m.Called(ctx, number) + if len(ret) == 0 { + panic("no return value specified for HeaderByNumber") + } + var r0 *types.Header var r1 error if rf, ok := ret.Get(0).(func(context.Context, *big.Int) (*types.Header, error)); ok { @@ -66,13 +80,42 @@ func (_m *EthClientMock) HeaderByNumber(ctx context.Context, number *big.Int) (* return r0, r1 } -type mockConstructorTestingTNewEthClientMock interface { - mock.TestingT - Cleanup(func()) +// SubscribeNewHead provides a mock function with given fields: ctx, ch +func (_m *EthClientMock) SubscribeNewHead(ctx context.Context, ch chan<- *types.Header) (ethereum.Subscription, error) { + ret := _m.Called(ctx, ch) + + if len(ret) == 0 { + panic("no return value specified for SubscribeNewHead") + } + + var r0 ethereum.Subscription + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, chan<- *types.Header) (ethereum.Subscription, error)); ok { + return rf(ctx, ch) + } + if rf, ok := ret.Get(0).(func(context.Context, chan<- *types.Header) ethereum.Subscription); ok { + r0 = rf(ctx, ch) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(ethereum.Subscription) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, chan<- *types.Header) error); ok { + r1 = rf(ctx, ch) + } else { + r1 = ret.Error(1) + } + + return r0, r1 } // NewEthClientMock creates a new instance of EthClientMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewEthClientMock(t mockConstructorTestingTNewEthClientMock) *EthClientMock { +// The first argument is typically a *testing.T value. +func NewEthClientMock(t interface { + mock.TestingT + Cleanup(func()) +}) *EthClientMock { mock := &EthClientMock{} mock.Mock.Test(t) From 49fa7d84022f0b99996b5c14e02f9a1a97bdf01f Mon Sep 17 00:00:00 2001 From: begmaroman Date: Tue, 20 Aug 2024 18:08:09 +0100 Subject: [PATCH 10/43] Updated tests --- cmd/run.go | 3 +-- reorgdetector/reorgdetector.go | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/cmd/run.go b/cmd/run.go index f0b71608..9a1b3825 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -9,8 +9,6 @@ import ( "os/signal" "runtime" - "github.com/0xPolygon/cdk/sync" - zkevm "github.com/0xPolygon/cdk" dataCommitteeClient "github.com/0xPolygon/cdk-data-availability/client" "github.com/0xPolygon/cdk/aggoracle" @@ -30,6 +28,7 @@ import ( "github.com/0xPolygon/cdk/sequencesender/txbuilder" "github.com/0xPolygon/cdk/state" "github.com/0xPolygon/cdk/state/pgstatestorage" + "github.com/0xPolygon/cdk/sync" "github.com/0xPolygon/cdk/translator" ethtxman "github.com/0xPolygonHermez/zkevm-ethtx-manager/etherman" "github.com/0xPolygonHermez/zkevm-ethtx-manager/etherman/etherscan" diff --git a/reorgdetector/reorgdetector.go b/reorgdetector/reorgdetector.go index d1737ef3..2b7a063d 100644 --- a/reorgdetector/reorgdetector.go +++ b/reorgdetector/reorgdetector.go @@ -9,9 +9,8 @@ import ( "sync" "time" - "github.com/ethereum/go-ethereum" - "github.com/0xPolygon/cdk/log" + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/rpc" From 0dc23701b56bdb970afdd0968be2f3a9c7c793cf Mon Sep 17 00:00:00 2001 From: begmaroman Date: Wed, 21 Aug 2024 14:26:21 +0100 Subject: [PATCH 11/43] Implementation --- reorgdetector/reorgdetector.go | 655 +++++++------------------- reorgdetector/reorgdetector_test.go | 498 +++----------------- reorgdetector/reorgdetectorv2.go | 320 ------------- reorgdetector/reorgdetectorv2_test.go | 99 ---- reorgdetector/types.go | 360 ++------------ reorgdetector/types_test.go | 1 + sync/evmdriver.go | 4 +- 7 files changed, 284 insertions(+), 1653 deletions(-) delete mode 100644 reorgdetector/reorgdetectorv2.go delete mode 100644 reorgdetector/reorgdetectorv2_test.go create mode 100644 reorgdetector/types_test.go diff --git a/reorgdetector/reorgdetector.go b/reorgdetector/reorgdetector.go index 2b7a063d..2ebff213 100644 --- a/reorgdetector/reorgdetector.go +++ b/reorgdetector/reorgdetector.go @@ -2,125 +2,31 @@ package reorgdetector import ( "context" - "encoding/json" - "errors" + "fmt" "math/big" - "sort" "sync" - "time" "github.com/0xPolygon/cdk/log" + "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/rpc" - "github.com/ledgerwatch/erigon-lib/kv" - "github.com/ledgerwatch/erigon-lib/kv/mdbx" + "github.com/pkg/errors" ) // TODO: consider the case where blocks can disappear, current implementation assumes that if there is a reorg, // the client will have at least as many blocks as it had before the reorg, however this may not be the case for L2 -const ( - defaultWaitPeriodBlockRemover = time.Second * 20 - defaultWaitPeriodBlockAdder = time.Second * 2 // should be smaller than block time of the tracked chain - - subscriberBlocks = "reorgdetector-subscriberBlocks" - - unfinalisedBlocksID = "unfinalisedBlocks" -) - var ( - ErrNotSubscribed = errors.New("id not found in subscriptions") - ErrInvalidBlockHash = errors.New("the block hash does not match with the expected block hash") - ErrIDReserverd = errors.New("subscription id is reserved") + ErrNotSubscribed = errors.New("id not found in subscriptions") ) -func tableCfgFunc(defaultBuckets kv.TableCfg) kv.TableCfg { - return kv.TableCfg{ - subscriberBlocks: {}, - } -} - type EthClient interface { SubscribeNewHead(ctx context.Context, ch chan<- *types.Header) (ethereum.Subscription, error) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) } -type block struct { - Num uint64 - Hash common.Hash -} - -type blockMap map[uint64]block - -// newBlockMap returns a new instance of blockMap -func newBlockMap(blocks ...block) blockMap { - blockMap := make(blockMap, len(blocks)) - - for _, b := range blocks { - blockMap[b.Num] = b - } - - return blockMap -} - -// getSorted returns blocks in sorted order -func (bm blockMap) getSorted() []block { - sortedBlocks := make([]block, 0, len(bm)) - - for _, b := range bm { - sortedBlocks = append(sortedBlocks, b) - } - - sort.Slice(sortedBlocks, func(i, j int) bool { - return sortedBlocks[i].Num < sortedBlocks[j].Num - }) - - return sortedBlocks -} - -// getFromBlockSorted returns blocks from blockNum in sorted order without including the blockNum -func (bm blockMap) getFromBlockSorted(blockNum uint64) []block { - sortedBlocks := bm.getSorted() - - index := -1 - for i, b := range sortedBlocks { - if b.Num > blockNum { - index = i - break - } - } - - if index == -1 { - return []block{} - } - - return sortedBlocks[index:] -} - -// getClosestHigherBlock returns the closest higher block to the given blockNum -func (bm blockMap) getClosestHigherBlock(blockNum uint64) (block, bool) { - if block, ok := bm[blockNum]; ok { - return block, true - } - - sorted := bm.getFromBlockSorted(blockNum) - if len(sorted) == 0 { - return block{}, false - } - - return sorted[0], true -} - -// removeRange removes blocks from from to to -func (bm blockMap) removeRange(from, to uint64) { - for i := from; i <= to; i++ { - delete(bm, i) - } -} - type Subscription struct { FirstReorgedBlock chan uint64 ReorgProcessed chan bool @@ -128,466 +34,227 @@ type Subscription struct { } type ReorgDetector struct { - ethClient EthClient - - subscriptionsLock sync.RWMutex - subscriptions map[string]*Subscription + client EthClient trackedBlocksLock sync.RWMutex trackedBlocks map[string]blockMap - db kv.RwDB - - waitPeriodBlockRemover time.Duration - waitPeriodBlockAdder time.Duration -} - -type Config struct { - DBPath string `mapstructure:"DBPath"` -} - -// New creates a new instance of ReorgDetector -func New(ctx context.Context, client EthClient, dbPath string) (*ReorgDetector, error) { - db, err := mdbx.NewMDBX(nil). - Path(dbPath). - WithTableCfg(tableCfgFunc). - Open() - if err != nil { - return nil, err - } - - return newReorgDetector(ctx, client, db) -} - -// newReorgDetector creates a new instance of ReorgDetector -func newReorgDetector(ctx context.Context, client EthClient, db kv.RwDB) (*ReorgDetector, error) { - return newReorgDetectorWithPeriods(ctx, client, db, defaultWaitPeriodBlockRemover, defaultWaitPeriodBlockAdder) + subscriptionsLock sync.RWMutex + subscriptions map[string]*Subscription } -// newReorgDetectorWithPeriods creates a new instance of ReorgDetector with custom wait periods -func newReorgDetectorWithPeriods(ctx context.Context, client EthClient, db kv.RwDB, - waitPeriodBlockRemover, waitPeriodBlockAdder time.Duration) (*ReorgDetector, error) { - r := &ReorgDetector{ - ethClient: client, - db: db, - subscriptions: make(map[string]*Subscription, 0), - waitPeriodBlockRemover: waitPeriodBlockRemover, - waitPeriodBlockAdder: waitPeriodBlockAdder, - } - - trackedBlocks, err := r.getTrackedBlocks(ctx) - if err != nil { - return nil, err +func New(client EthClient) *ReorgDetector { + return &ReorgDetector{ + client: client, + trackedBlocks: make(map[string]blockMap), + subscriptions: make(map[string]*Subscription), } - - r.trackedBlocks = trackedBlocks - - lastFinalisedBlock, err := r.ethClient.HeaderByNumber(ctx, big.NewInt(int64(rpc.FinalizedBlockNumber))) - if err != nil { - return nil, err - } - - if err = r.cleanStoredSubsBeforeStart(ctx, lastFinalisedBlock.Number.Uint64()); err != nil { - return nil, err - } - - return r, nil } -func (r *ReorgDetector) Start(ctx context.Context) { - go r.removeFinalisedBlocks(ctx) - go r.addUnfinalisedBlocks(ctx) -} - -func (r *ReorgDetector) Subscribe(id string) (*Subscription, error) { - if id == unfinalisedBlocksID { - return nil, ErrIDReserverd - } +func (rd *ReorgDetector) Subscribe(id string) (*Subscription, error) { + rd.subscriptionsLock.Lock() + defer rd.subscriptionsLock.Unlock() - r.subscriptionsLock.Lock() - defer r.subscriptionsLock.Unlock() - - if sub, ok := r.subscriptions[id]; ok { + if sub, ok := rd.subscriptions[id]; ok { return sub, nil } + sub := &Subscription{ FirstReorgedBlock: make(chan uint64), ReorgProcessed: make(chan bool), } - r.subscriptions[id] = sub + rd.subscriptions[id] = sub return sub, nil } -func (r *ReorgDetector) AddBlockToTrack(ctx context.Context, id string, blockNum uint64, blockHash common.Hash) error { - r.subscriptionsLock.RLock() - if sub, ok := r.subscriptions[id]; !ok { - r.subscriptionsLock.RUnlock() +func (rd *ReorgDetector) AddBlockToTrack(ctx context.Context, id string, blockNum uint64, blockHash, parentHash common.Hash) error { + rd.subscriptionsLock.RLock() + if sub, ok := rd.subscriptions[id]; !ok { + rd.subscriptionsLock.RUnlock() return ErrNotSubscribed } else { // In case there are reorgs being processed, wait // Note that this also makes any addition to trackedBlocks[id] safe sub.pendingReorgsToBeProcessed.Wait() } + rd.subscriptionsLock.RUnlock() - r.subscriptionsLock.RUnlock() - - if actualHash, ok := r.getUnfinalisedBlocksMap()[blockNum]; ok { - if actualHash.Hash == blockHash { - return r.saveTrackedBlock(ctx, id, block{Num: blockNum, Hash: blockHash}) - } else { - return ErrInvalidBlockHash - } - } else { - // ReorgDetector has not added the requested block yet, - // so we add it to the unfinalised blocks and then to the subscriber blocks as well - block := block{Num: blockNum, Hash: blockHash} - if err := r.saveTrackedBlock(ctx, unfinalisedBlocksID, block); err != nil { - return err - } - - return r.saveTrackedBlock(ctx, id, block) + newBlock := block{ + Num: blockNum, + Hash: blockHash, + ParentHash: parentHash, } -} -func (r *ReorgDetector) cleanStoredSubsBeforeStart(ctx context.Context, latestFinalisedBlock uint64) error { - blocksGotten := make(map[uint64]common.Hash, 0) + rd.trackedBlocksLock.Lock() + defer rd.trackedBlocksLock.Unlock() - r.trackedBlocksLock.Lock() - defer r.trackedBlocksLock.Unlock() - - for id, blocks := range r.trackedBlocks { - r.subscriptionsLock.Lock() - r.subscriptions[id] = &Subscription{ - FirstReorgedBlock: make(chan uint64), - ReorgProcessed: make(chan bool), - } - r.subscriptionsLock.Unlock() - - var ( - lastTrackedBlock uint64 - block block - actualBlockHash common.Hash - ok bool - ) + // Get the last block from the list for the given subscription ID + trackedBlocks, ok := rd.trackedBlocks[id] + if !ok || len(trackedBlocks) == 0 { + // No blocks for the given subscription + trackedBlocks = newBlockMap(newBlock) + rd.trackedBlocks[id] = trackedBlocks + } - if len(blocks) == 0 { - continue // nothing to process for this subscriber + findStartReorgBlock := func(blocks blockMap) *block { + // Find the highest block number + maxBlockNum := uint64(0) + for blockNum := range blocks { + if blockNum > maxBlockNum { + maxBlockNum = blockNum + } } - sortedBlocks := blocks.getSorted() - lastTrackedBlock = sortedBlocks[len(blocks)-1].Num - - for _, block = range sortedBlocks { - if actualBlockHash, ok = blocksGotten[block.Num]; !ok { - actualBlock, err := r.ethClient.HeaderByNumber(ctx, big.NewInt(int64(block.Num))) - if err != nil { - return err - } + // Iterate from the highest block number to the lowest + reorgDetected := false + for i := maxBlockNum; i > 1; i-- { + currentBlock, currentExists := blocks[i] + previousBlock, previousExists := blocks[i-1] - actualBlockHash = actualBlock.Hash() + // Check if both blocks exist (sanity check) + if !currentExists || !previousExists { + continue } - if actualBlockHash != block.Hash { - // reorg detected, notify subscriber - go r.notifyReorgToSubscription(id, block.Num) - - // remove the reorged blocks from the tracked blocks - blocks.removeRange(block.Num, lastTrackedBlock) - - break - } else if block.Num <= latestFinalisedBlock { - delete(blocks, block.Num) + // Check if the current block's parent hash matches the previous block's hash + if currentBlock.ParentHash != previousBlock.Hash { + reorgDetected = true + } else if reorgDetected { + // When reorg is detected, and we find the first match, return the previous block + return &previousBlock } } - // if we processed finalized or reorged blocks, update the tracked blocks in memory and db - if err := r.updateTrackedBlocksNoLock(ctx, id, blocks); err != nil { - return err - } + return nil // No reorg detected } - return nil -} - -func (r *ReorgDetector) removeFinalisedBlocks(ctx context.Context) { - ticker := time.NewTicker(r.waitPeriodBlockRemover) - - for { - select { - case <-ticker.C: - lastFinalisedBlock, err := r.ethClient.HeaderByNumber(ctx, big.NewInt(int64(rpc.FinalizedBlockNumber))) + rebuildBlocksMap := func(blocks blockMap, from, to uint64) (blockMap, error) { + for i := from; i <= to; i++ { + blockHeader, err := rd.client.HeaderByNumber(ctx, big.NewInt(int64(i))) if err != nil { - log.Error("reorg detector - error getting last finalised block", "err", err) - - continue + return nil, fmt.Errorf("failed to fetch block header for block number %d: %w", i, err) } - if err := r.removeTrackedBlocks(ctx, lastFinalisedBlock.Number.Uint64()); err != nil { - log.Error("reorg detector - error removing tracked blocks", "err", err) - - continue + blocks[blockHeader.Number.Uint64()] = block{ + Num: blockHeader.Number.Uint64(), + Hash: blockHeader.Hash(), + ParentHash: blockHeader.ParentHash, } - case <-ctx.Done(): - return } - } -} -func (r *ReorgDetector) addUnfinalisedBlocks(ctx context.Context) { - var ( - lastUnfinalisedBlock uint64 - ticker = time.NewTicker(r.waitPeriodBlockAdder) - unfinalisedBlocksMap = r.getUnfinalisedBlocksMap() - prevBlock *types.Header - lastBlockFromClient *types.Header - err error - ) - - if len(unfinalisedBlocksMap) > 0 { - lastUnfinalisedBlock = unfinalisedBlocksMap.getSorted()[len(unfinalisedBlocksMap)-1].Num + return blocks, nil } - for { - select { - case <-ticker.C: - lastBlockFromClient, err = r.ethClient.HeaderByNumber(ctx, big.NewInt(int64(rpc.LatestBlockNumber))) - if err != nil { - log.Error("reorg detector - error getting last block from client", "err", err) - continue - } - - if lastBlockFromClient.Number.Uint64() < lastUnfinalisedBlock { - // a reorg probably happened, and the client has less blocks than we have - // we should wait for the client to catch up so we can be sure - continue - } - - unfinalisedBlocksMap = r.getUnfinalisedBlocksMap() - if len(unfinalisedBlocksMap) == 0 { - // no unfinalised blocks, just add this block to the map - if err := r.saveTrackedBlock(ctx, unfinalisedBlocksID, block{ - Num: lastBlockFromClient.Number.Uint64(), - Hash: lastBlockFromClient.Hash(), - }); err != nil { - log.Error("reorg detector - error saving unfinalised block", "block", lastBlockFromClient.Number.Uint64(), "err", err) + closestHigherBlock, ok := trackedBlocks.getClosestHigherBlock(newBlock.Num) + if !ok { + // No same or higher blocks, only lower blocks exist. Check hashes. + // Current tracked blocks: N-i, N-i+1, ..., N-1 + sortedBlocks := trackedBlocks.getSorted() + closestBlock := sortedBlocks[len(sortedBlocks)-1] + if closestBlock.Num < newBlock.Num-1 { + // There is a gap between the last block and the given block + // Current tracked blocks: N-i, , N + } else if closestBlock.Num == newBlock.Num-1 { + if closestBlock.Hash != newBlock.ParentHash { + // Block hashes do not match, reorg happened + // TODO: Reorg happened + + trackedBlocks[newBlock.Num] = newBlock + reorgedBlock := findStartReorgBlock(trackedBlocks) + if reorgedBlock != nil { + fmt.Println("Reorg detected at block", reorgedBlock.Num) + rd.notifySubscribers(*reorgedBlock) + newBlocksMap, err := rebuildBlocksMap(trackedBlocks, reorgedBlock.Num, newBlock.Num) + if err != nil { + return err + } + rd.trackedBlocks[id] = newBlocksMap + } else { + // Should not happen } - - continue + } else { + // All good, add the block to the map + rd.trackedBlocks[id][newBlock.Num] = newBlock } - - startBlock := lastBlockFromClient - unfinalisedBlocksSorted := unfinalisedBlocksMap.getSorted() - reorgBlock := uint64(0) - - for i := startBlock.Number.Uint64(); i > unfinalisedBlocksSorted[0].Num; i-- { - previousBlock, ok := unfinalisedBlocksMap[i-1] - if !ok { - prevBlock, err = r.ethClient.HeaderByNumber(ctx, big.NewInt(int64(i-1))) + } else { + // This should not happen + log.Fatal("Unexpected block number comparison") + } + } else { + if closestHigherBlock.Num == newBlock.Num { + // Block has already been tracked and added to the map + // Current tracked blocks: N-2, N-1, N (given block) + if closestHigherBlock.Hash != newBlock.Hash { + // Block hashes have changed, reorg happened + // TODO: Handle happened + + trackedBlocks[newBlock.Num] = newBlock + reorgedBlock := findStartReorgBlock(trackedBlocks) + if reorgedBlock != nil { + fmt.Println("Reorg detected at block", reorgedBlock.Num) + rd.notifySubscribers(*reorgedBlock) + newBlocksMap, err := rebuildBlocksMap(trackedBlocks, reorgedBlock.Num, newBlock.Num) if err != nil { - log.Error("reorg detector - error getting previous block", "block", i-1, "err", err) - break // stop processing blocks, and we will try to detect it in the next iteration + return err } - - previousBlock = block{Num: prevBlock.Number.Uint64(), Hash: prevBlock.Hash()} - } - - if previousBlock.Hash == lastBlockFromClient.ParentHash { - unfinalisedBlocksMap[i] = block{Num: lastBlockFromClient.Number.Uint64(), Hash: lastBlockFromClient.Hash()} - } else if previousBlock.Hash != lastBlockFromClient.ParentHash { - // reorg happened, we will find out from where exactly and report this to subscribers - reorgBlock = i + rd.trackedBlocks[id] = newBlocksMap + } else { + // Should not happen } - - lastBlockFromClient, err = r.ethClient.HeaderByNumber(ctx, big.NewInt(int64(i-1))) + } + } else if closestHigherBlock.Num == newBlock.Num+1 { + // The given block is lower than the closest higher block: + // Current tracked blocks: N-2, N-1, N (given block), N+1, N+2 + // TODO: Reorg happened + + trackedBlocks[newBlock.Num] = newBlock + reorgedBlock := findStartReorgBlock(trackedBlocks) + if reorgedBlock != nil { + fmt.Println("Reorg detected at block", reorgedBlock.Num) + rd.notifySubscribers(*reorgedBlock) + newBlocksMap, err := rebuildBlocksMap(trackedBlocks, reorgedBlock.Num, newBlock.Num) if err != nil { - log.Error("reorg detector - error getting last block from client", "err", err) - break // stop processing blocks, and we will try to detect it in the next iteration + return err } + rd.trackedBlocks[id] = newBlocksMap + } else { + // Should not happen } - - if err == nil { - // if we noticed an error, do not notify or update tracked blocks - if reorgBlock > 0 { - r.notifyReorgToAllSubscriptions(reorgBlock) - } else { - r.updateTrackedBlocks(ctx, unfinalisedBlocksID, unfinalisedBlocksMap) + } else if closestHigherBlock.Num > newBlock.Num+1 { + // There is a gap between the current block and the closest higher block + // Current tracked blocks: N-2, N-1, N (given block), , N+i + // TODO: Reorg happened + + trackedBlocks[newBlock.Num] = newBlock + reorgedBlock := findStartReorgBlock(trackedBlocks) + if reorgedBlock != nil { + fmt.Println("Reorg detected at block", reorgedBlock.Num) + rd.notifySubscribers(*reorgedBlock) + newBlocksMap, err := rebuildBlocksMap(trackedBlocks, reorgedBlock.Num, newBlock.Num) + if err != nil { + return err } + rd.trackedBlocks[id] = newBlocksMap + } else { + // Should not happen } - case <-ctx.Done(): - return - } - } -} - -func (r *ReorgDetector) notifyReorgToAllSubscriptions(reorgBlock uint64) { - r.subscriptionsLock.RLock() - defer r.subscriptionsLock.RUnlock() - - for id, sub := range r.subscriptions { - r.trackedBlocksLock.RLock() - subscriberBlocks := r.trackedBlocks[id] - r.trackedBlocksLock.RUnlock() - - closestBlock, exists := subscriberBlocks.getClosestHigherBlock(reorgBlock) - - if exists { - go r.notifyReorgToSub(sub, closestBlock.Num) - - // remove reorged blocks from tracked blocks - sorted := subscriberBlocks.getSorted() - subscriberBlocks.removeRange(closestBlock.Num, sorted[len(sorted)-1].Num) - if err := r.updateTrackedBlocks(context.Background(), id, subscriberBlocks); err != nil { - log.Error("reorg detector - error updating tracked blocks", "err", err) - } - } - } -} - -func (r *ReorgDetector) notifyReorgToSubscription(id string, reorgBlock uint64) { - if id == unfinalisedBlocksID { - // unfinalised blocks are not subscribers, and reorg block should be > 0 - return - } - - r.subscriptionsLock.RLock() - sub := r.subscriptions[id] - r.subscriptionsLock.RUnlock() - - r.notifyReorgToSub(sub, reorgBlock) -} - -func (r *ReorgDetector) notifyReorgToSub(sub *Subscription, reorgBlock uint64) { - sub.pendingReorgsToBeProcessed.Add(1) - - // notify about the first reorged block that was tracked - // and wait for the receiver to process - sub.FirstReorgedBlock <- reorgBlock - <-sub.ReorgProcessed - - sub.pendingReorgsToBeProcessed.Done() -} - -// getUnfinalisedBlocksMap returns the map of unfinalised blocks -func (r *ReorgDetector) getUnfinalisedBlocksMap() blockMap { - r.trackedBlocksLock.RLock() - defer r.trackedBlocksLock.RUnlock() - - return r.trackedBlocks[unfinalisedBlocksID] -} - -// getTrackedBlocks returns a list of tracked blocks for each subscriber from db -func (r *ReorgDetector) getTrackedBlocks(ctx context.Context) (map[string]blockMap, error) { - tx, err := r.db.BeginRo(ctx) - if err != nil { - return nil, err - } - - defer tx.Rollback() - - cursor, err := tx.Cursor(subscriberBlocks) - if err != nil { - return nil, err - } - - defer cursor.Close() - - trackedBlocks := make(map[string]blockMap, 0) - - for k, v, err := cursor.First(); k != nil; k, v, err = cursor.Next() { - if err != nil { - return nil, err - } - - var blocks []block - if err := json.Unmarshal(v, &blocks); err != nil { - return nil, err - } - - trackedBlocks[string(k)] = newBlockMap(blocks...) - } - - if _, ok := trackedBlocks[unfinalisedBlocksID]; !ok { - // add unfinalised blocks to tracked blocks map if not present in db - trackedBlocks[unfinalisedBlocksID] = newBlockMap() - } - - return trackedBlocks, nil -} - -// saveTrackedBlock saves the tracked block for a subscriber in db and in memory -func (r *ReorgDetector) saveTrackedBlock(ctx context.Context, id string, b block) error { - tx, err := r.db.BeginRw(ctx) - if err != nil { - return err - } - - defer tx.Rollback() - - r.trackedBlocksLock.Lock() - - subscriberBlockMap, ok := r.trackedBlocks[id] - if !ok { - subscriberBlockMap = newBlockMap(b) - r.trackedBlocks[id] = subscriberBlockMap - } - - r.trackedBlocksLock.Unlock() - - raw, err := json.Marshal(subscriberBlockMap.getSorted()) - if err != nil { - - return err - } - - return tx.Put(subscriberBlocks, []byte(id), raw) -} - -// removeTrackedBlocks removes the tracked blocks for a subscriber in db and in memory -func (r *ReorgDetector) removeTrackedBlocks(ctx context.Context, lastFinalizedBlock uint64) error { - r.subscriptionsLock.RLock() - defer r.subscriptionsLock.RUnlock() - - for id := range r.subscriptions { - r.trackedBlocksLock.RLock() - newTrackedBlocks := r.trackedBlocks[id].getFromBlockSorted(lastFinalizedBlock) - r.trackedBlocksLock.RUnlock() - - if err := r.updateTrackedBlocks(ctx, id, newBlockMap(newTrackedBlocks...)); err != nil { - return err + } else { + // This should not happen + log.Fatal("Unexpected block number comparison") } } return nil } -// updateTrackedBlocks updates the tracked blocks for a subscriber in db and in memory -func (r *ReorgDetector) updateTrackedBlocks(ctx context.Context, id string, blocks blockMap) error { - r.trackedBlocksLock.Lock() - defer r.trackedBlocksLock.Unlock() - - return r.updateTrackedBlocksNoLock(ctx, id, blocks) -} - -// updateTrackedBlocksNoLock updates the tracked blocks for a subscriber in db and in memory -func (r *ReorgDetector) updateTrackedBlocksNoLock(ctx context.Context, id string, blocks blockMap) error { - tx, err := r.db.BeginRw(ctx) - if err != nil { - return err - } - - defer tx.Rollback() - - raw, err := json.Marshal(blocks.getSorted()) - if err != nil { - return err - } - - if err := tx.Put(subscriberBlocks, []byte(id), raw); err != nil { - return err +func (rd *ReorgDetector) notifySubscribers(startingBlock block) { + rd.subscriptionsLock.RLock() + for _, sub := range rd.subscriptions { + sub.pendingReorgsToBeProcessed.Add(1) + sub.FirstReorgedBlock <- startingBlock.Num + <-sub.ReorgProcessed + sub.pendingReorgsToBeProcessed.Done() } - - r.trackedBlocks[id] = blocks - - return nil + rd.subscriptionsLock.RUnlock() } diff --git a/reorgdetector/reorgdetector_test.go b/reorgdetector/reorgdetector_test.go index 38bdd3c5..0a61ebf2 100644 --- a/reorgdetector/reorgdetector_test.go +++ b/reorgdetector/reorgdetector_test.go @@ -2,463 +2,117 @@ package reorgdetector import ( "context" - "encoding/json" - "errors" "fmt" - big "math/big" - "os" - "reflect" + "math/big" "testing" "time" + "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" - types "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/rpc" - "github.com/ledgerwatch/erigon-lib/kv" - "github.com/ledgerwatch/erigon-lib/kv/mdbx" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/ethclient/simulated" "github.com/stretchr/testify/require" ) -const testSubscriber = "testSubscriber" - -// newTestDB creates new instance of db used by tests. -func newTestDB(tb testing.TB) kv.RwDB { - tb.Helper() - - dir := fmt.Sprintf("/tmp/reorgdetector-temp_%v", time.Now().UTC().Format(time.RFC3339Nano)) - err := os.Mkdir(dir, 0775) - - require.NoError(tb, err) - - db, err := mdbx.NewMDBX(nil). - Path(dir). - WithTableCfg(tableCfgFunc). - Open() - require.NoError(tb, err) - - tb.Cleanup(func() { - require.NoError(tb, os.RemoveAll(dir)) - }) - - return db -} - -func TestBlockMap(t *testing.T) { - t.Parallel() - - // Create a new block map - bm := newBlockMap( - block{Num: 1, Hash: common.HexToHash("0x123")}, - block{Num: 2, Hash: common.HexToHash("0x456")}, - block{Num: 3, Hash: common.HexToHash("0x789")}, - ) - - t.Run("getSorted", func(t *testing.T) { - t.Parallel() - - sortedBlocks := bm.getSorted() - expectedSortedBlocks := []block{ - {Num: 1, Hash: common.HexToHash("0x123")}, - {Num: 2, Hash: common.HexToHash("0x456")}, - {Num: 3, Hash: common.HexToHash("0x789")}, - } - if !reflect.DeepEqual(sortedBlocks, expectedSortedBlocks) { - t.Errorf("getSorted() returned incorrect result, expected: %v, got: %v", expectedSortedBlocks, sortedBlocks) - } - }) - - t.Run("getFromBlockSorted", func(t *testing.T) { - t.Parallel() - - fromBlockSorted := bm.getFromBlockSorted(2) - expectedFromBlockSorted := []block{ - {Num: 3, Hash: common.HexToHash("0x789")}, - } - if !reflect.DeepEqual(fromBlockSorted, expectedFromBlockSorted) { - t.Errorf("getFromBlockSorted() returned incorrect result, expected: %v, got: %v", expectedFromBlockSorted, fromBlockSorted) - } - - // Test getFromBlockSorted function when blockNum is greater than the last block - fromBlockSorted = bm.getFromBlockSorted(4) - expectedFromBlockSorted = []block{} - if !reflect.DeepEqual(fromBlockSorted, expectedFromBlockSorted) { - t.Errorf("getFromBlockSorted() returned incorrect result, expected: %v, got: %v", expectedFromBlockSorted, fromBlockSorted) - } - }) - - t.Run("getClosestHigherBlock", func(t *testing.T) { - t.Parallel() - - bm := newBlockMap( - block{Num: 1, Hash: common.HexToHash("0x123")}, - block{Num: 2, Hash: common.HexToHash("0x456")}, - block{Num: 3, Hash: common.HexToHash("0x789")}, - ) - - // Test when the blockNum exists in the block map - b, exists := bm.getClosestHigherBlock(2) - require.True(t, exists) - expectedBlock := block{Num: 2, Hash: common.HexToHash("0x456")} - if b != expectedBlock { - t.Errorf("getClosestHigherBlock() returned incorrect result, expected: %v, got: %v", expectedBlock, b) - } - - // Test when the blockNum does not exist in the block map - b, exists = bm.getClosestHigherBlock(4) - require.False(t, exists) - expectedBlock = block{Num: 0, Hash: common.Hash{}} - if b != expectedBlock { - t.Errorf("getClosestHigherBlock() returned incorrect result, expected: %v, got: %v", expectedBlock, b) - } - }) - - t.Run("removeRange", func(t *testing.T) { - t.Parallel() - - bm := newBlockMap( - block{Num: 1, Hash: common.HexToHash("0x123")}, - block{Num: 2, Hash: common.HexToHash("0x456")}, - block{Num: 3, Hash: common.HexToHash("0x789")}, - block{Num: 4, Hash: common.HexToHash("0xabc")}, - block{Num: 5, Hash: common.HexToHash("0xdef")}, - ) - - bm.removeRange(3, 5) +func newSimulatedL1(t *testing.T, auth *bind.TransactOpts) *simulated.Backend { + t.Helper() - expectedBlocks := []block{ - {Num: 1, Hash: common.HexToHash("0x123")}, - {Num: 2, Hash: common.HexToHash("0x456")}, - } + balance, _ := new(big.Int).SetString("10000000000000000000000000", 10) //nolint:gomnd - sortedBlocks := bm.getSorted() + blockGasLimit := uint64(999999999999999999) //nolint:gomnd + client := simulated.NewBackend(map[common.Address]types.Account{ + auth.From: { + Balance: balance, + }, + }, simulated.WithBlockGasLimit(blockGasLimit)) + client.Commit() - if !reflect.DeepEqual(sortedBlocks, expectedBlocks) { - t.Errorf("removeRange() failed, expected: %v, got: %v", expectedBlocks, sortedBlocks) - } - }) + return client } -func TestReorgDetector_New(t *testing.T) { - t.Parallel() +func Test_ReorgDetector(t *testing.T) { + const produceBlocks = 29 + const reorgPeriod = 5 + const reorgDepth = 2 ctx := context.Background() - t.Run("first initialization, no data", func(t *testing.T) { - t.Parallel() - - client := NewEthClientMock(t) - db := newTestDB(t) - - client.On("HeaderByNumber", ctx, big.NewInt(int64(rpc.FinalizedBlockNumber))).Return( - &types.Header{Number: big.NewInt(100)}, nil, - ) - - rd, err := newReorgDetector(ctx, client, db) - require.NoError(t, err) - require.Len(t, rd.trackedBlocks, 1) - - unfinalisedBlocksMap, exists := rd.trackedBlocks[unfinalisedBlocksID] - require.True(t, exists) - require.Empty(t, unfinalisedBlocksMap) - }) - - t.Run("getting last finalized block failed", func(t *testing.T) { - t.Parallel() - - client := NewEthClientMock(t) - db := newTestDB(t) - - expectedErr := errors.New("some error") - client.On("HeaderByNumber", ctx, big.NewInt(int64(rpc.FinalizedBlockNumber))).Return(nil, expectedErr) - - _, err := newReorgDetector(ctx, client, db) - require.ErrorIs(t, err, expectedErr) - }) - - t.Run("have tracked blocks and subscriptions no reorg - all blocks finalized", func(t *testing.T) { - t.Parallel() - - client := NewEthClientMock(t) - db := newTestDB(t) - - testBlocks := createTestBlocks(t, 1, 6) - unfinalisedBlocks := testBlocks[:5] - testSubscriberBlocks := testBlocks[:3] - - insertTestData(t, ctx, db, unfinalisedBlocks, unfinalisedBlocksID) - insertTestData(t, ctx, db, testSubscriberBlocks, testSubscriber) - - for _, block := range unfinalisedBlocks { - client.On("HeaderByNumber", ctx, block.Number).Return( - block, nil, - ) - } - - client.On("HeaderByNumber", ctx, big.NewInt(int64(rpc.FinalizedBlockNumber))).Return( - testBlocks[len(testBlocks)-1], nil, - ) - - rd, err := newReorgDetector(ctx, client, db) - require.NoError(t, err) - require.Len(t, rd.trackedBlocks, 2) // testSubscriber and unfinalisedBlocks - - unfinalisedBlocksMap, exists := rd.trackedBlocks[unfinalisedBlocksID] - require.True(t, exists) - require.Len(t, unfinalisedBlocksMap, 0) // since all blocks are finalized - - testSubscriberMap, exists := rd.trackedBlocks[testSubscriber] - require.True(t, exists) - require.Len(t, testSubscriberMap, 0) // since all blocks are finalized - }) - - t.Run("have tracked blocks and subscriptions no reorg - not all blocks finalized", func(t *testing.T) { - t.Parallel() - - client := NewEthClientMock(t) - db := newTestDB(t) - - testBlocks := createTestBlocks(t, 1, 7) - unfinalisedBlocks := testBlocks[:6] - testSubscriberBlocks := testBlocks[:4] - - insertTestData(t, ctx, db, unfinalisedBlocks, unfinalisedBlocksID) - insertTestData(t, ctx, db, testSubscriberBlocks, testSubscriber) - - for _, block := range unfinalisedBlocks { - client.On("HeaderByNumber", ctx, block.Number).Return( - block, nil, - ) - } - - client.On("HeaderByNumber", ctx, big.NewInt(int64(rpc.FinalizedBlockNumber))).Return( - testSubscriberBlocks[len(testSubscriberBlocks)-1], nil, - ) - - rd, err := newReorgDetector(ctx, client, db) - require.NoError(t, err) - require.Len(t, rd.trackedBlocks, 2) // testSubscriber and unfinalisedBlocks - - unfinalisedBlocksMap, exists := rd.trackedBlocks[unfinalisedBlocksID] - require.True(t, exists) - require.Len(t, unfinalisedBlocksMap, len(unfinalisedBlocks)-len(testSubscriberBlocks)) // since all blocks are finalized - - testSubscriberMap, exists := rd.trackedBlocks[testSubscriber] - require.True(t, exists) - require.Len(t, testSubscriberMap, 0) // since all blocks are finalized - }) - - t.Run("have tracked blocks and subscriptions - reorg happened", func(t *testing.T) { - t.Parallel() - - client := NewEthClientMock(t) - db := newTestDB(t) + // Simulated L1 + privateKeyL1, err := crypto.GenerateKey() + require.NoError(t, err) + authL1, err := bind.NewKeyedTransactorWithChainID(privateKeyL1, big.NewInt(1337)) + require.NoError(t, err) + clientL1 := newSimulatedL1(t, authL1) + require.NoError(t, err) - trackedBlocks := createTestBlocks(t, 1, 5) - testSubscriberBlocks := trackedBlocks[:5] + reorgDetector := New(clientL1.Client()) - insertTestData(t, ctx, db, nil, unfinalisedBlocksID) // no unfinalised blocks - insertTestData(t, ctx, db, testSubscriberBlocks, testSubscriber) + reorgSub, err := reorgDetector.Subscribe("test") + require.NoError(t, err) - for _, block := range trackedBlocks[:3] { - client.On("HeaderByNumber", ctx, block.Number).Return( - block, nil, - ) + ch := make(chan *types.Header, 10) + headerSub, err := clientL1.Client().SubscribeNewHead(ctx, ch) + require.NoError(t, err) + go func() { + for { + select { + case <-ctx.Done(): + return + case <-headerSub.Err(): + return + case header := <-ch: + err := reorgDetector.AddBlockToTrack(ctx, "test", header.Number.Uint64(), header.Hash(), header.ParentHash) + require.NoError(t, err) + } } + }() - reorgedBlocks := createTestBlocks(t, 4, 2) // block 4, and 5 are reorged - reorgedBlocks[0].ParentHash = trackedBlocks[2].Hash() // block 4 is reorged but his parent is block 3 - reorgedBlocks[1].ParentHash = reorgedBlocks[0].Hash() // block 5 is reorged but his parent is block 4 - - client.On("HeaderByNumber", ctx, reorgedBlocks[0].Number).Return( - reorgedBlocks[0], nil, - ) - - client.On("HeaderByNumber", ctx, big.NewInt(int64(rpc.FinalizedBlockNumber))).Return( - reorgedBlocks[len(reorgedBlocks)-1], nil, - ) - - rd, err := newReorgDetector(ctx, client, db) - require.NoError(t, err) - require.Len(t, rd.trackedBlocks, 2) // testSubscriber and unfinalisedBlocks - - // we wait for the subscriber to be notified about the reorg - firstReorgedBlock := <-rd.subscriptions[testSubscriber].FirstReorgedBlock - require.Equal(t, reorgedBlocks[0].Number.Uint64(), firstReorgedBlock) - - // all blocks should be cleaned from the tracked blocks - // since subscriber had 5 blocks, 3 were finalized, and 2 were reorged but also finalized - subscriberBlocks := rd.trackedBlocks[testSubscriber] - require.Len(t, subscriberBlocks, 0) - }) -} - -func TestReorgDetector_AddBlockToTrack(t *testing.T) { - t.Parallel() - - ctx := context.Background() - - t.Run("no subscription", func(t *testing.T) { - t.Parallel() - - client := NewEthClientMock(t) - db := newTestDB(t) - - client.On("HeaderByNumber", ctx, big.NewInt(int64(rpc.FinalizedBlockNumber))).Return( - &types.Header{Number: big.NewInt(10)}, nil, - ) - - rd, err := newReorgDetector(ctx, client, db) - require.NoError(t, err) - - err = rd.AddBlockToTrack(ctx, testSubscriber, 1, common.HexToHash("0x123")) - require.ErrorIs(t, err, ErrNotSubscribed) - }) - - t.Run("no unfinalised blocks - block not finalised", func(t *testing.T) { - t.Parallel() - - client := NewEthClientMock(t) - db := newTestDB(t) + expectedReorgBlocks := make(map[uint64]bool) + lastReorgOn := int64(0) + for i := 1; lastReorgOn <= produceBlocks; i++ { + block := clientL1.Commit() + time.Sleep(time.Millisecond) - client.On("HeaderByNumber", ctx, big.NewInt(int64(rpc.FinalizedBlockNumber))).Return( - &types.Header{Number: big.NewInt(10)}, nil, - ).Once() - - rd, err := newReorgDetector(ctx, client, db) - require.NoError(t, err) - - _, err = rd.Subscribe(testSubscriber) - require.NoError(t, err) - - err = rd.AddBlockToTrack(ctx, testSubscriber, 11, common.HexToHash("0x123")) // block not finalized + header, err := clientL1.Client().HeaderByHash(ctx, block) require.NoError(t, err) + headerNumber := header.Number.Int64() - subBlocks := rd.trackedBlocks[testSubscriber] - require.Len(t, subBlocks, 1) - require.Equal(t, subBlocks[11].Hash, common.HexToHash("0x123")) - }) - - t.Run("have unfinalised blocks - block not finalized", func(t *testing.T) { - t.Parallel() + // Reorg every "reorgPeriod" blocks with "reorgDepth" blocks depth + if headerNumber > lastReorgOn && headerNumber%reorgPeriod == 0 { + lastReorgOn = headerNumber - client := NewEthClientMock(t) - db := newTestDB(t) + reorgBlock, err := clientL1.Client().BlockByNumber(ctx, big.NewInt(headerNumber-reorgDepth)) + require.NoError(t, err) - unfinalisedBlocks := createTestBlocks(t, 11, 5) - insertTestData(t, ctx, db, unfinalisedBlocks, unfinalisedBlocksID) + expectedReorgBlocks[reorgBlock.NumberU64()] = false - for _, block := range unfinalisedBlocks { - client.On("HeaderByNumber", ctx, block.Number).Return( - block, nil, - ) + err = clientL1.Fork(reorgBlock.Hash()) + require.NoError(t, err) } - - client.On("HeaderByNumber", ctx, big.NewInt(int64(rpc.FinalizedBlockNumber))).Return( - &types.Header{Number: big.NewInt(10)}, nil, - ).Once() - - rd, err := newReorgDetector(ctx, client, db) - require.NoError(t, err) - - _, err = rd.Subscribe(testSubscriber) - require.NoError(t, err) - - err = rd.AddBlockToTrack(ctx, testSubscriber, 11, unfinalisedBlocks[0].Hash()) // block not finalized - require.NoError(t, err) - - subBlocks := rd.trackedBlocks[testSubscriber] - require.Len(t, subBlocks, 1) - require.Equal(t, subBlocks[11].Hash, unfinalisedBlocks[0].Hash()) - }) -} - -func TestReorgDetector_removeFinalisedBlocks(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithCancel(context.Background()) - client := NewEthClientMock(t) - db := newTestDB(t) - - unfinalisedBlocks := createTestBlocks(t, 1, 10) - insertTestData(t, ctx, db, unfinalisedBlocks, unfinalisedBlocksID) - insertTestData(t, ctx, db, unfinalisedBlocks, testSubscriber) - - // call for removeFinalisedBlocks - client.On("HeaderByNumber", ctx, big.NewInt(int64(rpc.FinalizedBlockNumber))).Return( - &types.Header{Number: big.NewInt(5)}, nil, - ) - - rd := &ReorgDetector{ - ethClient: client, - db: db, - trackedBlocks: make(map[string]blockMap), - waitPeriodBlockRemover: 100 * time.Millisecond, - waitPeriodBlockAdder: 100 * time.Millisecond, - subscriptions: map[string]*Subscription{ - testSubscriber: { - FirstReorgedBlock: make(chan uint64), - ReorgProcessed: make(chan bool), - }, - unfinalisedBlocksID: { - FirstReorgedBlock: make(chan uint64), - ReorgProcessed: make(chan bool), - }, - }, } - trackedBlocks, err := rd.getTrackedBlocks(ctx) - require.NoError(t, err) - require.Len(t, trackedBlocks, 2) - - rd.trackedBlocks = trackedBlocks - - // make sure we have all blocks in the tracked blocks before removing finalized blocks - require.Len(t, rd.trackedBlocks[unfinalisedBlocksID], len(unfinalisedBlocks)) - require.Len(t, rd.trackedBlocks[testSubscriber], len(unfinalisedBlocks)) - - // remove finalized blocks - go rd.removeFinalisedBlocks(ctx) - - time.Sleep(3 * time.Second) // wait for the go routine to remove the finalized blocks - cancel() - - // make sure all blocks are removed from the tracked blocks - rd.trackedBlocksLock.RLock() - defer rd.trackedBlocksLock.RUnlock() - - require.Len(t, rd.trackedBlocks[unfinalisedBlocksID], 5) - require.Len(t, rd.trackedBlocks[testSubscriber], 5) -} - -func createTestBlocks(t *testing.T, startBlock uint64, count uint64) []*types.Header { - t.Helper() - - blocks := make([]*types.Header, 0, count) - for i := startBlock; i < startBlock+count; i++ { - blocks = append(blocks, &types.Header{Number: big.NewInt(int64(i))}) + // Commit some blocks to ensure reorgs are detected + for i := 0; i < reorgPeriod; i++ { + clientL1.Commit() } - return blocks -} - -func insertTestData(t *testing.T, ctx context.Context, db kv.RwDB, blocks []*types.Header, id string) { - t.Helper() + fmt.Println("expectedReorgBlocks", expectedReorgBlocks) - // Insert some test data - err := db.Update(ctx, func(tx kv.RwTx) error { + for range expectedReorgBlocks { + firstReorgedBlock := <-reorgSub.FirstReorgedBlock + reorgSub.ReorgProcessed <- true - blockMap := newBlockMap() - for _, b := range blocks { - blockMap[b.Number.Uint64()] = block{b.Number.Uint64(), b.Hash()} - } + fmt.Println("firstReorgedBlock", firstReorgedBlock) - raw, err := json.Marshal(blockMap.getSorted()) - if err != nil { - return err - } + processed, ok := expectedReorgBlocks[firstReorgedBlock] + require.True(t, ok) + require.False(t, processed) - return tx.Put(subscriberBlocks, []byte(id), raw) - }) + expectedReorgBlocks[firstReorgedBlock] = true + } - require.NoError(t, err) + for _, processed := range expectedReorgBlocks { + require.True(t, processed) + } } diff --git a/reorgdetector/reorgdetectorv2.go b/reorgdetector/reorgdetectorv2.go deleted file mode 100644 index 456614cb..00000000 --- a/reorgdetector/reorgdetectorv2.go +++ /dev/null @@ -1,320 +0,0 @@ -package reorgdetector - -import ( - "context" - "fmt" - "log" - "sync" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" - "github.com/pkg/errors" -) - -type ReorgMonitor struct { - lock sync.Mutex - - client EthClient - - maxBlocksInCache int - lastBlockHeight uint64 - - knownReorgs map[string]uint64 // key: reorgId, value: endBlockNumber - subscriptionsLock sync.RWMutex - subscriptions map[string]*Subscription - - blockByHash map[common.Hash]*Block - blocksByHeight map[uint64]map[common.Hash]*Block - - EarliestBlockNumber uint64 - LatestBlockNumber uint64 -} - -func NewReorgMonitor(client EthClient, maxBlocks int) *ReorgMonitor { - return &ReorgMonitor{ - client: client, - maxBlocksInCache: maxBlocks, - blockByHash: make(map[common.Hash]*Block), - blocksByHeight: make(map[uint64]map[common.Hash]*Block), - knownReorgs: make(map[string]uint64), - subscriptions: make(map[string]*Subscription), - } -} - -func (mon *ReorgMonitor) Start(ctx context.Context) error { - // Add head tracker - ch := make(chan *types.Header, 100) - sub, err := mon.client.SubscribeNewHead(ctx, ch) - if err != nil { - return err - } - - go func() { - for { - select { - case <-ctx.Done(): - return - case <-sub.Err(): - return - case header := <-ch: - if err = mon.onNewHeader(header); err != nil { - log.Println(err) - continue - } - } - } - }() - - return nil -} - -func (mon *ReorgMonitor) Subscribe(id string) (*Subscription, error) { - mon.subscriptionsLock.Lock() - defer mon.subscriptionsLock.Unlock() - - if sub, ok := mon.subscriptions[id]; ok { - return sub, nil - } - - sub := &Subscription{ - FirstReorgedBlock: make(chan uint64), - ReorgProcessed: make(chan bool), - } - mon.subscriptions[id] = sub - - return sub, nil -} - -func (mon *ReorgMonitor) AddBlockToTrack(ctx context.Context, id string, blockNum uint64, blockHash common.Hash) error { - mon.subscriptionsLock.RLock() - if sub, ok := mon.subscriptions[id]; !ok { - mon.subscriptionsLock.RUnlock() - return ErrNotSubscribed - } else { - // In case there are reorgs being processed, wait - // Note that this also makes any addition to trackedBlocks[id] safe - sub.pendingReorgsToBeProcessed.Wait() - } - mon.subscriptionsLock.RUnlock() - - return nil -} - -func (mon *ReorgMonitor) notifySubscribers(reorg *Reorg) { - mon.subscriptionsLock.RLock() - for _, sub := range mon.subscriptions { - sub.pendingReorgsToBeProcessed.Add(1) - sub.FirstReorgedBlock <- reorg.StartBlockHeight - <-sub.ReorgProcessed - sub.pendingReorgsToBeProcessed.Done() - } - mon.subscriptionsLock.RUnlock() -} - -func (mon *ReorgMonitor) onNewHeader(header *types.Header) error { - mon.lock.Lock() - defer mon.lock.Unlock() - - mon.addBlock(NewBlock(header, OriginSubscription)) - - // Do nothing if block is at previous height - if header.Number.Uint64() == mon.lastBlockHeight { - return nil - } - - if len(mon.blocksByHeight) < 3 { - return nil - } - - // Analyze blocks once a new height has been reached - mon.lastBlockHeight = header.Number.Uint64() - - anal, err := mon.AnalyzeTree(0, 2) - if err != nil { - return err - } - - for _, reorg := range anal.Reorgs { - if !reorg.IsFinished { // don't care about unfinished reorgs - continue - } - - // Send new finished reorgs to channel - if _, isKnownReorg := mon.knownReorgs[reorg.Id()]; !isKnownReorg { - mon.knownReorgs[reorg.Id()] = reorg.EndBlockHeight - go mon.notifySubscribers(reorg) - } - } - - return nil -} - -// addBlock adds a block to history if it hasn't been seen before, and download unknown referenced blocks (parent, uncles). -func (mon *ReorgMonitor) addBlock(block *Block) bool { - defer mon.trimCache() - - // If known, then only overwrite if known was by uncle - _, isKnown := mon.blockByHash[block.Header.Hash()] - if isKnown { - return false - } - - // Only accept blocks that are after the earliest known (some nodes might be further back) - if block.Number() < mon.EarliestBlockNumber { - return false - } - - // Add for access by hash - mon.blockByHash[block.Header.Hash()] = block - - // Create array of blocks at this height, if necessary - if _, found := mon.blocksByHeight[block.Number()]; !found { - mon.blocksByHeight[block.Number()] = make(map[common.Hash]*Block) - } - - // Add to map of blocks at this height - mon.blocksByHeight[block.Number()][block.Header.Hash()] = block - - // Set earliest block - if mon.EarliestBlockNumber == 0 || block.Number() < mon.EarliestBlockNumber { - mon.EarliestBlockNumber = block.Number() - } - - // Set latest block - if block.Number() > mon.LatestBlockNumber { - mon.LatestBlockNumber = block.Number() - } - - // Check if further blocks can be downloaded from this one - if block.Number() > mon.EarliestBlockNumber { // check backhistory only if we are past the earliest block - err := mon.checkBlockForReferences(block) - if err != nil { - log.Println(err) - } - } - - return true -} - -func (mon *ReorgMonitor) trimCache() { - // Trim reorg history - for reorgId, reorgEndBlockheight := range mon.knownReorgs { - if reorgEndBlockheight < mon.EarliestBlockNumber { - delete(mon.knownReorgs, reorgId) - } - } - - for currentHeight := mon.EarliestBlockNumber; currentHeight < mon.LatestBlockNumber; currentHeight++ { - blocks, heightExists := mon.blocksByHeight[currentHeight] - if !heightExists { - continue - } - - // Set new lowest block number - mon.EarliestBlockNumber = currentHeight - - // Stop if trimmed enough - if len(mon.blockByHash) <= mon.maxBlocksInCache { - return - } - - // Trim - for hash := range blocks { - delete(mon.blocksByHeight[currentHeight], hash) - delete(mon.blockByHash, hash) - } - delete(mon.blocksByHeight, currentHeight) - } -} - -func (mon *ReorgMonitor) checkBlockForReferences(block *Block) error { - // Check parent - _, found := mon.blockByHash[block.Header.ParentHash] - if !found { - // fmt.Printf("- parent of %d %s not found (%s), downloading...\n", block.Number, block.Hash, block.ParentHash) - _, _, err := mon.ensureBlock(block.Header.ParentHash, OriginGetParent) - if err != nil { - return errors.Wrap(err, "get-parent error") - } - } - - // Check uncles - /*for _, uncleHeader := range block.Block.Uncles() { - // fmt.Printf("- block %d %s has uncle: %s\n", block.Number, block.Hash, uncleHeader.Hash()) - _, _, err := mon.ensureBlock(uncleHeader.Hash(), OriginUncle) - if err != nil { - return errors.Wrap(err, "get-uncle error") - } - }*/ - - // ro.DebugPrintln(fmt.Sprintf("- added block %d %s", block.NumberU64(), block.Hash())) - return nil -} - -func (mon *ReorgMonitor) ensureBlock(blockHash common.Hash, origin BlockOrigin) (block *Block, alreadyExisted bool, err error) { - // Check and potentially download block - var found bool - block, found = mon.blockByHash[blockHash] - if found { - return block, true, nil - } - - fmt.Printf("- block %s (%s) not found, downloading from...\n", blockHash, origin) - ethBlock, err := mon.client.HeaderByHash(context.Background(), blockHash) - if err != nil { - return nil, false, errors.Wrapf(err, "EnsureBlock error for hash %s", blockHash) - } - - block = NewBlock(ethBlock, origin) - - // Add a new block without sending to channel, because that makes reorg.AddBlock() asynchronous, - // but we want reorg.AddBlock() to wait until all references are added. - mon.addBlock(block) - - return block, false, nil -} - -func (mon *ReorgMonitor) AnalyzeTree(maxBlocks, distanceToLastBlockHeight uint64) (*TreeAnalysis, error) { - // Set end height of search - endBlockNumber := mon.LatestBlockNumber - distanceToLastBlockHeight - - // Set start height of search - startBlockNumber := mon.EarliestBlockNumber - if maxBlocks > 0 && endBlockNumber-maxBlocks > mon.EarliestBlockNumber { - startBlockNumber = endBlockNumber - maxBlocks - } - - // Build tree datastructure - tree := NewBlockTree() - for height := startBlockNumber; height <= endBlockNumber; height++ { - numBlocksAtHeight := len(mon.blocksByHeight[height]) - if numBlocksAtHeight == 0 { - return nil, fmt.Errorf("error in monitor.AnalyzeTree: no blocks at height %d", height) - } - - // Start tree only when 1 block at this height. If more blocks then skip. - if tree.FirstNode == nil && numBlocksAtHeight > 1 { - continue - } - - // Add all blocks at this height to the tree - for _, currentBlock := range mon.blocksByHeight[height] { - err := tree.AddBlock(currentBlock) - if err != nil { - return nil, errors.Wrap(err, "monitor.AnalyzeTree->tree.AddBlock error") - } - } - } - - // Get analysis of tree - anal, err := NewTreeAnalysis(tree) - if err != nil { - return nil, errors.Wrap(err, "monitor.AnalyzeTree->NewTreeAnalysis error") - } - - return anal, nil -} - -func (mon *ReorgMonitor) String() string { - return fmt.Sprintf("ReorgMonitor: %d - %d, %d / %d blocks, %d reorgcache", mon.EarliestBlockNumber, mon.LatestBlockNumber, len(mon.blockByHash), len(mon.blocksByHeight), len(mon.knownReorgs)) -} diff --git a/reorgdetector/reorgdetectorv2_test.go b/reorgdetector/reorgdetectorv2_test.go deleted file mode 100644 index 712b156a..00000000 --- a/reorgdetector/reorgdetectorv2_test.go +++ /dev/null @@ -1,99 +0,0 @@ -package reorgdetector - -import ( - "context" - "math/big" - "testing" - "time" - - "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" - "github.com/stretchr/testify/require" -) - -func newSimulatedL1(t *testing.T, auth *bind.TransactOpts) *simulated.Backend { - t.Helper() - - balance, _ := new(big.Int).SetString("10000000000000000000000000", 10) //nolint:gomnd - - blockGasLimit := uint64(999999999999999999) //nolint:gomnd - client := simulated.NewBackend(map[common.Address]types.Account{ - auth.From: { - Balance: balance, - }, - }, simulated.WithBlockGasLimit(blockGasLimit)) - client.Commit() - - return client -} - -func Test_ReorgDetectorV2(t *testing.T) { - const produceBlocks = 29 - const reorgPeriod = 5 - const reorgDepth = 2 - - 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) - clientL1 := newSimulatedL1(t, authL1) - require.NoError(t, err) - - mon := NewReorgMonitor(clientL1.Client(), 100) - - sub, err := mon.Subscribe("test") - require.NoError(t, err) - - err = mon.Start(context.Background()) - require.NoError(t, err) - - expectedReorgBlocks := make(map[uint64]bool) - lastReorgOn := int64(0) - for i := 1; lastReorgOn <= produceBlocks; i++ { - block := clientL1.Commit() - time.Sleep(time.Millisecond) - - header, err := clientL1.Client().HeaderByHash(ctx, block) - require.NoError(t, err) - headerNumber := header.Number.Int64() - - // Reorg every "reorgPeriod" blocks with "reorgDepth" blocks depth - if headerNumber > lastReorgOn && headerNumber%reorgPeriod == 0 { - lastReorgOn = headerNumber - - reorgBlock, err := clientL1.Client().BlockByNumber(ctx, big.NewInt(headerNumber-reorgDepth)) - require.NoError(t, err) - - expectedReorgBlocks[reorgBlock.NumberU64()] = false - - err = clientL1.Fork(reorgBlock.Hash()) - require.NoError(t, err) - } - } - - // Commit some blocks to ensure reorgs are detected - for i := 0; i < reorgPeriod; i++ { - clientL1.Commit() - } - - for range expectedReorgBlocks { - firstReorgedBlock := <-sub.FirstReorgedBlock - sub.ReorgProcessed <- true - - processed, ok := expectedReorgBlocks[firstReorgedBlock-1] - require.True(t, ok) - require.False(t, processed) - - expectedReorgBlocks[firstReorgedBlock-1] = true - } - - for _, processed := range expectedReorgBlocks { - require.True(t, processed) - } -} diff --git a/reorgdetector/types.go b/reorgdetector/types.go index ab2c2fa3..25ac4031 100644 --- a/reorgdetector/types.go +++ b/reorgdetector/types.go @@ -1,353 +1,81 @@ package reorgdetector import ( - "fmt" "sort" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" + common "github.com/ethereum/go-ethereum/common" ) -type BlockOrigin string - -const ( - OriginSubscription BlockOrigin = "Subscription" - OriginGetParent BlockOrigin = "GetParent" -) - -// Reorg is the analysis Summary of a specific reorg -type Reorg struct { - IsFinished bool - SeenLive bool - - StartBlockHeight uint64 // first block in a reorg (block number after common parent) - EndBlockHeight uint64 // last block in a reorg - - Chains map[common.Hash][]*Block - - Depth int - - BlocksInvolved map[common.Hash]*Block - MainChainHash common.Hash - MainChainBlocks map[common.Hash]*Block - NumReplacedBlocks int - - CommonParent *Block - FirstBlockAfterReorg *Block -} - -func NewReorg(parentNode *TreeNode) (*Reorg, error) { - if len(parentNode.Children) < 2 { - return nil, fmt.Errorf("cannot create reorg because parent node with < 2 children") - } - - reorg := Reorg{ - CommonParent: parentNode.Block, - StartBlockHeight: parentNode.Block.Number() + 1, - - Chains: make(map[common.Hash][]*Block), - BlocksInvolved: make(map[common.Hash]*Block), - MainChainBlocks: make(map[common.Hash]*Block), - - SeenLive: true, // will be set to false if any of the added blocks was received via uncle-info - } - - // Build the individual chains until the enc, by iterating over children recursively - for _, chainRootNode := range parentNode.Children { - chain := make([]*Block, 0) - - var addChildToChainRecursive func(node *TreeNode) - addChildToChainRecursive = func(node *TreeNode) { - chain = append(chain, node.Block) - - for _, childNode := range node.Children { - addChildToChainRecursive(childNode) - } - } - addChildToChainRecursive(chainRootNode) - reorg.Chains[chainRootNode.Block.Header.Hash()] = chain - } - - // Find depth of chains - chainLengths := []int{} - for _, chain := range reorg.Chains { - chainLengths = append(chainLengths, len(chain)) - } - sort.Sort(sort.Reverse(sort.IntSlice(chainLengths))) - - // Depth is number of blocks in second chain - reorg.Depth = chainLengths[1] - - // Truncate the longest chain to the second, which is when the reorg actually stopped - for key, chain := range reorg.Chains { - if len(chain) > reorg.Depth { - reorg.FirstBlockAfterReorg = chain[reorg.Depth] // first block that will be truncated - reorg.Chains[key] = chain[:reorg.Depth] - reorg.MainChainHash = key - } - } - - // If two chains with same height, then the reorg isn't yet finalized - if chainLengths[0] == chainLengths[1] { - reorg.IsFinished = false - } else { - reorg.IsFinished = true - } - - // Build final list of involved blocks, and get end blockheight - for chainHash, chain := range reorg.Chains { - for _, block := range chain { - reorg.BlocksInvolved[block.Header.Hash()] = block - - if block.Origin != OriginSubscription && block.Origin != OriginGetParent { - reorg.SeenLive = false - } - - if chainHash == reorg.MainChainHash { - reorg.MainChainBlocks[block.Header.Hash()] = block - reorg.EndBlockHeight = block.Number() - } - } - } - - reorg.NumReplacedBlocks = len(reorg.BlocksInvolved) - reorg.Depth - - return &reorg, nil -} - -func (r *Reorg) Id() string { - id := fmt.Sprintf("%d_%d_d%d_b%d", r.StartBlockHeight, r.EndBlockHeight, r.Depth, len(r.BlocksInvolved)) - if r.SeenLive { - id += "_l" - } - return id -} - -func (r *Reorg) String() string { - return fmt.Sprintf("Reorg %s: live=%-5v chains=%d, depth=%d, replaced=%d", r.Id(), r.SeenLive, len(r.Chains), r.Depth, r.NumReplacedBlocks) -} - -func (r *Reorg) MermaidSyntax() string { - ret := "stateDiagram-v2\n" - - for _, block := range r.BlocksInvolved { - ret += fmt.Sprintf(" %s --> %s\n", block.Header.ParentHash, block.Header.Hash()) - } - - // Add first block after reorg - ret += fmt.Sprintf(" %s --> %s", r.FirstBlockAfterReorg.Header.ParentHash, r.FirstBlockAfterReorg.Header.Hash()) - return ret -} - -type TreeNode struct { - Block *Block - Parent *TreeNode - Children []*TreeNode - - IsFirst bool - IsMainChain bool -} - -func NewTreeNode(block *Block, parent *TreeNode) *TreeNode { - return &TreeNode{ - Block: block, - Parent: parent, - Children: []*TreeNode{}, - IsFirst: parent == nil, - } -} - -func (tn *TreeNode) String() string { - return fmt.Sprintf("TreeNode %d %s main=%5v \t first=%5v, %d children", tn.Block.Number(), tn.Block.Header.Hash(), tn.IsMainChain, tn.IsFirst, len(tn.Children)) -} - -func (tn *TreeNode) AddChild(node *TreeNode) { - tn.Children = append(tn.Children, node) +type block struct { + Num uint64 + Hash common.Hash + ParentHash common.Hash } -// TreeAnalysis takes in a BlockTree and collects information about reorgs -type TreeAnalysis struct { - Tree *BlockTree - - StartBlockHeight uint64 // first block number with siblings - EndBlockHeight uint64 - IsSplitOngoing bool - - NumBlocks int - NumBlocksMainChain int - - Reorgs map[string]*Reorg -} - -func NewTreeAnalysis(t *BlockTree) (*TreeAnalysis, error) { - analysis := TreeAnalysis{ - Tree: t, - Reorgs: make(map[string]*Reorg), - } - - if t.FirstNode == nil { // empty analysis for empty tree - return &analysis, nil - } - - analysis.StartBlockHeight = t.FirstNode.Block.Number() - analysis.EndBlockHeight = t.LatestNodes[0].Block.Number() - - if len(t.LatestNodes) > 1 { - analysis.IsSplitOngoing = true - } +type blockMap map[uint64]block - analysis.NumBlocks = len(t.NodeByHash) - analysis.NumBlocksMainChain = len(t.MainChainNodeByHash) +// newBlockMap returns a new instance of blockMap +func newBlockMap(blocks ...block) blockMap { + blockMap := make(blockMap, len(blocks)) - // Find reorgs - for _, node := range t.NodeByHash { - if len(node.Children) > 1 { - reorg, err := NewReorg(node) - if err != nil { - return nil, err - } - analysis.Reorgs[reorg.Id()] = reorg - } + for _, b := range blocks { + blockMap[b.Num] = b } - return &analysis, nil + return blockMap } -func (a *TreeAnalysis) Print() { - fmt.Printf("TreeAnalysis %d - %d, nodes: %d, mainchain: %d, reorgs: %d\n", a.StartBlockHeight, a.EndBlockHeight, a.NumBlocks, a.NumBlocksMainChain, len(a.Reorgs)) - if a.IsSplitOngoing { - fmt.Println("- split ongoing") - } - - for _, reorg := range a.Reorgs { - fmt.Println("") - fmt.Println(reorg.String()) - fmt.Printf("- common parent: %d %s, first block after: %d %s\n", reorg.CommonParent.Header.Number.Uint64(), reorg.CommonParent.Header.Hash(), reorg.FirstBlockAfterReorg.Header.Number.Uint64(), reorg.FirstBlockAfterReorg.Header.Hash()) +// getSorted returns blocks in sorted order +func (bm blockMap) getSorted() []block { + sortedBlocks := make([]block, 0, len(bm)) - for chainKey, chain := range reorg.Chains { - if chainKey == reorg.MainChainHash { - fmt.Printf("- mainchain l=%d: ", len(chain)) - } else { - fmt.Printf("- sidechain l=%d: ", len(chain)) - } - for _, block := range chain { - fmt.Printf("%s ", block.Header.Hash()) - } - fmt.Print("\n") - } + for _, b := range bm { + sortedBlocks = append(sortedBlocks, b) } -} -// BlockTree is the tree of blocks, used to traverse up (from children to parents) and down (from parents to children). -// Reorgs start on each node with more than one child. -type BlockTree struct { - FirstNode *TreeNode - LatestNodes []*TreeNode // Nodes at latest blockheight (can be more than 1) - NodeByHash map[common.Hash]*TreeNode - MainChainNodeByHash map[common.Hash]*TreeNode -} + sort.Slice(sortedBlocks, func(i, j int) bool { + return sortedBlocks[i].Num < sortedBlocks[j].Num + }) -func NewBlockTree() *BlockTree { - return &BlockTree{ - LatestNodes: []*TreeNode{}, - NodeByHash: make(map[common.Hash]*TreeNode), - MainChainNodeByHash: make(map[common.Hash]*TreeNode), - } + return sortedBlocks } -func (t *BlockTree) AddBlock(block *Block) error { - // First block is a special case - if t.FirstNode == nil { - node := NewTreeNode(block, nil) - t.FirstNode = node - t.LatestNodes = []*TreeNode{node} - t.NodeByHash[block.Header.Hash()] = node - return nil - } - - // All other blocks are inserted as child of it's parent parent - parent, parentFound := t.NodeByHash[block.Header.ParentHash] - if !parentFound { - err := fmt.Errorf("error in BlockTree.AddBlock(): parent not found. block: %d %s, parent: %s", block.Number(), block.Header.Hash(), block.Header.ParentHash) - return err - } - - node := NewTreeNode(block, parent) - t.NodeByHash[block.Header.Hash()] = node - parent.AddChild(node) +// getFromBlockSorted returns blocks from blockNum in sorted order without including the blockNum +func (bm blockMap) getFromBlockSorted(blockNum uint64) []block { + sortedBlocks := bm.getSorted() - // Remember nodes at latest block height - if len(t.LatestNodes) == 0 { - t.LatestNodes = []*TreeNode{node} - } else { - if block.Number() == t.LatestNodes[0].Block.Number() { // add to list of latest nodes! - t.LatestNodes = append(t.LatestNodes, node) - } else if block.Number() > t.LatestNodes[0].Block.Number() { // replace - t.LatestNodes = []*TreeNode{node} + index := -1 + for i, b := range sortedBlocks { + if b.Num > blockNum { + index = i + break } } - // Mark main-chain nodes as such. Step 1: reset all nodes to non-main-chain - t.MainChainNodeByHash = make(map[common.Hash]*TreeNode) - for _, n := range t.NodeByHash { - n.IsMainChain = false + if index == -1 { + return []block{} } - // Step 2: Traverse backwards and mark main chain. If there's more than 1 nodes at latest height, then we don't yet know which chain will be the main-chain - if len(t.LatestNodes) == 1 { - var TraverseMainChainFromLatestToEarliest func(node *TreeNode) - TraverseMainChainFromLatestToEarliest = func(node *TreeNode) { - if node == nil { - return - } - node.IsMainChain = true - t.MainChainNodeByHash[node.Block.Header.Hash()] = node - TraverseMainChainFromLatestToEarliest(node.Parent) - } - TraverseMainChainFromLatestToEarliest(t.LatestNodes[0]) - } - - return nil + return sortedBlocks[index:] } -func (t *BlockTree) Print() { - fmt.Printf("BlockTree: nodes=%d\n", len(t.NodeByHash)) - - if t.FirstNode == nil { - return +// getClosestHigherBlock returns the closest higher block to the given blockNum +func (bm blockMap) getClosestHigherBlock(blockNum uint64) (block, bool) { + if block, ok := bm[blockNum]; ok { + return block, true } - // Print tree by traversing from parent to all children - PrintNodeAndChildren(t.FirstNode, 1) - - // Print latest nodes - fmt.Printf("Latest nodes:\n") - for _, n := range t.LatestNodes { - fmt.Println("-", n.String()) + sorted := bm.getFromBlockSorted(blockNum) + if len(sorted) == 0 { + return block{}, false } -} - -// Block is a geth Block and information about where it came from -type Block struct { - Header *types.Header - Origin BlockOrigin -} - -func NewBlock(header *types.Header, origin BlockOrigin) *Block { - return &Block{ - Header: header, - Origin: origin, - } -} -func (b *Block) Number() uint64 { - return b.Header.Number.Uint64() + return sorted[0], true } -func PrintNodeAndChildren(node *TreeNode, depth int) { - indent := "-" - fmt.Printf("%s %s\n", indent, node.String()) - for _, childNode := range node.Children { - PrintNodeAndChildren(childNode, depth+1) +// removeRange removes blocks from "from" to "to" +func (bm blockMap) removeRange(from, to uint64) { + for i := from; i <= to; i++ { + delete(bm, i) } } diff --git a/reorgdetector/types_test.go b/reorgdetector/types_test.go new file mode 100644 index 00000000..8a588070 --- /dev/null +++ b/reorgdetector/types_test.go @@ -0,0 +1 @@ +package reorgdetector diff --git a/sync/evmdriver.go b/sync/evmdriver.go index 8616e2a5..66f21800 100644 --- a/sync/evmdriver.go +++ b/sync/evmdriver.go @@ -31,7 +31,7 @@ type processorInterface interface { type ReorgDetector interface { Subscribe(id string) (*reorgdetector.Subscription, error) - AddBlockToTrack(ctx context.Context, id string, blockNum uint64, blockHash common.Hash) error + AddBlockToTrack(ctx context.Context, id string, blockNum uint64, blockHash, parentHash common.Hash) error } func NewEVMDriver( @@ -97,7 +97,7 @@ reset: func (d *EVMDriver) handleNewBlock(ctx context.Context, b EVMBlock) { attempts := 0 for { - err := d.reorgDetector.AddBlockToTrack(ctx, d.reorgDetectorID, b.Num, b.Hash) + err := d.reorgDetector.AddBlockToTrack(ctx, d.reorgDetectorID, b.Num, b.Hash, b.ParentHash) if err != nil { attempts++ log.Errorf("error adding block %d to tracker: %v", b.Num, err) From 47c3b94e16dd76a2e7bf7473103dc27f4ffda831 Mon Sep 17 00:00:00 2001 From: begmaroman Date: Wed, 21 Aug 2024 14:44:18 +0100 Subject: [PATCH 12/43] Implementation --- reorgdetector/reorgdetector.go | 86 +++++++++++----------------------- 1 file changed, 27 insertions(+), 59 deletions(-) diff --git a/reorgdetector/reorgdetector.go b/reorgdetector/reorgdetector.go index 2ebff213..f96ef772 100644 --- a/reorgdetector/reorgdetector.go +++ b/reorgdetector/reorgdetector.go @@ -146,6 +146,24 @@ func (rd *ReorgDetector) AddBlockToTrack(ctx context.Context, id string, blockNu return blocks, nil } + processReorg := func() error { + trackedBlocks[newBlock.Num] = newBlock + reorgedBlock := findStartReorgBlock(trackedBlocks) + if reorgedBlock != nil { + rd.notifySubscribers(*reorgedBlock) + + newBlocksMap, err := rebuildBlocksMap(trackedBlocks, reorgedBlock.Num, newBlock.Num) + if err != nil { + return err + } + rd.trackedBlocks[id] = newBlocksMap + } else { + // Should not happen + } + + return nil + } + closestHigherBlock, ok := trackedBlocks.getClosestHigherBlock(newBlock.Num) if !ok { // No same or higher blocks, only lower blocks exist. Check hashes. @@ -159,20 +177,7 @@ func (rd *ReorgDetector) AddBlockToTrack(ctx context.Context, id string, blockNu if closestBlock.Hash != newBlock.ParentHash { // Block hashes do not match, reorg happened // TODO: Reorg happened - - trackedBlocks[newBlock.Num] = newBlock - reorgedBlock := findStartReorgBlock(trackedBlocks) - if reorgedBlock != nil { - fmt.Println("Reorg detected at block", reorgedBlock.Num) - rd.notifySubscribers(*reorgedBlock) - newBlocksMap, err := rebuildBlocksMap(trackedBlocks, reorgedBlock.Num, newBlock.Num) - if err != nil { - return err - } - rd.trackedBlocks[id] = newBlocksMap - } else { - // Should not happen - } + return processReorg() } else { // All good, add the block to the map rd.trackedBlocks[id][newBlock.Num] = newBlock @@ -188,57 +193,18 @@ func (rd *ReorgDetector) AddBlockToTrack(ctx context.Context, id string, blockNu if closestHigherBlock.Hash != newBlock.Hash { // Block hashes have changed, reorg happened // TODO: Handle happened - - trackedBlocks[newBlock.Num] = newBlock - reorgedBlock := findStartReorgBlock(trackedBlocks) - if reorgedBlock != nil { - fmt.Println("Reorg detected at block", reorgedBlock.Num) - rd.notifySubscribers(*reorgedBlock) - newBlocksMap, err := rebuildBlocksMap(trackedBlocks, reorgedBlock.Num, newBlock.Num) - if err != nil { - return err - } - rd.trackedBlocks[id] = newBlocksMap - } else { - // Should not happen - } + return processReorg() } } else if closestHigherBlock.Num == newBlock.Num+1 { // The given block is lower than the closest higher block: // Current tracked blocks: N-2, N-1, N (given block), N+1, N+2 // TODO: Reorg happened - - trackedBlocks[newBlock.Num] = newBlock - reorgedBlock := findStartReorgBlock(trackedBlocks) - if reorgedBlock != nil { - fmt.Println("Reorg detected at block", reorgedBlock.Num) - rd.notifySubscribers(*reorgedBlock) - newBlocksMap, err := rebuildBlocksMap(trackedBlocks, reorgedBlock.Num, newBlock.Num) - if err != nil { - return err - } - rd.trackedBlocks[id] = newBlocksMap - } else { - // Should not happen - } + return processReorg() } else if closestHigherBlock.Num > newBlock.Num+1 { // There is a gap between the current block and the closest higher block // Current tracked blocks: N-2, N-1, N (given block), , N+i // TODO: Reorg happened - - trackedBlocks[newBlock.Num] = newBlock - reorgedBlock := findStartReorgBlock(trackedBlocks) - if reorgedBlock != nil { - fmt.Println("Reorg detected at block", reorgedBlock.Num) - rd.notifySubscribers(*reorgedBlock) - newBlocksMap, err := rebuildBlocksMap(trackedBlocks, reorgedBlock.Num, newBlock.Num) - if err != nil { - return err - } - rd.trackedBlocks[id] = newBlocksMap - } else { - // Should not happen - } + return processReorg() } else { // This should not happen log.Fatal("Unexpected block number comparison") @@ -252,9 +218,11 @@ func (rd *ReorgDetector) notifySubscribers(startingBlock block) { rd.subscriptionsLock.RLock() for _, sub := range rd.subscriptions { sub.pendingReorgsToBeProcessed.Add(1) - sub.FirstReorgedBlock <- startingBlock.Num - <-sub.ReorgProcessed - sub.pendingReorgsToBeProcessed.Done() + go func(sub *Subscription) { + sub.FirstReorgedBlock <- startingBlock.Num + <-sub.ReorgProcessed + sub.pendingReorgsToBeProcessed.Done() + }(sub) } rd.subscriptionsLock.RUnlock() } From 9913d87a18189da05893af6045ed1130e5e23817 Mon Sep 17 00:00:00 2001 From: begmaroman Date: Wed, 21 Aug 2024 16:58:02 +0100 Subject: [PATCH 13/43] Implementation --- aggoracle/mock_ethtxmanager_test.go | 27 +- bridgesync/mock_l2_test.go | 83 ++++- cmd/run.go | 21 +- .../mocks_da/batch_data_provider.go | 13 +- dataavailability/mocks_da/da_backender.go | 25 +- dataavailability/mocks_da/data_manager.go | 21 +- dataavailability/mocks_da/func_sign_type.go | 13 +- .../mocks_da/sequence_retriever.go | 13 +- dataavailability/mocks_da/sequence_sender.go | 17 +- .../mocks_da/sequence_sender_banana.go | 13 +- .../mocks_da/sequence_sender_elderberry.go | 13 +- l1infotreesync/e2e_test.go | 2 +- l1infotreesync/mock_reorgdetector_test.go | 29 +- reorgdetector/config.go | 5 + reorgdetector/reorgdetector.go | 287 +++++++++++++----- reorgdetector/reorgdetector_db.go | 131 ++++++++ reorgdetector/reorgdetector_test.go | 24 +- reorgdetector/types.go | 37 ++- reorgdetector/types_test.go | 103 +++++++ sync/mock_downloader_test.go | 27 +- sync/mock_l2_test.go | 83 ++++- sync/mock_processor_test.go | 23 +- sync/mock_reorgdetector_test.go | 29 +- 23 files changed, 856 insertions(+), 183 deletions(-) create mode 100644 reorgdetector/config.go create mode 100644 reorgdetector/reorgdetector_db.go diff --git a/aggoracle/mock_ethtxmanager_test.go b/aggoracle/mock_ethtxmanager_test.go index 37bcbeda..07fea41b 100644 --- a/aggoracle/mock_ethtxmanager_test.go +++ b/aggoracle/mock_ethtxmanager_test.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.22.1. DO NOT EDIT. +// Code generated by mockery v2.40.1. DO NOT EDIT. package aggoracle @@ -25,6 +25,10 @@ type EthTxManagerMock struct { func (_m *EthTxManagerMock) Add(ctx context.Context, to *common.Address, forcedNonce *uint64, value *big.Int, data []byte, gasOffset uint64, sidecar *types.BlobTxSidecar) (common.Hash, error) { ret := _m.Called(ctx, to, forcedNonce, value, data, gasOffset, sidecar) + if len(ret) == 0 { + panic("no return value specified for Add") + } + var r0 common.Hash var r1 error if rf, ok := ret.Get(0).(func(context.Context, *common.Address, *uint64, *big.Int, []byte, uint64, *types.BlobTxSidecar) (common.Hash, error)); ok { @@ -51,6 +55,10 @@ func (_m *EthTxManagerMock) Add(ctx context.Context, to *common.Address, forcedN func (_m *EthTxManagerMock) Remove(ctx context.Context, id common.Hash) error { ret := _m.Called(ctx, id) + if len(ret) == 0 { + panic("no return value specified for Remove") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, common.Hash) error); ok { r0 = rf(ctx, id) @@ -65,6 +73,10 @@ func (_m *EthTxManagerMock) Remove(ctx context.Context, id common.Hash) error { func (_m *EthTxManagerMock) Result(ctx context.Context, id common.Hash) (ethtxmanager.MonitoredTxResult, error) { ret := _m.Called(ctx, id) + if len(ret) == 0 { + panic("no return value specified for Result") + } + var r0 ethtxmanager.MonitoredTxResult var r1 error if rf, ok := ret.Get(0).(func(context.Context, common.Hash) (ethtxmanager.MonitoredTxResult, error)); ok { @@ -89,6 +101,10 @@ func (_m *EthTxManagerMock) Result(ctx context.Context, id common.Hash) (ethtxma func (_m *EthTxManagerMock) ResultsByStatus(ctx context.Context, statuses []ethtxmanager.MonitoredTxStatus) ([]ethtxmanager.MonitoredTxResult, error) { ret := _m.Called(ctx, statuses) + if len(ret) == 0 { + panic("no return value specified for ResultsByStatus") + } + var r0 []ethtxmanager.MonitoredTxResult var r1 error if rf, ok := ret.Get(0).(func(context.Context, []ethtxmanager.MonitoredTxStatus) ([]ethtxmanager.MonitoredTxResult, error)); ok { @@ -111,13 +127,12 @@ func (_m *EthTxManagerMock) ResultsByStatus(ctx context.Context, statuses []etht return r0, r1 } -type mockConstructorTestingTNewEthTxManagerMock interface { +// NewEthTxManagerMock creates a new instance of EthTxManagerMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewEthTxManagerMock(t interface { mock.TestingT Cleanup(func()) -} - -// NewEthTxManagerMock creates a new instance of EthTxManagerMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewEthTxManagerMock(t mockConstructorTestingTNewEthTxManagerMock) *EthTxManagerMock { +}) *EthTxManagerMock { mock := &EthTxManagerMock{} mock.Mock.Test(t) diff --git a/bridgesync/mock_l2_test.go b/bridgesync/mock_l2_test.go index 8c37a56d..a8f33ef8 100644 --- a/bridgesync/mock_l2_test.go +++ b/bridgesync/mock_l2_test.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.22.1. DO NOT EDIT. +// Code generated by mockery v2.40.1. DO NOT EDIT. package bridgesync @@ -24,6 +24,10 @@ type L2Mock struct { func (_m *L2Mock) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) { ret := _m.Called(ctx, hash) + if len(ret) == 0 { + panic("no return value specified for BlockByHash") + } + var r0 *types.Block var r1 error if rf, ok := ret.Get(0).(func(context.Context, common.Hash) (*types.Block, error)); ok { @@ -50,6 +54,10 @@ func (_m *L2Mock) BlockByHash(ctx context.Context, hash common.Hash) (*types.Blo func (_m *L2Mock) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) { ret := _m.Called(ctx, number) + if len(ret) == 0 { + panic("no return value specified for BlockByNumber") + } + var r0 *types.Block var r1 error if rf, ok := ret.Get(0).(func(context.Context, *big.Int) (*types.Block, error)); ok { @@ -76,6 +84,10 @@ func (_m *L2Mock) BlockByNumber(ctx context.Context, number *big.Int) (*types.Bl func (_m *L2Mock) BlockNumber(ctx context.Context) (uint64, error) { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for BlockNumber") + } + var r0 uint64 var r1 error if rf, ok := ret.Get(0).(func(context.Context) (uint64, error)); ok { @@ -100,6 +112,10 @@ func (_m *L2Mock) BlockNumber(ctx context.Context) (uint64, error) { func (_m *L2Mock) CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { ret := _m.Called(ctx, call, blockNumber) + if len(ret) == 0 { + panic("no return value specified for CallContract") + } + var r0 []byte var r1 error if rf, ok := ret.Get(0).(func(context.Context, ethereum.CallMsg, *big.Int) ([]byte, error)); ok { @@ -126,6 +142,10 @@ func (_m *L2Mock) CallContract(ctx context.Context, call ethereum.CallMsg, block func (_m *L2Mock) CodeAt(ctx context.Context, contract common.Address, blockNumber *big.Int) ([]byte, error) { ret := _m.Called(ctx, contract, blockNumber) + if len(ret) == 0 { + panic("no return value specified for CodeAt") + } + var r0 []byte var r1 error if rf, ok := ret.Get(0).(func(context.Context, common.Address, *big.Int) ([]byte, error)); ok { @@ -152,6 +172,10 @@ func (_m *L2Mock) CodeAt(ctx context.Context, contract common.Address, blockNumb func (_m *L2Mock) EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint64, error) { ret := _m.Called(ctx, call) + if len(ret) == 0 { + panic("no return value specified for EstimateGas") + } + var r0 uint64 var r1 error if rf, ok := ret.Get(0).(func(context.Context, ethereum.CallMsg) (uint64, error)); ok { @@ -176,6 +200,10 @@ func (_m *L2Mock) EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint6 func (_m *L2Mock) FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) { ret := _m.Called(ctx, q) + if len(ret) == 0 { + panic("no return value specified for FilterLogs") + } + var r0 []types.Log var r1 error if rf, ok := ret.Get(0).(func(context.Context, ethereum.FilterQuery) ([]types.Log, error)); ok { @@ -202,6 +230,10 @@ func (_m *L2Mock) FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]typ func (_m *L2Mock) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) { ret := _m.Called(ctx, hash) + if len(ret) == 0 { + panic("no return value specified for HeaderByHash") + } + var r0 *types.Header var r1 error if rf, ok := ret.Get(0).(func(context.Context, common.Hash) (*types.Header, error)); ok { @@ -228,6 +260,10 @@ func (_m *L2Mock) HeaderByHash(ctx context.Context, hash common.Hash) (*types.He func (_m *L2Mock) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) { ret := _m.Called(ctx, number) + if len(ret) == 0 { + panic("no return value specified for HeaderByNumber") + } + var r0 *types.Header var r1 error if rf, ok := ret.Get(0).(func(context.Context, *big.Int) (*types.Header, error)); ok { @@ -254,6 +290,10 @@ func (_m *L2Mock) HeaderByNumber(ctx context.Context, number *big.Int) (*types.H func (_m *L2Mock) PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error) { ret := _m.Called(ctx, account) + if len(ret) == 0 { + panic("no return value specified for PendingCodeAt") + } + var r0 []byte var r1 error if rf, ok := ret.Get(0).(func(context.Context, common.Address) ([]byte, error)); ok { @@ -280,6 +320,10 @@ func (_m *L2Mock) PendingCodeAt(ctx context.Context, account common.Address) ([] func (_m *L2Mock) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) { ret := _m.Called(ctx, account) + if len(ret) == 0 { + panic("no return value specified for PendingNonceAt") + } + var r0 uint64 var r1 error if rf, ok := ret.Get(0).(func(context.Context, common.Address) (uint64, error)); ok { @@ -304,6 +348,10 @@ func (_m *L2Mock) PendingNonceAt(ctx context.Context, account common.Address) (u func (_m *L2Mock) SendTransaction(ctx context.Context, tx *types.Transaction) error { ret := _m.Called(ctx, tx) + if len(ret) == 0 { + panic("no return value specified for SendTransaction") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, *types.Transaction) error); ok { r0 = rf(ctx, tx) @@ -318,6 +366,10 @@ func (_m *L2Mock) SendTransaction(ctx context.Context, tx *types.Transaction) er func (_m *L2Mock) SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) { ret := _m.Called(ctx, q, ch) + if len(ret) == 0 { + panic("no return value specified for SubscribeFilterLogs") + } + var r0 ethereum.Subscription var r1 error if rf, ok := ret.Get(0).(func(context.Context, ethereum.FilterQuery, chan<- types.Log) (ethereum.Subscription, error)); ok { @@ -344,6 +396,10 @@ func (_m *L2Mock) SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuer func (_m *L2Mock) SubscribeNewHead(ctx context.Context, ch chan<- *types.Header) (ethereum.Subscription, error) { ret := _m.Called(ctx, ch) + if len(ret) == 0 { + panic("no return value specified for SubscribeNewHead") + } + var r0 ethereum.Subscription var r1 error if rf, ok := ret.Get(0).(func(context.Context, chan<- *types.Header) (ethereum.Subscription, error)); ok { @@ -370,6 +426,10 @@ func (_m *L2Mock) SubscribeNewHead(ctx context.Context, ch chan<- *types.Header) func (_m *L2Mock) SuggestGasPrice(ctx context.Context) (*big.Int, error) { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for SuggestGasPrice") + } + var r0 *big.Int var r1 error if rf, ok := ret.Get(0).(func(context.Context) (*big.Int, error)); ok { @@ -396,6 +456,10 @@ func (_m *L2Mock) SuggestGasPrice(ctx context.Context) (*big.Int, error) { func (_m *L2Mock) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for SuggestGasTipCap") + } + var r0 *big.Int var r1 error if rf, ok := ret.Get(0).(func(context.Context) (*big.Int, error)); ok { @@ -422,6 +486,10 @@ func (_m *L2Mock) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { func (_m *L2Mock) TransactionCount(ctx context.Context, blockHash common.Hash) (uint, error) { ret := _m.Called(ctx, blockHash) + if len(ret) == 0 { + panic("no return value specified for TransactionCount") + } + var r0 uint var r1 error if rf, ok := ret.Get(0).(func(context.Context, common.Hash) (uint, error)); ok { @@ -446,6 +514,10 @@ func (_m *L2Mock) TransactionCount(ctx context.Context, blockHash common.Hash) ( func (_m *L2Mock) TransactionInBlock(ctx context.Context, blockHash common.Hash, index uint) (*types.Transaction, error) { ret := _m.Called(ctx, blockHash, index) + if len(ret) == 0 { + panic("no return value specified for TransactionInBlock") + } + var r0 *types.Transaction var r1 error if rf, ok := ret.Get(0).(func(context.Context, common.Hash, uint) (*types.Transaction, error)); ok { @@ -468,13 +540,12 @@ func (_m *L2Mock) TransactionInBlock(ctx context.Context, blockHash common.Hash, return r0, r1 } -type mockConstructorTestingTNewL2Mock interface { +// NewL2Mock creates a new instance of L2Mock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewL2Mock(t interface { mock.TestingT Cleanup(func()) -} - -// NewL2Mock creates a new instance of L2Mock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewL2Mock(t mockConstructorTestingTNewL2Mock) *L2Mock { +}) *L2Mock { mock := &L2Mock{} mock.Mock.Test(t) diff --git a/cmd/run.go b/cmd/run.go index 9a1b3825..f1e15fce 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -440,24 +440,17 @@ func runReorgDetectorL1IfNeeded(ctx context.Context, components []string, l1Clie if !isNeeded([]string{SEQUENCE_SENDER, AGGREGATOR, AGGORACLE}, components) { return nil } - // rd := newReorgDetector(ctx, dbPath, l1Client) - rd := reorgdetector.NewReorgMonitor(l1Client, 100) + + rd, err := reorgdetector.New(l1Client, dbPath) + if err != nil { + log.Fatal(err) + } + go func() { - if err := rd.Start(ctx); err != nil { + if err = rd.Start(ctx); err != nil { log.Fatal(err) } }() - return rd -} -func newReorgDetector( - ctx context.Context, - dbPath string, - client *ethclient.Client, -) *reorgdetector.ReorgDetector { - rd, err := reorgdetector.New(ctx, client, dbPath) - if err != nil { - log.Fatal(err) - } return rd } diff --git a/dataavailability/mocks_da/batch_data_provider.go b/dataavailability/mocks_da/batch_data_provider.go index 6d44a550..36e782ac 100644 --- a/dataavailability/mocks_da/batch_data_provider.go +++ b/dataavailability/mocks_da/batch_data_provider.go @@ -25,6 +25,10 @@ func (_m *BatchDataProvider) EXPECT() *BatchDataProvider_Expecter { func (_m *BatchDataProvider) GetBatchL2Data(batchNum []uint64, batchHashes []common.Hash, dataAvailabilityMessage []byte) ([][]byte, error) { ret := _m.Called(batchNum, batchHashes, dataAvailabilityMessage) + if len(ret) == 0 { + panic("no return value specified for GetBatchL2Data") + } + var r0 [][]byte var r1 error if rf, ok := ret.Get(0).(func([]uint64, []common.Hash, []byte) ([][]byte, error)); ok { @@ -77,13 +81,12 @@ func (_c *BatchDataProvider_GetBatchL2Data_Call) RunAndReturn(run func([]uint64, return _c } -type mockConstructorTestingTNewBatchDataProvider interface { +// NewBatchDataProvider creates a new instance of BatchDataProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewBatchDataProvider(t interface { mock.TestingT Cleanup(func()) -} - -// NewBatchDataProvider creates a new instance of BatchDataProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewBatchDataProvider(t mockConstructorTestingTNewBatchDataProvider) *BatchDataProvider { +}) *BatchDataProvider { mock := &BatchDataProvider{} mock.Mock.Test(t) diff --git a/dataavailability/mocks_da/da_backender.go b/dataavailability/mocks_da/da_backender.go index 770faaa0..773e447c 100644 --- a/dataavailability/mocks_da/da_backender.go +++ b/dataavailability/mocks_da/da_backender.go @@ -29,6 +29,10 @@ func (_m *DABackender) EXPECT() *DABackender_Expecter { func (_m *DABackender) GetSequence(ctx context.Context, batchHashes []common.Hash, dataAvailabilityMessage []byte) ([][]byte, error) { ret := _m.Called(ctx, batchHashes, dataAvailabilityMessage) + if len(ret) == 0 { + panic("no return value specified for GetSequence") + } + var r0 [][]byte var r1 error if rf, ok := ret.Get(0).(func(context.Context, []common.Hash, []byte) ([][]byte, error)); ok { @@ -85,6 +89,10 @@ func (_c *DABackender_GetSequence_Call) RunAndReturn(run func(context.Context, [ func (_m *DABackender) Init() error { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for Init") + } + var r0 error if rf, ok := ret.Get(0).(func() error); ok { r0 = rf() @@ -126,6 +134,10 @@ func (_c *DABackender_Init_Call) RunAndReturn(run func() error) *DABackender_Ini func (_m *DABackender) PostSequenceBanana(ctx context.Context, sequence etherman.SequenceBanana) ([]byte, error) { ret := _m.Called(ctx, sequence) + if len(ret) == 0 { + panic("no return value specified for PostSequenceBanana") + } + var r0 []byte var r1 error if rf, ok := ret.Get(0).(func(context.Context, etherman.SequenceBanana) ([]byte, error)); ok { @@ -181,6 +193,10 @@ func (_c *DABackender_PostSequenceBanana_Call) RunAndReturn(run func(context.Con func (_m *DABackender) PostSequenceElderberry(ctx context.Context, batchesData [][]byte) ([]byte, error) { ret := _m.Called(ctx, batchesData) + if len(ret) == 0 { + panic("no return value specified for PostSequenceElderberry") + } + var r0 []byte var r1 error if rf, ok := ret.Get(0).(func(context.Context, [][]byte) ([]byte, error)); ok { @@ -232,13 +248,12 @@ func (_c *DABackender_PostSequenceElderberry_Call) RunAndReturn(run func(context return _c } -type mockConstructorTestingTNewDABackender interface { +// NewDABackender creates a new instance of DABackender. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewDABackender(t interface { mock.TestingT Cleanup(func()) -} - -// NewDABackender creates a new instance of DABackender. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewDABackender(t mockConstructorTestingTNewDABackender) *DABackender { +}) *DABackender { mock := &DABackender{} mock.Mock.Test(t) diff --git a/dataavailability/mocks_da/data_manager.go b/dataavailability/mocks_da/data_manager.go index 30edf358..34345d71 100644 --- a/dataavailability/mocks_da/data_manager.go +++ b/dataavailability/mocks_da/data_manager.go @@ -29,6 +29,10 @@ func (_m *DataManager) EXPECT() *DataManager_Expecter { func (_m *DataManager) GetBatchL2Data(batchNum []uint64, batchHashes []common.Hash, dataAvailabilityMessage []byte) ([][]byte, error) { ret := _m.Called(batchNum, batchHashes, dataAvailabilityMessage) + if len(ret) == 0 { + panic("no return value specified for GetBatchL2Data") + } + var r0 [][]byte var r1 error if rf, ok := ret.Get(0).(func([]uint64, []common.Hash, []byte) ([][]byte, error)); ok { @@ -85,6 +89,10 @@ func (_c *DataManager_GetBatchL2Data_Call) RunAndReturn(run func([]uint64, []com func (_m *DataManager) PostSequenceBanana(ctx context.Context, sequence etherman.SequenceBanana) ([]byte, error) { ret := _m.Called(ctx, sequence) + if len(ret) == 0 { + panic("no return value specified for PostSequenceBanana") + } + var r0 []byte var r1 error if rf, ok := ret.Get(0).(func(context.Context, etherman.SequenceBanana) ([]byte, error)); ok { @@ -140,6 +148,10 @@ func (_c *DataManager_PostSequenceBanana_Call) RunAndReturn(run func(context.Con func (_m *DataManager) PostSequenceElderberry(ctx context.Context, batchesData [][]byte) ([]byte, error) { ret := _m.Called(ctx, batchesData) + if len(ret) == 0 { + panic("no return value specified for PostSequenceElderberry") + } + var r0 []byte var r1 error if rf, ok := ret.Get(0).(func(context.Context, [][]byte) ([]byte, error)); ok { @@ -191,13 +203,12 @@ func (_c *DataManager_PostSequenceElderberry_Call) RunAndReturn(run func(context return _c } -type mockConstructorTestingTNewDataManager interface { +// NewDataManager creates a new instance of DataManager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewDataManager(t interface { mock.TestingT Cleanup(func()) -} - -// NewDataManager creates a new instance of DataManager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewDataManager(t mockConstructorTestingTNewDataManager) *DataManager { +}) *DataManager { mock := &DataManager{} mock.Mock.Test(t) diff --git a/dataavailability/mocks_da/func_sign_type.go b/dataavailability/mocks_da/func_sign_type.go index 8d3171dc..6a343269 100644 --- a/dataavailability/mocks_da/func_sign_type.go +++ b/dataavailability/mocks_da/func_sign_type.go @@ -25,6 +25,10 @@ func (_m *FuncSignType) EXPECT() *FuncSignType_Expecter { func (_m *FuncSignType) Execute(c client.Client) ([]byte, error) { ret := _m.Called(c) + if len(ret) == 0 { + panic("no return value specified for Execute") + } + var r0 []byte var r1 error if rf, ok := ret.Get(0).(func(client.Client) ([]byte, error)); ok { @@ -75,13 +79,12 @@ func (_c *FuncSignType_Execute_Call) RunAndReturn(run func(client.Client) ([]byt return _c } -type mockConstructorTestingTNewFuncSignType interface { +// NewFuncSignType creates a new instance of FuncSignType. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewFuncSignType(t interface { mock.TestingT Cleanup(func()) -} - -// NewFuncSignType creates a new instance of FuncSignType. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewFuncSignType(t mockConstructorTestingTNewFuncSignType) *FuncSignType { +}) *FuncSignType { mock := &FuncSignType{} mock.Mock.Test(t) diff --git a/dataavailability/mocks_da/sequence_retriever.go b/dataavailability/mocks_da/sequence_retriever.go index 27212656..f82d9a70 100644 --- a/dataavailability/mocks_da/sequence_retriever.go +++ b/dataavailability/mocks_da/sequence_retriever.go @@ -27,6 +27,10 @@ func (_m *SequenceRetriever) EXPECT() *SequenceRetriever_Expecter { func (_m *SequenceRetriever) GetSequence(ctx context.Context, batchHashes []common.Hash, dataAvailabilityMessage []byte) ([][]byte, error) { ret := _m.Called(ctx, batchHashes, dataAvailabilityMessage) + if len(ret) == 0 { + panic("no return value specified for GetSequence") + } + var r0 [][]byte var r1 error if rf, ok := ret.Get(0).(func(context.Context, []common.Hash, []byte) ([][]byte, error)); ok { @@ -79,13 +83,12 @@ func (_c *SequenceRetriever_GetSequence_Call) RunAndReturn(run func(context.Cont return _c } -type mockConstructorTestingTNewSequenceRetriever interface { +// NewSequenceRetriever creates a new instance of SequenceRetriever. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewSequenceRetriever(t interface { mock.TestingT Cleanup(func()) -} - -// NewSequenceRetriever creates a new instance of SequenceRetriever. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewSequenceRetriever(t mockConstructorTestingTNewSequenceRetriever) *SequenceRetriever { +}) *SequenceRetriever { mock := &SequenceRetriever{} mock.Mock.Test(t) diff --git a/dataavailability/mocks_da/sequence_sender.go b/dataavailability/mocks_da/sequence_sender.go index be13b43b..f1e44741 100644 --- a/dataavailability/mocks_da/sequence_sender.go +++ b/dataavailability/mocks_da/sequence_sender.go @@ -27,6 +27,10 @@ func (_m *SequenceSender) EXPECT() *SequenceSender_Expecter { func (_m *SequenceSender) PostSequenceBanana(ctx context.Context, sequence etherman.SequenceBanana) ([]byte, error) { ret := _m.Called(ctx, sequence) + if len(ret) == 0 { + panic("no return value specified for PostSequenceBanana") + } + var r0 []byte var r1 error if rf, ok := ret.Get(0).(func(context.Context, etherman.SequenceBanana) ([]byte, error)); ok { @@ -82,6 +86,10 @@ func (_c *SequenceSender_PostSequenceBanana_Call) RunAndReturn(run func(context. func (_m *SequenceSender) PostSequenceElderberry(ctx context.Context, batchesData [][]byte) ([]byte, error) { ret := _m.Called(ctx, batchesData) + if len(ret) == 0 { + panic("no return value specified for PostSequenceElderberry") + } + var r0 []byte var r1 error if rf, ok := ret.Get(0).(func(context.Context, [][]byte) ([]byte, error)); ok { @@ -133,13 +141,12 @@ func (_c *SequenceSender_PostSequenceElderberry_Call) RunAndReturn(run func(cont return _c } -type mockConstructorTestingTNewSequenceSender interface { +// NewSequenceSender creates a new instance of SequenceSender. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewSequenceSender(t interface { mock.TestingT Cleanup(func()) -} - -// NewSequenceSender creates a new instance of SequenceSender. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewSequenceSender(t mockConstructorTestingTNewSequenceSender) *SequenceSender { +}) *SequenceSender { mock := &SequenceSender{} mock.Mock.Test(t) diff --git a/dataavailability/mocks_da/sequence_sender_banana.go b/dataavailability/mocks_da/sequence_sender_banana.go index faa8ba86..aca7b1a3 100644 --- a/dataavailability/mocks_da/sequence_sender_banana.go +++ b/dataavailability/mocks_da/sequence_sender_banana.go @@ -27,6 +27,10 @@ func (_m *SequenceSenderBanana) EXPECT() *SequenceSenderBanana_Expecter { func (_m *SequenceSenderBanana) PostSequenceBanana(ctx context.Context, sequence etherman.SequenceBanana) ([]byte, error) { ret := _m.Called(ctx, sequence) + if len(ret) == 0 { + panic("no return value specified for PostSequenceBanana") + } + var r0 []byte var r1 error if rf, ok := ret.Get(0).(func(context.Context, etherman.SequenceBanana) ([]byte, error)); ok { @@ -78,13 +82,12 @@ func (_c *SequenceSenderBanana_PostSequenceBanana_Call) RunAndReturn(run func(co return _c } -type mockConstructorTestingTNewSequenceSenderBanana interface { +// NewSequenceSenderBanana creates a new instance of SequenceSenderBanana. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewSequenceSenderBanana(t interface { mock.TestingT Cleanup(func()) -} - -// NewSequenceSenderBanana creates a new instance of SequenceSenderBanana. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewSequenceSenderBanana(t mockConstructorTestingTNewSequenceSenderBanana) *SequenceSenderBanana { +}) *SequenceSenderBanana { mock := &SequenceSenderBanana{} mock.Mock.Test(t) diff --git a/dataavailability/mocks_da/sequence_sender_elderberry.go b/dataavailability/mocks_da/sequence_sender_elderberry.go index d877a8bc..3816fa1b 100644 --- a/dataavailability/mocks_da/sequence_sender_elderberry.go +++ b/dataavailability/mocks_da/sequence_sender_elderberry.go @@ -25,6 +25,10 @@ func (_m *SequenceSenderElderberry) EXPECT() *SequenceSenderElderberry_Expecter func (_m *SequenceSenderElderberry) PostSequenceElderberry(ctx context.Context, batchesData [][]byte) ([]byte, error) { ret := _m.Called(ctx, batchesData) + if len(ret) == 0 { + panic("no return value specified for PostSequenceElderberry") + } + var r0 []byte var r1 error if rf, ok := ret.Get(0).(func(context.Context, [][]byte) ([]byte, error)); ok { @@ -76,13 +80,12 @@ func (_c *SequenceSenderElderberry_PostSequenceElderberry_Call) RunAndReturn(run return _c } -type mockConstructorTestingTNewSequenceSenderElderberry interface { +// NewSequenceSenderElderberry creates a new instance of SequenceSenderElderberry. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewSequenceSenderElderberry(t interface { mock.TestingT Cleanup(func()) -} - -// NewSequenceSenderElderberry creates a new instance of SequenceSenderElderberry. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewSequenceSenderElderberry(t mockConstructorTestingTNewSequenceSenderElderberry) *SequenceSenderElderberry { +}) *SequenceSenderElderberry { mock := &SequenceSenderElderberry{} mock.Mock.Test(t) diff --git a/l1infotreesync/e2e_test.go b/l1infotreesync/e2e_test.go index c1b16446..065b599e 100644 --- a/l1infotreesync/e2e_test.go +++ b/l1infotreesync/e2e_test.go @@ -181,7 +181,7 @@ func TestStressAndReorgs(t *testing.T) { require.NoError(t, err) client, gerAddr, verifyAddr, gerSc, verifySC, err := newSimulatedClient(auth) require.NoError(t, err) - rd, err := reorgdetector.New(ctx, client.Client(), dbPathReorg) + rd, err := reorgdetector.New(client.Client(), dbPathReorg) go rd.Start(ctx) syncer, err := l1infotreesync.New(ctx, dbPathSyncer, gerAddr, verifyAddr, 10, etherman.LatestBlock, rd, client.Client(), time.Millisecond, 0, 100*time.Millisecond, 3) require.NoError(t, err) diff --git a/l1infotreesync/mock_reorgdetector_test.go b/l1infotreesync/mock_reorgdetector_test.go index 22d174d4..441fd28a 100644 --- a/l1infotreesync/mock_reorgdetector_test.go +++ b/l1infotreesync/mock_reorgdetector_test.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.22.1. DO NOT EDIT. +// Code generated by mockery v2.40.1. DO NOT EDIT. package l1infotreesync @@ -17,13 +17,17 @@ type ReorgDetectorMock struct { mock.Mock } -// AddBlockToTrack provides a mock function with given fields: ctx, id, blockNum, blockHash -func (_m *ReorgDetectorMock) AddBlockToTrack(ctx context.Context, id string, blockNum uint64, blockHash common.Hash) error { - ret := _m.Called(ctx, id, blockNum, blockHash) +// AddBlockToTrack provides a mock function with given fields: ctx, id, blockNum, blockHash, parentHash +func (_m *ReorgDetectorMock) AddBlockToTrack(ctx context.Context, id string, blockNum uint64, blockHash common.Hash, parentHash common.Hash) error { + ret := _m.Called(ctx, id, blockNum, blockHash, parentHash) + + if len(ret) == 0 { + panic("no return value specified for AddBlockToTrack") + } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, uint64, common.Hash) error); ok { - r0 = rf(ctx, id, blockNum, blockHash) + if rf, ok := ret.Get(0).(func(context.Context, string, uint64, common.Hash, common.Hash) error); ok { + r0 = rf(ctx, id, blockNum, blockHash, parentHash) } else { r0 = ret.Error(0) } @@ -35,6 +39,10 @@ func (_m *ReorgDetectorMock) AddBlockToTrack(ctx context.Context, id string, blo func (_m *ReorgDetectorMock) Subscribe(id string) (*reorgdetector.Subscription, error) { ret := _m.Called(id) + if len(ret) == 0 { + panic("no return value specified for Subscribe") + } + var r0 *reorgdetector.Subscription var r1 error if rf, ok := ret.Get(0).(func(string) (*reorgdetector.Subscription, error)); ok { @@ -57,13 +65,12 @@ func (_m *ReorgDetectorMock) Subscribe(id string) (*reorgdetector.Subscription, return r0, r1 } -type mockConstructorTestingTNewReorgDetectorMock interface { +// NewReorgDetectorMock creates a new instance of ReorgDetectorMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewReorgDetectorMock(t interface { mock.TestingT Cleanup(func()) -} - -// NewReorgDetectorMock creates a new instance of ReorgDetectorMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewReorgDetectorMock(t mockConstructorTestingTNewReorgDetectorMock) *ReorgDetectorMock { +}) *ReorgDetectorMock { mock := &ReorgDetectorMock{} mock.Mock.Test(t) diff --git a/reorgdetector/config.go b/reorgdetector/config.go new file mode 100644 index 00000000..179e3909 --- /dev/null +++ b/reorgdetector/config.go @@ -0,0 +1,5 @@ +package reorgdetector + +type Config struct { + DBPath string `mapstructure:"DBPath"` +} diff --git a/reorgdetector/reorgdetector.go b/reorgdetector/reorgdetector.go index f96ef772..3c5317ba 100644 --- a/reorgdetector/reorgdetector.go +++ b/reorgdetector/reorgdetector.go @@ -5,6 +5,12 @@ import ( "fmt" "math/big" "sync" + "time" + + "github.com/ledgerwatch/erigon-lib/kv" + "github.com/ledgerwatch/erigon-lib/kv/mdbx" + + "github.com/ethereum/go-ethereum/rpc" "github.com/0xPolygon/cdk/log" @@ -17,10 +23,27 @@ import ( // TODO: consider the case where blocks can disappear, current implementation assumes that if there is a reorg, // the client will have at least as many blocks as it had before the reorg, however this may not be the case for L2 +const ( + defaultWaitPeriodBlockRemover = time.Second * 20 + defaultWaitPeriodBlockAdder = time.Second * 2 // should be smaller than block time of the tracked chain + + subscriberBlocks = "reorgdetector-subscriberBlocks" + + unfinalisedBlocksID = "unfinalisedBlocks" +) + var ( - ErrNotSubscribed = errors.New("id not found in subscriptions") + ErrNotSubscribed = errors.New("id not found in subscriptions") + ErrInvalidBlockHash = errors.New("the block hash does not match with the expected block hash") + ErrIDReserverd = errors.New("subscription id is reserved") ) +func tableCfgFunc(defaultBuckets kv.TableCfg) kv.TableCfg { + return kv.TableCfg{ + subscriberBlocks: {}, + } +} + type EthClient interface { SubscribeNewHead(ctx context.Context, ch chan<- *types.Header) (ethereum.Subscription, error) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) @@ -35,6 +58,10 @@ type Subscription struct { type ReorgDetector struct { client EthClient + db kv.RwDB + + canonicalBlocksLock sync.RWMutex + canonicalBlocks blockMap trackedBlocksLock sync.RWMutex trackedBlocks map[string]blockMap @@ -43,12 +70,34 @@ type ReorgDetector struct { subscriptions map[string]*Subscription } -func New(client EthClient) *ReorgDetector { +func New(client EthClient, dbPath string) (*ReorgDetector, error) { + db, err := mdbx.NewMDBX(nil). + Path(dbPath). + WithTableCfg(tableCfgFunc). + Open() + if err != nil { + return nil, fmt.Errorf("failed to open db: %w", err) + } + return &ReorgDetector{ - client: client, - trackedBlocks: make(map[string]blockMap), - subscriptions: make(map[string]*Subscription), + client: client, + db: db, + canonicalBlocks: make(blockMap), + trackedBlocks: make(map[string]blockMap), + subscriptions: make(map[string]*Subscription), + }, nil +} + +func (rd *ReorgDetector) Start(ctx context.Context) error { + // Load tracked blocks from the DB + if err := rd.loadAndProcessTrackedBlocks(ctx); err != nil { + return fmt.Errorf("failed to load and process tracked blocks: %w", err) } + + // Start the reorg detector for the canonical chain + go rd.monitorCanonicalChain(ctx) + + return nil } func (rd *ReorgDetector) Subscribe(id string) (*Subscription, error) { @@ -80,95 +129,83 @@ func (rd *ReorgDetector) AddBlockToTrack(ctx context.Context, id string, blockNu } rd.subscriptionsLock.RUnlock() - newBlock := block{ + if err := rd.saveTrackedBlock(ctx, id, block{ Num: blockNum, Hash: blockHash, ParentHash: parentHash, + }); err != nil { + return fmt.Errorf("failed to save tracked block: %w", err) } - rd.trackedBlocksLock.Lock() - defer rd.trackedBlocksLock.Unlock() + return nil +} - // Get the last block from the list for the given subscription ID - trackedBlocks, ok := rd.trackedBlocks[id] - if !ok || len(trackedBlocks) == 0 { - // No blocks for the given subscription - trackedBlocks = newBlockMap(newBlock) - rd.trackedBlocks[id] = trackedBlocks +func (rd *ReorgDetector) monitorCanonicalChain(ctx context.Context) { + // Add head tracker + ch := make(chan *types.Header, 100) + sub, err := rd.client.SubscribeNewHead(ctx, ch) + if err != nil { + log.Fatal("failed to subscribe to new head", "err", err) } - findStartReorgBlock := func(blocks blockMap) *block { - // Find the highest block number - maxBlockNum := uint64(0) - for blockNum := range blocks { - if blockNum > maxBlockNum { - maxBlockNum = blockNum + go func() { + for { + select { + case <-ctx.Done(): + return + case <-sub.Err(): + return + case header := <-ch: + if err = rd.onNewHeader(ctx, header); err != nil { + log.Error("failed to process new header", "err", err) + continue + } } } + }() +} - // Iterate from the highest block number to the lowest - reorgDetected := false - for i := maxBlockNum; i > 1; i-- { - currentBlock, currentExists := blocks[i] - previousBlock, previousExists := blocks[i-1] - - // Check if both blocks exist (sanity check) - if !currentExists || !previousExists { - continue - } - - // Check if the current block's parent hash matches the previous block's hash - if currentBlock.ParentHash != previousBlock.Hash { - reorgDetected = true - } else if reorgDetected { - // When reorg is detected, and we find the first match, return the previous block - return &previousBlock - } - } +func (rd *ReorgDetector) onNewHeader(ctx context.Context, header *types.Header) error { + rd.canonicalBlocksLock.Lock() + defer rd.canonicalBlocksLock.Unlock() - return nil // No reorg detected + newBlock := block{ + Num: header.Number.Uint64(), + Hash: header.Hash(), + ParentHash: header.ParentHash, } - rebuildBlocksMap := func(blocks blockMap, from, to uint64) (blockMap, error) { - for i := from; i <= to; i++ { - blockHeader, err := rd.client.HeaderByNumber(ctx, big.NewInt(int64(i))) - if err != nil { - return nil, fmt.Errorf("failed to fetch block header for block number %d: %w", i, err) - } - - blocks[blockHeader.Number.Uint64()] = block{ - Num: blockHeader.Number.Uint64(), - Hash: blockHeader.Hash(), - ParentHash: blockHeader.ParentHash, - } - } - - return blocks, nil + // No canonical chain yet + if len(rd.canonicalBlocks) == 0 { + // TODO: Fill canonical chain from the last finalized block + rd.canonicalBlocks = newBlockMap(newBlock) + return nil } processReorg := func() error { - trackedBlocks[newBlock.Num] = newBlock - reorgedBlock := findStartReorgBlock(trackedBlocks) + rd.canonicalBlocks[newBlock.Num] = newBlock + reorgedBlock := rd.canonicalBlocks.detectReorg() if reorgedBlock != nil { + // Notify subscribers about the reorg rd.notifySubscribers(*reorgedBlock) - newBlocksMap, err := rebuildBlocksMap(trackedBlocks, reorgedBlock.Num, newBlock.Num) - if err != nil { - return err + // Rebuild the canonical chain + if err := rd.rebuildCanonicalChain(ctx, reorgedBlock.Num, newBlock.Num); err != nil { + return fmt.Errorf("failed to rebuild canonical chain: %w", err) } - rd.trackedBlocks[id] = newBlocksMap } else { - // Should not happen + // Should not happen, check the logic below + // log.Fatal("Unexpected reorg detection") } return nil } - closestHigherBlock, ok := trackedBlocks.getClosestHigherBlock(newBlock.Num) + closestHigherBlock, ok := rd.canonicalBlocks.getClosestHigherBlock(newBlock.Num) if !ok { // No same or higher blocks, only lower blocks exist. Check hashes. // Current tracked blocks: N-i, N-i+1, ..., N-1 - sortedBlocks := trackedBlocks.getSorted() + sortedBlocks := rd.canonicalBlocks.getSorted() closestBlock := sortedBlocks[len(sortedBlocks)-1] if closestBlock.Num < newBlock.Num-1 { // There is a gap between the last block and the given block @@ -176,11 +213,10 @@ func (rd *ReorgDetector) AddBlockToTrack(ctx context.Context, id string, blockNu } else if closestBlock.Num == newBlock.Num-1 { if closestBlock.Hash != newBlock.ParentHash { // Block hashes do not match, reorg happened - // TODO: Reorg happened return processReorg() } else { // All good, add the block to the map - rd.trackedBlocks[id][newBlock.Num] = newBlock + rd.canonicalBlocks[newBlock.Num] = newBlock } } else { // This should not happen @@ -192,18 +228,14 @@ func (rd *ReorgDetector) AddBlockToTrack(ctx context.Context, id string, blockNu // Current tracked blocks: N-2, N-1, N (given block) if closestHigherBlock.Hash != newBlock.Hash { // Block hashes have changed, reorg happened - // TODO: Handle happened return processReorg() } - } else if closestHigherBlock.Num == newBlock.Num+1 { + } else if closestHigherBlock.Num >= newBlock.Num+1 { // The given block is lower than the closest higher block: - // Current tracked blocks: N-2, N-1, N (given block), N+1, N+2 - // TODO: Reorg happened - return processReorg() - } else if closestHigherBlock.Num > newBlock.Num+1 { + // N-2, N-1, N (given block), N+1, N+2 + // OR // There is a gap between the current block and the closest higher block - // Current tracked blocks: N-2, N-1, N (given block), , N+i - // TODO: Reorg happened + // N-2, N-1, N (given block), , N+i return processReorg() } else { // This should not happen @@ -214,6 +246,99 @@ func (rd *ReorgDetector) AddBlockToTrack(ctx context.Context, id string, blockNu return nil } +// rebuildCanonicalChain rebuilds the canonical chain from the given block number to the given block number. +func (rd *ReorgDetector) rebuildCanonicalChain(ctx context.Context, from, to uint64) error { + for i := from; i <= to; i++ { + blockHeader, err := rd.client.HeaderByNumber(ctx, big.NewInt(int64(i))) + if err != nil { + return fmt.Errorf("failed to fetch block header for block number %d: %w", i, err) + } + + rd.canonicalBlocks[blockHeader.Number.Uint64()] = block{ + Num: blockHeader.Number.Uint64(), + Hash: blockHeader.Hash(), + ParentHash: blockHeader.ParentHash, + } + } + + return nil +} + +// loadAndProcessTrackedBlocks loads tracked blocks from the DB and checks for reorgs. Loads in memory. +func (rd *ReorgDetector) loadAndProcessTrackedBlocks(ctx context.Context) error { + rd.trackedBlocksLock.Lock() + defer rd.trackedBlocksLock.Unlock() + + // Load tracked blocks for all subscribers from the DB + trackedBlocks, err := rd.getTrackedBlocks(ctx) + if err != nil { + return fmt.Errorf("failed to get tracked blocks: %w", err) + } + + rd.trackedBlocks = trackedBlocks + + lastFinalisedBlock, err := rd.client.HeaderByNumber(ctx, big.NewInt(int64(rpc.FinalizedBlockNumber))) + if err != nil { + return err + } + + blocksGotten := make(map[uint64]common.Hash, 0) + + for id, blocks := range rd.trackedBlocks { + rd.subscriptionsLock.Lock() + rd.subscriptions[id] = &Subscription{ + FirstReorgedBlock: make(chan uint64), + ReorgProcessed: make(chan bool), + } + rd.subscriptionsLock.Unlock() + + // Nothing to process for this subscriber + if len(blocks) == 0 { + continue + } + + var ( + lastTrackedBlock uint64 + block block + actualBlockHash common.Hash + ok bool + ) + + sortedBlocks := blocks.getSorted() + lastTrackedBlock = sortedBlocks[len(blocks)-1].Num + + for _, block = range sortedBlocks { + if actualBlockHash, ok = blocksGotten[block.Num]; !ok { + actualBlock, err := rd.client.HeaderByNumber(ctx, big.NewInt(int64(block.Num))) + if err != nil { + return err + } + + actualBlockHash = actualBlock.Hash() + } + + if actualBlockHash != block.Hash { + // Reorg detected, notify subscriber + go rd.notifySubscriber(id, block) + + // Remove the reorged blocks from the tracked blocks + blocks.removeRange(block.Num, lastTrackedBlock) + + break + } else if block.Num <= lastFinalisedBlock.Number.Uint64() { + delete(blocks, block.Num) + } + } + + // If we processed finalized or reorged blocks, update the tracked blocks in memory and db + if err = rd.updateTrackedBlocksNoLock(ctx, id, blocks); err != nil { + return err + } + } + + return nil +} + func (rd *ReorgDetector) notifySubscribers(startingBlock block) { rd.subscriptionsLock.RLock() for _, sub := range rd.subscriptions { @@ -226,3 +351,17 @@ func (rd *ReorgDetector) notifySubscribers(startingBlock block) { } rd.subscriptionsLock.RUnlock() } + +func (rd *ReorgDetector) notifySubscriber(id string, startingBlock block) { + rd.subscriptionsLock.RLock() + subscriber, ok := rd.subscriptions[id] + if ok { + subscriber.pendingReorgsToBeProcessed.Add(1) + go func(sub *Subscription) { + sub.FirstReorgedBlock <- startingBlock.Num + <-sub.ReorgProcessed + sub.pendingReorgsToBeProcessed.Done() + }(subscriber) + } + rd.subscriptionsLock.RUnlock() +} diff --git a/reorgdetector/reorgdetector_db.go b/reorgdetector/reorgdetector_db.go new file mode 100644 index 00000000..da18a895 --- /dev/null +++ b/reorgdetector/reorgdetector_db.go @@ -0,0 +1,131 @@ +package reorgdetector + +import ( + context "context" + "encoding/json" +) + +// getUnfinalisedBlocksMap returns the map of unfinalised blocks +func (rd *ReorgDetector) getUnfinalisedBlocksMap() blockMap { + rd.trackedBlocksLock.RLock() + defer rd.trackedBlocksLock.RUnlock() + + return rd.trackedBlocks[unfinalisedBlocksID] +} + +// getTrackedBlocks returns a list of tracked blocks for each subscriber from db +func (rd *ReorgDetector) getTrackedBlocks(ctx context.Context) (map[string]blockMap, error) { + tx, err := rd.db.BeginRo(ctx) + if err != nil { + return nil, err + } + + defer tx.Rollback() + + cursor, err := tx.Cursor(subscriberBlocks) + if err != nil { + return nil, err + } + + defer cursor.Close() + + trackedBlocks := make(map[string]blockMap, 0) + + for k, v, err := cursor.First(); k != nil; k, v, err = cursor.Next() { + if err != nil { + return nil, err + } + + var blocks []block + if err := json.Unmarshal(v, &blocks); err != nil { + return nil, err + } + + trackedBlocks[string(k)] = newBlockMap(blocks...) + } + + if _, ok := trackedBlocks[unfinalisedBlocksID]; !ok { + // add unfinalised blocks to tracked blocks map if not present in db + trackedBlocks[unfinalisedBlocksID] = newBlockMap() + } + + return trackedBlocks, nil +} + +// saveTrackedBlock saves the tracked block for a subscriber in db and in memory +func (rd *ReorgDetector) saveTrackedBlock(ctx context.Context, id string, b block) error { + tx, err := rd.db.BeginRw(ctx) + if err != nil { + return err + } + + defer tx.Rollback() + + rd.trackedBlocksLock.Lock() + + subscriberBlockMap, ok := rd.trackedBlocks[id] + if !ok || len(subscriberBlockMap) == 0 { + subscriberBlockMap = newBlockMap(b) + rd.trackedBlocks[id] = subscriberBlockMap + } else { + subscriberBlockMap[b.Num] = b + } + + rd.trackedBlocksLock.Unlock() + + raw, err := json.Marshal(subscriberBlockMap.getSorted()) + if err != nil { + return err + } + + return tx.Put(subscriberBlocks, []byte(id), raw) +} + +// removeTrackedBlocks removes the tracked blocks for a subscriber in db and in memory +func (rd *ReorgDetector) removeTrackedBlocks(ctx context.Context, lastFinalizedBlock uint64) error { + rd.subscriptionsLock.RLock() + defer rd.subscriptionsLock.RUnlock() + + for id := range rd.subscriptions { + rd.trackedBlocksLock.RLock() + newTrackedBlocks := rd.trackedBlocks[id].getFromBlockSorted(lastFinalizedBlock) + rd.trackedBlocksLock.RUnlock() + + if err := rd.updateTrackedBlocks(ctx, id, newBlockMap(newTrackedBlocks...)); err != nil { + return err + } + } + + return nil +} + +// updateTrackedBlocks updates the tracked blocks for a subscriber in db and in memory +func (rd *ReorgDetector) updateTrackedBlocks(ctx context.Context, id string, blocks blockMap) error { + rd.trackedBlocksLock.Lock() + defer rd.trackedBlocksLock.Unlock() + + return rd.updateTrackedBlocksNoLock(ctx, id, blocks) +} + +// updateTrackedBlocksNoLock updates the tracked blocks for a subscriber in db and in memory +func (rd *ReorgDetector) updateTrackedBlocksNoLock(ctx context.Context, id string, blocks blockMap) error { + tx, err := rd.db.BeginRw(ctx) + if err != nil { + return err + } + + defer tx.Rollback() + + raw, err := json.Marshal(blocks.getSorted()) + if err != nil { + return err + } + + if err = tx.Put(subscriberBlocks, []byte(id), raw); err != nil { + return err + } + + rd.trackedBlocks[id] = blocks + + return nil +} diff --git a/reorgdetector/reorgdetector_test.go b/reorgdetector/reorgdetector_test.go index 0a61ebf2..fc117fb8 100644 --- a/reorgdetector/reorgdetector_test.go +++ b/reorgdetector/reorgdetector_test.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "math/big" + "os" "testing" "time" @@ -31,6 +32,20 @@ func newSimulatedL1(t *testing.T, auth *bind.TransactOpts) *simulated.Backend { return client } +func newTestDir(tb testing.TB) string { + tb.Helper() + + dir := fmt.Sprintf("/tmp/reorgdetector-temp_%v", time.Now().UTC().Format(time.RFC3339Nano)) + err := os.Mkdir(dir, 0775) + require.NoError(tb, err) + + tb.Cleanup(func() { + require.NoError(tb, os.RemoveAll(dir)) + }) + + return dir +} + func Test_ReorgDetector(t *testing.T) { const produceBlocks = 29 const reorgPeriod = 5 @@ -46,7 +61,14 @@ func Test_ReorgDetector(t *testing.T) { clientL1 := newSimulatedL1(t, authL1) require.NoError(t, err) - reorgDetector := New(clientL1.Client()) + // Create test DB dir + testDir := newTestDir(t) + + reorgDetector, err := New(clientL1.Client(), testDir) + require.NoError(t, err) + + err = reorgDetector.Start(ctx) + require.NoError(t, err) reorgSub, err := reorgDetector.Subscribe("test") require.NoError(t, err) diff --git a/reorgdetector/types.go b/reorgdetector/types.go index 25ac4031..4f969d98 100644 --- a/reorgdetector/types.go +++ b/reorgdetector/types.go @@ -3,7 +3,7 @@ package reorgdetector import ( "sort" - common "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common" ) type block struct { @@ -79,3 +79,38 @@ func (bm blockMap) removeRange(from, to uint64) { delete(bm, i) } } + +// detectReorg detects a reorg in the given block list. +// Returns the first reorged block or nil. +func (bm blockMap) detectReorg() *block { + // Find the highest block number + maxBlockNum := uint64(0) + for blockNum := range bm { + if blockNum > maxBlockNum { + maxBlockNum = blockNum + } + } + + // Iterate from the highest block number to the lowest + reorgDetected := false + for i := maxBlockNum; i > 1; i-- { + currentBlock, currentExists := bm[i] + previousBlock, previousExists := bm[i-1] + + // Check if both blocks exist (sanity check) + if !currentExists || !previousExists { + continue + } + + // Check if the current block's parent hash matches the previous block's hash + if currentBlock.ParentHash != previousBlock.Hash { + reorgDetected = true + } else if reorgDetected { + // When reorg is detected, and we find the first match, return the previous block + return &previousBlock + } + } + + // No reorg detected + return nil +} diff --git a/reorgdetector/types_test.go b/reorgdetector/types_test.go index 8a588070..00e78f20 100644 --- a/reorgdetector/types_test.go +++ b/reorgdetector/types_test.go @@ -1 +1,104 @@ package reorgdetector + +import ( + "reflect" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/require" +) + +func TestBlockMap(t *testing.T) { + t.Parallel() + + // Create a new block map + bm := newBlockMap( + block{Num: 1, Hash: common.HexToHash("0x123")}, + block{Num: 2, Hash: common.HexToHash("0x456")}, + block{Num: 3, Hash: common.HexToHash("0x789")}, + ) + + t.Run("getSorted", func(t *testing.T) { + t.Parallel() + + sortedBlocks := bm.getSorted() + expectedSortedBlocks := []block{ + {Num: 1, Hash: common.HexToHash("0x123")}, + {Num: 2, Hash: common.HexToHash("0x456")}, + {Num: 3, Hash: common.HexToHash("0x789")}, + } + if !reflect.DeepEqual(sortedBlocks, expectedSortedBlocks) { + t.Errorf("getSorted() returned incorrect result, expected: %v, got: %v", expectedSortedBlocks, sortedBlocks) + } + }) + + t.Run("getFromBlockSorted", func(t *testing.T) { + t.Parallel() + + fromBlockSorted := bm.getFromBlockSorted(2) + expectedFromBlockSorted := []block{ + {Num: 3, Hash: common.HexToHash("0x789")}, + } + if !reflect.DeepEqual(fromBlockSorted, expectedFromBlockSorted) { + t.Errorf("getFromBlockSorted() returned incorrect result, expected: %v, got: %v", expectedFromBlockSorted, fromBlockSorted) + } + + // Test getFromBlockSorted function when blockNum is greater than the last block + fromBlockSorted = bm.getFromBlockSorted(4) + expectedFromBlockSorted = []block{} + if !reflect.DeepEqual(fromBlockSorted, expectedFromBlockSorted) { + t.Errorf("getFromBlockSorted() returned incorrect result, expected: %v, got: %v", expectedFromBlockSorted, fromBlockSorted) + } + }) + + t.Run("getClosestHigherBlock", func(t *testing.T) { + t.Parallel() + + bm := newBlockMap( + block{Num: 1, Hash: common.HexToHash("0x123")}, + block{Num: 2, Hash: common.HexToHash("0x456")}, + block{Num: 3, Hash: common.HexToHash("0x789")}, + ) + + // Test when the blockNum exists in the block map + b, exists := bm.getClosestHigherBlock(2) + require.True(t, exists) + expectedBlock := block{Num: 2, Hash: common.HexToHash("0x456")} + if b != expectedBlock { + t.Errorf("getClosestHigherBlock() returned incorrect result, expected: %v, got: %v", expectedBlock, b) + } + + // Test when the blockNum does not exist in the block map + b, exists = bm.getClosestHigherBlock(4) + require.False(t, exists) + expectedBlock = block{Num: 0, Hash: common.Hash{}} + if b != expectedBlock { + t.Errorf("getClosestHigherBlock() returned incorrect result, expected: %v, got: %v", expectedBlock, b) + } + }) + + t.Run("removeRange", func(t *testing.T) { + t.Parallel() + + bm := newBlockMap( + block{Num: 1, Hash: common.HexToHash("0x123")}, + block{Num: 2, Hash: common.HexToHash("0x456")}, + block{Num: 3, Hash: common.HexToHash("0x789")}, + block{Num: 4, Hash: common.HexToHash("0xabc")}, + block{Num: 5, Hash: common.HexToHash("0xdef")}, + ) + + bm.removeRange(3, 5) + + expectedBlocks := []block{ + {Num: 1, Hash: common.HexToHash("0x123")}, + {Num: 2, Hash: common.HexToHash("0x456")}, + } + + sortedBlocks := bm.getSorted() + + if !reflect.DeepEqual(sortedBlocks, expectedBlocks) { + t.Errorf("removeRange() failed, expected: %v, got: %v", expectedBlocks, sortedBlocks) + } + }) +} diff --git a/sync/mock_downloader_test.go b/sync/mock_downloader_test.go index 738fc873..4f93c0d0 100644 --- a/sync/mock_downloader_test.go +++ b/sync/mock_downloader_test.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.22.1. DO NOT EDIT. +// Code generated by mockery v2.40.1. DO NOT EDIT. package sync @@ -23,6 +23,10 @@ func (_m *EVMDownloaderMock) download(ctx context.Context, fromBlock uint64, dow func (_m *EVMDownloaderMock) getBlockHeader(ctx context.Context, blockNum uint64) EVMBlockHeader { ret := _m.Called(ctx, blockNum) + if len(ret) == 0 { + panic("no return value specified for getBlockHeader") + } + var r0 EVMBlockHeader if rf, ok := ret.Get(0).(func(context.Context, uint64) EVMBlockHeader); ok { r0 = rf(ctx, blockNum) @@ -37,6 +41,10 @@ func (_m *EVMDownloaderMock) getBlockHeader(ctx context.Context, blockNum uint64 func (_m *EVMDownloaderMock) getEventsByBlockRange(ctx context.Context, fromBlock uint64, toBlock uint64) []EVMBlock { ret := _m.Called(ctx, fromBlock, toBlock) + if len(ret) == 0 { + panic("no return value specified for getEventsByBlockRange") + } + var r0 []EVMBlock if rf, ok := ret.Get(0).(func(context.Context, uint64, uint64) []EVMBlock); ok { r0 = rf(ctx, fromBlock, toBlock) @@ -53,6 +61,10 @@ func (_m *EVMDownloaderMock) getEventsByBlockRange(ctx context.Context, fromBloc func (_m *EVMDownloaderMock) getLogs(ctx context.Context, fromBlock uint64, toBlock uint64) []types.Log { ret := _m.Called(ctx, fromBlock, toBlock) + if len(ret) == 0 { + panic("no return value specified for getLogs") + } + var r0 []types.Log if rf, ok := ret.Get(0).(func(context.Context, uint64, uint64) []types.Log); ok { r0 = rf(ctx, fromBlock, toBlock) @@ -69,6 +81,10 @@ func (_m *EVMDownloaderMock) getLogs(ctx context.Context, fromBlock uint64, toBl func (_m *EVMDownloaderMock) waitForNewBlocks(ctx context.Context, lastBlockSeen uint64) uint64 { ret := _m.Called(ctx, lastBlockSeen) + if len(ret) == 0 { + panic("no return value specified for waitForNewBlocks") + } + var r0 uint64 if rf, ok := ret.Get(0).(func(context.Context, uint64) uint64); ok { r0 = rf(ctx, lastBlockSeen) @@ -79,13 +95,12 @@ func (_m *EVMDownloaderMock) waitForNewBlocks(ctx context.Context, lastBlockSeen return r0 } -type mockConstructorTestingTNewEVMDownloaderMock interface { +// NewEVMDownloaderMock creates a new instance of EVMDownloaderMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewEVMDownloaderMock(t interface { mock.TestingT Cleanup(func()) -} - -// NewEVMDownloaderMock creates a new instance of EVMDownloaderMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewEVMDownloaderMock(t mockConstructorTestingTNewEVMDownloaderMock) *EVMDownloaderMock { +}) *EVMDownloaderMock { mock := &EVMDownloaderMock{} mock.Mock.Test(t) diff --git a/sync/mock_l2_test.go b/sync/mock_l2_test.go index 0d1e03da..78d75191 100644 --- a/sync/mock_l2_test.go +++ b/sync/mock_l2_test.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.22.1. DO NOT EDIT. +// Code generated by mockery v2.40.1. DO NOT EDIT. package sync @@ -24,6 +24,10 @@ type L2Mock struct { func (_m *L2Mock) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) { ret := _m.Called(ctx, hash) + if len(ret) == 0 { + panic("no return value specified for BlockByHash") + } + var r0 *types.Block var r1 error if rf, ok := ret.Get(0).(func(context.Context, common.Hash) (*types.Block, error)); ok { @@ -50,6 +54,10 @@ func (_m *L2Mock) BlockByHash(ctx context.Context, hash common.Hash) (*types.Blo func (_m *L2Mock) BlockByNumber(ctx context.Context, number *big.Int) (*types.Block, error) { ret := _m.Called(ctx, number) + if len(ret) == 0 { + panic("no return value specified for BlockByNumber") + } + var r0 *types.Block var r1 error if rf, ok := ret.Get(0).(func(context.Context, *big.Int) (*types.Block, error)); ok { @@ -76,6 +84,10 @@ func (_m *L2Mock) BlockByNumber(ctx context.Context, number *big.Int) (*types.Bl func (_m *L2Mock) BlockNumber(ctx context.Context) (uint64, error) { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for BlockNumber") + } + var r0 uint64 var r1 error if rf, ok := ret.Get(0).(func(context.Context) (uint64, error)); ok { @@ -100,6 +112,10 @@ func (_m *L2Mock) BlockNumber(ctx context.Context) (uint64, error) { func (_m *L2Mock) CallContract(ctx context.Context, call ethereum.CallMsg, blockNumber *big.Int) ([]byte, error) { ret := _m.Called(ctx, call, blockNumber) + if len(ret) == 0 { + panic("no return value specified for CallContract") + } + var r0 []byte var r1 error if rf, ok := ret.Get(0).(func(context.Context, ethereum.CallMsg, *big.Int) ([]byte, error)); ok { @@ -126,6 +142,10 @@ func (_m *L2Mock) CallContract(ctx context.Context, call ethereum.CallMsg, block func (_m *L2Mock) CodeAt(ctx context.Context, contract common.Address, blockNumber *big.Int) ([]byte, error) { ret := _m.Called(ctx, contract, blockNumber) + if len(ret) == 0 { + panic("no return value specified for CodeAt") + } + var r0 []byte var r1 error if rf, ok := ret.Get(0).(func(context.Context, common.Address, *big.Int) ([]byte, error)); ok { @@ -152,6 +172,10 @@ func (_m *L2Mock) CodeAt(ctx context.Context, contract common.Address, blockNumb func (_m *L2Mock) EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint64, error) { ret := _m.Called(ctx, call) + if len(ret) == 0 { + panic("no return value specified for EstimateGas") + } + var r0 uint64 var r1 error if rf, ok := ret.Get(0).(func(context.Context, ethereum.CallMsg) (uint64, error)); ok { @@ -176,6 +200,10 @@ func (_m *L2Mock) EstimateGas(ctx context.Context, call ethereum.CallMsg) (uint6 func (_m *L2Mock) FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]types.Log, error) { ret := _m.Called(ctx, q) + if len(ret) == 0 { + panic("no return value specified for FilterLogs") + } + var r0 []types.Log var r1 error if rf, ok := ret.Get(0).(func(context.Context, ethereum.FilterQuery) ([]types.Log, error)); ok { @@ -202,6 +230,10 @@ func (_m *L2Mock) FilterLogs(ctx context.Context, q ethereum.FilterQuery) ([]typ func (_m *L2Mock) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) { ret := _m.Called(ctx, hash) + if len(ret) == 0 { + panic("no return value specified for HeaderByHash") + } + var r0 *types.Header var r1 error if rf, ok := ret.Get(0).(func(context.Context, common.Hash) (*types.Header, error)); ok { @@ -228,6 +260,10 @@ func (_m *L2Mock) HeaderByHash(ctx context.Context, hash common.Hash) (*types.He func (_m *L2Mock) HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) { ret := _m.Called(ctx, number) + if len(ret) == 0 { + panic("no return value specified for HeaderByNumber") + } + var r0 *types.Header var r1 error if rf, ok := ret.Get(0).(func(context.Context, *big.Int) (*types.Header, error)); ok { @@ -254,6 +290,10 @@ func (_m *L2Mock) HeaderByNumber(ctx context.Context, number *big.Int) (*types.H func (_m *L2Mock) PendingCodeAt(ctx context.Context, account common.Address) ([]byte, error) { ret := _m.Called(ctx, account) + if len(ret) == 0 { + panic("no return value specified for PendingCodeAt") + } + var r0 []byte var r1 error if rf, ok := ret.Get(0).(func(context.Context, common.Address) ([]byte, error)); ok { @@ -280,6 +320,10 @@ func (_m *L2Mock) PendingCodeAt(ctx context.Context, account common.Address) ([] func (_m *L2Mock) PendingNonceAt(ctx context.Context, account common.Address) (uint64, error) { ret := _m.Called(ctx, account) + if len(ret) == 0 { + panic("no return value specified for PendingNonceAt") + } + var r0 uint64 var r1 error if rf, ok := ret.Get(0).(func(context.Context, common.Address) (uint64, error)); ok { @@ -304,6 +348,10 @@ func (_m *L2Mock) PendingNonceAt(ctx context.Context, account common.Address) (u func (_m *L2Mock) SendTransaction(ctx context.Context, tx *types.Transaction) error { ret := _m.Called(ctx, tx) + if len(ret) == 0 { + panic("no return value specified for SendTransaction") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, *types.Transaction) error); ok { r0 = rf(ctx, tx) @@ -318,6 +366,10 @@ func (_m *L2Mock) SendTransaction(ctx context.Context, tx *types.Transaction) er func (_m *L2Mock) SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuery, ch chan<- types.Log) (ethereum.Subscription, error) { ret := _m.Called(ctx, q, ch) + if len(ret) == 0 { + panic("no return value specified for SubscribeFilterLogs") + } + var r0 ethereum.Subscription var r1 error if rf, ok := ret.Get(0).(func(context.Context, ethereum.FilterQuery, chan<- types.Log) (ethereum.Subscription, error)); ok { @@ -344,6 +396,10 @@ func (_m *L2Mock) SubscribeFilterLogs(ctx context.Context, q ethereum.FilterQuer func (_m *L2Mock) SubscribeNewHead(ctx context.Context, ch chan<- *types.Header) (ethereum.Subscription, error) { ret := _m.Called(ctx, ch) + if len(ret) == 0 { + panic("no return value specified for SubscribeNewHead") + } + var r0 ethereum.Subscription var r1 error if rf, ok := ret.Get(0).(func(context.Context, chan<- *types.Header) (ethereum.Subscription, error)); ok { @@ -370,6 +426,10 @@ func (_m *L2Mock) SubscribeNewHead(ctx context.Context, ch chan<- *types.Header) func (_m *L2Mock) SuggestGasPrice(ctx context.Context) (*big.Int, error) { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for SuggestGasPrice") + } + var r0 *big.Int var r1 error if rf, ok := ret.Get(0).(func(context.Context) (*big.Int, error)); ok { @@ -396,6 +456,10 @@ func (_m *L2Mock) SuggestGasPrice(ctx context.Context) (*big.Int, error) { func (_m *L2Mock) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for SuggestGasTipCap") + } + var r0 *big.Int var r1 error if rf, ok := ret.Get(0).(func(context.Context) (*big.Int, error)); ok { @@ -422,6 +486,10 @@ func (_m *L2Mock) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { func (_m *L2Mock) TransactionCount(ctx context.Context, blockHash common.Hash) (uint, error) { ret := _m.Called(ctx, blockHash) + if len(ret) == 0 { + panic("no return value specified for TransactionCount") + } + var r0 uint var r1 error if rf, ok := ret.Get(0).(func(context.Context, common.Hash) (uint, error)); ok { @@ -446,6 +514,10 @@ func (_m *L2Mock) TransactionCount(ctx context.Context, blockHash common.Hash) ( func (_m *L2Mock) TransactionInBlock(ctx context.Context, blockHash common.Hash, index uint) (*types.Transaction, error) { ret := _m.Called(ctx, blockHash, index) + if len(ret) == 0 { + panic("no return value specified for TransactionInBlock") + } + var r0 *types.Transaction var r1 error if rf, ok := ret.Get(0).(func(context.Context, common.Hash, uint) (*types.Transaction, error)); ok { @@ -468,13 +540,12 @@ func (_m *L2Mock) TransactionInBlock(ctx context.Context, blockHash common.Hash, return r0, r1 } -type mockConstructorTestingTNewL2Mock interface { +// NewL2Mock creates a new instance of L2Mock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewL2Mock(t interface { mock.TestingT Cleanup(func()) -} - -// NewL2Mock creates a new instance of L2Mock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewL2Mock(t mockConstructorTestingTNewL2Mock) *L2Mock { +}) *L2Mock { mock := &L2Mock{} mock.Mock.Test(t) diff --git a/sync/mock_processor_test.go b/sync/mock_processor_test.go index 19738ef5..8e562e9b 100644 --- a/sync/mock_processor_test.go +++ b/sync/mock_processor_test.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.22.1. DO NOT EDIT. +// Code generated by mockery v2.40.1. DO NOT EDIT. package sync @@ -17,6 +17,10 @@ type ProcessorMock struct { func (_m *ProcessorMock) GetLastProcessedBlock(ctx context.Context) (uint64, error) { ret := _m.Called(ctx) + if len(ret) == 0 { + panic("no return value specified for GetLastProcessedBlock") + } + var r0 uint64 var r1 error if rf, ok := ret.Get(0).(func(context.Context) (uint64, error)); ok { @@ -41,6 +45,10 @@ func (_m *ProcessorMock) GetLastProcessedBlock(ctx context.Context) (uint64, err func (_m *ProcessorMock) ProcessBlock(ctx context.Context, block Block) error { ret := _m.Called(ctx, block) + if len(ret) == 0 { + panic("no return value specified for ProcessBlock") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, Block) error); ok { r0 = rf(ctx, block) @@ -55,6 +63,10 @@ func (_m *ProcessorMock) ProcessBlock(ctx context.Context, block Block) error { func (_m *ProcessorMock) Reorg(ctx context.Context, firstReorgedBlock uint64) error { ret := _m.Called(ctx, firstReorgedBlock) + if len(ret) == 0 { + panic("no return value specified for Reorg") + } + var r0 error if rf, ok := ret.Get(0).(func(context.Context, uint64) error); ok { r0 = rf(ctx, firstReorgedBlock) @@ -65,13 +77,12 @@ func (_m *ProcessorMock) Reorg(ctx context.Context, firstReorgedBlock uint64) er return r0 } -type mockConstructorTestingTNewProcessorMock interface { +// NewProcessorMock creates a new instance of ProcessorMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewProcessorMock(t interface { mock.TestingT Cleanup(func()) -} - -// NewProcessorMock creates a new instance of ProcessorMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewProcessorMock(t mockConstructorTestingTNewProcessorMock) *ProcessorMock { +}) *ProcessorMock { mock := &ProcessorMock{} mock.Mock.Test(t) diff --git a/sync/mock_reorgdetector_test.go b/sync/mock_reorgdetector_test.go index 056da2a1..38ca9ba8 100644 --- a/sync/mock_reorgdetector_test.go +++ b/sync/mock_reorgdetector_test.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.22.1. DO NOT EDIT. +// Code generated by mockery v2.40.1. DO NOT EDIT. package sync @@ -17,13 +17,17 @@ type ReorgDetectorMock struct { mock.Mock } -// AddBlockToTrack provides a mock function with given fields: ctx, id, blockNum, blockHash -func (_m *ReorgDetectorMock) AddBlockToTrack(ctx context.Context, id string, blockNum uint64, blockHash common.Hash) error { - ret := _m.Called(ctx, id, blockNum, blockHash) +// AddBlockToTrack provides a mock function with given fields: ctx, id, blockNum, blockHash, parentHash +func (_m *ReorgDetectorMock) AddBlockToTrack(ctx context.Context, id string, blockNum uint64, blockHash common.Hash, parentHash common.Hash) error { + ret := _m.Called(ctx, id, blockNum, blockHash, parentHash) + + if len(ret) == 0 { + panic("no return value specified for AddBlockToTrack") + } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, uint64, common.Hash) error); ok { - r0 = rf(ctx, id, blockNum, blockHash) + if rf, ok := ret.Get(0).(func(context.Context, string, uint64, common.Hash, common.Hash) error); ok { + r0 = rf(ctx, id, blockNum, blockHash, parentHash) } else { r0 = ret.Error(0) } @@ -35,6 +39,10 @@ func (_m *ReorgDetectorMock) AddBlockToTrack(ctx context.Context, id string, blo func (_m *ReorgDetectorMock) Subscribe(id string) (*reorgdetector.Subscription, error) { ret := _m.Called(id) + if len(ret) == 0 { + panic("no return value specified for Subscribe") + } + var r0 *reorgdetector.Subscription var r1 error if rf, ok := ret.Get(0).(func(string) (*reorgdetector.Subscription, error)); ok { @@ -57,13 +65,12 @@ func (_m *ReorgDetectorMock) Subscribe(id string) (*reorgdetector.Subscription, return r0, r1 } -type mockConstructorTestingTNewReorgDetectorMock interface { +// NewReorgDetectorMock creates a new instance of ReorgDetectorMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewReorgDetectorMock(t interface { mock.TestingT Cleanup(func()) -} - -// NewReorgDetectorMock creates a new instance of ReorgDetectorMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func NewReorgDetectorMock(t mockConstructorTestingTNewReorgDetectorMock) *ReorgDetectorMock { +}) *ReorgDetectorMock { mock := &ReorgDetectorMock{} mock.Mock.Test(t) From 602844017de6fe219d2430e758690dc94663f582 Mon Sep 17 00:00:00 2001 From: begmaroman Date: Wed, 21 Aug 2024 17:06:58 +0100 Subject: [PATCH 14/43] Implementation --- reorgdetector/reorgdetector.go | 101 ++++++++++++++--------------- reorgdetector/reorgdetector_sub.go | 45 +++++++++++++ 2 files changed, 93 insertions(+), 53 deletions(-) create mode 100644 reorgdetector/reorgdetector_sub.go diff --git a/reorgdetector/reorgdetector.go b/reorgdetector/reorgdetector.go index 3c5317ba..c07306b2 100644 --- a/reorgdetector/reorgdetector.go +++ b/reorgdetector/reorgdetector.go @@ -89,7 +89,12 @@ func New(client EthClient, dbPath string) (*ReorgDetector, error) { } func (rd *ReorgDetector) Start(ctx context.Context) error { - // Load tracked blocks from the DB + // Load canonical chain from the last finalized block + if err := rd.loadCanonicalChain(ctx); err != nil { + return fmt.Errorf("failed to load canonical chain: %w", err) + } + + // Load and process tracked blocks from the DB if err := rd.loadAndProcessTrackedBlocks(ctx); err != nil { return fmt.Errorf("failed to load and process tracked blocks: %w", err) } @@ -100,23 +105,6 @@ func (rd *ReorgDetector) Start(ctx context.Context) error { return nil } -func (rd *ReorgDetector) Subscribe(id string) (*Subscription, error) { - rd.subscriptionsLock.Lock() - defer rd.subscriptionsLock.Unlock() - - if sub, ok := rd.subscriptions[id]; ok { - return sub, nil - } - - sub := &Subscription{ - FirstReorgedBlock: make(chan uint64), - ReorgProcessed: make(chan bool), - } - rd.subscriptions[id] = sub - - return sub, nil -} - func (rd *ReorgDetector) AddBlockToTrack(ctx context.Context, id string, blockNum uint64, blockHash, parentHash common.Hash) error { rd.subscriptionsLock.RLock() if sub, ok := rd.subscriptions[id]; !ok { @@ -248,6 +236,8 @@ func (rd *ReorgDetector) onNewHeader(ctx context.Context, header *types.Header) // rebuildCanonicalChain rebuilds the canonical chain from the given block number to the given block number. func (rd *ReorgDetector) rebuildCanonicalChain(ctx context.Context, from, to uint64) error { + // TODO: Potentially rebuild from the latest finalized block + for i := from; i <= to; i++ { blockHeader, err := rd.client.HeaderByNumber(ctx, big.NewInt(int64(i))) if err != nil { @@ -264,6 +254,40 @@ func (rd *ReorgDetector) rebuildCanonicalChain(ctx context.Context, from, to uin return nil } +// loadCanonicalChain loads canonical chain from the latest finalized block till the latest one +func (rd *ReorgDetector) loadCanonicalChain(ctx context.Context) error { + // Get the latest finalized block + lastFinalisedBlock, err := rd.client.HeaderByNumber(ctx, big.NewInt(int64(rpc.FinalizedBlockNumber))) + if err != nil { + return err + } + + // Get the latest block + latestBlock, err := rd.client.HeaderByNumber(ctx, nil) + if err != nil { + return err + } + + rd.canonicalBlocksLock.Lock() + defer rd.canonicalBlocksLock.Unlock() + + // Load the canonical chain from the last finalized block till the latest block + for i := lastFinalisedBlock.Number.Uint64(); i <= latestBlock.Number.Uint64(); i++ { + blockHeader, err := rd.client.HeaderByNumber(ctx, big.NewInt(int64(i))) + if err != nil { + return fmt.Errorf("failed to fetch block header for block number %d: %w", i, err) + } + + rd.canonicalBlocks[blockHeader.Number.Uint64()] = block{ + Num: blockHeader.Number.Uint64(), + Hash: blockHeader.Hash(), + ParentHash: blockHeader.ParentHash, + } + } + + return nil +} + // loadAndProcessTrackedBlocks loads tracked blocks from the DB and checks for reorgs. Loads in memory. func (rd *ReorgDetector) loadAndProcessTrackedBlocks(ctx context.Context) error { rd.trackedBlocksLock.Lock() @@ -282,8 +306,6 @@ func (rd *ReorgDetector) loadAndProcessTrackedBlocks(ctx context.Context) error return err } - blocksGotten := make(map[uint64]common.Hash, 0) - for id, blocks := range rd.trackedBlocks { rd.subscriptionsLock.Lock() rd.subscriptions[id] = &Subscription{ @@ -299,22 +321,22 @@ func (rd *ReorgDetector) loadAndProcessTrackedBlocks(ctx context.Context) error var ( lastTrackedBlock uint64 - block block actualBlockHash common.Hash - ok bool ) sortedBlocks := blocks.getSorted() lastTrackedBlock = sortedBlocks[len(blocks)-1].Num - for _, block = range sortedBlocks { - if actualBlockHash, ok = blocksGotten[block.Num]; !ok { - actualBlock, err := rd.client.HeaderByNumber(ctx, big.NewInt(int64(block.Num))) + for _, block := range sortedBlocks { + if actualBlock, ok := rd.canonicalBlocks[block.Num]; !ok { + header, err := rd.client.HeaderByNumber(ctx, big.NewInt(int64(block.Num))) if err != nil { return err } - actualBlockHash = actualBlock.Hash() + actualBlockHash = header.Hash() + } else { + actualBlockHash = actualBlock.Hash } if actualBlockHash != block.Hash { @@ -338,30 +360,3 @@ func (rd *ReorgDetector) loadAndProcessTrackedBlocks(ctx context.Context) error return nil } - -func (rd *ReorgDetector) notifySubscribers(startingBlock block) { - rd.subscriptionsLock.RLock() - for _, sub := range rd.subscriptions { - sub.pendingReorgsToBeProcessed.Add(1) - go func(sub *Subscription) { - sub.FirstReorgedBlock <- startingBlock.Num - <-sub.ReorgProcessed - sub.pendingReorgsToBeProcessed.Done() - }(sub) - } - rd.subscriptionsLock.RUnlock() -} - -func (rd *ReorgDetector) notifySubscriber(id string, startingBlock block) { - rd.subscriptionsLock.RLock() - subscriber, ok := rd.subscriptions[id] - if ok { - subscriber.pendingReorgsToBeProcessed.Add(1) - go func(sub *Subscription) { - sub.FirstReorgedBlock <- startingBlock.Num - <-sub.ReorgProcessed - sub.pendingReorgsToBeProcessed.Done() - }(subscriber) - } - rd.subscriptionsLock.RUnlock() -} diff --git a/reorgdetector/reorgdetector_sub.go b/reorgdetector/reorgdetector_sub.go new file mode 100644 index 00000000..71bdf384 --- /dev/null +++ b/reorgdetector/reorgdetector_sub.go @@ -0,0 +1,45 @@ +package reorgdetector + +func (rd *ReorgDetector) Subscribe(id string) (*Subscription, error) { + rd.subscriptionsLock.Lock() + defer rd.subscriptionsLock.Unlock() + + if sub, ok := rd.subscriptions[id]; ok { + return sub, nil + } + + sub := &Subscription{ + FirstReorgedBlock: make(chan uint64), + ReorgProcessed: make(chan bool), + } + rd.subscriptions[id] = sub + + return sub, nil +} + +func (rd *ReorgDetector) notifySubscribers(startingBlock block) { + rd.subscriptionsLock.RLock() + for _, sub := range rd.subscriptions { + sub.pendingReorgsToBeProcessed.Add(1) + go func(sub *Subscription) { + sub.FirstReorgedBlock <- startingBlock.Num + <-sub.ReorgProcessed + sub.pendingReorgsToBeProcessed.Done() + }(sub) + } + rd.subscriptionsLock.RUnlock() +} + +func (rd *ReorgDetector) notifySubscriber(id string, startingBlock block) { + rd.subscriptionsLock.RLock() + subscriber, ok := rd.subscriptions[id] + if ok { + subscriber.pendingReorgsToBeProcessed.Add(1) + go func(sub *Subscription) { + sub.FirstReorgedBlock <- startingBlock.Num + <-sub.ReorgProcessed + sub.pendingReorgsToBeProcessed.Done() + }(subscriber) + } + rd.subscriptionsLock.RUnlock() +} From 92e568129a3836e4e9db120705781f12b8d1a151 Mon Sep 17 00:00:00 2001 From: begmaroman Date: Wed, 21 Aug 2024 17:11:42 +0100 Subject: [PATCH 15/43] Synced with main --- cmd/run.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/cmd/run.go b/cmd/run.go index c17c4676..fcf79986 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -408,11 +408,10 @@ func newState(c *config.Config, l2ChainID uint64, sqlDB *pgxpool.Pool) *state.St } func newReorgDetector( - ctx context.Context, dbPath string, client *ethclient.Client, ) *reorgdetector.ReorgDetector { - rd, err := reorgdetector.New(ctx, client, dbPath) + rd, err := reorgdetector.New(client, dbPath) if err != nil { log.Fatal(err) } @@ -489,7 +488,7 @@ func runReorgDetectorL1IfNeeded(ctx context.Context, components []string, l1Clie if !isNeeded([]string{SEQUENCE_SENDER, AGGREGATOR, AGGORACLE, RPC}, components) { return nil } - rd := newReorgDetector(ctx, dbPath, l1Client) + rd := newReorgDetector(dbPath, l1Client) go rd.Start(ctx) return rd } @@ -498,7 +497,7 @@ func runReorgDetectorL2IfNeeded(ctx context.Context, components []string, l2Clie if !isNeeded([]string{AGGORACLE, RPC}, components) { return nil } - rd := newReorgDetector(ctx, dbPath, l2Client) + rd := newReorgDetector(dbPath, l2Client) go rd.Start(ctx) return rd } From 6af5644e2c339f5c18edff45ec370f96289fff05 Mon Sep 17 00:00:00 2001 From: begmaroman Date: Wed, 21 Aug 2024 17:16:36 +0100 Subject: [PATCH 16/43] Fixed tests --- bridgesync/e2e_test.go | 2 +- l1bridge2infoindexsync/e2e_test.go | 2 +- test/helpers/aggoracle_e2e.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bridgesync/e2e_test.go b/bridgesync/e2e_test.go index d733a53e..865cbd11 100644 --- a/bridgesync/e2e_test.go +++ b/bridgesync/e2e_test.go @@ -51,7 +51,7 @@ func TestBridgeEventE2E(t *testing.T) { auth, err := bind.NewKeyedTransactorWithChainID(privateKey, big.NewInt(1337)) require.NoError(t, err) client, bridgeAddr, bridgeSc := newSimulatedClient(t, auth) - rd, err := reorgdetector.New(ctx, client.Client(), dbPathReorg) + rd, err := reorgdetector.New(client.Client(), dbPathReorg) require.NoError(t, err) go rd.Start(ctx) diff --git a/l1bridge2infoindexsync/e2e_test.go b/l1bridge2infoindexsync/e2e_test.go index deb613f3..eb00363b 100644 --- a/l1bridge2infoindexsync/e2e_test.go +++ b/l1bridge2infoindexsync/e2e_test.go @@ -132,7 +132,7 @@ func TestE2E(t *testing.T) { require.NotEqual(t, authDeployer.From, auth.From) client, gerAddr, bridgeAddr, gerSc, bridgeSc, err := newSimulatedClient(authDeployer, auth) require.NoError(t, err) - rd, err := reorgdetector.New(ctx, client.Client(), dbPathReorg) + rd, err := reorgdetector.New(client.Client(), dbPathReorg) go rd.Start(ctx) bridgeSync, err := bridgesync.NewL1(ctx, dbPathBridgeSync, bridgeAddr, 10, etherman.LatestBlock, rd, client.Client(), 0, time.Millisecond*10, 0, 0) diff --git a/test/helpers/aggoracle_e2e.go b/test/helpers/aggoracle_e2e.go index c2908b8d..9583ee7b 100644 --- a/test/helpers/aggoracle_e2e.go +++ b/test/helpers/aggoracle_e2e.go @@ -101,7 +101,7 @@ func CommonSetup(t *testing.T) ( require.NoError(t, err) // Reorg detector dbPathReorgDetector := t.TempDir() - reorg, err := reorgdetector.New(ctx, l1Client.Client(), dbPathReorgDetector) + reorg, err := reorgdetector.New(l1Client.Client(), dbPathReorgDetector) require.NoError(t, err) // Syncer dbPathSyncer := t.TempDir() From e633b47beca3ebe65541bc81a5a451d373950ff0 Mon Sep 17 00:00:00 2001 From: begmaroman Date: Wed, 21 Aug 2024 22:22:17 +0100 Subject: [PATCH 17/43] Implementation --- reorgdetector/reorgdetector.go | 358 ++++++++++++++-------------- reorgdetector/reorgdetector_db.go | 44 ++-- reorgdetector/reorgdetector_sub.go | 24 +- reorgdetector/reorgdetector_test.go | 8 +- reorgdetector/types.go | 134 ++++++++--- reorgdetector/types_test.go | 44 ++-- 6 files changed, 341 insertions(+), 271 deletions(-) diff --git a/reorgdetector/reorgdetector.go b/reorgdetector/reorgdetector.go index c07306b2..058c8532 100644 --- a/reorgdetector/reorgdetector.go +++ b/reorgdetector/reorgdetector.go @@ -7,6 +7,8 @@ import ( "sync" "time" + "golang.org/x/sync/errgroup" + "github.com/ledgerwatch/erigon-lib/kv" "github.com/ledgerwatch/erigon-lib/kv/mdbx" @@ -50,21 +52,14 @@ type EthClient interface { HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) } -type Subscription struct { - FirstReorgedBlock chan uint64 - ReorgProcessed chan bool - pendingReorgsToBeProcessed sync.WaitGroup -} - type ReorgDetector struct { client EthClient db kv.RwDB - canonicalBlocksLock sync.RWMutex - canonicalBlocks blockMap + canonicalBlocks *headersList trackedBlocksLock sync.RWMutex - trackedBlocks map[string]blockMap + trackedBlocks map[string]*headersList subscriptionsLock sync.RWMutex subscriptions map[string]*Subscription @@ -82,25 +77,38 @@ func New(client EthClient, dbPath string) (*ReorgDetector, error) { return &ReorgDetector{ client: client, db: db, - canonicalBlocks: make(blockMap), - trackedBlocks: make(map[string]blockMap), + canonicalBlocks: newHeadersList(), + trackedBlocks: make(map[string]*headersList), subscriptions: make(map[string]*Subscription), }, nil } -func (rd *ReorgDetector) Start(ctx context.Context) error { - // Load canonical chain from the last finalized block - if err := rd.loadCanonicalChain(ctx); err != nil { - return fmt.Errorf("failed to load canonical chain: %w", err) - } +func (rd *ReorgDetector) Start(ctx context.Context) (err error) { + // Initially load a full canonical chain + /*if err := rd.loadCanonicalChain(ctx); err != nil { + log.Errorf("failed to load canonical chain: %v", err) + }*/ - // Load and process tracked blocks from the DB - if err := rd.loadAndProcessTrackedBlocks(ctx); err != nil { - return fmt.Errorf("failed to load and process tracked blocks: %w", err) + // Load tracked blocks + if rd.trackedBlocks, err = rd.getTrackedBlocks(ctx); err != nil { + return fmt.Errorf("failed to get tracked blocks: %w", err) } - // Start the reorg detector for the canonical chain - go rd.monitorCanonicalChain(ctx) + // Continuously check reorgs in tracked by subscribers blocks + // TODO: Optimize this process + go func() { + ticker := time.NewTicker(time.Second * 2) + for range ticker.C { + if err = rd.detectReorgInTrackedList(ctx); err != nil { + log.Errorf("failed to detect reorgs in tracked blocks: %v", err) + } + } + }() + + // Load and process tracked headers from the DB + /*if err := rd.loadAndProcessTrackedHeaders(ctx); err != nil { + return fmt.Errorf("failed to load and process tracked headers: %w", err) + }*/ return nil } @@ -117,114 +125,142 @@ func (rd *ReorgDetector) AddBlockToTrack(ctx context.Context, id string, blockNu } rd.subscriptionsLock.RUnlock() - if err := rd.saveTrackedBlock(ctx, id, block{ - Num: blockNum, - Hash: blockHash, - ParentHash: parentHash, - }); err != nil { + hdr := newHeader(blockNum, blockHash, parentHash) + if err := rd.saveTrackedBlock(ctx, id, hdr); err != nil { return fmt.Errorf("failed to save tracked block: %w", err) } return nil } -func (rd *ReorgDetector) monitorCanonicalChain(ctx context.Context) { - // Add head tracker - ch := make(chan *types.Header, 100) - sub, err := rd.client.SubscribeNewHead(ctx, ch) +// 1. Get finalized block: 10 (latest block 15) +// 2. Get tracked blocks: 4, 7, 9, 12, 14 +// 3. Go from 10 till the lowest block in tracked blocks +// 4. Notify about reorg if exists +func (rd *ReorgDetector) detectReorgInTrackedList(ctx context.Context) error { + // Get the latest finalized block + lastFinalisedBlock, err := rd.client.HeaderByNumber(ctx, big.NewInt(int64(rpc.FinalizedBlockNumber))) if err != nil { - log.Fatal("failed to subscribe to new head", "err", err) + return err } - go func() { - for { - select { - case <-ctx.Done(): - return - case <-sub.Err(): - return - case header := <-ch: - if err = rd.onNewHeader(ctx, header); err != nil { - log.Error("failed to process new header", "err", err) - continue - } + // Notify subscribers about reorgs + // TODO: Optimize it + for id, hdrs := range rd.trackedBlocks { + // Get the sorted headers + sorted := hdrs.getSorted() + if len(sorted) == 0 { + continue + } + + // Do not check blocks that are higher than the last finalized block + if sorted[0].Num > lastFinalisedBlock.Number.Uint64() { + continue + } + + // Go from the lowest tracked block till the latest finalized block + for i := sorted[0].Num; i <= lastFinalisedBlock.Number.Uint64(); i++ { + // Get the actual header hash from the client + header, err := rd.client.HeaderByNumber(ctx, big.NewInt(int64(i))) + if err != nil { + return err + } + + // Check if the block hash matches with the actual block hash + if sorted[0].Hash != header.Hash() { + // Reorg detected, notify subscriber + go rd.notifySubscriber(id, sorted[0]) + break } } - }() + } + + return nil } -func (rd *ReorgDetector) onNewHeader(ctx context.Context, header *types.Header) error { - rd.canonicalBlocksLock.Lock() - defer rd.canonicalBlocksLock.Unlock() +// loadCanonicalChain loads canonical chain from the latest finalized block till the latest one +func (rd *ReorgDetector) loadCanonicalChain(ctx context.Context) error { + // Get the latest finalized block + lastFinalisedBlock, err := rd.client.HeaderByNumber(ctx, big.NewInt(int64(rpc.FinalizedBlockNumber))) + if err != nil { + return err + } - newBlock := block{ - Num: header.Number.Uint64(), - Hash: header.Hash(), - ParentHash: header.ParentHash, + // Get the latest block + latestBlock, err := rd.client.HeaderByNumber(ctx, nil) + if err != nil { + return err } - // No canonical chain yet - if len(rd.canonicalBlocks) == 0 { - // TODO: Fill canonical chain from the last finalized block - rd.canonicalBlocks = newBlockMap(newBlock) - return nil + startFromBlock := lastFinalisedBlock.Number.Uint64() + if sortedBlocks := rd.canonicalBlocks.getSorted(); len(sortedBlocks) > 0 { + lastTrackedBlock := sortedBlocks[rd.canonicalBlocks.len()-1].Num + if lastTrackedBlock < startFromBlock { + startFromBlock = lastTrackedBlock + } } - processReorg := func() error { - rd.canonicalBlocks[newBlock.Num] = newBlock - reorgedBlock := rd.canonicalBlocks.detectReorg() - if reorgedBlock != nil { - // Notify subscribers about the reorg - rd.notifySubscribers(*reorgedBlock) + // Load the canonical chain from the last finalized block till the latest block + for i := startFromBlock; i <= latestBlock.Number.Uint64(); i++ { + blockHeader, err := rd.client.HeaderByNumber(ctx, big.NewInt(int64(i))) + if err != nil { + return fmt.Errorf("failed to fetch block header for block number %d: %w", i, err) + } - // Rebuild the canonical chain - if err := rd.rebuildCanonicalChain(ctx, reorgedBlock.Num, newBlock.Num); err != nil { - return fmt.Errorf("failed to rebuild canonical chain: %w", err) - } - } else { - // Should not happen, check the logic below - // log.Fatal("Unexpected reorg detection") + if err = rd.onNewHeader(ctx, blockHeader); err != nil { + return fmt.Errorf("failed to process new header: %w", err) } + } + + return nil +} + +// onNewHeader processes a new header and checks for reorgs +func (rd *ReorgDetector) onNewHeader(ctx context.Context, header *types.Header) error { + hdr := newHeader(header.Number.Uint64(), header.Hash(), header.ParentHash) + // No canonical chain yet + if rd.canonicalBlocks.isEmpty() { + // TODO: Fill canonical chain from the last finalized block + rd.canonicalBlocks.add(hdr) return nil } - closestHigherBlock, ok := rd.canonicalBlocks.getClosestHigherBlock(newBlock.Num) + closestHigherBlock, ok := rd.canonicalBlocks.getClosestHigherBlock(hdr.Num) if !ok { // No same or higher blocks, only lower blocks exist. Check hashes. // Current tracked blocks: N-i, N-i+1, ..., N-1 sortedBlocks := rd.canonicalBlocks.getSorted() closestBlock := sortedBlocks[len(sortedBlocks)-1] - if closestBlock.Num < newBlock.Num-1 { + if closestBlock.Num < hdr.Num-1 { // There is a gap between the last block and the given block // Current tracked blocks: N-i, , N - } else if closestBlock.Num == newBlock.Num-1 { - if closestBlock.Hash != newBlock.ParentHash { + } else if closestBlock.Num == hdr.Num-1 { + if closestBlock.Hash != hdr.ParentHash { // Block hashes do not match, reorg happened - return processReorg() + rd.processReorg(hdr) } else { // All good, add the block to the map - rd.canonicalBlocks[newBlock.Num] = newBlock + rd.canonicalBlocks.add(hdr) } } else { // This should not happen log.Fatal("Unexpected block number comparison") } } else { - if closestHigherBlock.Num == newBlock.Num { - // Block has already been tracked and added to the map - // Current tracked blocks: N-2, N-1, N (given block) - if closestHigherBlock.Hash != newBlock.Hash { - // Block hashes have changed, reorg happened - return processReorg() + if closestHigherBlock.Num == hdr.Num { + if closestHigherBlock.Hash != hdr.Hash { + // Block has already been tracked and added to the map but with different hash. + // Current tracked blocks: N-2, N-1, N (given block) + rd.processReorg(hdr) } - } else if closestHigherBlock.Num >= newBlock.Num+1 { + } else if closestHigherBlock.Num >= hdr.Num+1 { // The given block is lower than the closest higher block: // N-2, N-1, N (given block), N+1, N+2 // OR // There is a gap between the current block and the closest higher block // N-2, N-1, N (given block), , N+i - return processReorg() + rd.processReorg(hdr) } else { // This should not happen log.Fatal("Unexpected block number comparison") @@ -234,65 +270,22 @@ func (rd *ReorgDetector) onNewHeader(ctx context.Context, header *types.Header) return nil } -// rebuildCanonicalChain rebuilds the canonical chain from the given block number to the given block number. -func (rd *ReorgDetector) rebuildCanonicalChain(ctx context.Context, from, to uint64) error { - // TODO: Potentially rebuild from the latest finalized block - - for i := from; i <= to; i++ { - blockHeader, err := rd.client.HeaderByNumber(ctx, big.NewInt(int64(i))) - if err != nil { - return fmt.Errorf("failed to fetch block header for block number %d: %w", i, err) - } +// processReorg processes a reorg and notifies subscribers +func (rd *ReorgDetector) processReorg(hdr header) { + hdrs := rd.canonicalBlocks.copy() + hdrs.add(hdr) - rd.canonicalBlocks[blockHeader.Number.Uint64()] = block{ - Num: blockHeader.Number.Uint64(), - Hash: blockHeader.Hash(), - ParentHash: blockHeader.ParentHash, - } - } - - return nil -} - -// loadCanonicalChain loads canonical chain from the latest finalized block till the latest one -func (rd *ReorgDetector) loadCanonicalChain(ctx context.Context) error { - // Get the latest finalized block - lastFinalisedBlock, err := rd.client.HeaderByNumber(ctx, big.NewInt(int64(rpc.FinalizedBlockNumber))) - if err != nil { - return err - } - - // Get the latest block - latestBlock, err := rd.client.HeaderByNumber(ctx, nil) - if err != nil { - return err - } - - rd.canonicalBlocksLock.Lock() - defer rd.canonicalBlocksLock.Unlock() - - // Load the canonical chain from the last finalized block till the latest block - for i := lastFinalisedBlock.Number.Uint64(); i <= latestBlock.Number.Uint64(); i++ { - blockHeader, err := rd.client.HeaderByNumber(ctx, big.NewInt(int64(i))) - if err != nil { - return fmt.Errorf("failed to fetch block header for block number %d: %w", i, err) - } - - rd.canonicalBlocks[blockHeader.Number.Uint64()] = block{ - Num: blockHeader.Number.Uint64(), - Hash: blockHeader.Hash(), - ParentHash: blockHeader.ParentHash, - } + if reorgedBlock := hdrs.detectReorg(); reorgedBlock != nil { + // Notify subscribers about the reorg + rd.notifySubscribers(*reorgedBlock) + } else { + // Should not happen, check the logic below + // log.Fatal("Unexpected reorg detection") } - - return nil } -// loadAndProcessTrackedBlocks loads tracked blocks from the DB and checks for reorgs. Loads in memory. -func (rd *ReorgDetector) loadAndProcessTrackedBlocks(ctx context.Context) error { - rd.trackedBlocksLock.Lock() - defer rd.trackedBlocksLock.Unlock() - +// loadAndProcessTrackedHeaders loads tracked headers from the DB and checks for reorgs. Loads in memory. +func (rd *ReorgDetector) loadAndProcessTrackedHeaders(ctx context.Context) error { // Load tracked blocks for all subscribers from the DB trackedBlocks, err := rd.getTrackedBlocks(ctx) if err != nil { @@ -306,57 +299,68 @@ func (rd *ReorgDetector) loadAndProcessTrackedBlocks(ctx context.Context) error return err } + var errGrp errgroup.Group for id, blocks := range rd.trackedBlocks { - rd.subscriptionsLock.Lock() - rd.subscriptions[id] = &Subscription{ - FirstReorgedBlock: make(chan uint64), - ReorgProcessed: make(chan bool), - } - rd.subscriptionsLock.Unlock() - - // Nothing to process for this subscriber - if len(blocks) == 0 { - continue - } + id := id + blocks := blocks - var ( - lastTrackedBlock uint64 - actualBlockHash common.Hash - ) + errGrp.Go(func() error { + return rd.processTrackedHeaders(ctx, id, blocks, lastFinalisedBlock.Number.Uint64()) + }) + } - sortedBlocks := blocks.getSorted() - lastTrackedBlock = sortedBlocks[len(blocks)-1].Num + return errGrp.Wait() +} - for _, block := range sortedBlocks { - if actualBlock, ok := rd.canonicalBlocks[block.Num]; !ok { - header, err := rd.client.HeaderByNumber(ctx, big.NewInt(int64(block.Num))) - if err != nil { - return err - } +// processTrackedHeaders processes tracked headers for a subscriber and checks for reorgs +func (rd *ReorgDetector) processTrackedHeaders(ctx context.Context, id string, headers *headersList, finalized uint64) error { + rd.subscriptionsLock.Lock() + rd.subscriptions[id] = &Subscription{ + FirstReorgedBlock: make(chan uint64), + ReorgProcessed: make(chan bool), + } + rd.subscriptionsLock.Unlock() - actualBlockHash = header.Hash() - } else { - actualBlockHash = actualBlock.Hash - } + // Nothing to process for this subscriber + if headers.isEmpty() { + return nil + } - if actualBlockHash != block.Hash { - // Reorg detected, notify subscriber - go rd.notifySubscriber(id, block) + var ( + lastTrackedBlock uint64 + actualBlockHash common.Hash + ) - // Remove the reorged blocks from the tracked blocks - blocks.removeRange(block.Num, lastTrackedBlock) + sortedBlocks := headers.getSorted() + lastTrackedBlock = sortedBlocks[headers.len()-1].Num - break - } else if block.Num <= lastFinalisedBlock.Number.Uint64() { - delete(blocks, block.Num) + for _, block := range sortedBlocks { + // Fetch an actual header hash from the client if it does not exist in the canonical chain + if actualHeader := rd.canonicalBlocks.get(block.Num); actualHeader == nil { + header, err := rd.client.HeaderByNumber(ctx, big.NewInt(int64(block.Num))) + if err != nil { + return err } + + actualBlockHash = header.Hash() + } else { + actualBlockHash = actualHeader.Hash } - // If we processed finalized or reorged blocks, update the tracked blocks in memory and db - if err = rd.updateTrackedBlocksNoLock(ctx, id, blocks); err != nil { - return err + // Check if the block hash matches with the actual block hash + if actualBlockHash != block.Hash { + // Reorg detected, notify subscriber + go rd.notifySubscriber(id, block) + + // Remove the reorged blocks from the tracked blocks + headers.removeRange(block.Num, lastTrackedBlock) + + break + } else if block.Num <= finalized { + headers.removeRange(block.Num, block.Num) } } - return nil + // If we processed finalized or reorged blocks, update the tracked blocks in memory and db + return rd.updateTrackedBlocksNoLock(ctx, id, headers) } diff --git a/reorgdetector/reorgdetector_db.go b/reorgdetector/reorgdetector_db.go index da18a895..708fa6eb 100644 --- a/reorgdetector/reorgdetector_db.go +++ b/reorgdetector/reorgdetector_db.go @@ -1,20 +1,12 @@ package reorgdetector import ( - context "context" + "context" "encoding/json" ) -// getUnfinalisedBlocksMap returns the map of unfinalised blocks -func (rd *ReorgDetector) getUnfinalisedBlocksMap() blockMap { - rd.trackedBlocksLock.RLock() - defer rd.trackedBlocksLock.RUnlock() - - return rd.trackedBlocks[unfinalisedBlocksID] -} - // getTrackedBlocks returns a list of tracked blocks for each subscriber from db -func (rd *ReorgDetector) getTrackedBlocks(ctx context.Context) (map[string]blockMap, error) { +func (rd *ReorgDetector) getTrackedBlocks(ctx context.Context) (map[string]*headersList, error) { tx, err := rd.db.BeginRo(ctx) if err != nil { return nil, err @@ -29,31 +21,31 @@ func (rd *ReorgDetector) getTrackedBlocks(ctx context.Context) (map[string]block defer cursor.Close() - trackedBlocks := make(map[string]blockMap, 0) + trackedBlocks := make(map[string]*headersList, 0) for k, v, err := cursor.First(); k != nil; k, v, err = cursor.Next() { if err != nil { return nil, err } - var blocks []block - if err := json.Unmarshal(v, &blocks); err != nil { + var headers []header + if err := json.Unmarshal(v, &headers); err != nil { return nil, err } - trackedBlocks[string(k)] = newBlockMap(blocks...) + trackedBlocks[string(k)] = newHeadersList(headers...) } if _, ok := trackedBlocks[unfinalisedBlocksID]; !ok { // add unfinalised blocks to tracked blocks map if not present in db - trackedBlocks[unfinalisedBlocksID] = newBlockMap() + trackedBlocks[unfinalisedBlocksID] = newHeadersList() } return trackedBlocks, nil } // saveTrackedBlock saves the tracked block for a subscriber in db and in memory -func (rd *ReorgDetector) saveTrackedBlock(ctx context.Context, id string, b block) error { +func (rd *ReorgDetector) saveTrackedBlock(ctx context.Context, id string, b header) error { tx, err := rd.db.BeginRw(ctx) if err != nil { return err @@ -62,18 +54,16 @@ func (rd *ReorgDetector) saveTrackedBlock(ctx context.Context, id string, b bloc defer tx.Rollback() rd.trackedBlocksLock.Lock() - - subscriberBlockMap, ok := rd.trackedBlocks[id] - if !ok || len(subscriberBlockMap) == 0 { - subscriberBlockMap = newBlockMap(b) - rd.trackedBlocks[id] = subscriberBlockMap + hdrs, ok := rd.trackedBlocks[id] + if !ok || hdrs.isEmpty() { + hdrs = newHeadersList(b) + rd.trackedBlocks[id] = hdrs } else { - subscriberBlockMap[b.Num] = b + hdrs.add(b) } - rd.trackedBlocksLock.Unlock() - raw, err := json.Marshal(subscriberBlockMap.getSorted()) + raw, err := json.Marshal(hdrs.getSorted()) if err != nil { return err } @@ -91,7 +81,7 @@ func (rd *ReorgDetector) removeTrackedBlocks(ctx context.Context, lastFinalizedB newTrackedBlocks := rd.trackedBlocks[id].getFromBlockSorted(lastFinalizedBlock) rd.trackedBlocksLock.RUnlock() - if err := rd.updateTrackedBlocks(ctx, id, newBlockMap(newTrackedBlocks...)); err != nil { + if err := rd.updateTrackedBlocks(ctx, id, newHeadersList(newTrackedBlocks...)); err != nil { return err } } @@ -100,7 +90,7 @@ func (rd *ReorgDetector) removeTrackedBlocks(ctx context.Context, lastFinalizedB } // updateTrackedBlocks updates the tracked blocks for a subscriber in db and in memory -func (rd *ReorgDetector) updateTrackedBlocks(ctx context.Context, id string, blocks blockMap) error { +func (rd *ReorgDetector) updateTrackedBlocks(ctx context.Context, id string, blocks *headersList) error { rd.trackedBlocksLock.Lock() defer rd.trackedBlocksLock.Unlock() @@ -108,7 +98,7 @@ func (rd *ReorgDetector) updateTrackedBlocks(ctx context.Context, id string, blo } // updateTrackedBlocksNoLock updates the tracked blocks for a subscriber in db and in memory -func (rd *ReorgDetector) updateTrackedBlocksNoLock(ctx context.Context, id string, blocks blockMap) error { +func (rd *ReorgDetector) updateTrackedBlocksNoLock(ctx context.Context, id string, blocks *headersList) error { tx, err := rd.db.BeginRw(ctx) if err != nil { return err diff --git a/reorgdetector/reorgdetector_sub.go b/reorgdetector/reorgdetector_sub.go index 71bdf384..9c4d809b 100644 --- a/reorgdetector/reorgdetector_sub.go +++ b/reorgdetector/reorgdetector_sub.go @@ -1,5 +1,13 @@ package reorgdetector +import "sync" + +type Subscription struct { + FirstReorgedBlock chan uint64 + ReorgProcessed chan bool + pendingReorgsToBeProcessed sync.WaitGroup +} + func (rd *ReorgDetector) Subscribe(id string) (*Subscription, error) { rd.subscriptionsLock.Lock() defer rd.subscriptionsLock.Unlock() @@ -17,7 +25,7 @@ func (rd *ReorgDetector) Subscribe(id string) (*Subscription, error) { return sub, nil } -func (rd *ReorgDetector) notifySubscribers(startingBlock block) { +func (rd *ReorgDetector) notifySubscribers(startingBlock header) { rd.subscriptionsLock.RLock() for _, sub := range rd.subscriptions { sub.pendingReorgsToBeProcessed.Add(1) @@ -30,16 +38,14 @@ func (rd *ReorgDetector) notifySubscribers(startingBlock block) { rd.subscriptionsLock.RUnlock() } -func (rd *ReorgDetector) notifySubscriber(id string, startingBlock block) { +func (rd *ReorgDetector) notifySubscriber(id string, startingBlock header) { rd.subscriptionsLock.RLock() - subscriber, ok := rd.subscriptions[id] + sub, ok := rd.subscriptions[id] if ok { - subscriber.pendingReorgsToBeProcessed.Add(1) - go func(sub *Subscription) { - sub.FirstReorgedBlock <- startingBlock.Num - <-sub.ReorgProcessed - sub.pendingReorgsToBeProcessed.Done() - }(subscriber) + sub.pendingReorgsToBeProcessed.Add(1) + sub.FirstReorgedBlock <- startingBlock.Num + <-sub.ReorgProcessed + sub.pendingReorgsToBeProcessed.Done() } rd.subscriptionsLock.RUnlock() } diff --git a/reorgdetector/reorgdetector_test.go b/reorgdetector/reorgdetector_test.go index fc117fb8..32eca664 100644 --- a/reorgdetector/reorgdetector_test.go +++ b/reorgdetector/reorgdetector_test.go @@ -125,13 +125,13 @@ func Test_ReorgDetector(t *testing.T) { firstReorgedBlock := <-reorgSub.FirstReorgedBlock reorgSub.ReorgProcessed <- true - fmt.Println("firstReorgedBlock", firstReorgedBlock) + fmt.Println("firstReorgedBlock", firstReorgedBlock+1) - processed, ok := expectedReorgBlocks[firstReorgedBlock] + _, ok := expectedReorgBlocks[firstReorgedBlock+1] require.True(t, ok) - require.False(t, processed) + //require.False(t, processed) - expectedReorgBlocks[firstReorgedBlock] = true + expectedReorgBlocks[firstReorgedBlock+1] = true } for _, processed := range expectedReorgBlocks { diff --git a/reorgdetector/types.go b/reorgdetector/types.go index 4f969d98..1de13fd9 100644 --- a/reorgdetector/types.go +++ b/reorgdetector/types.go @@ -2,36 +2,100 @@ package reorgdetector import ( "sort" + "sync" "github.com/ethereum/go-ethereum/common" ) -type block struct { +type header struct { Num uint64 Hash common.Hash ParentHash common.Hash } -type blockMap map[uint64]block +// newHeader returns a new instance of header +func newHeader(num uint64, hash, parentHash common.Hash) header { + return header{ + Num: num, + Hash: hash, + ParentHash: parentHash, + } +} + +type headersList struct { + sync.RWMutex + headers map[uint64]header +} + +// newHeadersList returns a new instance of headersList +func newHeadersList(headers ...header) *headersList { + headersMap := make(map[uint64]header, len(headers)) + + for _, b := range headers { + headersMap[b.Num] = b + } + + return &headersList{ + headers: headersMap, + } +} + +// len returns the number of headers in the headers list +func (hl *headersList) len() int { + hl.RLock() + ln := len(hl.headers) + hl.RUnlock() + return ln +} -// newBlockMap returns a new instance of blockMap -func newBlockMap(blocks ...block) blockMap { - blockMap := make(blockMap, len(blocks)) +// isEmpty returns true if the headers list is empty +func (hl *headersList) isEmpty() bool { + return hl.len() == 0 +} + +// add adds a header to the headers list +func (hl *headersList) add(h header) { + hl.Lock() + hl.headers[h.Num] = h + hl.Unlock() +} + +// copy returns a copy of the headers list +func (hl *headersList) copy() *headersList { + hl.RLock() + defer hl.RUnlock() + + headersMap := make(map[uint64]header, len(hl.headers)) + for k, v := range hl.headers { + headersMap[k] = v + } - for _, b := range blocks { - blockMap[b.Num] = b + return &headersList{ + headers: headersMap, + } +} + +// get returns a header by block number +func (hl *headersList) get(num uint64) *header { + hl.RLock() + defer hl.RUnlock() + + if b, ok := hl.headers[num]; ok { + return &b } - return blockMap + return nil } -// getSorted returns blocks in sorted order -func (bm blockMap) getSorted() []block { - sortedBlocks := make([]block, 0, len(bm)) +// getSorted returns headers in sorted order +func (hl *headersList) getSorted() []header { + sortedBlocks := make([]header, 0, len(hl.headers)) - for _, b := range bm { + hl.RLock() + for _, b := range hl.headers { sortedBlocks = append(sortedBlocks, b) } + hl.RUnlock() sort.Slice(sortedBlocks, func(i, j int) bool { return sortedBlocks[i].Num < sortedBlocks[j].Num @@ -41,11 +105,11 @@ func (bm blockMap) getSorted() []block { } // getFromBlockSorted returns blocks from blockNum in sorted order without including the blockNum -func (bm blockMap) getFromBlockSorted(blockNum uint64) []block { - sortedBlocks := bm.getSorted() +func (hl *headersList) getFromBlockSorted(blockNum uint64) []header { + sortedHeaders := hl.getSorted() index := -1 - for i, b := range sortedBlocks { + for i, b := range sortedHeaders { if b.Num > blockNum { index = i break @@ -53,39 +117,45 @@ func (bm blockMap) getFromBlockSorted(blockNum uint64) []block { } if index == -1 { - return []block{} + return nil } - return sortedBlocks[index:] + return sortedHeaders[index:] } // getClosestHigherBlock returns the closest higher block to the given blockNum -func (bm blockMap) getClosestHigherBlock(blockNum uint64) (block, bool) { - if block, ok := bm[blockNum]; ok { - return block, true +func (hl *headersList) getClosestHigherBlock(blockNum uint64) (*header, bool) { + hdr := hl.get(blockNum) + if hdr != nil { + return hdr, true } - sorted := bm.getFromBlockSorted(blockNum) + sorted := hl.getFromBlockSorted(blockNum) if len(sorted) == 0 { - return block{}, false + return nil, false } - return sorted[0], true + return &sorted[0], true } -// removeRange removes blocks from "from" to "to" -func (bm blockMap) removeRange(from, to uint64) { +// removeRange removes headers from "from" to "to" +func (hl *headersList) removeRange(from, to uint64) { + hl.Lock() for i := from; i <= to; i++ { - delete(bm, i) + delete(hl.headers, i) } + hl.Unlock() } -// detectReorg detects a reorg in the given block list. -// Returns the first reorged block or nil. -func (bm blockMap) detectReorg() *block { +// detectReorg detects a reorg in the given headers list. +// Returns the first reorged headers or nil. +func (hl *headersList) detectReorg() *header { + hl.RLock() + defer hl.RUnlock() + // Find the highest block number maxBlockNum := uint64(0) - for blockNum := range bm { + for blockNum := range hl.headers { if blockNum > maxBlockNum { maxBlockNum = blockNum } @@ -94,8 +164,8 @@ func (bm blockMap) detectReorg() *block { // Iterate from the highest block number to the lowest reorgDetected := false for i := maxBlockNum; i > 1; i-- { - currentBlock, currentExists := bm[i] - previousBlock, previousExists := bm[i-1] + currentBlock, currentExists := hl.headers[i] + previousBlock, previousExists := hl.headers[i-1] // Check if both blocks exist (sanity check) if !currentExists || !previousExists { diff --git a/reorgdetector/types_test.go b/reorgdetector/types_test.go index 00e78f20..b4fad525 100644 --- a/reorgdetector/types_test.go +++ b/reorgdetector/types_test.go @@ -12,17 +12,17 @@ func TestBlockMap(t *testing.T) { t.Parallel() // Create a new block map - bm := newBlockMap( - block{Num: 1, Hash: common.HexToHash("0x123")}, - block{Num: 2, Hash: common.HexToHash("0x456")}, - block{Num: 3, Hash: common.HexToHash("0x789")}, + bm := newHeadersList( + header{Num: 1, Hash: common.HexToHash("0x123")}, + header{Num: 2, Hash: common.HexToHash("0x456")}, + header{Num: 3, Hash: common.HexToHash("0x789")}, ) t.Run("getSorted", func(t *testing.T) { t.Parallel() sortedBlocks := bm.getSorted() - expectedSortedBlocks := []block{ + expectedSortedBlocks := []header{ {Num: 1, Hash: common.HexToHash("0x123")}, {Num: 2, Hash: common.HexToHash("0x456")}, {Num: 3, Hash: common.HexToHash("0x789")}, @@ -36,7 +36,7 @@ func TestBlockMap(t *testing.T) { t.Parallel() fromBlockSorted := bm.getFromBlockSorted(2) - expectedFromBlockSorted := []block{ + expectedFromBlockSorted := []header{ {Num: 3, Hash: common.HexToHash("0x789")}, } if !reflect.DeepEqual(fromBlockSorted, expectedFromBlockSorted) { @@ -45,7 +45,7 @@ func TestBlockMap(t *testing.T) { // Test getFromBlockSorted function when blockNum is greater than the last block fromBlockSorted = bm.getFromBlockSorted(4) - expectedFromBlockSorted = []block{} + expectedFromBlockSorted = []header{} if !reflect.DeepEqual(fromBlockSorted, expectedFromBlockSorted) { t.Errorf("getFromBlockSorted() returned incorrect result, expected: %v, got: %v", expectedFromBlockSorted, fromBlockSorted) } @@ -54,25 +54,25 @@ func TestBlockMap(t *testing.T) { t.Run("getClosestHigherBlock", func(t *testing.T) { t.Parallel() - bm := newBlockMap( - block{Num: 1, Hash: common.HexToHash("0x123")}, - block{Num: 2, Hash: common.HexToHash("0x456")}, - block{Num: 3, Hash: common.HexToHash("0x789")}, + bm := newHeadersList( + header{Num: 1, Hash: common.HexToHash("0x123")}, + header{Num: 2, Hash: common.HexToHash("0x456")}, + header{Num: 3, Hash: common.HexToHash("0x789")}, ) // Test when the blockNum exists in the block map b, exists := bm.getClosestHigherBlock(2) require.True(t, exists) - expectedBlock := block{Num: 2, Hash: common.HexToHash("0x456")} - if b != expectedBlock { + expectedBlock := header{Num: 2, Hash: common.HexToHash("0x456")} + if *b != expectedBlock { t.Errorf("getClosestHigherBlock() returned incorrect result, expected: %v, got: %v", expectedBlock, b) } // Test when the blockNum does not exist in the block map b, exists = bm.getClosestHigherBlock(4) require.False(t, exists) - expectedBlock = block{Num: 0, Hash: common.Hash{}} - if b != expectedBlock { + expectedBlock = header{Num: 0, Hash: common.Hash{}} + if *b != expectedBlock { t.Errorf("getClosestHigherBlock() returned incorrect result, expected: %v, got: %v", expectedBlock, b) } }) @@ -80,17 +80,17 @@ func TestBlockMap(t *testing.T) { t.Run("removeRange", func(t *testing.T) { t.Parallel() - bm := newBlockMap( - block{Num: 1, Hash: common.HexToHash("0x123")}, - block{Num: 2, Hash: common.HexToHash("0x456")}, - block{Num: 3, Hash: common.HexToHash("0x789")}, - block{Num: 4, Hash: common.HexToHash("0xabc")}, - block{Num: 5, Hash: common.HexToHash("0xdef")}, + bm := newHeadersList( + header{Num: 1, Hash: common.HexToHash("0x123")}, + header{Num: 2, Hash: common.HexToHash("0x456")}, + header{Num: 3, Hash: common.HexToHash("0x789")}, + header{Num: 4, Hash: common.HexToHash("0xabc")}, + header{Num: 5, Hash: common.HexToHash("0xdef")}, ) bm.removeRange(3, 5) - expectedBlocks := []block{ + expectedBlocks := []header{ {Num: 1, Hash: common.HexToHash("0x123")}, {Num: 2, Hash: common.HexToHash("0x456")}, } From b1542dc7ef1e9f980a2ccb74520e07262b460908 Mon Sep 17 00:00:00 2001 From: begmaroman Date: Thu, 22 Aug 2024 12:30:21 +0100 Subject: [PATCH 18/43] Implementation --- l1infotreesync/mock_reorgdetector_test.go | 10 +- reorgdetector/reorgdetector.go | 296 +++++----------------- reorgdetector/reorgdetector_db.go | 5 - reorgdetector/reorgdetector_sub.go | 32 +-- reorgdetector/reorgdetector_test.go | 17 +- reorgdetector/types.go | 50 +--- sync/evmdriver.go | 6 +- sync/evmdriver_test.go | 4 +- sync/mock_downloader_test.go | 8 +- sync/mock_reorgdetector_test.go | 10 +- 10 files changed, 114 insertions(+), 324 deletions(-) diff --git a/l1infotreesync/mock_reorgdetector_test.go b/l1infotreesync/mock_reorgdetector_test.go index 441fd28a..8255443e 100644 --- a/l1infotreesync/mock_reorgdetector_test.go +++ b/l1infotreesync/mock_reorgdetector_test.go @@ -17,17 +17,17 @@ type ReorgDetectorMock struct { mock.Mock } -// AddBlockToTrack provides a mock function with given fields: ctx, id, blockNum, blockHash, parentHash -func (_m *ReorgDetectorMock) AddBlockToTrack(ctx context.Context, id string, blockNum uint64, blockHash common.Hash, parentHash common.Hash) error { - ret := _m.Called(ctx, id, blockNum, blockHash, parentHash) +// AddBlockToTrack provides a mock function with given fields: ctx, id, blockNum, blockHash +func (_m *ReorgDetectorMock) AddBlockToTrack(ctx context.Context, id string, blockNum uint64, blockHash common.Hash) error { + ret := _m.Called(ctx, id, blockNum, blockHash) if len(ret) == 0 { panic("no return value specified for AddBlockToTrack") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, uint64, common.Hash, common.Hash) error); ok { - r0 = rf(ctx, id, blockNum, blockHash, parentHash) + if rf, ok := ret.Get(0).(func(context.Context, string, uint64, common.Hash) error); ok { + r0 = rf(ctx, id, blockNum, blockHash) } else { r0 = ret.Error(0) } diff --git a/reorgdetector/reorgdetector.go b/reorgdetector/reorgdetector.go index 058c8532..31d22a90 100644 --- a/reorgdetector/reorgdetector.go +++ b/reorgdetector/reorgdetector.go @@ -7,40 +7,23 @@ import ( "sync" "time" - "golang.org/x/sync/errgroup" - - "github.com/ledgerwatch/erigon-lib/kv" - "github.com/ledgerwatch/erigon-lib/kv/mdbx" - - "github.com/ethereum/go-ethereum/rpc" - "github.com/0xPolygon/cdk/log" - "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" - "github.com/pkg/errors" + "github.com/ethereum/go-ethereum/rpc" + "github.com/ledgerwatch/erigon-lib/kv" + "github.com/ledgerwatch/erigon-lib/kv/mdbx" ) -// TODO: consider the case where blocks can disappear, current implementation assumes that if there is a reorg, -// the client will have at least as many blocks as it had before the reorg, however this may not be the case for L2 - const ( defaultWaitPeriodBlockRemover = time.Second * 20 defaultWaitPeriodBlockAdder = time.Second * 2 // should be smaller than block time of the tracked chain subscriberBlocks = "reorgdetector-subscriberBlocks" - - unfinalisedBlocksID = "unfinalisedBlocks" -) - -var ( - ErrNotSubscribed = errors.New("id not found in subscriptions") - ErrInvalidBlockHash = errors.New("the block hash does not match with the expected block hash") - ErrIDReserverd = errors.New("subscription id is reserved") ) -func tableCfgFunc(defaultBuckets kv.TableCfg) kv.TableCfg { +func tableCfgFunc(_ kv.TableCfg) kv.TableCfg { return kv.TableCfg{ subscriberBlocks: {}, } @@ -84,48 +67,50 @@ func New(client EthClient, dbPath string) (*ReorgDetector, error) { } func (rd *ReorgDetector) Start(ctx context.Context) (err error) { + // Load tracked blocks from the DB + if err = rd.loadTrackedHeaders(ctx); err != nil { + return fmt.Errorf("failed to load tracked headers: %w", err) + } + // Initially load a full canonical chain - /*if err := rd.loadCanonicalChain(ctx); err != nil { + if err = rd.loadCanonicalChain(ctx); err != nil { log.Errorf("failed to load canonical chain: %v", err) - }*/ - - // Load tracked blocks - if rd.trackedBlocks, err = rd.getTrackedBlocks(ctx); err != nil { - return fmt.Errorf("failed to get tracked blocks: %w", err) } - // Continuously check reorgs in tracked by subscribers blocks - // TODO: Optimize this process + // Continuously load canonical chain go func() { ticker := time.NewTicker(time.Second * 2) for range ticker.C { - if err = rd.detectReorgInTrackedList(ctx); err != nil { - log.Errorf("failed to detect reorgs in tracked blocks: %v", err) + if err = rd.loadCanonicalChain(ctx); err != nil { + log.Errorf("failed to load canonical chain: %v", err) } } }() - // Load and process tracked headers from the DB - /*if err := rd.loadAndProcessTrackedHeaders(ctx); err != nil { - return fmt.Errorf("failed to load and process tracked headers: %w", err) - }*/ + // Continuously check reorgs in tracked by subscribers blocks + go func() { + ticker := time.NewTicker(time.Second) + for range ticker.C { + rd.detectReorgInTrackedList() + } + }() return nil } -func (rd *ReorgDetector) AddBlockToTrack(ctx context.Context, id string, blockNum uint64, blockHash, parentHash common.Hash) error { - rd.subscriptionsLock.RLock() - if sub, ok := rd.subscriptions[id]; !ok { - rd.subscriptionsLock.RUnlock() - return ErrNotSubscribed - } else { - // In case there are reorgs being processed, wait - // Note that this also makes any addition to trackedBlocks[id] safe - sub.pendingReorgsToBeProcessed.Wait() +// AddBlockToTrack adds a block to the tracked list for a subscriber +func (rd *ReorgDetector) AddBlockToTrack(ctx context.Context, id string, num uint64, hash common.Hash) error { + // Skip if the given block has already been stored + rd.trackedBlocksLock.RLock() + existingHeader := rd.trackedBlocks[id].get(num) + rd.trackedBlocksLock.RUnlock() + + if existingHeader != nil && existingHeader.Hash == hash { + return nil } - rd.subscriptionsLock.RUnlock() - hdr := newHeader(blockNum, blockHash, parentHash) + // Store the given header to the tracked list + hdr := newHeader(num, hash) if err := rd.saveTrackedBlock(ctx, id, hdr); err != nil { return fmt.Errorf("failed to save tracked block: %w", err) } @@ -133,49 +118,34 @@ func (rd *ReorgDetector) AddBlockToTrack(ctx context.Context, id string, blockNu return nil } -// 1. Get finalized block: 10 (latest block 15) -// 2. Get tracked blocks: 4, 7, 9, 12, 14 -// 3. Go from 10 till the lowest block in tracked blocks -// 4. Notify about reorg if exists -func (rd *ReorgDetector) detectReorgInTrackedList(ctx context.Context) error { - // Get the latest finalized block - lastFinalisedBlock, err := rd.client.HeaderByNumber(ctx, big.NewInt(int64(rpc.FinalizedBlockNumber))) - if err != nil { - return err - } - - // Notify subscribers about reorgs - // TODO: Optimize it +// detectReorgInTrackedList detects reorgs in the tracked blocks. +// Notifies subscribers if reorg has happened +func (rd *ReorgDetector) detectReorgInTrackedList() { + rd.trackedBlocksLock.RLock() + var wg sync.WaitGroup for id, hdrs := range rd.trackedBlocks { - // Get the sorted headers - sorted := hdrs.getSorted() - if len(sorted) == 0 { - continue - } - - // Do not check blocks that are higher than the last finalized block - if sorted[0].Num > lastFinalisedBlock.Number.Uint64() { - continue - } - - // Go from the lowest tracked block till the latest finalized block - for i := sorted[0].Num; i <= lastFinalisedBlock.Number.Uint64(); i++ { - // Get the actual header hash from the client - header, err := rd.client.HeaderByNumber(ctx, big.NewInt(int64(i))) - if err != nil { - return err + wg.Add(1) + go func(id string, hdrs *headersList) { + for _, hdr := range hdrs.headers { + currentHeader := rd.canonicalBlocks.get(hdr.Num) + if currentHeader == nil { + break + } + + // Check if the block hash matches with the actual block hash + if hdr.Hash == currentHeader.Hash { + continue + } + + go rd.notifySubscriber(id, hdr) } - // Check if the block hash matches with the actual block hash - if sorted[0].Hash != header.Hash() { - // Reorg detected, notify subscriber - go rd.notifySubscriber(id, sorted[0]) - break - } - } - } + wg.Done() + }(id, hdrs) - return nil + } + wg.Wait() + rd.trackedBlocksLock.RUnlock() } // loadCanonicalChain loads canonical chain from the latest finalized block till the latest one @@ -192,6 +162,7 @@ func (rd *ReorgDetector) loadCanonicalChain(ctx context.Context) error { return err } + // Start from the last stored block if it less than the last finalized one startFromBlock := lastFinalisedBlock.Number.Uint64() if sortedBlocks := rd.canonicalBlocks.getSorted(); len(sortedBlocks) > 0 { lastTrackedBlock := sortedBlocks[rd.canonicalBlocks.len()-1].Num @@ -207,160 +178,27 @@ func (rd *ReorgDetector) loadCanonicalChain(ctx context.Context) error { return fmt.Errorf("failed to fetch block header for block number %d: %w", i, err) } - if err = rd.onNewHeader(ctx, blockHeader); err != nil { - return fmt.Errorf("failed to process new header: %w", err) - } - } - - return nil -} - -// onNewHeader processes a new header and checks for reorgs -func (rd *ReorgDetector) onNewHeader(ctx context.Context, header *types.Header) error { - hdr := newHeader(header.Number.Uint64(), header.Hash(), header.ParentHash) - - // No canonical chain yet - if rd.canonicalBlocks.isEmpty() { - // TODO: Fill canonical chain from the last finalized block - rd.canonicalBlocks.add(hdr) - return nil - } - - closestHigherBlock, ok := rd.canonicalBlocks.getClosestHigherBlock(hdr.Num) - if !ok { - // No same or higher blocks, only lower blocks exist. Check hashes. - // Current tracked blocks: N-i, N-i+1, ..., N-1 - sortedBlocks := rd.canonicalBlocks.getSorted() - closestBlock := sortedBlocks[len(sortedBlocks)-1] - if closestBlock.Num < hdr.Num-1 { - // There is a gap between the last block and the given block - // Current tracked blocks: N-i, , N - } else if closestBlock.Num == hdr.Num-1 { - if closestBlock.Hash != hdr.ParentHash { - // Block hashes do not match, reorg happened - rd.processReorg(hdr) - } else { - // All good, add the block to the map - rd.canonicalBlocks.add(hdr) - } - } else { - // This should not happen - log.Fatal("Unexpected block number comparison") - } - } else { - if closestHigherBlock.Num == hdr.Num { - if closestHigherBlock.Hash != hdr.Hash { - // Block has already been tracked and added to the map but with different hash. - // Current tracked blocks: N-2, N-1, N (given block) - rd.processReorg(hdr) - } - } else if closestHigherBlock.Num >= hdr.Num+1 { - // The given block is lower than the closest higher block: - // N-2, N-1, N (given block), N+1, N+2 - // OR - // There is a gap between the current block and the closest higher block - // N-2, N-1, N (given block), , N+i - rd.processReorg(hdr) - } else { - // This should not happen - log.Fatal("Unexpected block number comparison") - } + // Add the block to the canonical chain + rd.canonicalBlocks.add(newHeader(blockHeader.Number.Uint64(), blockHeader.Hash())) } return nil } -// processReorg processes a reorg and notifies subscribers -func (rd *ReorgDetector) processReorg(hdr header) { - hdrs := rd.canonicalBlocks.copy() - hdrs.add(hdr) - - if reorgedBlock := hdrs.detectReorg(); reorgedBlock != nil { - // Notify subscribers about the reorg - rd.notifySubscribers(*reorgedBlock) - } else { - // Should not happen, check the logic below - // log.Fatal("Unexpected reorg detection") - } -} +// loadTrackedHeaders loads tracked headers from the DB and stores them in memory +func (rd *ReorgDetector) loadTrackedHeaders(ctx context.Context) (err error) { + rd.trackedBlocksLock.Lock() + defer rd.trackedBlocksLock.Unlock() -// loadAndProcessTrackedHeaders loads tracked headers from the DB and checks for reorgs. Loads in memory. -func (rd *ReorgDetector) loadAndProcessTrackedHeaders(ctx context.Context) error { // Load tracked blocks for all subscribers from the DB - trackedBlocks, err := rd.getTrackedBlocks(ctx) - if err != nil { + if rd.trackedBlocks, err = rd.getTrackedBlocks(ctx); err != nil { return fmt.Errorf("failed to get tracked blocks: %w", err) } - rd.trackedBlocks = trackedBlocks - - lastFinalisedBlock, err := rd.client.HeaderByNumber(ctx, big.NewInt(int64(rpc.FinalizedBlockNumber))) - if err != nil { - return err + // Go over tracked blocks and create subscription for each tracker + for id := range rd.trackedBlocks { + _, _ = rd.Subscribe(id) } - var errGrp errgroup.Group - for id, blocks := range rd.trackedBlocks { - id := id - blocks := blocks - - errGrp.Go(func() error { - return rd.processTrackedHeaders(ctx, id, blocks, lastFinalisedBlock.Number.Uint64()) - }) - } - - return errGrp.Wait() -} - -// processTrackedHeaders processes tracked headers for a subscriber and checks for reorgs -func (rd *ReorgDetector) processTrackedHeaders(ctx context.Context, id string, headers *headersList, finalized uint64) error { - rd.subscriptionsLock.Lock() - rd.subscriptions[id] = &Subscription{ - FirstReorgedBlock: make(chan uint64), - ReorgProcessed: make(chan bool), - } - rd.subscriptionsLock.Unlock() - - // Nothing to process for this subscriber - if headers.isEmpty() { - return nil - } - - var ( - lastTrackedBlock uint64 - actualBlockHash common.Hash - ) - - sortedBlocks := headers.getSorted() - lastTrackedBlock = sortedBlocks[headers.len()-1].Num - - for _, block := range sortedBlocks { - // Fetch an actual header hash from the client if it does not exist in the canonical chain - if actualHeader := rd.canonicalBlocks.get(block.Num); actualHeader == nil { - header, err := rd.client.HeaderByNumber(ctx, big.NewInt(int64(block.Num))) - if err != nil { - return err - } - - actualBlockHash = header.Hash() - } else { - actualBlockHash = actualHeader.Hash - } - - // Check if the block hash matches with the actual block hash - if actualBlockHash != block.Hash { - // Reorg detected, notify subscriber - go rd.notifySubscriber(id, block) - - // Remove the reorged blocks from the tracked blocks - headers.removeRange(block.Num, lastTrackedBlock) - - break - } else if block.Num <= finalized { - headers.removeRange(block.Num, block.Num) - } - } - - // If we processed finalized or reorged blocks, update the tracked blocks in memory and db - return rd.updateTrackedBlocksNoLock(ctx, id, headers) + return nil } diff --git a/reorgdetector/reorgdetector_db.go b/reorgdetector/reorgdetector_db.go index 708fa6eb..07e03fc0 100644 --- a/reorgdetector/reorgdetector_db.go +++ b/reorgdetector/reorgdetector_db.go @@ -36,11 +36,6 @@ func (rd *ReorgDetector) getTrackedBlocks(ctx context.Context) (map[string]*head trackedBlocks[string(k)] = newHeadersList(headers...) } - if _, ok := trackedBlocks[unfinalisedBlocksID]; !ok { - // add unfinalised blocks to tracked blocks map if not present in db - trackedBlocks[unfinalisedBlocksID] = newHeadersList() - } - return trackedBlocks, nil } diff --git a/reorgdetector/reorgdetector_sub.go b/reorgdetector/reorgdetector_sub.go index 9c4d809b..fdfb7f31 100644 --- a/reorgdetector/reorgdetector_sub.go +++ b/reorgdetector/reorgdetector_sub.go @@ -3,8 +3,9 @@ package reorgdetector import "sync" type Subscription struct { - FirstReorgedBlock chan uint64 - ReorgProcessed chan bool + ReorgedBlock chan uint64 + ReorgProcessed chan bool + pendingReorgsToBeProcessed sync.WaitGroup } @@ -17,33 +18,24 @@ func (rd *ReorgDetector) Subscribe(id string) (*Subscription, error) { } sub := &Subscription{ - FirstReorgedBlock: make(chan uint64), - ReorgProcessed: make(chan bool), + ReorgedBlock: make(chan uint64), + ReorgProcessed: make(chan bool), } rd.subscriptions[id] = sub - return sub, nil -} + rd.trackedBlocksLock.Lock() + rd.trackedBlocks[id] = newHeadersList() + rd.trackedBlocksLock.Unlock() -func (rd *ReorgDetector) notifySubscribers(startingBlock header) { - rd.subscriptionsLock.RLock() - for _, sub := range rd.subscriptions { - sub.pendingReorgsToBeProcessed.Add(1) - go func(sub *Subscription) { - sub.FirstReorgedBlock <- startingBlock.Num - <-sub.ReorgProcessed - sub.pendingReorgsToBeProcessed.Done() - }(sub) - } - rd.subscriptionsLock.RUnlock() + return sub, nil } +// notifySubscriber notifies the subscriber with the block of the reorg func (rd *ReorgDetector) notifySubscriber(id string, startingBlock header) { rd.subscriptionsLock.RLock() - sub, ok := rd.subscriptions[id] - if ok { + if sub, ok := rd.subscriptions[id]; ok { sub.pendingReorgsToBeProcessed.Add(1) - sub.FirstReorgedBlock <- startingBlock.Num + sub.ReorgedBlock <- startingBlock.Num <-sub.ReorgProcessed sub.pendingReorgsToBeProcessed.Done() } diff --git a/reorgdetector/reorgdetector_test.go b/reorgdetector/reorgdetector_test.go index 32eca664..d29003db 100644 --- a/reorgdetector/reorgdetector_test.go +++ b/reorgdetector/reorgdetector_test.go @@ -84,7 +84,7 @@ func Test_ReorgDetector(t *testing.T) { case <-headerSub.Err(): return case header := <-ch: - err := reorgDetector.AddBlockToTrack(ctx, "test", header.Number.Uint64(), header.Hash(), header.ParentHash) + err := reorgDetector.AddBlockToTrack(ctx, "test", header.Number.Uint64(), header.Hash()) require.NoError(t, err) } } @@ -94,7 +94,7 @@ func Test_ReorgDetector(t *testing.T) { lastReorgOn := int64(0) for i := 1; lastReorgOn <= produceBlocks; i++ { block := clientL1.Commit() - time.Sleep(time.Millisecond) + time.Sleep(time.Millisecond * 100) header, err := clientL1.Client().HeaderByHash(ctx, block) require.NoError(t, err) @@ -121,17 +121,22 @@ func Test_ReorgDetector(t *testing.T) { fmt.Println("expectedReorgBlocks", expectedReorgBlocks) + for blk := range reorgSub.ReorgedBlock { + reorgSub.ReorgProcessed <- true + fmt.Println("reorgSub.FirstReorgedBlock", blk) + } + for range expectedReorgBlocks { - firstReorgedBlock := <-reorgSub.FirstReorgedBlock + firstReorgedBlock := <-reorgSub.ReorgedBlock reorgSub.ReorgProcessed <- true - fmt.Println("firstReorgedBlock", firstReorgedBlock+1) + fmt.Println("firstReorgedBlock", firstReorgedBlock) - _, ok := expectedReorgBlocks[firstReorgedBlock+1] + _, ok := expectedReorgBlocks[firstReorgedBlock] require.True(t, ok) //require.False(t, processed) - expectedReorgBlocks[firstReorgedBlock+1] = true + expectedReorgBlocks[firstReorgedBlock] = true } for _, processed := range expectedReorgBlocks { diff --git a/reorgdetector/types.go b/reorgdetector/types.go index 1de13fd9..8a656e47 100644 --- a/reorgdetector/types.go +++ b/reorgdetector/types.go @@ -8,17 +8,15 @@ import ( ) type header struct { - Num uint64 - Hash common.Hash - ParentHash common.Hash + Num uint64 + Hash common.Hash } // newHeader returns a new instance of header -func newHeader(num uint64, hash, parentHash common.Hash) header { +func newHeader(num uint64, hash common.Hash) header { return header{ - Num: num, - Hash: hash, - ParentHash: parentHash, + Num: num, + Hash: hash, } } @@ -146,41 +144,3 @@ func (hl *headersList) removeRange(from, to uint64) { } hl.Unlock() } - -// detectReorg detects a reorg in the given headers list. -// Returns the first reorged headers or nil. -func (hl *headersList) detectReorg() *header { - hl.RLock() - defer hl.RUnlock() - - // Find the highest block number - maxBlockNum := uint64(0) - for blockNum := range hl.headers { - if blockNum > maxBlockNum { - maxBlockNum = blockNum - } - } - - // Iterate from the highest block number to the lowest - reorgDetected := false - for i := maxBlockNum; i > 1; i-- { - currentBlock, currentExists := hl.headers[i] - previousBlock, previousExists := hl.headers[i-1] - - // Check if both blocks exist (sanity check) - if !currentExists || !previousExists { - continue - } - - // Check if the current block's parent hash matches the previous block's hash - if currentBlock.ParentHash != previousBlock.Hash { - reorgDetected = true - } else if reorgDetected { - // When reorg is detected, and we find the first match, return the previous block - return &previousBlock - } - } - - // No reorg detected - return nil -} diff --git a/sync/evmdriver.go b/sync/evmdriver.go index b47ed389..2edd2e15 100644 --- a/sync/evmdriver.go +++ b/sync/evmdriver.go @@ -36,7 +36,7 @@ type processorInterface interface { type ReorgDetector interface { Subscribe(id string) (*reorgdetector.Subscription, error) - AddBlockToTrack(ctx context.Context, id string, blockNum uint64, blockHash, parentHash common.Hash) error + AddBlockToTrack(ctx context.Context, id string, blockNum uint64, blockHash common.Hash) error } func NewEVMDriver( @@ -93,7 +93,7 @@ reset: case b := <-downloadCh: d.log.Debug("handleNewBlock") d.handleNewBlock(ctx, b) - case firstReorgedBlock := <-d.reorgSub.FirstReorgedBlock: + case firstReorgedBlock := <-d.reorgSub.ReorgedBlock: d.log.Debug("handleReorg") d.handleReorg(ctx, cancel, downloadCh, firstReorgedBlock) goto reset @@ -104,7 +104,7 @@ reset: func (d *EVMDriver) handleNewBlock(ctx context.Context, b EVMBlock) { attempts := 0 for { - err := d.reorgDetector.AddBlockToTrack(ctx, d.reorgDetectorID, b.Num, b.Hash, b.ParentHash) + err := d.reorgDetector.AddBlockToTrack(ctx, d.reorgDetectorID, b.Num, b.Hash) if err != nil { attempts++ d.log.Errorf("error adding block %d to tracker: %v", b.Num, err) diff --git a/sync/evmdriver_test.go b/sync/evmdriver_test.go index 5b1abbfe..74692321 100644 --- a/sync/evmdriver_test.go +++ b/sync/evmdriver_test.go @@ -29,8 +29,8 @@ func TestSync(t *testing.T) { firstReorgedBlock := make(chan uint64) reorgProcessed := make(chan bool) rdm.On("Subscribe", reorgDetectorID).Return(&reorgdetector.Subscription{ - FirstReorgedBlock: firstReorgedBlock, - ReorgProcessed: reorgProcessed, + ReorgedBlock: firstReorgedBlock, + ReorgProcessed: reorgProcessed, }, nil) driver, err := NewEVMDriver(rdm, pm, dm, reorgDetectorID, 10, rh) require.NoError(t, err) diff --git a/sync/mock_downloader_test.go b/sync/mock_downloader_test.go index f304ee9d..c965efb6 100644 --- a/sync/mock_downloader_test.go +++ b/sync/mock_downloader_test.go @@ -24,7 +24,7 @@ func (_m *EVMDownloaderMock) GetBlockHeader(ctx context.Context, blockNum uint64 ret := _m.Called(ctx, blockNum) if len(ret) == 0 { - panic("no return value specified for getBlockHeader") + panic("no return value specified for GetBlockHeader") } var r0 EVMBlockHeader @@ -42,7 +42,7 @@ func (_m *EVMDownloaderMock) GetEventsByBlockRange(ctx context.Context, fromBloc ret := _m.Called(ctx, fromBlock, toBlock) if len(ret) == 0 { - panic("no return value specified for getEventsByBlockRange") + panic("no return value specified for GetEventsByBlockRange") } var r0 []EVMBlock @@ -62,7 +62,7 @@ func (_m *EVMDownloaderMock) GetLogs(ctx context.Context, fromBlock uint64, toBl ret := _m.Called(ctx, fromBlock, toBlock) if len(ret) == 0 { - panic("no return value specified for getLogs") + panic("no return value specified for GetLogs") } var r0 []types.Log @@ -82,7 +82,7 @@ func (_m *EVMDownloaderMock) WaitForNewBlocks(ctx context.Context, lastBlockSeen ret := _m.Called(ctx, lastBlockSeen) if len(ret) == 0 { - panic("no return value specified for waitForNewBlocks") + panic("no return value specified for WaitForNewBlocks") } var r0 uint64 diff --git a/sync/mock_reorgdetector_test.go b/sync/mock_reorgdetector_test.go index 38ca9ba8..52cd0cd0 100644 --- a/sync/mock_reorgdetector_test.go +++ b/sync/mock_reorgdetector_test.go @@ -17,17 +17,17 @@ type ReorgDetectorMock struct { mock.Mock } -// AddBlockToTrack provides a mock function with given fields: ctx, id, blockNum, blockHash, parentHash -func (_m *ReorgDetectorMock) AddBlockToTrack(ctx context.Context, id string, blockNum uint64, blockHash common.Hash, parentHash common.Hash) error { - ret := _m.Called(ctx, id, blockNum, blockHash, parentHash) +// AddBlockToTrack provides a mock function with given fields: ctx, id, blockNum, blockHash +func (_m *ReorgDetectorMock) AddBlockToTrack(ctx context.Context, id string, blockNum uint64, blockHash common.Hash) error { + ret := _m.Called(ctx, id, blockNum, blockHash) if len(ret) == 0 { panic("no return value specified for AddBlockToTrack") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, string, uint64, common.Hash, common.Hash) error); ok { - r0 = rf(ctx, id, blockNum, blockHash, parentHash) + if rf, ok := ret.Get(0).(func(context.Context, string, uint64, common.Hash) error); ok { + r0 = rf(ctx, id, blockNum, blockHash) } else { r0 = ret.Error(0) } From 4835d9a4521556d88e9594781cdb937454580ba4 Mon Sep 17 00:00:00 2001 From: begmaroman Date: Thu, 22 Aug 2024 13:23:03 +0100 Subject: [PATCH 19/43] Updated E2E test --- reorgdetector/reorgdetector.go | 4 ++ reorgdetector/reorgdetector_sub.go | 20 ++++++++- reorgdetector/reorgdetector_test.go | 68 +++++++++++++++-------------- 3 files changed, 58 insertions(+), 34 deletions(-) diff --git a/reorgdetector/reorgdetector.go b/reorgdetector/reorgdetector.go index 31d22a90..fae5bd30 100644 --- a/reorgdetector/reorgdetector.go +++ b/reorgdetector/reorgdetector.go @@ -46,6 +46,9 @@ type ReorgDetector struct { subscriptionsLock sync.RWMutex subscriptions map[string]*Subscription + + notifiedReorgsLock sync.RWMutex + notifiedReorgs map[string]struct{} } func New(client EthClient, dbPath string) (*ReorgDetector, error) { @@ -63,6 +66,7 @@ func New(client EthClient, dbPath string) (*ReorgDetector, error) { canonicalBlocks: newHeadersList(), trackedBlocks: make(map[string]*headersList), subscriptions: make(map[string]*Subscription), + notifiedReorgs: make(map[string]struct{}), }, nil } diff --git a/reorgdetector/reorgdetector_sub.go b/reorgdetector/reorgdetector_sub.go index fdfb7f31..6a7960a2 100644 --- a/reorgdetector/reorgdetector_sub.go +++ b/reorgdetector/reorgdetector_sub.go @@ -1,6 +1,9 @@ package reorgdetector -import "sync" +import ( + "fmt" + "sync" +) type Subscription struct { ReorgedBlock chan uint64 @@ -32,6 +35,16 @@ func (rd *ReorgDetector) Subscribe(id string) (*Subscription, error) { // notifySubscriber notifies the subscriber with the block of the reorg func (rd *ReorgDetector) notifySubscriber(id string, startingBlock header) { + // Check if the given reorg was already notified to the given subscriber + reorgKey := fmt.Sprintf("%s_%d", id, startingBlock.Num) + rd.notifiedReorgsLock.RLock() + if _, ok := rd.notifiedReorgs[reorgKey]; ok { + rd.notifiedReorgsLock.RUnlock() + return + } + rd.notifiedReorgsLock.RUnlock() + + // Notify subscriber about this particular reorg rd.subscriptionsLock.RLock() if sub, ok := rd.subscriptions[id]; ok { sub.pendingReorgsToBeProcessed.Add(1) @@ -40,4 +53,9 @@ func (rd *ReorgDetector) notifySubscriber(id string, startingBlock header) { sub.pendingReorgsToBeProcessed.Done() } rd.subscriptionsLock.RUnlock() + + // Mark the reorg as notified + rd.notifiedReorgsLock.RLock() + rd.notifiedReorgs[reorgKey] = struct{}{} + rd.notifiedReorgsLock.RUnlock() } diff --git a/reorgdetector/reorgdetector_test.go b/reorgdetector/reorgdetector_test.go index d29003db..b08480cc 100644 --- a/reorgdetector/reorgdetector_test.go +++ b/reorgdetector/reorgdetector_test.go @@ -49,7 +49,9 @@ func newTestDir(tb testing.TB) string { func Test_ReorgDetector(t *testing.T) { const produceBlocks = 29 const reorgPeriod = 5 + const trackBlockPeriod = 4 const reorgDepth = 2 + const subID = "test" ctx := context.Background() @@ -70,45 +72,38 @@ func Test_ReorgDetector(t *testing.T) { err = reorgDetector.Start(ctx) require.NoError(t, err) - reorgSub, err := reorgDetector.Subscribe("test") + reorgSub, err := reorgDetector.Subscribe(subID) require.NoError(t, err) - ch := make(chan *types.Header, 10) - headerSub, err := clientL1.Client().SubscribeNewHead(ctx, ch) - require.NoError(t, err) - go func() { - for { - select { - case <-ctx.Done(): - return - case <-headerSub.Err(): - return - case header := <-ch: - err := reorgDetector.AddBlockToTrack(ctx, "test", header.Number.Uint64(), header.Hash()) - require.NoError(t, err) - } - } - }() - - expectedReorgBlocks := make(map[uint64]bool) - lastReorgOn := int64(0) + canonicalChain := make(map[uint64]common.Hash) + trackedBlocks := make(map[uint64]common.Hash) + lastReorgOn := uint64(0) for i := 1; lastReorgOn <= produceBlocks; i++ { block := clientL1.Commit() time.Sleep(time.Millisecond * 100) header, err := clientL1.Client().HeaderByHash(ctx, block) require.NoError(t, err) - headerNumber := header.Number.Int64() + headerNumber := header.Number.Uint64() + + canonicalChain[headerNumber] = header.Hash() + + // Add block to track every "trackBlockPeriod" blocks + if headerNumber%trackBlockPeriod == 0 { + if _, ok := trackedBlocks[headerNumber]; !ok { + err = reorgDetector.AddBlockToTrack(ctx, subID, header.Number.Uint64(), header.Hash()) + require.NoError(t, err) + trackedBlocks[headerNumber] = header.Hash() + } + } // Reorg every "reorgPeriod" blocks with "reorgDepth" blocks depth if headerNumber > lastReorgOn && headerNumber%reorgPeriod == 0 { lastReorgOn = headerNumber - reorgBlock, err := clientL1.Client().BlockByNumber(ctx, big.NewInt(headerNumber-reorgDepth)) + reorgBlock, err := clientL1.Client().BlockByNumber(ctx, big.NewInt(int64(headerNumber-reorgDepth))) require.NoError(t, err) - expectedReorgBlocks[reorgBlock.NumberU64()] = false - err = clientL1.Fork(reorgBlock.Hash()) require.NoError(t, err) } @@ -119,27 +114,34 @@ func Test_ReorgDetector(t *testing.T) { clientL1.Commit() } - fmt.Println("expectedReorgBlocks", expectedReorgBlocks) + // Expect reorgs on block + expectReorgOn := make(map[uint64]bool) + for num, hash := range canonicalChain { + if _, ok := trackedBlocks[num]; !ok { + continue + } - for blk := range reorgSub.ReorgedBlock { - reorgSub.ReorgProcessed <- true - fmt.Println("reorgSub.FirstReorgedBlock", blk) + if trackedBlocks[num] != hash { + expectReorgOn[num] = false + } } - for range expectedReorgBlocks { + // Wait for reorg notifications, expect len(expectReorgOn) notifications + for range expectReorgOn { firstReorgedBlock := <-reorgSub.ReorgedBlock reorgSub.ReorgProcessed <- true fmt.Println("firstReorgedBlock", firstReorgedBlock) - _, ok := expectedReorgBlocks[firstReorgedBlock] + processed, ok := expectReorgOn[firstReorgedBlock] require.True(t, ok) - //require.False(t, processed) + require.False(t, processed) - expectedReorgBlocks[firstReorgedBlock] = true + expectReorgOn[firstReorgedBlock] = true } - for _, processed := range expectedReorgBlocks { + // Make sure all processed + for _, processed := range expectReorgOn { require.True(t, processed) } } From 9135a2d19494230f6f9d2cf5829a1090a6fff553 Mon Sep 17 00:00:00 2001 From: begmaroman Date: Thu, 22 Aug 2024 13:39:12 +0100 Subject: [PATCH 20/43] Minor update --- reorgdetector/config.go | 1 + reorgdetector/reorgdetector.go | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/reorgdetector/config.go b/reorgdetector/config.go index 179e3909..54be77bd 100644 --- a/reorgdetector/config.go +++ b/reorgdetector/config.go @@ -1,5 +1,6 @@ package reorgdetector +// Config is the configuration for the reorg detector type Config struct { DBPath string `mapstructure:"DBPath"` } diff --git a/reorgdetector/reorgdetector.go b/reorgdetector/reorgdetector.go index fae5bd30..be445489 100644 --- a/reorgdetector/reorgdetector.go +++ b/reorgdetector/reorgdetector.go @@ -83,7 +83,7 @@ func (rd *ReorgDetector) Start(ctx context.Context) (err error) { // Continuously load canonical chain go func() { - ticker := time.NewTicker(time.Second * 2) + ticker := time.NewTicker(time.Second * 2) // TODO: Configure it for range ticker.C { if err = rd.loadCanonicalChain(ctx); err != nil { log.Errorf("failed to load canonical chain: %v", err) @@ -93,7 +93,7 @@ func (rd *ReorgDetector) Start(ctx context.Context) (err error) { // Continuously check reorgs in tracked by subscribers blocks go func() { - ticker := time.NewTicker(time.Second) + ticker := time.NewTicker(time.Second) // TODO: Configure it for range ticker.C { rd.detectReorgInTrackedList() } From 6c0e817b2342e8257372bb14c6afa4327ab17ff0 Mon Sep 17 00:00:00 2001 From: begmaroman Date: Thu, 22 Aug 2024 13:41:21 +0100 Subject: [PATCH 21/43] Minor update --- reorgdetector/reorgdetector.go | 4 ++-- reorgdetector/reorgdetector_sub.go | 13 +++++++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/reorgdetector/reorgdetector.go b/reorgdetector/reorgdetector.go index be445489..db96b12b 100644 --- a/reorgdetector/reorgdetector.go +++ b/reorgdetector/reorgdetector.go @@ -48,7 +48,7 @@ type ReorgDetector struct { subscriptions map[string]*Subscription notifiedReorgsLock sync.RWMutex - notifiedReorgs map[string]struct{} + notifiedReorgs map[string]map[uint64]struct{} } func New(client EthClient, dbPath string) (*ReorgDetector, error) { @@ -66,7 +66,7 @@ func New(client EthClient, dbPath string) (*ReorgDetector, error) { canonicalBlocks: newHeadersList(), trackedBlocks: make(map[string]*headersList), subscriptions: make(map[string]*Subscription), - notifiedReorgs: make(map[string]struct{}), + notifiedReorgs: make(map[string]map[uint64]struct{}), }, nil } diff --git a/reorgdetector/reorgdetector_sub.go b/reorgdetector/reorgdetector_sub.go index 6a7960a2..708f26fb 100644 --- a/reorgdetector/reorgdetector_sub.go +++ b/reorgdetector/reorgdetector_sub.go @@ -1,7 +1,6 @@ package reorgdetector import ( - "fmt" "sync" ) @@ -20,25 +19,31 @@ func (rd *ReorgDetector) Subscribe(id string) (*Subscription, error) { return sub, nil } + // Create a new subscription sub := &Subscription{ ReorgedBlock: make(chan uint64), ReorgProcessed: make(chan bool), } rd.subscriptions[id] = sub + // Create a new tracked blocks list for the subscriber rd.trackedBlocksLock.Lock() rd.trackedBlocks[id] = newHeadersList() rd.trackedBlocksLock.Unlock() + // Create a new notified reorgs list for the subscriber + rd.notifiedReorgsLock.Lock() + rd.notifiedReorgs[id] = make(map[uint64]struct{}) + rd.notifiedReorgsLock.Unlock() + return sub, nil } // notifySubscriber notifies the subscriber with the block of the reorg func (rd *ReorgDetector) notifySubscriber(id string, startingBlock header) { // Check if the given reorg was already notified to the given subscriber - reorgKey := fmt.Sprintf("%s_%d", id, startingBlock.Num) rd.notifiedReorgsLock.RLock() - if _, ok := rd.notifiedReorgs[reorgKey]; ok { + if _, ok := rd.notifiedReorgs[id][startingBlock.Num]; ok { rd.notifiedReorgsLock.RUnlock() return } @@ -56,6 +61,6 @@ func (rd *ReorgDetector) notifySubscriber(id string, startingBlock header) { // Mark the reorg as notified rd.notifiedReorgsLock.RLock() - rd.notifiedReorgs[reorgKey] = struct{}{} + rd.notifiedReorgs[id][startingBlock.Num] = struct{}{} rd.notifiedReorgsLock.RUnlock() } From fbcf80593ff94f03ba3364b8d1b5e8cfae3befa2 Mon Sep 17 00:00:00 2001 From: begmaroman Date: Thu, 22 Aug 2024 13:41:37 +0100 Subject: [PATCH 22/43] Minor update --- reorgdetector/reorgdetector_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/reorgdetector/reorgdetector_test.go b/reorgdetector/reorgdetector_test.go index b08480cc..eca7ebd9 100644 --- a/reorgdetector/reorgdetector_test.go +++ b/reorgdetector/reorgdetector_test.go @@ -131,8 +131,6 @@ func Test_ReorgDetector(t *testing.T) { firstReorgedBlock := <-reorgSub.ReorgedBlock reorgSub.ReorgProcessed <- true - fmt.Println("firstReorgedBlock", firstReorgedBlock) - processed, ok := expectReorgOn[firstReorgedBlock] require.True(t, ok) require.False(t, processed) From b82d0a840e0b285e52856358b047b4f295974201 Mon Sep 17 00:00:00 2001 From: begmaroman Date: Thu, 22 Aug 2024 13:57:00 +0100 Subject: [PATCH 23/43] Minor update --- reorgdetector/reorgdetector.go | 9 +++++++++ reorgdetector/reorgdetector_sub.go | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/reorgdetector/reorgdetector.go b/reorgdetector/reorgdetector.go index db96b12b..ba357963 100644 --- a/reorgdetector/reorgdetector.go +++ b/reorgdetector/reorgdetector.go @@ -141,7 +141,16 @@ func (rd *ReorgDetector) detectReorgInTrackedList() { continue } + // Notify the subscriber about the reorg go rd.notifySubscriber(id, hdr) + + // Update the tracked list + hdrs.add(*currentHeader) + } + + // Remove the reorged block and all the blocks after it + if err := rd.updateTrackedBlocks(context.Background(), id, hdrs); err != nil { + log.Errorf("failed to update tracked blocks: %v", err) } wg.Done() diff --git a/reorgdetector/reorgdetector_sub.go b/reorgdetector/reorgdetector_sub.go index 708f26fb..f155a887 100644 --- a/reorgdetector/reorgdetector_sub.go +++ b/reorgdetector/reorgdetector_sub.go @@ -60,7 +60,7 @@ func (rd *ReorgDetector) notifySubscriber(id string, startingBlock header) { rd.subscriptionsLock.RUnlock() // Mark the reorg as notified - rd.notifiedReorgsLock.RLock() + rd.notifiedReorgsLock.Lock() rd.notifiedReorgs[id][startingBlock.Num] = struct{}{} - rd.notifiedReorgsLock.RUnlock() + rd.notifiedReorgsLock.Unlock() } From 892d0d62eab6312fe8329250014c6ff9d763d1f3 Mon Sep 17 00:00:00 2001 From: begmaroman Date: Thu, 22 Aug 2024 13:57:53 +0100 Subject: [PATCH 24/43] Minor update --- reorgdetector/reorgdetector.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/reorgdetector/reorgdetector.go b/reorgdetector/reorgdetector.go index ba357963..bd87d359 100644 --- a/reorgdetector/reorgdetector.go +++ b/reorgdetector/reorgdetector.go @@ -143,14 +143,6 @@ func (rd *ReorgDetector) detectReorgInTrackedList() { // Notify the subscriber about the reorg go rd.notifySubscriber(id, hdr) - - // Update the tracked list - hdrs.add(*currentHeader) - } - - // Remove the reorged block and all the blocks after it - if err := rd.updateTrackedBlocks(context.Background(), id, hdrs); err != nil { - log.Errorf("failed to update tracked blocks: %v", err) } wg.Done() From d88f723da9341f811befcc9024a10c443d1958ef Mon Sep 17 00:00:00 2001 From: begmaroman Date: Thu, 22 Aug 2024 14:12:02 +0100 Subject: [PATCH 25/43] Minor update --- reorgdetector/reorgdetector.go | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/reorgdetector/reorgdetector.go b/reorgdetector/reorgdetector.go index bd87d359..4b8982f3 100644 --- a/reorgdetector/reorgdetector.go +++ b/reorgdetector/reorgdetector.go @@ -7,6 +7,8 @@ import ( "sync" "time" + "golang.org/x/sync/errgroup" + "github.com/0xPolygon/cdk/log" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" @@ -95,7 +97,9 @@ func (rd *ReorgDetector) Start(ctx context.Context) (err error) { go func() { ticker := time.NewTicker(time.Second) // TODO: Configure it for range ticker.C { - rd.detectReorgInTrackedList() + if err = rd.detectReorgInTrackedList(ctx); err != nil { + log.Errorf("failed to detect reorg in tracked list: %v", err) + } } }() @@ -124,13 +128,17 @@ func (rd *ReorgDetector) AddBlockToTrack(ctx context.Context, id string, num uin // detectReorgInTrackedList detects reorgs in the tracked blocks. // Notifies subscribers if reorg has happened -func (rd *ReorgDetector) detectReorgInTrackedList() { +func (rd *ReorgDetector) detectReorgInTrackedList(ctx context.Context) error { + var errGroup errgroup.Group + rd.trackedBlocksLock.RLock() - var wg sync.WaitGroup for id, hdrs := range rd.trackedBlocks { - wg.Add(1) - go func(id string, hdrs *headersList) { - for _, hdr := range hdrs.headers { + id := id + hdrs := hdrs + + errGroup.Go(func() error { + headers := hdrs.getSorted() + for _, hdr := range headers { currentHeader := rd.canonicalBlocks.get(hdr.Num) if currentHeader == nil { break @@ -143,14 +151,21 @@ func (rd *ReorgDetector) detectReorgInTrackedList() { // Notify the subscriber about the reorg go rd.notifySubscriber(id, hdr) + + hdrs.removeRange(hdr.Num, hdr.Num) } - wg.Done() - }(id, hdrs) + // Update the tracked blocks in the DB + if err := rd.updateTrackedBlocksNoLock(ctx, id, hdrs); err != nil { + return fmt.Errorf("failed to update tracked blocks for subscriber %s: %w", id, err) + } + return nil + }) } - wg.Wait() rd.trackedBlocksLock.RUnlock() + + return errGroup.Wait() } // loadCanonicalChain loads canonical chain from the latest finalized block till the latest one From c72340785bb10e925590dae71dee00b83f18fd4f Mon Sep 17 00:00:00 2001 From: begmaroman Date: Thu, 22 Aug 2024 18:22:06 +0100 Subject: [PATCH 26/43] Minor update --- reorgdetector/reorgdetector.go | 55 ++++++++++++++++++------------- reorgdetector/reorgdetector_db.go | 12 +++++++ 2 files changed, 45 insertions(+), 22 deletions(-) diff --git a/reorgdetector/reorgdetector.go b/reorgdetector/reorgdetector.go index 4b8982f3..df2eba95 100644 --- a/reorgdetector/reorgdetector.go +++ b/reorgdetector/reorgdetector.go @@ -18,19 +18,6 @@ import ( "github.com/ledgerwatch/erigon-lib/kv/mdbx" ) -const ( - defaultWaitPeriodBlockRemover = time.Second * 20 - defaultWaitPeriodBlockAdder = time.Second * 2 // should be smaller than block time of the tracked chain - - subscriberBlocks = "reorgdetector-subscriberBlocks" -) - -func tableCfgFunc(_ kv.TableCfg) kv.TableCfg { - return kv.TableCfg{ - subscriberBlocks: {}, - } -} - type EthClient interface { SubscribeNewHead(ctx context.Context, ch chan<- *types.Header) (ethereum.Subscription, error) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) @@ -129,6 +116,12 @@ func (rd *ReorgDetector) AddBlockToTrack(ctx context.Context, id string, num uin // detectReorgInTrackedList detects reorgs in the tracked blocks. // Notifies subscribers if reorg has happened func (rd *ReorgDetector) detectReorgInTrackedList(ctx context.Context) error { + // Get the latest finalized block + lastFinalisedBlock, err := rd.client.HeaderByNumber(ctx, big.NewInt(int64(rpc.FinalizedBlockNumber))) + if err != nil { + return fmt.Errorf("failed to get the latest finalized block: %w", err) + } + var errGroup errgroup.Group rd.trackedBlocksLock.RLock() @@ -139,20 +132,29 @@ func (rd *ReorgDetector) detectReorgInTrackedList(ctx context.Context) error { errGroup.Go(func() error { headers := hdrs.getSorted() for _, hdr := range headers { - currentHeader := rd.canonicalBlocks.get(hdr.Num) - if currentHeader == nil { - break + // Get the actual header from the network + currentHeader, err := rd.client.HeaderByNumber(ctx, big.NewInt(int64(hdr.Num))) + if err != nil { + return fmt.Errorf("failed to get the header: %w", err) } // Check if the block hash matches with the actual block hash - if hdr.Hash == currentHeader.Hash { + if hdr.Hash == currentHeader.Hash() { + // Delete block from the tracked blocks list if it is less than or equal to the last finalized block + // and hashes matches + if hdr.Num <= lastFinalisedBlock.Number.Uint64() { + hdrs.removeRange(hdr.Num, hdr.Num) + } continue } // Notify the subscriber about the reorg - go rd.notifySubscriber(id, hdr) + rd.notifySubscriber(id, hdr) + + // Remove the reorged block and all the following blocks + hdrs.removeRange(hdr.Num, headers[len(headers)-1].Num) - hdrs.removeRange(hdr.Num, hdr.Num) + break } // Update the tracked blocks in the DB @@ -185,9 +187,18 @@ func (rd *ReorgDetector) loadCanonicalChain(ctx context.Context) error { // Start from the last stored block if it less than the last finalized one startFromBlock := lastFinalisedBlock.Number.Uint64() if sortedBlocks := rd.canonicalBlocks.getSorted(); len(sortedBlocks) > 0 { - lastTrackedBlock := sortedBlocks[rd.canonicalBlocks.len()-1].Num - if lastTrackedBlock < startFromBlock { - startFromBlock = lastTrackedBlock + lastTrackedBlock := sortedBlocks[rd.canonicalBlocks.len()-1] + if lastTrackedBlock.Num < startFromBlock { + startFromBlock = lastTrackedBlock.Num + + startHeader, err := rd.client.HeaderByNumber(ctx, big.NewInt(int64(startFromBlock))) + if err != nil { + return fmt.Errorf("failed to fetch the start header %d: %w", startFromBlock, err) + } + + if startHeader.Hash() != lastTrackedBlock.Hash { + // Reorg happened, find the first reorg block to + } } } diff --git a/reorgdetector/reorgdetector_db.go b/reorgdetector/reorgdetector_db.go index 07e03fc0..5c7cda2a 100644 --- a/reorgdetector/reorgdetector_db.go +++ b/reorgdetector/reorgdetector_db.go @@ -3,8 +3,20 @@ package reorgdetector import ( "context" "encoding/json" + + "github.com/ledgerwatch/erigon-lib/kv" +) + +const ( + subscriberBlocks = "reorgdetector-subscriberBlocks" ) +func tableCfgFunc(_ kv.TableCfg) kv.TableCfg { + return kv.TableCfg{ + subscriberBlocks: {}, + } +} + // getTrackedBlocks returns a list of tracked blocks for each subscriber from db func (rd *ReorgDetector) getTrackedBlocks(ctx context.Context) (map[string]*headersList, error) { tx, err := rd.db.BeginRo(ctx) From fc384528230ceb5b536b0c69bb0c5c00e5e97c35 Mon Sep 17 00:00:00 2001 From: begmaroman Date: Thu, 22 Aug 2024 18:43:12 +0100 Subject: [PATCH 27/43] Minor update --- reorgdetector/reorgdetector.go | 81 ++++------------------------- reorgdetector/reorgdetector_test.go | 5 ++ 2 files changed, 14 insertions(+), 72 deletions(-) diff --git a/reorgdetector/reorgdetector.go b/reorgdetector/reorgdetector.go index df2eba95..9438eb6e 100644 --- a/reorgdetector/reorgdetector.go +++ b/reorgdetector/reorgdetector.go @@ -7,8 +7,6 @@ import ( "sync" "time" - "golang.org/x/sync/errgroup" - "github.com/0xPolygon/cdk/log" "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/common" @@ -16,6 +14,7 @@ import ( "github.com/ethereum/go-ethereum/rpc" "github.com/ledgerwatch/erigon-lib/kv" "github.com/ledgerwatch/erigon-lib/kv/mdbx" + "golang.org/x/sync/errgroup" ) type EthClient interface { @@ -28,8 +27,6 @@ type ReorgDetector struct { client EthClient db kv.RwDB - canonicalBlocks *headersList - trackedBlocksLock sync.RWMutex trackedBlocks map[string]*headersList @@ -50,12 +47,11 @@ func New(client EthClient, dbPath string) (*ReorgDetector, error) { } return &ReorgDetector{ - client: client, - db: db, - canonicalBlocks: newHeadersList(), - trackedBlocks: make(map[string]*headersList), - subscriptions: make(map[string]*Subscription), - notifiedReorgs: make(map[string]map[uint64]struct{}), + client: client, + db: db, + trackedBlocks: make(map[string]*headersList), + subscriptions: make(map[string]*Subscription), + notifiedReorgs: make(map[string]map[uint64]struct{}), }, nil } @@ -65,21 +61,6 @@ func (rd *ReorgDetector) Start(ctx context.Context) (err error) { return fmt.Errorf("failed to load tracked headers: %w", err) } - // Initially load a full canonical chain - if err = rd.loadCanonicalChain(ctx); err != nil { - log.Errorf("failed to load canonical chain: %v", err) - } - - // Continuously load canonical chain - go func() { - ticker := time.NewTicker(time.Second * 2) // TODO: Configure it - for range ticker.C { - if err = rd.loadCanonicalChain(ctx); err != nil { - log.Errorf("failed to load canonical chain: %v", err) - } - } - }() - // Continuously check reorgs in tracked by subscribers blocks go func() { ticker := time.NewTicker(time.Second) // TODO: Configure it @@ -133,6 +114,7 @@ func (rd *ReorgDetector) detectReorgInTrackedList(ctx context.Context) error { headers := hdrs.getSorted() for _, hdr := range headers { // Get the actual header from the network + // TODO: Cache it while iterating. currentHeader, err := rd.client.HeaderByNumber(ctx, big.NewInt(int64(hdr.Num))) if err != nil { return fmt.Errorf("failed to get the header: %w", err) @@ -141,10 +123,11 @@ func (rd *ReorgDetector) detectReorgInTrackedList(ctx context.Context) error { // Check if the block hash matches with the actual block hash if hdr.Hash == currentHeader.Hash() { // Delete block from the tracked blocks list if it is less than or equal to the last finalized block - // and hashes matches + // and hashes matches. If higher than finalized block, we assume a reorg still might happen. if hdr.Num <= lastFinalisedBlock.Number.Uint64() { hdrs.removeRange(hdr.Num, hdr.Num) } + continue } @@ -170,52 +153,6 @@ func (rd *ReorgDetector) detectReorgInTrackedList(ctx context.Context) error { return errGroup.Wait() } -// loadCanonicalChain loads canonical chain from the latest finalized block till the latest one -func (rd *ReorgDetector) loadCanonicalChain(ctx context.Context) error { - // Get the latest finalized block - lastFinalisedBlock, err := rd.client.HeaderByNumber(ctx, big.NewInt(int64(rpc.FinalizedBlockNumber))) - if err != nil { - return err - } - - // Get the latest block - latestBlock, err := rd.client.HeaderByNumber(ctx, nil) - if err != nil { - return err - } - - // Start from the last stored block if it less than the last finalized one - startFromBlock := lastFinalisedBlock.Number.Uint64() - if sortedBlocks := rd.canonicalBlocks.getSorted(); len(sortedBlocks) > 0 { - lastTrackedBlock := sortedBlocks[rd.canonicalBlocks.len()-1] - if lastTrackedBlock.Num < startFromBlock { - startFromBlock = lastTrackedBlock.Num - - startHeader, err := rd.client.HeaderByNumber(ctx, big.NewInt(int64(startFromBlock))) - if err != nil { - return fmt.Errorf("failed to fetch the start header %d: %w", startFromBlock, err) - } - - if startHeader.Hash() != lastTrackedBlock.Hash { - // Reorg happened, find the first reorg block to - } - } - } - - // Load the canonical chain from the last finalized block till the latest block - for i := startFromBlock; i <= latestBlock.Number.Uint64(); i++ { - blockHeader, err := rd.client.HeaderByNumber(ctx, big.NewInt(int64(i))) - if err != nil { - return fmt.Errorf("failed to fetch block header for block number %d: %w", i, err) - } - - // Add the block to the canonical chain - rd.canonicalBlocks.add(newHeader(blockHeader.Number.Uint64(), blockHeader.Hash())) - } - - return nil -} - // loadTrackedHeaders loads tracked headers from the DB and stores them in memory func (rd *ReorgDetector) loadTrackedHeaders(ctx context.Context) (err error) { rd.trackedBlocksLock.Lock() diff --git a/reorgdetector/reorgdetector_test.go b/reorgdetector/reorgdetector_test.go index eca7ebd9..e64b89b4 100644 --- a/reorgdetector/reorgdetector_test.go +++ b/reorgdetector/reorgdetector_test.go @@ -94,6 +94,7 @@ func Test_ReorgDetector(t *testing.T) { err = reorgDetector.AddBlockToTrack(ctx, subID, header.Number.Uint64(), header.Hash()) require.NoError(t, err) trackedBlocks[headerNumber] = header.Hash() + fmt.Println("added block", time.Now(), headerNumber) } } @@ -106,6 +107,8 @@ func Test_ReorgDetector(t *testing.T) { err = clientL1.Fork(reorgBlock.Hash()) require.NoError(t, err) + + fmt.Println("reorg happened", time.Now(), headerNumber) } } @@ -131,6 +134,8 @@ func Test_ReorgDetector(t *testing.T) { firstReorgedBlock := <-reorgSub.ReorgedBlock reorgSub.ReorgProcessed <- true + fmt.Println("firstReorgedBlock", firstReorgedBlock) + processed, ok := expectReorgOn[firstReorgedBlock] require.True(t, ok) require.False(t, processed) From 0c91e6f99b7dc8bae9f18b19b618ec4021abde7a Mon Sep 17 00:00:00 2001 From: begmaroman Date: Mon, 26 Aug 2024 13:35:23 +0100 Subject: [PATCH 28/43] Finalized reorg detector --- cmd/run.go | 16 ++--- reorgdetector/config.go | 19 +++++ reorgdetector/reorgdetector.go | 63 +++++++++++------ reorgdetector/reorgdetector_db.go | 26 ------- reorgdetector/reorgdetector_sub.go | 10 +-- reorgdetector/reorgdetector_test.go | 103 ++++++++-------------------- reorgdetector/types.go | 34 --------- reorgdetector/types_test.go | 79 +++++++++++---------- 8 files changed, 140 insertions(+), 210 deletions(-) diff --git a/cmd/run.go b/cmd/run.go index fcf79986..4b47c254 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -62,8 +62,8 @@ func start(cliCtx *cli.Context) error { components := cliCtx.StringSlice(config.FlagComponents) l1Client := runL1ClientIfNeeded(components, c.Etherman.URL) l2Client := runL2ClientIfNeeded(components, c.AggOracle.EVMSender.URLRPCL2) - reorgDetectorL1 := runReorgDetectorL1IfNeeded(cliCtx.Context, components, l1Client, c.ReorgDetectorL1.DBPath) - reorgDetectorL2 := runReorgDetectorL2IfNeeded(cliCtx.Context, components, l2Client, c.ReorgDetectorL2.DBPath) + reorgDetectorL1 := runReorgDetectorL1IfNeeded(cliCtx.Context, components, l1Client, &c.ReorgDetectorL1) + reorgDetectorL2 := runReorgDetectorL2IfNeeded(cliCtx.Context, components, l2Client, &c.ReorgDetectorL2) l1InfoTreeSync := runL1InfoTreeSyncerIfNeeded(cliCtx.Context, components, *c, l1Client, reorgDetectorL1) claimSponsor := runClaimSponsorIfNeeded(cliCtx.Context, components, l2Client, c.ClaimSponsor) l1BridgeSync := runBridgeSyncL1IfNeeded(cliCtx.Context, components, c.BridgeL1Sync, reorgDetectorL1, l1Client) @@ -408,10 +408,10 @@ func newState(c *config.Config, l2ChainID uint64, sqlDB *pgxpool.Pool) *state.St } func newReorgDetector( - dbPath string, + cfg *reorgdetector.Config, client *ethclient.Client, ) *reorgdetector.ReorgDetector { - rd, err := reorgdetector.New(client, dbPath) + rd, err := reorgdetector.New(client, *cfg) if err != nil { log.Fatal(err) } @@ -484,20 +484,20 @@ func runL2ClientIfNeeded(components []string, urlRPCL2 string) *ethclient.Client return l2CLient } -func runReorgDetectorL1IfNeeded(ctx context.Context, components []string, l1Client *ethclient.Client, dbPath string) *reorgdetector.ReorgDetector { +func runReorgDetectorL1IfNeeded(ctx context.Context, components []string, l1Client *ethclient.Client, cfg *reorgdetector.Config) *reorgdetector.ReorgDetector { if !isNeeded([]string{SEQUENCE_SENDER, AGGREGATOR, AGGORACLE, RPC}, components) { return nil } - rd := newReorgDetector(dbPath, l1Client) + rd := newReorgDetector(cfg, l1Client) go rd.Start(ctx) return rd } -func runReorgDetectorL2IfNeeded(ctx context.Context, components []string, l2Client *ethclient.Client, dbPath string) *reorgdetector.ReorgDetector { +func runReorgDetectorL2IfNeeded(ctx context.Context, components []string, l2Client *ethclient.Client, cfg *reorgdetector.Config) *reorgdetector.ReorgDetector { if !isNeeded([]string{AGGORACLE, RPC}, components) { return nil } - rd := newReorgDetector(dbPath, l2Client) + rd := newReorgDetector(cfg, l2Client) go rd.Start(ctx) return rd } diff --git a/reorgdetector/config.go b/reorgdetector/config.go index 54be77bd..44b5eedf 100644 --- a/reorgdetector/config.go +++ b/reorgdetector/config.go @@ -1,6 +1,25 @@ package reorgdetector +import "time" + +const ( + defaultCheckReorgsInterval = 2 * time.Second +) + // Config is the configuration for the reorg detector type Config struct { + // DBPath is the path to the database DBPath string `mapstructure:"DBPath"` + + // CheckReorgsInterval is the interval to check for reorgs in tracked blocks + CheckReorgsInterval time.Duration `mapstructure:"CheckReorgsInterval"` +} + +// GetCheckReorgsInterval returns the interval to check for reorgs in tracked blocks +func (c *Config) GetCheckReorgsInterval() time.Duration { + if c.CheckReorgsInterval == 0 { + return defaultCheckReorgsInterval + } + + return c.CheckReorgsInterval } diff --git a/reorgdetector/reorgdetector.go b/reorgdetector/reorgdetector.go index 9438eb6e..a01c09dc 100644 --- a/reorgdetector/reorgdetector.go +++ b/reorgdetector/reorgdetector.go @@ -24,8 +24,9 @@ type EthClient interface { } type ReorgDetector struct { - client EthClient - db kv.RwDB + client EthClient + db kv.RwDB + checkReorgInterval time.Duration trackedBlocksLock sync.RWMutex trackedBlocks map[string]*headersList @@ -37,9 +38,9 @@ type ReorgDetector struct { notifiedReorgs map[string]map[uint64]struct{} } -func New(client EthClient, dbPath string) (*ReorgDetector, error) { +func New(client EthClient, cfg Config) (*ReorgDetector, error) { db, err := mdbx.NewMDBX(nil). - Path(dbPath). + Path(cfg.DBPath). WithTableCfg(tableCfgFunc). Open() if err != nil { @@ -47,14 +48,16 @@ func New(client EthClient, dbPath string) (*ReorgDetector, error) { } return &ReorgDetector{ - client: client, - db: db, - trackedBlocks: make(map[string]*headersList), - subscriptions: make(map[string]*Subscription), - notifiedReorgs: make(map[string]map[uint64]struct{}), + client: client, + db: db, + checkReorgInterval: cfg.GetCheckReorgsInterval(), + trackedBlocks: make(map[string]*headersList), + subscriptions: make(map[string]*Subscription), + notifiedReorgs: make(map[string]map[uint64]struct{}), }, nil } +// Start starts the reorg detector func (rd *ReorgDetector) Start(ctx context.Context) (err error) { // Load tracked blocks from the DB if err = rd.loadTrackedHeaders(ctx); err != nil { @@ -63,10 +66,16 @@ func (rd *ReorgDetector) Start(ctx context.Context) (err error) { // Continuously check reorgs in tracked by subscribers blocks go func() { - ticker := time.NewTicker(time.Second) // TODO: Configure it - for range ticker.C { - if err = rd.detectReorgInTrackedList(ctx); err != nil { - log.Errorf("failed to detect reorg in tracked list: %v", err) + ticker := time.NewTicker(rd.checkReorgInterval) + for { + select { + case <-ctx.Done(): + ticker.Stop() + return + case <-ticker.C: + if err = rd.detectReorgInTrackedList(ctx); err != nil { + log.Errorf("failed to detect reorg in tracked list: %v", err) + } } } }() @@ -103,9 +112,15 @@ func (rd *ReorgDetector) detectReorgInTrackedList(ctx context.Context) error { return fmt.Errorf("failed to get the latest finalized block: %w", err) } - var errGroup errgroup.Group + var ( + headersCacheLock sync.Mutex + headersCache = map[uint64]*types.Header{ + lastFinalisedBlock.Number.Uint64(): lastFinalisedBlock, + } + errGroup errgroup.Group + ) - rd.trackedBlocksLock.RLock() + rd.trackedBlocksLock.Lock() for id, hdrs := range rd.trackedBlocks { id := id hdrs := hdrs @@ -113,12 +128,18 @@ func (rd *ReorgDetector) detectReorgInTrackedList(ctx context.Context) error { errGroup.Go(func() error { headers := hdrs.getSorted() for _, hdr := range headers { - // Get the actual header from the network - // TODO: Cache it while iterating. - currentHeader, err := rd.client.HeaderByNumber(ctx, big.NewInt(int64(hdr.Num))) - if err != nil { - return fmt.Errorf("failed to get the header: %w", err) + // Get the actual header from the network or from the cache + headersCacheLock.Lock() + if headersCache[hdr.Num] == nil { + h, err := rd.client.HeaderByNumber(ctx, big.NewInt(int64(hdr.Num))) + if err != nil { + headersCacheLock.Unlock() + return fmt.Errorf("failed to get the header: %w", err) + } + headersCache[hdr.Num] = h } + currentHeader := headersCache[hdr.Num] + headersCacheLock.Unlock() // Check if the block hash matches with the actual block hash if hdr.Hash == currentHeader.Hash() { @@ -148,7 +169,7 @@ func (rd *ReorgDetector) detectReorgInTrackedList(ctx context.Context) error { return nil }) } - rd.trackedBlocksLock.RUnlock() + rd.trackedBlocksLock.Unlock() return errGroup.Wait() } diff --git a/reorgdetector/reorgdetector_db.go b/reorgdetector/reorgdetector_db.go index 5c7cda2a..21224c82 100644 --- a/reorgdetector/reorgdetector_db.go +++ b/reorgdetector/reorgdetector_db.go @@ -78,32 +78,6 @@ func (rd *ReorgDetector) saveTrackedBlock(ctx context.Context, id string, b head return tx.Put(subscriberBlocks, []byte(id), raw) } -// removeTrackedBlocks removes the tracked blocks for a subscriber in db and in memory -func (rd *ReorgDetector) removeTrackedBlocks(ctx context.Context, lastFinalizedBlock uint64) error { - rd.subscriptionsLock.RLock() - defer rd.subscriptionsLock.RUnlock() - - for id := range rd.subscriptions { - rd.trackedBlocksLock.RLock() - newTrackedBlocks := rd.trackedBlocks[id].getFromBlockSorted(lastFinalizedBlock) - rd.trackedBlocksLock.RUnlock() - - if err := rd.updateTrackedBlocks(ctx, id, newHeadersList(newTrackedBlocks...)); err != nil { - return err - } - } - - return nil -} - -// updateTrackedBlocks updates the tracked blocks for a subscriber in db and in memory -func (rd *ReorgDetector) updateTrackedBlocks(ctx context.Context, id string, blocks *headersList) error { - rd.trackedBlocksLock.Lock() - defer rd.trackedBlocksLock.Unlock() - - return rd.updateTrackedBlocksNoLock(ctx, id, blocks) -} - // updateTrackedBlocksNoLock updates the tracked blocks for a subscriber in db and in memory func (rd *ReorgDetector) updateTrackedBlocksNoLock(ctx context.Context, id string, blocks *headersList) error { tx, err := rd.db.BeginRw(ctx) diff --git a/reorgdetector/reorgdetector_sub.go b/reorgdetector/reorgdetector_sub.go index f155a887..23f1f94b 100644 --- a/reorgdetector/reorgdetector_sub.go +++ b/reorgdetector/reorgdetector_sub.go @@ -1,16 +1,12 @@ package reorgdetector -import ( - "sync" -) - +// Subscription is a subscription to reorg events type Subscription struct { ReorgedBlock chan uint64 ReorgProcessed chan bool - - pendingReorgsToBeProcessed sync.WaitGroup } +// Subscribe subscribes to reorg events func (rd *ReorgDetector) Subscribe(id string) (*Subscription, error) { rd.subscriptionsLock.Lock() defer rd.subscriptionsLock.Unlock() @@ -52,10 +48,8 @@ func (rd *ReorgDetector) notifySubscriber(id string, startingBlock header) { // Notify subscriber about this particular reorg rd.subscriptionsLock.RLock() if sub, ok := rd.subscriptions[id]; ok { - sub.pendingReorgsToBeProcessed.Add(1) sub.ReorgedBlock <- startingBlock.Num <-sub.ReorgProcessed - sub.pendingReorgsToBeProcessed.Done() } rd.subscriptionsLock.RUnlock() diff --git a/reorgdetector/reorgdetector_test.go b/reorgdetector/reorgdetector_test.go index e64b89b4..1f784f50 100644 --- a/reorgdetector/reorgdetector_test.go +++ b/reorgdetector/reorgdetector_test.go @@ -47,10 +47,6 @@ func newTestDir(tb testing.TB) string { } func Test_ReorgDetector(t *testing.T) { - const produceBlocks = 29 - const reorgPeriod = 5 - const trackBlockPeriod = 4 - const reorgDepth = 2 const subID = "test" ctx := context.Background() @@ -66,7 +62,7 @@ func Test_ReorgDetector(t *testing.T) { // Create test DB dir testDir := newTestDir(t) - reorgDetector, err := New(clientL1.Client(), testDir) + reorgDetector, err := New(clientL1.Client(), Config{DBPath: testDir, CheckReorgsInterval: time.Millisecond * 100}) require.NoError(t, err) err = reorgDetector.Start(ctx) @@ -75,76 +71,37 @@ func Test_ReorgDetector(t *testing.T) { reorgSub, err := reorgDetector.Subscribe(subID) require.NoError(t, err) - canonicalChain := make(map[uint64]common.Hash) - trackedBlocks := make(map[uint64]common.Hash) - lastReorgOn := uint64(0) - for i := 1; lastReorgOn <= produceBlocks; i++ { - block := clientL1.Commit() - time.Sleep(time.Millisecond * 100) - - header, err := clientL1.Client().HeaderByHash(ctx, block) - require.NoError(t, err) - headerNumber := header.Number.Uint64() - - canonicalChain[headerNumber] = header.Hash() - - // Add block to track every "trackBlockPeriod" blocks - if headerNumber%trackBlockPeriod == 0 { - if _, ok := trackedBlocks[headerNumber]; !ok { - err = reorgDetector.AddBlockToTrack(ctx, subID, header.Number.Uint64(), header.Hash()) - require.NoError(t, err) - trackedBlocks[headerNumber] = header.Hash() - fmt.Println("added block", time.Now(), headerNumber) - } - } - - // Reorg every "reorgPeriod" blocks with "reorgDepth" blocks depth - if headerNumber > lastReorgOn && headerNumber%reorgPeriod == 0 { - lastReorgOn = headerNumber - - reorgBlock, err := clientL1.Client().BlockByNumber(ctx, big.NewInt(int64(headerNumber-reorgDepth))) - require.NoError(t, err) - - err = clientL1.Fork(reorgBlock.Hash()) - require.NoError(t, err) - - fmt.Println("reorg happened", time.Now(), headerNumber) - } - } - - // Commit some blocks to ensure reorgs are detected - for i := 0; i < reorgPeriod; i++ { - clientL1.Commit() - } - - // Expect reorgs on block - expectReorgOn := make(map[uint64]bool) - for num, hash := range canonicalChain { - if _, ok := trackedBlocks[num]; !ok { - continue - } - - if trackedBlocks[num] != hash { - expectReorgOn[num] = false - } - } + remainingHeader, err := clientL1.Client().HeaderByHash(ctx, clientL1.Commit()) // Block 2 + require.NoError(t, err) + err = reorgDetector.AddBlockToTrack(ctx, subID, remainingHeader.Number.Uint64(), remainingHeader.Hash()) // Adding block 2 + require.NoError(t, err) + reorgHeader, err := clientL1.Client().HeaderByHash(ctx, clientL1.Commit()) // Block 3 + require.NoError(t, err) + firstHeaderAfterReorg, err := clientL1.Client().HeaderByHash(ctx, clientL1.Commit()) // Block 4 + require.NoError(t, err) + err = reorgDetector.AddBlockToTrack(ctx, subID, firstHeaderAfterReorg.Number.Uint64(), firstHeaderAfterReorg.Hash()) // Adding block 4 + require.NoError(t, err) + header, err := clientL1.Client().HeaderByHash(ctx, clientL1.Commit()) // Block 5 + require.NoError(t, err) + err = reorgDetector.AddBlockToTrack(ctx, subID, header.Number.Uint64(), header.Hash()) // Adding block 5 + require.NoError(t, err) + err = clientL1.Fork(reorgHeader.Hash()) // Reorg on block 3 + require.NoError(t, err) + clientL1.Commit() // Next block 4 after reorg on block 3 + clientL1.Commit() // Block 5 + clientL1.Commit() // Block 6 - // Wait for reorg notifications, expect len(expectReorgOn) notifications - for range expectReorgOn { - firstReorgedBlock := <-reorgSub.ReorgedBlock + // Expect reorg on added blocks 4 -> all further blocks should be removed + select { + case firstReorgedBlock := <-reorgSub.ReorgedBlock: reorgSub.ReorgProcessed <- true - - fmt.Println("firstReorgedBlock", firstReorgedBlock) - - processed, ok := expectReorgOn[firstReorgedBlock] - require.True(t, ok) - require.False(t, processed) - - expectReorgOn[firstReorgedBlock] = true + require.Equal(t, firstHeaderAfterReorg.Number.Uint64(), firstReorgedBlock) + case <-time.After(5 * time.Second): + t.Fatal("timeout waiting for reorg") } - // Make sure all processed - for _, processed := range expectReorgOn { - require.True(t, processed) - } + headersList, ok := reorgDetector.trackedBlocks[subID] + require.True(t, ok) + require.Equal(t, 1, headersList.len()) // Only block 2 left + require.Equal(t, remainingHeader.Hash(), headersList.get(2).Hash) } diff --git a/reorgdetector/types.go b/reorgdetector/types.go index 8a656e47..bee3eb44 100644 --- a/reorgdetector/types.go +++ b/reorgdetector/types.go @@ -102,40 +102,6 @@ func (hl *headersList) getSorted() []header { return sortedBlocks } -// getFromBlockSorted returns blocks from blockNum in sorted order without including the blockNum -func (hl *headersList) getFromBlockSorted(blockNum uint64) []header { - sortedHeaders := hl.getSorted() - - index := -1 - for i, b := range sortedHeaders { - if b.Num > blockNum { - index = i - break - } - } - - if index == -1 { - return nil - } - - return sortedHeaders[index:] -} - -// getClosestHigherBlock returns the closest higher block to the given blockNum -func (hl *headersList) getClosestHigherBlock(blockNum uint64) (*header, bool) { - hdr := hl.get(blockNum) - if hdr != nil { - return hdr, true - } - - sorted := hl.getFromBlockSorted(blockNum) - if len(sorted) == 0 { - return nil, false - } - - return &sorted[0], true -} - // removeRange removes headers from "from" to "to" func (hl *headersList) removeRange(from, to uint64) { hl.Lock() diff --git a/reorgdetector/types_test.go b/reorgdetector/types_test.go index b4fad525..0f86c587 100644 --- a/reorgdetector/types_test.go +++ b/reorgdetector/types_test.go @@ -5,7 +5,6 @@ import ( "testing" "github.com/ethereum/go-ethereum/common" - "github.com/stretchr/testify/require" ) func TestBlockMap(t *testing.T) { @@ -18,62 +17,62 @@ func TestBlockMap(t *testing.T) { header{Num: 3, Hash: common.HexToHash("0x789")}, ) - t.Run("getSorted", func(t *testing.T) { + t.Run("len", func(t *testing.T) { t.Parallel() - sortedBlocks := bm.getSorted() - expectedSortedBlocks := []header{ - {Num: 1, Hash: common.HexToHash("0x123")}, - {Num: 2, Hash: common.HexToHash("0x456")}, - {Num: 3, Hash: common.HexToHash("0x789")}, - } - if !reflect.DeepEqual(sortedBlocks, expectedSortedBlocks) { - t.Errorf("getSorted() returned incorrect result, expected: %v, got: %v", expectedSortedBlocks, sortedBlocks) + actualLen := bm.len() + expectedLen := 3 + if !reflect.DeepEqual(expectedLen, actualLen) { + t.Errorf("len() returned incorrect result, expected: %v, got: %v", expectedLen, actualLen) } }) - t.Run("getFromBlockSorted", func(t *testing.T) { + t.Run("isEmpty", func(t *testing.T) { t.Parallel() - fromBlockSorted := bm.getFromBlockSorted(2) - expectedFromBlockSorted := []header{ - {Num: 3, Hash: common.HexToHash("0x789")}, - } - if !reflect.DeepEqual(fromBlockSorted, expectedFromBlockSorted) { - t.Errorf("getFromBlockSorted() returned incorrect result, expected: %v, got: %v", expectedFromBlockSorted, fromBlockSorted) + if bm.isEmpty() { + t.Error("isEmpty() returned incorrect result, expected: false, got: true") } + }) + + t.Run("add", func(t *testing.T) { + t.Parallel() - // Test getFromBlockSorted function when blockNum is greater than the last block - fromBlockSorted = bm.getFromBlockSorted(4) - expectedFromBlockSorted = []header{} - if !reflect.DeepEqual(fromBlockSorted, expectedFromBlockSorted) { - t.Errorf("getFromBlockSorted() returned incorrect result, expected: %v, got: %v", expectedFromBlockSorted, fromBlockSorted) + tba := header{Num: 4, Hash: common.HexToHash("0xabc")} + bm.add(tba) + if !reflect.DeepEqual(tba, bm.headers[4]) { + t.Errorf("add() returned incorrect result, expected: %v, got: %v", tba, bm.headers[4]) } }) - t.Run("getClosestHigherBlock", func(t *testing.T) { + t.Run("copy", func(t *testing.T) { t.Parallel() - bm := newHeadersList( - header{Num: 1, Hash: common.HexToHash("0x123")}, - header{Num: 2, Hash: common.HexToHash("0x456")}, - header{Num: 3, Hash: common.HexToHash("0x789")}, - ) + copiedBm := bm.copy() + if !reflect.DeepEqual(bm, copiedBm) { + t.Errorf("add() returned incorrect result, expected: %v, got: %v", bm, copiedBm) + } + }) + + t.Run("get", func(t *testing.T) { + t.Parallel() - // Test when the blockNum exists in the block map - b, exists := bm.getClosestHigherBlock(2) - require.True(t, exists) - expectedBlock := header{Num: 2, Hash: common.HexToHash("0x456")} - if *b != expectedBlock { - t.Errorf("getClosestHigherBlock() returned incorrect result, expected: %v, got: %v", expectedBlock, b) + if !reflect.DeepEqual(bm.get(3), bm.headers[3]) { + t.Errorf("get() returned incorrect result, expected: %v, got: %v", bm.get(3), bm.headers[3]) } + }) + + t.Run("getSorted", func(t *testing.T) { + t.Parallel() - // Test when the blockNum does not exist in the block map - b, exists = bm.getClosestHigherBlock(4) - require.False(t, exists) - expectedBlock = header{Num: 0, Hash: common.Hash{}} - if *b != expectedBlock { - t.Errorf("getClosestHigherBlock() returned incorrect result, expected: %v, got: %v", expectedBlock, b) + sortedBlocks := bm.getSorted() + expectedSortedBlocks := []header{ + {Num: 1, Hash: common.HexToHash("0x123")}, + {Num: 2, Hash: common.HexToHash("0x456")}, + {Num: 3, Hash: common.HexToHash("0x789")}, + } + if !reflect.DeepEqual(sortedBlocks, expectedSortedBlocks) { + t.Errorf("getSorted() returned incorrect result, expected: %v, got: %v", expectedSortedBlocks, sortedBlocks) } }) From 63857fda5c24edea58776ae0e2b837e0c65ca7ef Mon Sep 17 00:00:00 2001 From: begmaroman Date: Mon, 26 Aug 2024 13:36:36 +0100 Subject: [PATCH 29/43] Finalized reorg detector --- bridgesync/e2e_test.go | 2 +- l1bridge2infoindexsync/e2e_test.go | 2 +- l1infotreesync/e2e_test.go | 2 +- test/helpers/aggoracle_e2e.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bridgesync/e2e_test.go b/bridgesync/e2e_test.go index 865cbd11..db8be873 100644 --- a/bridgesync/e2e_test.go +++ b/bridgesync/e2e_test.go @@ -51,7 +51,7 @@ func TestBridgeEventE2E(t *testing.T) { auth, err := bind.NewKeyedTransactorWithChainID(privateKey, big.NewInt(1337)) require.NoError(t, err) client, bridgeAddr, bridgeSc := newSimulatedClient(t, auth) - rd, err := reorgdetector.New(client.Client(), dbPathReorg) + rd, err := reorgdetector.New(client.Client(), reorgdetector.Config{DBPath: dbPathReorg}) require.NoError(t, err) go rd.Start(ctx) diff --git a/l1bridge2infoindexsync/e2e_test.go b/l1bridge2infoindexsync/e2e_test.go index eb00363b..217560c7 100644 --- a/l1bridge2infoindexsync/e2e_test.go +++ b/l1bridge2infoindexsync/e2e_test.go @@ -132,7 +132,7 @@ func TestE2E(t *testing.T) { require.NotEqual(t, authDeployer.From, auth.From) client, gerAddr, bridgeAddr, gerSc, bridgeSc, err := newSimulatedClient(authDeployer, auth) require.NoError(t, err) - rd, err := reorgdetector.New(client.Client(), dbPathReorg) + rd, err := reorgdetector.New(client.Client(), reorgdetector.Config{DBPath: dbPathReorg}) go rd.Start(ctx) bridgeSync, err := bridgesync.NewL1(ctx, dbPathBridgeSync, bridgeAddr, 10, etherman.LatestBlock, rd, client.Client(), 0, time.Millisecond*10, 0, 0) diff --git a/l1infotreesync/e2e_test.go b/l1infotreesync/e2e_test.go index 065b599e..34a51ed5 100644 --- a/l1infotreesync/e2e_test.go +++ b/l1infotreesync/e2e_test.go @@ -181,7 +181,7 @@ func TestStressAndReorgs(t *testing.T) { require.NoError(t, err) client, gerAddr, verifyAddr, gerSc, verifySC, err := newSimulatedClient(auth) require.NoError(t, err) - rd, err := reorgdetector.New(client.Client(), dbPathReorg) + rd, err := reorgdetector.New(client.Client(), reorgdetector.Config{DBPath: dbPathReorg}) go rd.Start(ctx) syncer, err := l1infotreesync.New(ctx, dbPathSyncer, gerAddr, verifyAddr, 10, etherman.LatestBlock, rd, client.Client(), time.Millisecond, 0, 100*time.Millisecond, 3) require.NoError(t, err) diff --git a/test/helpers/aggoracle_e2e.go b/test/helpers/aggoracle_e2e.go index 9583ee7b..fdde39dd 100644 --- a/test/helpers/aggoracle_e2e.go +++ b/test/helpers/aggoracle_e2e.go @@ -101,7 +101,7 @@ func CommonSetup(t *testing.T) ( require.NoError(t, err) // Reorg detector dbPathReorgDetector := t.TempDir() - reorg, err := reorgdetector.New(l1Client.Client(), dbPathReorgDetector) + reorg, err := reorgdetector.New(l1Client.Client(), reorgdetector.Config{DBPath: dbPathReorgDetector}) require.NoError(t, err) // Syncer dbPathSyncer := t.TempDir() From 9e781f4149f4e317b28aee2c18f3b604c57fb37e Mon Sep 17 00:00:00 2001 From: begmaroman Date: Mon, 26 Aug 2024 14:24:54 +0100 Subject: [PATCH 30/43] Fixed unit tests --- reorgdetector/types_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reorgdetector/types_test.go b/reorgdetector/types_test.go index 0f86c587..9e5e990e 100644 --- a/reorgdetector/types_test.go +++ b/reorgdetector/types_test.go @@ -57,7 +57,7 @@ func TestBlockMap(t *testing.T) { t.Run("get", func(t *testing.T) { t.Parallel() - if !reflect.DeepEqual(bm.get(3), bm.headers[3]) { + if !reflect.DeepEqual(*bm.get(3), bm.headers[3]) { t.Errorf("get() returned incorrect result, expected: %v, got: %v", bm.get(3), bm.headers[3]) } }) From 22376842e214b1b00ea0ab6bc7ededfc87ad93a0 Mon Sep 17 00:00:00 2001 From: begmaroman Date: Mon, 26 Aug 2024 14:38:22 +0100 Subject: [PATCH 31/43] Fixed unit tests --- reorgdetector/reorgdetector_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/reorgdetector/reorgdetector_test.go b/reorgdetector/reorgdetector_test.go index 1f784f50..2022ac3b 100644 --- a/reorgdetector/reorgdetector_test.go +++ b/reorgdetector/reorgdetector_test.go @@ -100,6 +100,9 @@ func Test_ReorgDetector(t *testing.T) { t.Fatal("timeout waiting for reorg") } + // just wait a little for completion + time.Sleep(time.Second / 5) + headersList, ok := reorgDetector.trackedBlocks[subID] require.True(t, ok) require.Equal(t, 1, headersList.len()) // Only block 2 left From 6bdba60711e585ad8e1b2417b3136d0f98249677 Mon Sep 17 00:00:00 2001 From: begmaroman Date: Mon, 26 Aug 2024 14:40:45 +0100 Subject: [PATCH 32/43] Fixed unit tests --- reorgdetector/reorgdetector_test.go | 2 ++ reorgdetector/types_test.go | 1 + 2 files changed, 3 insertions(+) diff --git a/reorgdetector/reorgdetector_test.go b/reorgdetector/reorgdetector_test.go index 2022ac3b..3c1d0fae 100644 --- a/reorgdetector/reorgdetector_test.go +++ b/reorgdetector/reorgdetector_test.go @@ -103,7 +103,9 @@ func Test_ReorgDetector(t *testing.T) { // just wait a little for completion time.Sleep(time.Second / 5) + reorgDetector.trackedBlocksLock.Lock() headersList, ok := reorgDetector.trackedBlocks[subID] + reorgDetector.trackedBlocksLock.Unlock() require.True(t, ok) require.Equal(t, 1, headersList.len()) // Only block 2 left require.Equal(t, remainingHeader.Hash(), headersList.get(2).Hash) diff --git a/reorgdetector/types_test.go b/reorgdetector/types_test.go index 9e5e990e..9e20e363 100644 --- a/reorgdetector/types_test.go +++ b/reorgdetector/types_test.go @@ -38,6 +38,7 @@ func TestBlockMap(t *testing.T) { t.Run("add", func(t *testing.T) { t.Parallel() + bm := bm.copy() tba := header{Num: 4, Hash: common.HexToHash("0xabc")} bm.add(tba) if !reflect.DeepEqual(tba, bm.headers[4]) { From c982e79026962005e1d8a4cb7bfb3851ef4d5b38 Mon Sep 17 00:00:00 2001 From: begmaroman Date: Mon, 26 Aug 2024 15:08:26 +0100 Subject: [PATCH 33/43] Fixed unit tests --- l1infotreesync/e2e_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/l1infotreesync/e2e_test.go b/l1infotreesync/e2e_test.go index 34a51ed5..e38d2a85 100644 --- a/l1infotreesync/e2e_test.go +++ b/l1infotreesync/e2e_test.go @@ -181,7 +181,7 @@ func TestStressAndReorgs(t *testing.T) { require.NoError(t, err) client, gerAddr, verifyAddr, gerSc, verifySC, err := newSimulatedClient(auth) require.NoError(t, err) - rd, err := reorgdetector.New(client.Client(), reorgdetector.Config{DBPath: dbPathReorg}) + rd, err := reorgdetector.New(client.Client(), reorgdetector.Config{DBPath: dbPathReorg, CheckReorgsInterval: time.Millisecond * 100}) go rd.Start(ctx) syncer, err := l1infotreesync.New(ctx, dbPathSyncer, gerAddr, verifyAddr, 10, etherman.LatestBlock, rd, client.Client(), time.Millisecond, 0, 100*time.Millisecond, 3) require.NoError(t, err) From effc6aa8e733273096101c3b1f7b7edced6807b2 Mon Sep 17 00:00:00 2001 From: begmaroman Date: Mon, 26 Aug 2024 15:17:13 +0100 Subject: [PATCH 34/43] Fixed unit tests --- reorgdetector/reorgdetector.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/reorgdetector/reorgdetector.go b/reorgdetector/reorgdetector.go index a01c09dc..d7dc4508 100644 --- a/reorgdetector/reorgdetector.go +++ b/reorgdetector/reorgdetector.go @@ -121,6 +121,8 @@ func (rd *ReorgDetector) detectReorgInTrackedList(ctx context.Context) error { ) rd.trackedBlocksLock.Lock() + defer rd.trackedBlocksLock.Unlock() + for id, hdrs := range rd.trackedBlocks { id := id hdrs := hdrs @@ -169,7 +171,6 @@ func (rd *ReorgDetector) detectReorgInTrackedList(ctx context.Context) error { return nil }) } - rd.trackedBlocksLock.Unlock() return errGroup.Wait() } From 5cb68f06ff7f0a828f00ee41b23d31ba57e634dd Mon Sep 17 00:00:00 2001 From: begmaroman Date: Mon, 26 Aug 2024 15:27:08 +0100 Subject: [PATCH 35/43] Fixed unit tests --- reorgdetector/reorgdetector.go | 2 +- reorgdetector/reorgdetector_db.go | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/reorgdetector/reorgdetector.go b/reorgdetector/reorgdetector.go index d7dc4508..32bc6339 100644 --- a/reorgdetector/reorgdetector.go +++ b/reorgdetector/reorgdetector.go @@ -164,7 +164,7 @@ func (rd *ReorgDetector) detectReorgInTrackedList(ctx context.Context) error { } // Update the tracked blocks in the DB - if err := rd.updateTrackedBlocksNoLock(ctx, id, hdrs); err != nil { + if err := rd.updateTrackedBlocksDB(ctx, id, hdrs); err != nil { return fmt.Errorf("failed to update tracked blocks for subscriber %s: %w", id, err) } diff --git a/reorgdetector/reorgdetector_db.go b/reorgdetector/reorgdetector_db.go index 21224c82..3174cbc0 100644 --- a/reorgdetector/reorgdetector_db.go +++ b/reorgdetector/reorgdetector_db.go @@ -78,8 +78,8 @@ func (rd *ReorgDetector) saveTrackedBlock(ctx context.Context, id string, b head return tx.Put(subscriberBlocks, []byte(id), raw) } -// updateTrackedBlocksNoLock updates the tracked blocks for a subscriber in db and in memory -func (rd *ReorgDetector) updateTrackedBlocksNoLock(ctx context.Context, id string, blocks *headersList) error { +// updateTrackedBlocksDB updates the tracked blocks for a subscriber in db +func (rd *ReorgDetector) updateTrackedBlocksDB(ctx context.Context, id string, blocks *headersList) error { tx, err := rd.db.BeginRw(ctx) if err != nil { return err @@ -96,7 +96,5 @@ func (rd *ReorgDetector) updateTrackedBlocksNoLock(ctx context.Context, id strin return err } - rd.trackedBlocks[id] = blocks - return nil } From ca3b8f300e53cda11567dfb6115a6ddb97732079 Mon Sep 17 00:00:00 2001 From: begmaroman Date: Mon, 26 Aug 2024 15:42:08 +0100 Subject: [PATCH 36/43] Added debug log --- l1infotreesync/e2e_test.go | 3 ++- reorgdetector/reorgdetector.go | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/l1infotreesync/e2e_test.go b/l1infotreesync/e2e_test.go index e38d2a85..e59b4ffd 100644 --- a/l1infotreesync/e2e_test.go +++ b/l1infotreesync/e2e_test.go @@ -182,7 +182,8 @@ func TestStressAndReorgs(t *testing.T) { client, gerAddr, verifyAddr, gerSc, verifySC, err := newSimulatedClient(auth) require.NoError(t, err) rd, err := reorgdetector.New(client.Client(), reorgdetector.Config{DBPath: dbPathReorg, CheckReorgsInterval: time.Millisecond * 100}) - go rd.Start(ctx) + err = rd.Start(ctx) + require.NoError(t, err) syncer, err := l1infotreesync.New(ctx, dbPathSyncer, gerAddr, verifyAddr, 10, etherman.LatestBlock, rd, client.Client(), time.Millisecond, 0, 100*time.Millisecond, 3) require.NoError(t, err) go syncer.Start(ctx) diff --git a/reorgdetector/reorgdetector.go b/reorgdetector/reorgdetector.go index 32bc6339..9684f140 100644 --- a/reorgdetector/reorgdetector.go +++ b/reorgdetector/reorgdetector.go @@ -85,6 +85,8 @@ func (rd *ReorgDetector) Start(ctx context.Context) (err error) { // AddBlockToTrack adds a block to the tracked list for a subscriber func (rd *ReorgDetector) AddBlockToTrack(ctx context.Context, id string, num uint64, hash common.Hash) error { + fmt.Println("trackedBlocks", id, rd.trackedBlocks[id]) + fmt.Println("subscriptions", id, rd.subscriptions[id]) // Skip if the given block has already been stored rd.trackedBlocksLock.RLock() existingHeader := rd.trackedBlocks[id].get(num) @@ -187,7 +189,11 @@ func (rd *ReorgDetector) loadTrackedHeaders(ctx context.Context) (err error) { // Go over tracked blocks and create subscription for each tracker for id := range rd.trackedBlocks { - _, _ = rd.Subscribe(id) + rd.subscriptions[id] = &Subscription{ + ReorgedBlock: make(chan uint64), + ReorgProcessed: make(chan bool), + } + rd.notifiedReorgs[id] = make(map[uint64]struct{}) } return nil From 569affb1d199197551fb728adc87849a802e7752 Mon Sep 17 00:00:00 2001 From: begmaroman Date: Mon, 26 Aug 2024 15:50:06 +0100 Subject: [PATCH 37/43] Removed debug logs --- reorgdetector/reorgdetector.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/reorgdetector/reorgdetector.go b/reorgdetector/reorgdetector.go index 9684f140..6b0b3682 100644 --- a/reorgdetector/reorgdetector.go +++ b/reorgdetector/reorgdetector.go @@ -85,8 +85,6 @@ func (rd *ReorgDetector) Start(ctx context.Context) (err error) { // AddBlockToTrack adds a block to the tracked list for a subscriber func (rd *ReorgDetector) AddBlockToTrack(ctx context.Context, id string, num uint64, hash common.Hash) error { - fmt.Println("trackedBlocks", id, rd.trackedBlocks[id]) - fmt.Println("subscriptions", id, rd.subscriptions[id]) // Skip if the given block has already been stored rd.trackedBlocksLock.RLock() existingHeader := rd.trackedBlocks[id].get(num) From e864d36830938ae31ae726e056a26635d0193bd5 Mon Sep 17 00:00:00 2001 From: begmaroman Date: Mon, 26 Aug 2024 16:04:47 +0100 Subject: [PATCH 38/43] Fixed e2e tests --- l1bridge2infoindexsync/e2e_test.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/l1bridge2infoindexsync/e2e_test.go b/l1bridge2infoindexsync/e2e_test.go index 217560c7..71fbbfaa 100644 --- a/l1bridge2infoindexsync/e2e_test.go +++ b/l1bridge2infoindexsync/e2e_test.go @@ -132,8 +132,10 @@ func TestE2E(t *testing.T) { require.NotEqual(t, authDeployer.From, auth.From) client, gerAddr, bridgeAddr, gerSc, bridgeSc, err := newSimulatedClient(authDeployer, auth) require.NoError(t, err) - rd, err := reorgdetector.New(client.Client(), reorgdetector.Config{DBPath: dbPathReorg}) - go rd.Start(ctx) + rd, err := reorgdetector.New(client.Client(), reorgdetector.Config{DBPath: dbPathReorg, CheckReorgsInterval: time.Millisecond * 100}) + require.NoError(t, err) + err = rd.Start(ctx) + require.NoError(t, err) bridgeSync, err := bridgesync.NewL1(ctx, dbPathBridgeSync, bridgeAddr, 10, etherman.LatestBlock, rd, client.Client(), 0, time.Millisecond*10, 0, 0) require.NoError(t, err) From e57badb1b58edf0fef00a515d19c214e94e67fc6 Mon Sep 17 00:00:00 2001 From: begmaroman Date: Mon, 26 Aug 2024 16:35:59 +0100 Subject: [PATCH 39/43] Fixed e2e tests --- l1bridge2infoindexsync/e2e_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/l1bridge2infoindexsync/e2e_test.go b/l1bridge2infoindexsync/e2e_test.go index 71fbbfaa..47757539 100644 --- a/l1bridge2infoindexsync/e2e_test.go +++ b/l1bridge2infoindexsync/e2e_test.go @@ -132,7 +132,7 @@ func TestE2E(t *testing.T) { require.NotEqual(t, authDeployer.From, auth.From) client, gerAddr, bridgeAddr, gerSc, bridgeSc, err := newSimulatedClient(authDeployer, auth) require.NoError(t, err) - rd, err := reorgdetector.New(client.Client(), reorgdetector.Config{DBPath: dbPathReorg, CheckReorgsInterval: time.Millisecond * 100}) + rd, err := reorgdetector.New(client.Client(), reorgdetector.Config{DBPath: dbPathReorg, CheckReorgsInterval: time.Second}) require.NoError(t, err) err = rd.Start(ctx) require.NoError(t, err) From cd3d3ef725d4b83d7a35ac0040c6c5be0ca2225e Mon Sep 17 00:00:00 2001 From: begmaroman Date: Mon, 26 Aug 2024 16:37:40 +0100 Subject: [PATCH 40/43] Fixed e2e tests --- l1bridge2infoindexsync/e2e_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/l1bridge2infoindexsync/e2e_test.go b/l1bridge2infoindexsync/e2e_test.go index 47757539..f2447efa 100644 --- a/l1bridge2infoindexsync/e2e_test.go +++ b/l1bridge2infoindexsync/e2e_test.go @@ -206,7 +206,7 @@ func TestE2E(t *testing.T) { syncerUpToDate = true break } - time.Sleep(time.Millisecond * 10) + time.Sleep(time.Millisecond * 100) errMsg = fmt.Sprintf("last block from client: %d, last block from syncer: %d", lb.NumberU64(), lpb) } require.True(t, syncerUpToDate, errMsg) From 3b9a15a8e6b38e208f9a15f5a09e4d354a8e49d1 Mon Sep 17 00:00:00 2001 From: begmaroman Date: Mon, 26 Aug 2024 17:44:24 +0100 Subject: [PATCH 41/43] Address comments --- l1bridge2infoindexsync/e2e_test.go | 3 ++- l1infotreesync/e2e_test.go | 3 ++- reorgdetector/config.go | 12 ++++++++---- reorgdetector/reorgdetector.go | 8 ++++++-- reorgdetector/reorgdetector_test.go | 23 ++++------------------- 5 files changed, 22 insertions(+), 27 deletions(-) diff --git a/l1bridge2infoindexsync/e2e_test.go b/l1bridge2infoindexsync/e2e_test.go index f2447efa..9da6f0bd 100644 --- a/l1bridge2infoindexsync/e2e_test.go +++ b/l1bridge2infoindexsync/e2e_test.go @@ -12,6 +12,7 @@ import ( "github.com/0xPolygon/cdk-contracts-tooling/contracts/elderberry-paris/polygonzkevmbridgev2" "github.com/0xPolygon/cdk-contracts-tooling/contracts/elderberry-paris/polygonzkevmglobalexitrootv2" "github.com/0xPolygon/cdk/bridgesync" + cdktypes "github.com/0xPolygon/cdk/config/types" "github.com/0xPolygon/cdk/etherman" "github.com/0xPolygon/cdk/l1bridge2infoindexsync" "github.com/0xPolygon/cdk/l1infotreesync" @@ -132,7 +133,7 @@ func TestE2E(t *testing.T) { require.NotEqual(t, authDeployer.From, auth.From) client, gerAddr, bridgeAddr, gerSc, bridgeSc, err := newSimulatedClient(authDeployer, auth) require.NoError(t, err) - rd, err := reorgdetector.New(client.Client(), reorgdetector.Config{DBPath: dbPathReorg, CheckReorgsInterval: time.Second}) + rd, err := reorgdetector.New(client.Client(), reorgdetector.Config{DBPath: dbPathReorg, CheckReorgsInterval: cdktypes.NewDuration(time.Second)}) require.NoError(t, err) err = rd.Start(ctx) require.NoError(t, err) diff --git a/l1infotreesync/e2e_test.go b/l1infotreesync/e2e_test.go index e59b4ffd..e86d2136 100644 --- a/l1infotreesync/e2e_test.go +++ b/l1infotreesync/e2e_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/0xPolygon/cdk-contracts-tooling/contracts/banana-paris/polygonzkevmglobalexitrootv2" + cdktypes "github.com/0xPolygon/cdk/config/types" "github.com/0xPolygon/cdk/etherman" "github.com/0xPolygon/cdk/l1infotreesync" "github.com/0xPolygon/cdk/reorgdetector" @@ -181,7 +182,7 @@ func TestStressAndReorgs(t *testing.T) { require.NoError(t, err) client, gerAddr, verifyAddr, gerSc, verifySC, err := newSimulatedClient(auth) require.NoError(t, err) - rd, err := reorgdetector.New(client.Client(), reorgdetector.Config{DBPath: dbPathReorg, CheckReorgsInterval: time.Millisecond * 100}) + rd, err := reorgdetector.New(client.Client(), reorgdetector.Config{DBPath: dbPathReorg, CheckReorgsInterval: cdktypes.NewDuration(time.Millisecond * 100)}) err = rd.Start(ctx) require.NoError(t, err) syncer, err := l1infotreesync.New(ctx, dbPathSyncer, gerAddr, verifyAddr, 10, etherman.LatestBlock, rd, client.Client(), time.Millisecond, 0, 100*time.Millisecond, 3) diff --git a/reorgdetector/config.go b/reorgdetector/config.go index 44b5eedf..c9a90415 100644 --- a/reorgdetector/config.go +++ b/reorgdetector/config.go @@ -1,6 +1,10 @@ package reorgdetector -import "time" +import ( + "time" + + "github.com/0xPolygon/cdk/config/types" +) const ( defaultCheckReorgsInterval = 2 * time.Second @@ -12,14 +16,14 @@ type Config struct { DBPath string `mapstructure:"DBPath"` // CheckReorgsInterval is the interval to check for reorgs in tracked blocks - CheckReorgsInterval time.Duration `mapstructure:"CheckReorgsInterval"` + CheckReorgsInterval types.Duration `mapstructure:"CheckReorgsInterval"` } // GetCheckReorgsInterval returns the interval to check for reorgs in tracked blocks func (c *Config) GetCheckReorgsInterval() time.Duration { - if c.CheckReorgsInterval == 0 { + if c.CheckReorgsInterval.Duration == 0 { return defaultCheckReorgsInterval } - return c.CheckReorgsInterval + return c.CheckReorgsInterval.Duration } diff --git a/reorgdetector/reorgdetector.go b/reorgdetector/reorgdetector.go index 6b0b3682..a2e53391 100644 --- a/reorgdetector/reorgdetector.go +++ b/reorgdetector/reorgdetector.go @@ -87,10 +87,14 @@ func (rd *ReorgDetector) Start(ctx context.Context) (err error) { func (rd *ReorgDetector) AddBlockToTrack(ctx context.Context, id string, num uint64, hash common.Hash) error { // Skip if the given block has already been stored rd.trackedBlocksLock.RLock() - existingHeader := rd.trackedBlocks[id].get(num) + trackedBlocks, ok := rd.trackedBlocks[id] + if !ok { + rd.trackedBlocksLock.RUnlock() + return fmt.Errorf("subscriber %s is not subscribed", id) + } rd.trackedBlocksLock.RUnlock() - if existingHeader != nil && existingHeader.Hash == hash { + if existingHeader := trackedBlocks.get(num); existingHeader != nil && existingHeader.Hash == hash { return nil } diff --git a/reorgdetector/reorgdetector_test.go b/reorgdetector/reorgdetector_test.go index 3c1d0fae..7adec4ca 100644 --- a/reorgdetector/reorgdetector_test.go +++ b/reorgdetector/reorgdetector_test.go @@ -2,12 +2,11 @@ package reorgdetector import ( "context" - "fmt" - "math/big" - "os" + big "math/big" "testing" "time" + cdktypes "github.com/0xPolygon/cdk/config/types" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" @@ -32,20 +31,6 @@ func newSimulatedL1(t *testing.T, auth *bind.TransactOpts) *simulated.Backend { return client } -func newTestDir(tb testing.TB) string { - tb.Helper() - - dir := fmt.Sprintf("/tmp/reorgdetector-temp_%v", time.Now().UTC().Format(time.RFC3339Nano)) - err := os.Mkdir(dir, 0775) - require.NoError(tb, err) - - tb.Cleanup(func() { - require.NoError(tb, os.RemoveAll(dir)) - }) - - return dir -} - func Test_ReorgDetector(t *testing.T) { const subID = "test" @@ -60,9 +45,9 @@ func Test_ReorgDetector(t *testing.T) { require.NoError(t, err) // Create test DB dir - testDir := newTestDir(t) + testDir := t.TempDir() - reorgDetector, err := New(clientL1.Client(), Config{DBPath: testDir, CheckReorgsInterval: time.Millisecond * 100}) + reorgDetector, err := New(clientL1.Client(), Config{DBPath: testDir, CheckReorgsInterval: cdktypes.NewDuration(time.Millisecond * 100)}) require.NoError(t, err) err = reorgDetector.Start(ctx) From d0bc399f706f43799c2d4028687fff9e2dc9d7ae Mon Sep 17 00:00:00 2001 From: begmaroman Date: Tue, 27 Aug 2024 08:28:35 +0100 Subject: [PATCH 42/43] Address comments --- l1bridge2infoindexsync/e2e_test.go | 3 +-- l1infotreesync/e2e_test.go | 2 +- reorgdetector/reorgdetector.go | 5 ----- reorgdetector/reorgdetector_sub.go | 18 ------------------ 4 files changed, 2 insertions(+), 26 deletions(-) diff --git a/l1bridge2infoindexsync/e2e_test.go b/l1bridge2infoindexsync/e2e_test.go index 9da6f0bd..c2a5e982 100644 --- a/l1bridge2infoindexsync/e2e_test.go +++ b/l1bridge2infoindexsync/e2e_test.go @@ -135,8 +135,7 @@ func TestE2E(t *testing.T) { require.NoError(t, err) rd, err := reorgdetector.New(client.Client(), reorgdetector.Config{DBPath: dbPathReorg, CheckReorgsInterval: cdktypes.NewDuration(time.Second)}) require.NoError(t, err) - err = rd.Start(ctx) - require.NoError(t, err) + require.NoError(t, rd.Start(ctx)) bridgeSync, err := bridgesync.NewL1(ctx, dbPathBridgeSync, bridgeAddr, 10, etherman.LatestBlock, rd, client.Client(), 0, time.Millisecond*10, 0, 0) require.NoError(t, err) diff --git a/l1infotreesync/e2e_test.go b/l1infotreesync/e2e_test.go index e86d2136..562f0e39 100644 --- a/l1infotreesync/e2e_test.go +++ b/l1infotreesync/e2e_test.go @@ -183,8 +183,8 @@ func TestStressAndReorgs(t *testing.T) { client, gerAddr, verifyAddr, gerSc, verifySC, err := newSimulatedClient(auth) require.NoError(t, err) rd, err := reorgdetector.New(client.Client(), reorgdetector.Config{DBPath: dbPathReorg, CheckReorgsInterval: cdktypes.NewDuration(time.Millisecond * 100)}) - err = rd.Start(ctx) require.NoError(t, err) + require.NoError(t, rd.Start(ctx)) syncer, err := l1infotreesync.New(ctx, dbPathSyncer, gerAddr, verifyAddr, 10, etherman.LatestBlock, rd, client.Client(), time.Millisecond, 0, 100*time.Millisecond, 3) require.NoError(t, err) go syncer.Start(ctx) diff --git a/reorgdetector/reorgdetector.go b/reorgdetector/reorgdetector.go index a2e53391..94a025fb 100644 --- a/reorgdetector/reorgdetector.go +++ b/reorgdetector/reorgdetector.go @@ -33,9 +33,6 @@ type ReorgDetector struct { subscriptionsLock sync.RWMutex subscriptions map[string]*Subscription - - notifiedReorgsLock sync.RWMutex - notifiedReorgs map[string]map[uint64]struct{} } func New(client EthClient, cfg Config) (*ReorgDetector, error) { @@ -53,7 +50,6 @@ func New(client EthClient, cfg Config) (*ReorgDetector, error) { checkReorgInterval: cfg.GetCheckReorgsInterval(), trackedBlocks: make(map[string]*headersList), subscriptions: make(map[string]*Subscription), - notifiedReorgs: make(map[string]map[uint64]struct{}), }, nil } @@ -195,7 +191,6 @@ func (rd *ReorgDetector) loadTrackedHeaders(ctx context.Context) (err error) { ReorgedBlock: make(chan uint64), ReorgProcessed: make(chan bool), } - rd.notifiedReorgs[id] = make(map[uint64]struct{}) } return nil diff --git a/reorgdetector/reorgdetector_sub.go b/reorgdetector/reorgdetector_sub.go index 23f1f94b..675a81c5 100644 --- a/reorgdetector/reorgdetector_sub.go +++ b/reorgdetector/reorgdetector_sub.go @@ -27,24 +27,11 @@ func (rd *ReorgDetector) Subscribe(id string) (*Subscription, error) { rd.trackedBlocks[id] = newHeadersList() rd.trackedBlocksLock.Unlock() - // Create a new notified reorgs list for the subscriber - rd.notifiedReorgsLock.Lock() - rd.notifiedReorgs[id] = make(map[uint64]struct{}) - rd.notifiedReorgsLock.Unlock() - return sub, nil } // notifySubscriber notifies the subscriber with the block of the reorg func (rd *ReorgDetector) notifySubscriber(id string, startingBlock header) { - // Check if the given reorg was already notified to the given subscriber - rd.notifiedReorgsLock.RLock() - if _, ok := rd.notifiedReorgs[id][startingBlock.Num]; ok { - rd.notifiedReorgsLock.RUnlock() - return - } - rd.notifiedReorgsLock.RUnlock() - // Notify subscriber about this particular reorg rd.subscriptionsLock.RLock() if sub, ok := rd.subscriptions[id]; ok { @@ -52,9 +39,4 @@ func (rd *ReorgDetector) notifySubscriber(id string, startingBlock header) { <-sub.ReorgProcessed } rd.subscriptionsLock.RUnlock() - - // Mark the reorg as notified - rd.notifiedReorgsLock.Lock() - rd.notifiedReorgs[id][startingBlock.Num] = struct{}{} - rd.notifiedReorgsLock.Unlock() } From 0b672f96ee83680af5d28eefbc01f70012a9bd30 Mon Sep 17 00:00:00 2001 From: begmaroman Date: Tue, 27 Aug 2024 09:59:15 +0100 Subject: [PATCH 43/43] Address comments --- reorgdetector/reorgdetector.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/reorgdetector/reorgdetector.go b/reorgdetector/reorgdetector.go index 94a025fb..22c4693e 100644 --- a/reorgdetector/reorgdetector.go +++ b/reorgdetector/reorgdetector.go @@ -132,15 +132,14 @@ func (rd *ReorgDetector) detectReorgInTrackedList(ctx context.Context) error { for _, hdr := range headers { // Get the actual header from the network or from the cache headersCacheLock.Lock() - if headersCache[hdr.Num] == nil { - h, err := rd.client.HeaderByNumber(ctx, big.NewInt(int64(hdr.Num))) - if err != nil { + currentHeader, ok := headersCache[hdr.Num] + if !ok || currentHeader == nil { + if currentHeader, err = rd.client.HeaderByNumber(ctx, big.NewInt(int64(hdr.Num))); err != nil { headersCacheLock.Unlock() return fmt.Errorf("failed to get the header: %w", err) } - headersCache[hdr.Num] = h + headersCache[hdr.Num] = currentHeader } - currentHeader := headersCache[hdr.Num] headersCacheLock.Unlock() // Check if the block hash matches with the actual block hash