diff --git a/go.mod b/go.mod index 4cddfeb..45dd531 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/hashicorp/golang-lru v1.0.2 github.com/hyperledger/firefly-common v1.5.6-0.20250630201730-e234335c0381 github.com/hyperledger/firefly-signer v1.1.21 - github.com/hyperledger/firefly-transaction-manager v1.4.0 + github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250910153533-14142cf9f697 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.0 github.com/stretchr/testify v1.9.0 diff --git a/go.sum b/go.sum index 3788ce0..58cd774 100644 --- a/go.sum +++ b/go.sum @@ -106,6 +106,14 @@ github.com/hyperledger/firefly-signer v1.1.21 h1:r7cTOw6e/6AtiXLf84wZy6Z7zppzlc1 github.com/hyperledger/firefly-signer v1.1.21/go.mod h1:axrlSQeKrd124UdHF5L3MkTjb5DeTcbJxJNCZ3JmcWM= github.com/hyperledger/firefly-transaction-manager v1.4.0 h1:l9DCizLTohKtKec5dewNlydhAeko1/DmTfCRF8le9m0= github.com/hyperledger/firefly-transaction-manager v1.4.0/go.mod h1:mEd9dOH8ds6ajgfPh6nnP3Pd3f8XIZtQRnucqAIJHRs= +github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250910122026-4f65f76eb9eb h1:doSlc4SN1LIA+kMMW/vBDHaud+5Ad5+eWRitbfGBxho= +github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250910122026-4f65f76eb9eb/go.mod h1:KHGvK/QqD2jpRZ04lQoX/k1T2o644NCkRlr3FbvKqnA= +github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250910151057-7bc7bb81591c h1:RNd7cMvH8Mr/wE2Y2B4Vy0+5l0FN4G6UMC4rMqJeFjE= +github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250910151057-7bc7bb81591c/go.mod h1:KHGvK/QqD2jpRZ04lQoX/k1T2o644NCkRlr3FbvKqnA= +github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250910153251-07127ff35b09 h1:1Frz0u69ETPkFbFGcLxPyWJrzUnBv+yVKNdZUfT7wBE= +github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250910153251-07127ff35b09/go.mod h1:KHGvK/QqD2jpRZ04lQoX/k1T2o644NCkRlr3FbvKqnA= +github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250910153533-14142cf9f697 h1:leUNAoiwMidZYwH+F6bmCa7kvN3qcPGH25k2HcMptyg= +github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250910153533-14142cf9f697/go.mod h1:KHGvK/QqD2jpRZ04lQoX/k1T2o644NCkRlr3FbvKqnA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= diff --git a/internal/ethereum/blocklistener.go b/internal/ethereum/blocklistener.go index 6cd4675..a3ac358 100644 --- a/internal/ethereum/blocklistener.go +++ b/internal/ethereum/blocklistener.go @@ -56,20 +56,17 @@ type blockListener struct { initialBlockHeightObtained chan struct{} newHeadsTap chan struct{} newHeadsSub rpcbackend.Subscription - highestBlock int64 - mux sync.Mutex + highestBlockSet bool + highestBlock uint64 + mux sync.RWMutex consumers map[fftypes.UUID]*blockUpdateConsumer blockPollingInterval time.Duration - unstableHeadLength int - canonicalChain *list.List hederaCompatibilityMode bool blockCache *lru.Cache -} -type minimalBlockInfo struct { - number int64 - hash string - parentHash string + // canonical chain + unstableHeadLength int + canonicalChain *list.List } func newBlockListener(ctx context.Context, c *ethConnector, conf config.Section, wsConf *wsclient.WSConfig) (bl *blockListener, err error) { @@ -81,7 +78,8 @@ func newBlockListener(ctx context.Context, c *ethConnector, conf config.Section, startDone: make(chan struct{}), initialBlockHeightObtained: make(chan struct{}), newHeadsTap: make(chan struct{}), - highestBlock: -1, + highestBlockSet: false, + highestBlock: 0, consumers: make(map[fftypes.UUID]*blockUpdateConsumer), blockPollingInterval: conf.GetDuration(BlockPollingInterval), canonicalChain: list.New(), @@ -165,10 +163,7 @@ func (bl *blockListener) establishBlockHeightWithRetry() error { return true, rpcErr.Error() } - bl.mux.Lock() - bl.highestBlock = hexBlockHeight.BigInt().Int64() - bl.mux.Unlock() - + bl.setHighestBlock(hexBlockHeight.BigInt().Uint64()) return false, nil }) } @@ -260,7 +255,7 @@ func (bl *blockListener) listenLoop() { default: candidate := bl.reconcileCanonicalChain(bi) // Check this is the lowest position to notify from - if candidate != nil && (notifyPos == nil || candidate.Value.(*minimalBlockInfo).number <= notifyPos.Value.(*minimalBlockInfo).number) { + if candidate != nil && (notifyPos == nil || candidate.Value.(*ffcapi.MinimalBlockInfo).BlockNumber <= notifyPos.Value.(*ffcapi.MinimalBlockInfo).BlockNumber) { notifyPos = candidate } } @@ -268,7 +263,7 @@ func (bl *blockListener) listenLoop() { if notifyPos != nil { // We notify for all hashes from the point of change in the chain onwards for notifyPos != nil { - update.BlockHashes = append(update.BlockHashes, notifyPos.Value.(*minimalBlockInfo).hash) + update.BlockHashes = append(update.BlockHashes, notifyPos.Value.(*ffcapi.MinimalBlockInfo).BlockHash) notifyPos = notifyPos.Next() } @@ -295,16 +290,12 @@ func (bl *blockListener) listenLoop() { // head of the canonical chain we have. If these blocks do not just fit onto the end of the chain, then we // work backwards building a new view and notify about all blocks that are changed in that process. func (bl *blockListener) reconcileCanonicalChain(bi *blockInfoJSONRPC) *list.Element { - mbi := &minimalBlockInfo{ - number: bi.Number.BigInt().Int64(), - hash: bi.Hash.String(), - parentHash: bi.ParentHash.String(), + mbi := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(bi.Number.BigInt().Uint64()), + BlockHash: bi.Hash.String(), + ParentHash: bi.ParentHash.String(), } - bl.mux.Lock() - if mbi.number > bl.highestBlock { - bl.highestBlock = mbi.number - } - bl.mux.Unlock() + bl.checkAndSetHighestBlock(mbi.BlockNumber.Uint64()) // Find the position of this block in the block sequence pos := bl.canonicalChain.Back() @@ -313,15 +304,15 @@ func (bl *blockListener) reconcileCanonicalChain(bi *blockInfoJSONRPC) *list.Ele // We've eliminated all the existing chain (if there was any) return bl.handleNewBlock(mbi, nil) } - posBlock := pos.Value.(*minimalBlockInfo) + posBlock := pos.Value.(*ffcapi.MinimalBlockInfo) switch { - case posBlock.number == mbi.number && posBlock.hash == mbi.hash && posBlock.parentHash == mbi.parentHash: + case posBlock.Equal(mbi): // This is a duplicate - no need to notify of anything return nil - case posBlock.number == mbi.number: + case posBlock.BlockNumber.Uint64() == mbi.BlockNumber.Uint64(): // We are replacing a block in the chain return bl.handleNewBlock(mbi, pos.Prev()) - case posBlock.number < mbi.number: + case posBlock.BlockNumber.Uint64() < mbi.BlockNumber.Uint64(): // We have a position where this block goes return bl.handleNewBlock(mbi, pos) default: @@ -333,14 +324,14 @@ func (bl *blockListener) reconcileCanonicalChain(bi *blockInfoJSONRPC) *list.Ele // handleNewBlock rebuilds the canonical chain around a new block, checking if we need to rebuild our // view of the canonical chain behind it, or trimming anything after it that is invalidated by a new fork. -func (bl *blockListener) handleNewBlock(mbi *minimalBlockInfo, addAfter *list.Element) *list.Element { +func (bl *blockListener) handleNewBlock(mbi *ffcapi.MinimalBlockInfo, addAfter *list.Element) *list.Element { // If we have an existing canonical chain before this point, then we need to check we've not // invalidated that with this block. If we have, then we have to re-verify our whole canonical // chain from the first block. Then notify from the earliest point where it has diverged. if addAfter != nil { - prevBlock := addAfter.Value.(*minimalBlockInfo) - if prevBlock.number != (mbi.number-1) || prevBlock.hash != mbi.parentHash { - log.L(bl.ctx).Infof("Notified of block %d / %s that does not fit after block %d / %s (expected parent: %s)", mbi.number, mbi.hash, prevBlock.number, prevBlock.hash, mbi.parentHash) + prevBlock := addAfter.Value.(*ffcapi.MinimalBlockInfo) + if prevBlock.BlockNumber.Uint64() != (mbi.BlockNumber.Uint64()-1) || prevBlock.BlockHash != mbi.ParentHash { + log.L(bl.ctx).Infof("Notified of block %d / %s that does not fit after block %d / %s (expected parent: %s)", mbi.BlockNumber.Uint64(), mbi.BlockHash, prevBlock.BlockNumber.Uint64(), prevBlock.BlockHash, mbi.ParentHash) return bl.rebuildCanonicalChain() } } @@ -368,7 +359,7 @@ func (bl *blockListener) handleNewBlock(mbi *minimalBlockInfo, addAfter *list.El _ = bl.canonicalChain.Remove(bl.canonicalChain.Front()) } - log.L(bl.ctx).Debugf("Added block %d / %s parent=%s to in-memory canonical chain (new length=%d)", mbi.number, mbi.hash, mbi.parentHash, bl.canonicalChain.Len()) + log.L(bl.ctx).Debugf("Added block %d / %s parent=%s to in-memory canonical chain (new length=%d)", mbi.BlockNumber.Uint64(), mbi.BlockHash, mbi.ParentHash, bl.canonicalChain.Len()) return newElem } @@ -379,18 +370,18 @@ func (bl *blockListener) handleNewBlock(mbi *minimalBlockInfo, addAfter *list.El func (bl *blockListener) rebuildCanonicalChain() *list.Element { // If none of our blocks were valid, start from the first block number we've notified about previously lastValidBlock := bl.trimToLastValidBlock() - var nextBlockNumber int64 + var nextBlockNumber uint64 var expectedParentHash string if lastValidBlock != nil { - nextBlockNumber = lastValidBlock.number + 1 + nextBlockNumber = lastValidBlock.BlockNumber.Uint64() + 1 log.L(bl.ctx).Infof("Canonical chain partially rebuilding from block %d", nextBlockNumber) - expectedParentHash = lastValidBlock.hash + expectedParentHash = lastValidBlock.BlockHash } else { firstBlock := bl.canonicalChain.Front() if firstBlock == nil || firstBlock.Value == nil { return nil } - nextBlockNumber = firstBlock.Value.(*minimalBlockInfo).number + nextBlockNumber = firstBlock.Value.(*ffcapi.MinimalBlockInfo).BlockNumber.Uint64() log.L(bl.ctx).Warnf("Canonical chain re-initialized at block %d", nextBlockNumber) // Clear out the whole chain bl.canonicalChain = bl.canonicalChain.Init() @@ -412,19 +403,19 @@ func (bl *blockListener) rebuildCanonicalChain() *list.Element { log.L(bl.ctx).Infof("Canonical chain rebuilt the chain to the head block %d", nextBlockNumber-1) break } - mbi := &minimalBlockInfo{ - number: bi.Number.BigInt().Int64(), - hash: bi.Hash.String(), - parentHash: bi.ParentHash.String(), + mbi := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(bi.Number.BigInt().Uint64()), + BlockHash: bi.Hash.String(), + ParentHash: bi.ParentHash.String(), } // It's possible the chain will change while we're doing this, and we fall back to the next block notification // to sort that out. - if expectedParentHash != "" && mbi.parentHash != expectedParentHash { - log.L(bl.ctx).Infof("Canonical chain rebuilding stopped at block: %d due to mismatch hash for parent block (%d): %s (expected: %s)", nextBlockNumber, nextBlockNumber-1, mbi.parentHash, expectedParentHash) + if expectedParentHash != "" && mbi.ParentHash != expectedParentHash { + log.L(bl.ctx).Infof("Canonical chain rebuilding stopped at block: %d due to mismatch hash for parent block (%d): %s (expected: %s)", nextBlockNumber, nextBlockNumber-1, mbi.ParentHash, expectedParentHash) break } - expectedParentHash = mbi.hash + expectedParentHash = mbi.BlockHash nextBlockNumber++ // Note we do not trim to a length here, as we need to notify for every block we haven't notified for. @@ -434,33 +425,30 @@ func (bl *blockListener) rebuildCanonicalChain() *list.Element { notifyPos = newElem } - bl.mux.Lock() - if mbi.number > bl.highestBlock { - bl.highestBlock = mbi.number - } - bl.mux.Unlock() + bl.checkAndSetHighestBlock(mbi.BlockNumber.Uint64()) } return notifyPos } -func (bl *blockListener) trimToLastValidBlock() (lastValidBlock *minimalBlockInfo) { +func (bl *blockListener) trimToLastValidBlock() (lastValidBlock *ffcapi.MinimalBlockInfo) { // First remove from the end until we get a block that matches the current un-cached query view from the chain lastElem := bl.canonicalChain.Back() - var startingNumber *int64 + var startingNumber *uint64 for lastElem != nil && lastElem.Value != nil { // Query the block that is no at this blockNumber - currentViewBlock := lastElem.Value.(*minimalBlockInfo) + currentViewBlock := lastElem.Value.(*ffcapi.MinimalBlockInfo) if startingNumber == nil { - startingNumber = ¤tViewBlock.number + currentNumber := currentViewBlock.BlockNumber.Uint64() + startingNumber = ¤tNumber log.L(bl.ctx).Debugf("Canonical chain checking from last block: %d", startingNumber) } var freshBlockInfo *blockInfoJSONRPC var reason ffcapi.ErrorReason err := bl.c.retry.Do(bl.ctx, "rebuild listener canonical chain", func(_ int) (retry bool, err error) { - log.L(bl.ctx).Debugf("Canonical chain validating block: %d", currentViewBlock.number) - freshBlockInfo, reason, err = bl.getBlockInfoByNumber(bl.ctx, currentViewBlock.number, false, "") + log.L(bl.ctx).Debugf("Canonical chain validating block: %d", currentViewBlock.BlockNumber.Uint64()) + freshBlockInfo, reason, err = bl.getBlockInfoByNumber(bl.ctx, currentViewBlock.BlockNumber.Uint64(), false, "") return reason != ffcapi.ErrorReasonNotFound, err }) if err != nil { @@ -469,8 +457,8 @@ func (bl *blockListener) trimToLastValidBlock() (lastValidBlock *minimalBlockInf } } - if freshBlockInfo != nil && freshBlockInfo.Hash.String() == currentViewBlock.hash { - log.L(bl.ctx).Debugf("Canonical chain found last valid block %d", currentViewBlock.number) + if freshBlockInfo != nil && freshBlockInfo.Hash.String() == currentViewBlock.BlockHash { + log.L(bl.ctx).Debugf("Canonical chain found last valid block %d", currentViewBlock.BlockNumber.Uint64()) lastValidBlock = currentViewBlock // Trim everything after this point, as it's invalidated nextElem := lastElem.Next() @@ -484,8 +472,8 @@ func (bl *blockListener) trimToLastValidBlock() (lastValidBlock *minimalBlockInf lastElem = lastElem.Prev() } - if startingNumber != nil && lastValidBlock != nil && *startingNumber != lastValidBlock.number { - log.L(bl.ctx).Debugf("Canonical chain trimmed from block %d to block %d (total number of in memory blocks: %d)", startingNumber, lastValidBlock.number, bl.unstableHeadLength) + if startingNumber != nil && lastValidBlock != nil && *startingNumber != lastValidBlock.BlockNumber.Uint64() { + log.L(bl.ctx).Debugf("Canonical chain trimmed from block %d to block %d (total number of in memory blocks: %d)", startingNumber, lastValidBlock.BlockNumber.Uint64(), bl.unstableHeadLength) } return lastValidBlock } @@ -522,29 +510,45 @@ func (bl *blockListener) addConsumer(ctx context.Context, c *blockUpdateConsumer bl.consumers[*c.id] = c } -func (bl *blockListener) getHighestBlock(ctx context.Context) (int64, bool) { +func (bl *blockListener) getHighestBlock(ctx context.Context) (uint64, bool) { bl.checkAndStartListenerLoop() // block height will be established as the first step of listener startup process // so we don't need to wait for the entire startup process to finish to return the result bl.mux.Lock() - highestBlock := bl.highestBlock + highestBlockSet := bl.highestBlockSet bl.mux.Unlock() // if not yet initialized, wait to be initialized - if highestBlock < 0 { + if !highestBlockSet { select { case <-bl.initialBlockHeightObtained: case <-ctx.Done(): // Inform caller we timed out, or were closed - return -1, false + return 0, false } } bl.mux.Lock() - highestBlock = bl.highestBlock + highestBlock := bl.highestBlock bl.mux.Unlock() log.L(ctx).Debugf("ChainHead=%d", highestBlock) return highestBlock, true } +func (bl *blockListener) setHighestBlock(block uint64) { + bl.mux.Lock() + defer bl.mux.Unlock() + bl.highestBlock = block + bl.highestBlockSet = true +} + +func (bl *blockListener) checkAndSetHighestBlock(block uint64) { + bl.mux.Lock() + defer bl.mux.Unlock() + if block > bl.highestBlock { + bl.highestBlock = block + bl.highestBlockSet = true + } +} + func (bl *blockListener) waitClosed() { bl.mux.Lock() listenLoopDone := bl.listenLoopDone diff --git a/internal/ethereum/blocklistener_blockquery.go b/internal/ethereum/blocklistener_blockquery.go index 3ab9c0b..5b5d97f 100644 --- a/internal/ethereum/blocklistener_blockquery.go +++ b/internal/ethereum/blocklistener_blockquery.go @@ -53,10 +53,46 @@ func (bl *blockListener) addToBlockCache(blockInfo *blockInfoJSONRPC) { bl.blockCache.Add(blockInfo.Number.BigInt().String(), blockInfo) } -func (bl *blockListener) getBlockInfoByNumber(ctx context.Context, blockNumber int64, allowCache bool, expectedHashStr string) (*blockInfoJSONRPC, ffcapi.ErrorReason, error) { +func (bl *blockListener) getBlockInfoContainsTxHash(ctx context.Context, txHash string) (*ffcapi.MinimalBlockInfo, error) { + + // Query the chain to find the transaction block + // Note: should consider have an in-memory map of transaction hash to block for faster lookup + // The extra memory usage of the map should be outweighed by the speed improvement of lookup + // But I saw we have a ffcapi.MinimalBlockInfo struct that intentionally removes the tx hashes + // so need to figure out the reason first + + // TODO: add a cache if map cannot be used + res, reason, receiptErr := bl.c.TransactionReceipt(ctx, &ffcapi.TransactionReceiptRequest{ + TransactionHash: txHash, + }) + if receiptErr != nil && reason != ffcapi.ErrorReasonNotFound { + return nil, i18n.WrapError(ctx, receiptErr, msgs.MsgFailedToQueryReceipt, txHash) + } + if res == nil { + return nil, nil + } + txBlockHash := res.BlockHash + txBlockNumber := res.BlockNumber.Uint64() + // get the parent hash of the transaction block + bi, reason, err := bl.getBlockInfoByNumber(ctx, txBlockNumber, true, txBlockHash) + if err != nil && reason != ffcapi.ErrorReasonNotFound { // if the block info is not found, then there could be a fork, twe don't throw error in this case and treating it as block not found + return nil, i18n.WrapError(ctx, err, msgs.MsgFailedToQueryBlockInfo, txHash) + } + if bi == nil { + return nil, nil + } + + return &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(bi.Number.BigInt().Uint64()), + BlockHash: bi.Hash.String(), + ParentHash: bi.ParentHash.String(), + }, nil +} + +func (bl *blockListener) getBlockInfoByNumber(ctx context.Context, blockNumber uint64, allowCache bool, expectedHashStr string) (*blockInfoJSONRPC, ffcapi.ErrorReason, error) { var blockInfo *blockInfoJSONRPC if allowCache { - cached, ok := bl.blockCache.Get(strconv.FormatInt(blockNumber, 10)) + cached, ok := bl.blockCache.Get(strconv.FormatUint(blockNumber, 10)) if ok { blockInfo = cached.(*blockInfoJSONRPC) if expectedHashStr != "" && blockInfo.ParentHash.String() != expectedHashStr { @@ -67,16 +103,12 @@ func (bl *blockListener) getBlockInfoByNumber(ctx context.Context, blockNumber i } if blockInfo == nil { - rpcErr := bl.backend.CallRPC(ctx, &blockInfo, "eth_getBlockByNumber", ethtypes.NewHexInteger64(blockNumber), false /* only the txn hashes */) + rpcErr := bl.backend.CallRPC(ctx, &blockInfo, "eth_getBlockByNumber", ethtypes.NewHexIntegerU64(blockNumber), false /* only the txn hashes */) if rpcErr != nil { - if mapError(blockRPCMethods, rpcErr.Error()) == ffcapi.ErrorReasonNotFound { - log.L(ctx).Debugf("Received error signifying 'block not found': '%s'", rpcErr.Message) - return nil, ffcapi.ErrorReasonNotFound, i18n.NewError(ctx, msgs.MsgBlockNotAvailable) - } return nil, ffcapi.ErrorReason(""), rpcErr.Error() } if blockInfo == nil { - return nil, ffcapi.ErrorReason(""), nil + return nil, ffcapi.ErrorReasonNotFound, i18n.NewError(ctx, msgs.MsgBlockNotAvailable) } bl.addToBlockCache(blockInfo) } diff --git a/internal/ethereum/blocklistener_test.go b/internal/ethereum/blocklistener_test.go index 53588d9..57cff70 100644 --- a/internal/ethereum/blocklistener_test.go +++ b/internal/ethereum/blocklistener_test.go @@ -54,7 +54,7 @@ func TestBlockListenerStartGettingHighestBlockRetry(t *testing.T) { mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getFilterChanges", mock.Anything).Return(nil).Maybe() h, ok := bl.getHighestBlock(bl.ctx) - assert.Equal(t, int64(12345), h) + assert.Equal(t, uint64(12345), h) assert.True(t, ok) done() // Stop immediately in this case, while we're in the polling interval @@ -80,7 +80,7 @@ func TestBlockListenerStartGettingHighestBlockFailBeforeStop(t *testing.T) { h, ok := bl.getHighestBlock(bl.ctx) assert.False(t, ok) - assert.Equal(t, int64(-1), h) + assert.Equal(t, uint64(0), h) <-bl.listenLoopDone @@ -175,7 +175,7 @@ func TestBlockListenerOKSequential(t *testing.T) { done() <-bl.listenLoopDone - assert.Equal(t, int64(1003), bl.highestBlock) + assert.Equal(t, uint64(1003), bl.highestBlock) mRPC.AssertExpectations(t) @@ -385,7 +385,7 @@ func TestBlockListenerOKDuplicates(t *testing.T) { <-bl.listenLoopDone - assert.Equal(t, int64(1003), bl.highestBlock) + assert.Equal(t, uint64(1003), bl.highestBlock) mRPC.AssertExpectations(t) @@ -481,7 +481,7 @@ func TestBlockListenerReorgKeepLatestHeadInSameBatch(t *testing.T) { done() <-bl.listenLoopDone - assert.Equal(t, int64(1003), bl.highestBlock) + assert.Equal(t, uint64(1003), bl.highestBlock) mRPC.AssertExpectations(t) } @@ -599,7 +599,7 @@ func TestBlockListenerReorgKeepLatestHeadInSameBatchValidHashFirst(t *testing.T) done() <-bl.listenLoopDone - assert.Equal(t, int64(1003), bl.highestBlock) + assert.Equal(t, uint64(1003), bl.highestBlock) mRPC.AssertExpectations(t) } @@ -693,7 +693,7 @@ func TestBlockListenerReorgKeepLatestMiddleInSameBatch(t *testing.T) { done() <-bl.listenLoopDone - assert.Equal(t, int64(1003), bl.highestBlock) + assert.Equal(t, uint64(1003), bl.highestBlock) mRPC.AssertExpectations(t) } @@ -787,7 +787,7 @@ func TestBlockListenerReorgKeepLatestTailInSameBatch(t *testing.T) { done() <-bl.listenLoopDone - assert.Equal(t, int64(1003), bl.highestBlock) + assert.Equal(t, uint64(1003), bl.highestBlock) mRPC.AssertExpectations(t) } @@ -899,7 +899,7 @@ func TestBlockListenerReorgReplaceTail(t *testing.T) { done() <-bl.listenLoopDone - assert.Equal(t, int64(1003), bl.highestBlock) + assert.Equal(t, uint64(1003), bl.highestBlock) mRPC.AssertExpectations(t) @@ -1050,7 +1050,7 @@ func TestBlockListenerGap(t *testing.T) { done() <-bl.listenLoopDone - assert.Equal(t, int64(1005), bl.highestBlock) + assert.Equal(t, uint64(1005), bl.highestBlock) mRPC.AssertExpectations(t) @@ -1159,7 +1159,7 @@ func TestBlockListenerReorgWhileRebuilding(t *testing.T) { done() <-bl.listenLoopDone - assert.Equal(t, int64(1003), bl.highestBlock) + assert.Equal(t, uint64(1003), bl.highestBlock) mRPC.AssertExpectations(t) @@ -1274,7 +1274,7 @@ func TestBlockListenerReorgReplaceWholeCanonicalChain(t *testing.T) { done() <-bl.listenLoopDone - assert.Equal(t, int64(1003), bl.highestBlock) + assert.Equal(t, uint64(1003), bl.highestBlock) mRPC.AssertExpectations(t) @@ -1687,10 +1687,10 @@ func TestBlockListenerRebuildCanonicalFailTerminate(t *testing.T) { _, c, mRPC, done := newTestConnectorWithNoBlockerFilterDefaultMocks(t) bl := c.blockListener - bl.canonicalChain.PushBack(&minimalBlockInfo{ - number: 1000, - hash: ethtypes.MustNewHexBytes0xPrefix(fftypes.NewRandB32().String()).String(), - parentHash: ethtypes.MustNewHexBytes0xPrefix(fftypes.NewRandB32().String()).String(), + bl.canonicalChain.PushBack(&ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(1000), + BlockHash: ethtypes.MustNewHexBytes0xPrefix(fftypes.NewRandB32().String()).String(), + ParentHash: ethtypes.MustNewHexBytes0xPrefix(fftypes.NewRandB32().String()).String(), }) mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByNumber", mock.Anything, false). diff --git a/internal/ethereum/confirmation_reconciler.go b/internal/ethereum/confirmation_reconciler.go new file mode 100644 index 0000000..39865f0 --- /dev/null +++ b/internal/ethereum/confirmation_reconciler.go @@ -0,0 +1,328 @@ +// Copyright © 2025 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ethereum + +import ( + "container/list" + "context" + + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" +) + +func (bl *blockListener) reconcileConfirmationsForTransaction(ctx context.Context, txHash string, confirmMap *ffcapi.ConfirmationMap, targetConfirmationCount uint64) (*ffcapi.ConfirmationMapUpdateResult, error) { + // Initialize the output context + reconcileResult := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: confirmMap, + HasNewFork: false, + Rebuilt: false, + HasNewConfirmation: false, + Confirmed: false, + TargetConfirmationCount: targetConfirmationCount, + } + + txBlockInfo, err := bl.getBlockInfoContainsTxHash(ctx, txHash) + if err != nil { + log.L(ctx).Errorf("Failed to fetch block info using tx hash %s: %v", txHash, err) + return nil, err + } + + if txBlockInfo == nil { + log.L(ctx).Debugf("Transaction %s not found in any block", txHash) + return reconcileResult, nil + } + + // Compare the existing confirmation queue with the in-memory linked list + bl.compareAndUpdateConfirmationQueue(ctx, reconcileResult, txBlockInfo, targetConfirmationCount) + + return reconcileResult, nil +} + +// NOTE: this function only build up the confirmation queue uses the in-memory canonical chain +// it does not build up the canonical chain +// compareAndUpdateConfirmationQueue compares the existing confirmation queue with the in-memory linked list +// this function obtains the read lock on the canonical chain, so it should not make any long-running queries + +func (bl *blockListener) compareAndUpdateConfirmationQueue(ctx context.Context, reconcileResult *ffcapi.ConfirmationMapUpdateResult, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) { + bl.mux.RLock() + defer bl.mux.RUnlock() + txBlockNumber := txBlockInfo.BlockNumber.Uint64() + txBlockHash := txBlockInfo.BlockHash + + chainHead := bl.canonicalChain.Front().Value.(*ffcapi.MinimalBlockInfo) + chainTail := bl.canonicalChain.Back().Value.(*ffcapi.MinimalBlockInfo) + if chainTail == nil || chainTail.BlockNumber.Uint64() < txBlockNumber { + log.L(ctx).Debugf("Canonical chain is waiting for the transaction block %d to be indexed", txBlockNumber) + return + } + + // Initialize confirmation map and get existing queue + existingQueue := bl.initializeConfirmationMap(reconcileResult, txBlockInfo) + + if targetConfirmationCount == 0 { + // if the target confirmation count is 0, we should just return the transaction block + reconcileResult.Confirmed = true + // Only return the transaction block for zero confirmation + reconcileResult.ConfirmationMap.ConfirmationQueueMap[txBlockHash] = []*ffcapi.MinimalBlockInfo{txBlockInfo} + // For zero confirmation, determine if we processed new confirmations + if reconcileResult.Rebuilt || reconcileResult.HasNewFork { + // If a fork was detected, we processed new confirmations + reconcileResult.HasNewConfirmation = true + } + return + } + + // Validate and process existing confirmations + newQueue, currentBlock := bl.processExistingConfirmations(ctx, reconcileResult, txBlockInfo, existingQueue, chainHead, targetConfirmationCount) + + if currentBlock == nil { + // the tx block is not in the canonical chain + // we should just return the existing block confirmations and wait for future call to correct it + return + } + + // Build new confirmations from canonical chain only if not already confirmed + if !reconcileResult.Confirmed { + newQueue = bl.buildNewConfirmations(reconcileResult, newQueue, currentBlock, txBlockNumber, targetConfirmationCount) + } + reconcileResult.ConfirmationMap.ConfirmationQueueMap[txBlockHash] = newQueue + + if reconcileResult.CanonicalBlockHash != txBlockHash { + reconcileResult.CanonicalBlockHash = txBlockHash + } +} + +func (bl *blockListener) initializeConfirmationMap(reconcileResult *ffcapi.ConfirmationMapUpdateResult, txBlockInfo *ffcapi.MinimalBlockInfo) []*ffcapi.MinimalBlockInfo { + txBlockHash := txBlockInfo.BlockHash + + if reconcileResult.ConfirmationMap == nil || len(reconcileResult.ConfirmationMap.ConfirmationQueueMap) == 0 { + reconcileResult.ConfirmationMap = &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + txBlockHash: {txBlockInfo}, + }, + CanonicalBlockHash: txBlockHash, + } + reconcileResult.HasNewConfirmation = true + return nil + } + + existingQueue := reconcileResult.ConfirmationMap.ConfirmationQueueMap[txBlockHash] + if len(existingQueue) > 0 { + existingTxBlock := existingQueue[0] + if !existingTxBlock.Equal(txBlockInfo) { + // the tx block in the existing queue does not match the new tx block we queried from the chain + // rebuild a new confirmation queue with the new tx block + reconcileResult.HasNewFork = true + reconcileResult.Rebuilt = true + reconcileResult.ConfirmationMap.ConfirmationQueueMap[txBlockHash] = []*ffcapi.MinimalBlockInfo{txBlockInfo} + return nil + } + } + + return existingQueue +} + +func (bl *blockListener) processExistingConfirmations(ctx context.Context, reconcileResult *ffcapi.ConfirmationMapUpdateResult, txBlockInfo *ffcapi.MinimalBlockInfo, existingQueue []*ffcapi.MinimalBlockInfo, chainHead *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) ([]*ffcapi.MinimalBlockInfo, *list.Element) { + txBlockNumber := txBlockInfo.BlockNumber.Uint64() + + newQueue := []*ffcapi.MinimalBlockInfo{txBlockInfo} + + currentBlock := bl.canonicalChain.Front() + // iterate to the tx block if the chain head is earlier than the tx block + for currentBlock != nil && currentBlock.Value.(*ffcapi.MinimalBlockInfo).BlockNumber.Uint64() <= txBlockNumber { + if currentBlock.Value.(*ffcapi.MinimalBlockInfo).BlockNumber.Uint64() == txBlockNumber { + // the tx block is already in the canonical chain + // we need to check if the tx block is the same as the chain head + if !currentBlock.Value.(*ffcapi.MinimalBlockInfo).Equal(txBlockInfo) { + // the tx block information is different from the same block number in the canonical chain + // the tx confirmation block is not on the same fork as the canonical chain + // we should just return the existing block confirmations and wait for future call to correct it + return newQueue, nil + } + } + currentBlock = currentBlock.Next() + } + + if len(existingQueue) <= 1 { + return newQueue, currentBlock + } + + existingConfirmations := existingQueue[1:] + return bl.validateExistingConfirmations( + ctx, reconcileResult, newQueue, existingConfirmations, currentBlock, chainHead, txBlockInfo, targetConfirmationCount, + ) +} + +func (bl *blockListener) validateExistingConfirmations(ctx context.Context, reconcileResult *ffcapi.ConfirmationMapUpdateResult, newQueue []*ffcapi.MinimalBlockInfo, existingConfirmations []*ffcapi.MinimalBlockInfo, currentBlock *list.Element, chainHead *ffcapi.MinimalBlockInfo, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) ([]*ffcapi.MinimalBlockInfo, *list.Element) { + txBlockNumber := txBlockInfo.BlockNumber.Uint64() + lastExistingConfirmation := existingConfirmations[len(existingConfirmations)-1] + if lastExistingConfirmation.BlockNumber.Uint64() < chainHead.BlockNumber.Uint64() && + // ^^ the highest block number in the existing confirmations is lower than the highest block number in the canonical chain + (lastExistingConfirmation.BlockNumber.Uint64() != chainHead.BlockNumber.Uint64()-1 || + lastExistingConfirmation.BlockHash != chainHead.ParentHash) { + // ^^ and the last existing confirmation is not the parent of the canonical chain head + // Therefore, there is no connection between the existing confirmations and the canonical chain + // so that we cannot validate the existing confirmations are from the same fork as the canonical chain + // so we need to rebuild the confirmations queue + reconcileResult.Rebuilt = true + return newQueue, currentBlock + } + + var previousExistingConfirmation *ffcapi.MinimalBlockInfo + queueIndex := 0 + + connectionBlockNumber := currentBlock.Value.(*ffcapi.MinimalBlockInfo).BlockNumber.Uint64() + + for currentBlock != nil && queueIndex < len(existingConfirmations) { + existingConfirmation := existingConfirmations[queueIndex] + if existingConfirmation.BlockNumber.Uint64() <= txBlockNumber { + log.L(ctx).Debugf("Existing confirmation queue is corrupted, the first block is earlier than the tx block: %d", existingConfirmation.BlockNumber.Uint64()) + // if any block in the existing confirmation queue is earlier than the tx block + // the existing confirmation queue is no valid + // we need to rebuild the confirmations queue + reconcileResult.Rebuilt = true + return newQueue[:1], currentBlock + } + + // the existing confirmation queue is not tightly controlled by our canonical chain + // ^^ even though it supposed to be build by a canonical chain, we cannot rely on it + // because they are stored outside of current system + // Therefore, we need to check whether the existing confirmation queue is corrupted + isCorrupted := previousExistingConfirmation != nil && + (previousExistingConfirmation.BlockNumber.Uint64()+1 != existingConfirmation.BlockNumber.Uint64() || + previousExistingConfirmation.BlockHash != existingConfirmation.ParentHash) || + // check the link between the first confirmation block and the existing tx block + (existingConfirmation.BlockNumber.Uint64() == txBlockNumber+1 && + existingConfirmation.ParentHash != txBlockInfo.BlockHash) + // we allow gaps between the tx block and the first block in the existing confirmation queue + // NOTE: we don't allow gaps after the first block in the existing confirmation queue + // any gaps, we need to rebuild the confirmations queue + + if isCorrupted { + // any corruption in the existing confirmation queue will cause the confirmation queue to be rebuilt + // we don't keep any of the existing confirmations + reconcileResult.Rebuilt = true + return newQueue[:1], currentBlock + } + + currentBlockInfo := currentBlock.Value.(*ffcapi.MinimalBlockInfo) + if existingConfirmation.BlockNumber.Uint64() < currentBlockInfo.BlockNumber.Uint64() { + // NOTE: we are not doing the confirmation count check here + // because we've not reached the current head in the canonical chain to validate + // all the confirmations we copied over are still valid + newQueue = append(newQueue, existingConfirmation) + previousExistingConfirmation = existingConfirmation + queueIndex++ + continue + } + + if existingConfirmation.BlockNumber.Uint64() == currentBlockInfo.BlockNumber.Uint64() { + // existing confirmation has caught up to the current block + // checking the overlaps + + if !existingConfirmation.Equal(currentBlockInfo) { + // we detected a potential fork + if connectionBlockNumber == currentBlockInfo.BlockNumber.Uint64() && + !previousExistingConfirmation.IsParentOf(currentBlockInfo) { + // this is the connection node (first overlap between existing confirmation queue and canonical chain) + // if the first node doesn't chain to to the previous confirmation, it means all the historical confirmation are on a different fork + // therefore, we need to rebuild the confirmations queue + reconcileResult.Rebuilt = true + return newQueue[:1], currentBlock + } + + // other scenarios, the historical confirmation are still trustworthy and linked to our canonical chain + reconcileResult.HasNewFork = true + return newQueue, currentBlock + } + + newQueue = append(newQueue, existingConfirmation) + if existingConfirmation.BlockNumber.Uint64()-txBlockNumber >= targetConfirmationCount { + break + } + currentBlock = currentBlock.Next() + previousExistingConfirmation = existingConfirmation + queueIndex++ + continue + } + + reconcileResult.Rebuilt = true + return newQueue[:1], currentBlock + } + + // Check if we have enough confirmations + lastBlockInNewQueue := newQueue[len(newQueue)-1] + confirmationBlockNumber := txBlockNumber + targetConfirmationCount + if lastBlockInNewQueue.BlockNumber.Uint64() >= confirmationBlockNumber { + chainHead := bl.canonicalChain.Front().Value.(*ffcapi.MinimalBlockInfo) + // we've got a confirmation so whether the rest of the chain has forked is no longer relevant + // this could happen when user chose a different target confirmation count for the new checks + // but we still need to validate the existing confirmations are connectable to the canonical chain + // Check if the queue connects to the canonical chain + if lastBlockInNewQueue.BlockNumber.Uint64() >= chainHead.BlockNumber.Uint64() || + (lastBlockInNewQueue.BlockNumber.Uint64() == chainHead.BlockNumber.Uint64()-1 && + lastBlockInNewQueue.BlockHash == chainHead.ParentHash) { + reconcileResult.HasNewFork = false + reconcileResult.HasNewConfirmation = false + reconcileResult.Rebuilt = false + reconcileResult.Confirmed = true + + // Trim the queue to only include blocks up to the max confirmation count + trimmedQueue := []*ffcapi.MinimalBlockInfo{} + for _, confirmation := range newQueue { + if confirmation.BlockNumber.Uint64() > confirmationBlockNumber { + break + } + trimmedQueue = append(trimmedQueue, confirmation) + } + + // If we've trimmed off all the existing confirmations, we need to add the canonical chain head + // to tell us the head block we used to confirm the transaction + if len(trimmedQueue) == 1 { + trimmedQueue = append(trimmedQueue, chainHead) + } + return trimmedQueue, currentBlock + } + } + + return newQueue, currentBlock +} + +func (bl *blockListener) buildNewConfirmations(reconcileResult *ffcapi.ConfirmationMapUpdateResult, newQueue []*ffcapi.MinimalBlockInfo, currentBlock *list.Element, txBlockNumber uint64, targetConfirmationCount uint64) []*ffcapi.MinimalBlockInfo { + for currentBlock != nil { + currentBlockInfo := currentBlock.Value.(*ffcapi.MinimalBlockInfo) + if currentBlockInfo.BlockNumber.Uint64() > newQueue[len(newQueue)-1].BlockNumber.Uint64() { + reconcileResult.HasNewConfirmation = true + newQueue = append(newQueue, &ffcapi.MinimalBlockInfo{ + BlockHash: currentBlockInfo.BlockHash, + BlockNumber: fftypes.FFuint64(currentBlockInfo.BlockNumber.Uint64()), + ParentHash: currentBlockInfo.ParentHash, + }) + if currentBlockInfo.BlockNumber.Uint64() >= txBlockNumber+targetConfirmationCount { + reconcileResult.Confirmed = true + break + } + } + currentBlock = currentBlock.Next() + } + return newQueue +} + +func (c *ethConnector) ReconcileConfirmationsForTransaction(ctx context.Context, txHash string, confirmMap *ffcapi.ConfirmationMap, targetConfirmationCount uint64) (*ffcapi.ConfirmationMapUpdateResult, error) { + return c.blockListener.reconcileConfirmationsForTransaction(ctx, txHash, confirmMap, targetConfirmationCount) +} diff --git a/internal/ethereum/confirmation_reconciler_test.go b/internal/ethereum/confirmation_reconciler_test.go new file mode 100644 index 0000000..982bf47 --- /dev/null +++ b/internal/ethereum/confirmation_reconciler_test.go @@ -0,0 +1,1383 @@ +// Copyright © 2025 Kaleido, Inc. +// +// SPDX-License-Identifier: Apache-2.0 +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package ethereum + +import ( + "container/list" + "context" + "encoding/json" + "fmt" + "testing" + + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-signer/pkg/ethtypes" + "github.com/hyperledger/firefly-signer/pkg/rpcbackend" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +// Tests of the reconcileConfirmationsForTransaction function + +func TestReconcileConfirmationsForTransaction_TransactionNotFound(t *testing.T) { + + _, c, mRPC, _ := newTestConnectorWithNoBlockerFilterDefaultMocks(t) + + // Mock for TransactionReceipt call - return nil to simulate transaction not found + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getTransactionReceipt", generateTestHash(100)).Return(nil).Run(func(args mock.Arguments) { + err := json.Unmarshal([]byte("null"), args[1]) + assert.NoError(t, err) + }) + + // Execute the reconcileConfirmationsForTransaction function + result, err := c.ReconcileConfirmationsForTransaction(context.Background(), generateTestHash(100), nil, 5) + + // Assertions - expect an error when transaction doesn't exist + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.HasNewFork) + assert.False(t, result.Rebuilt) + assert.False(t, result.HasNewConfirmation) + assert.False(t, result.Confirmed) + assert.Nil(t, result.ConfirmationMap) + assert.Equal(t, uint64(5), result.TargetConfirmationCount) + + mRPC.AssertExpectations(t) +} + +func TestReconcileConfirmationsForTransaction_ReceiptRPCCallError(t *testing.T) { + + _, c, mRPC, _ := newTestConnectorWithNoBlockerFilterDefaultMocks(t) + + // Mock for TransactionReceipt call - return error to simulate RPC call error + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getTransactionReceipt", generateTestHash(100)).Return(&rpcbackend.RPCError{Message: "pop"}).Run(func(args mock.Arguments) { + err := json.Unmarshal([]byte("null"), args[1]) + assert.NoError(t, err) + }) + + // Execute the reconcileConfirmationsForTransaction function + result, err := c.ReconcileConfirmationsForTransaction(context.Background(), generateTestHash(100), &ffcapi.ConfirmationMap{}, 5) + + // Assertions - expect an error when RPC call fails + assert.Error(t, err) + assert.Nil(t, result) +} + +func TestReconcileConfirmationsForTransaction_BlockNotFound(t *testing.T) { + + _, c, mRPC, _ := newTestConnectorWithNoBlockerFilterDefaultMocks(t) + + // Mock for TransactionReceipt call + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getTransactionReceipt", + mock.MatchedBy(func(txHash string) bool { + assert.Equal(t, "0x6197ef1a58a2a592bb447efb651f0db7945de21aa8048801b250bd7b7431f9b6", txHash) + return true + })). + Return(nil). + Run(func(args mock.Arguments) { + err := json.Unmarshal([]byte(sampleJSONRPCReceipt), args[1]) + assert.NoError(t, err) + }) + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByNumber", mock.MatchedBy(func(bn *ethtypes.HexInteger) bool { + return bn.BigInt().String() == "1977" + }), false).Return(nil).Run(func(args mock.Arguments) { + err := json.Unmarshal([]byte("null"), args[1]) + assert.NoError(t, err) + }) + + // Execute the reconcileConfirmationsForTransaction function + result, err := c.ReconcileConfirmationsForTransaction(context.Background(), "0x6197ef1a58a2a592bb447efb651f0db7945de21aa8048801b250bd7b7431f9b6", &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(1977): { + {BlockNumber: fftypes.FFuint64(1977), BlockHash: generateTestHash(1977), ParentHash: generateTestHash(1976)}, + }, + }, + }, 5) + + // Assertions - expect an error when transaction doesn't exist + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.HasNewFork) + assert.False(t, result.Rebuilt) + assert.False(t, result.HasNewConfirmation) + assert.False(t, result.Confirmed) + assert.Equal(t, &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(1977): { + {BlockNumber: fftypes.FFuint64(1977), BlockHash: generateTestHash(1977), ParentHash: generateTestHash(1976)}, + }, + }, + }, result.ConfirmationMap) + assert.Equal(t, uint64(5), result.TargetConfirmationCount) + + mRPC.AssertExpectations(t) +} + +func TestReconcileConfirmationsForTransaction_BlockRPCCallError(t *testing.T) { + + _, c, mRPC, _ := newTestConnectorWithNoBlockerFilterDefaultMocks(t) + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getTransactionReceipt", + mock.MatchedBy(func(txHash string) bool { + assert.Equal(t, "0x6197ef1a58a2a592bb447efb651f0db7945de21aa8048801b250bd7b7431f9b6", txHash) + return true + })). + Return(nil). + Run(func(args mock.Arguments) { + err := json.Unmarshal([]byte(sampleJSONRPCReceipt), args[1]) + assert.NoError(t, err) + }) + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByNumber", mock.MatchedBy(func(bn *ethtypes.HexInteger) bool { + return bn.BigInt().String() == "1977" + }), false).Return(&rpcbackend.RPCError{Message: "pop"}) + + // Execute the reconcileConfirmationsForTransaction function + result, err := c.ReconcileConfirmationsForTransaction(context.Background(), "0x6197ef1a58a2a592bb447efb651f0db7945de21aa8048801b250bd7b7431f9b6", &ffcapi.ConfirmationMap{}, 5) + + // Assertions - expect an error when RPC call fails + assert.Error(t, err) + assert.Nil(t, result) +} + +func TestReconcileConfirmationsForTransaction_TxBlockNotInCanonicalChain(t *testing.T) { + + _, c, mRPC, _ := newTestConnectorWithNoBlockerFilterDefaultMocks(t) + bl := c.blockListener + bl.canonicalChain = createTestChain(1976, 1978) // Single block at 50, tx is at 100 + + // Mock for TransactionReceipt call + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getTransactionReceipt", + mock.MatchedBy(func(txHash string) bool { + assert.Equal(t, "0x6197ef1a58a2a592bb447efb651f0db7945de21aa8048801b250bd7b7431f9b6", txHash) + return true + })). + Return(nil). + Run(func(args mock.Arguments) { + err := json.Unmarshal([]byte(sampleJSONRPCReceipt), args[1]) + assert.NoError(t, err) + }) + + fakeParentHash := fftypes.NewRandB32().String() + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByNumber", mock.MatchedBy(func(bn *ethtypes.HexInteger) bool { + return bn.BigInt().String() == "1977" + }), false).Return(nil).Run(func(args mock.Arguments) { + *args[1].(**blockInfoJSONRPC) = &blockInfoJSONRPC{ + Number: ethtypes.NewHexInteger64(1977), + Hash: ethtypes.MustNewHexBytes0xPrefix(generateTestHash(1977)), + ParentHash: ethtypes.MustNewHexBytes0xPrefix(fakeParentHash), + } + }) + + // Execute the reconcileConfirmationsForTransaction function + result, err := c.ReconcileConfirmationsForTransaction(context.Background(), "0x6197ef1a58a2a592bb447efb651f0db7945de21aa8048801b250bd7b7431f9b6", &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(1977): {}, + }, + }, 5) + + // Assertions - expect the existing confirmation queue to be returned because the tx block doesn't match the same block number in the canonical chain + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.HasNewFork) + assert.False(t, result.Rebuilt) + assert.False(t, result.HasNewConfirmation) + assert.False(t, result.Confirmed) + assert.Equal(t, &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(1977): {}, + }, + }, result.ConfirmationMap) + assert.Equal(t, uint64(5), result.TargetConfirmationCount) + + mRPC.AssertExpectations(t) +} + +func TestReconcileConfirmationsForTransaction_NewConfirmation(t *testing.T) { + + _, c, mRPC, _ := newTestConnectorWithNoBlockerFilterDefaultMocks(t) + bl := c.blockListener + bl.canonicalChain = createTestChain(1976, 1978) // Single block at 50, tx is at 100 + + // Mock for TransactionReceipt call + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getTransactionReceipt", + mock.MatchedBy(func(txHash string) bool { + assert.Equal(t, "0x6197ef1a58a2a592bb447efb651f0db7945de21aa8048801b250bd7b7431f9b6", txHash) + return true + })). + Return(nil). + Run(func(args mock.Arguments) { + err := json.Unmarshal([]byte(sampleJSONRPCReceipt), args[1]) + assert.NoError(t, err) + }) + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByNumber", mock.MatchedBy(func(bn *ethtypes.HexInteger) bool { + return bn.BigInt().String() == "1977" + }), false).Return(nil).Run(func(args mock.Arguments) { + *args[1].(**blockInfoJSONRPC) = &blockInfoJSONRPC{ + Number: ethtypes.NewHexInteger64(1977), + Hash: ethtypes.MustNewHexBytes0xPrefix(generateTestHash(1977)), + ParentHash: ethtypes.MustNewHexBytes0xPrefix(generateTestHash(1976)), + } + }) + + // Execute the reconcileConfirmationsForTransaction function + result, err := c.ReconcileConfirmationsForTransaction(context.Background(), "0x6197ef1a58a2a592bb447efb651f0db7945de21aa8048801b250bd7b7431f9b6", &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(1977): {}, + }, + }, 5) + + // Assertions - expect the existing confirmation queue to be returned because the tx block doesn't match the same block number in the canonical chain + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.HasNewFork) + assert.False(t, result.Rebuilt) + assert.True(t, result.HasNewConfirmation) + assert.False(t, result.Confirmed) + assert.Equal(t, &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(1977): { + {BlockNumber: fftypes.FFuint64(1977), BlockHash: generateTestHash(1977), ParentHash: generateTestHash(1976)}, + {BlockNumber: fftypes.FFuint64(1978), BlockHash: generateTestHash(1978), ParentHash: generateTestHash(1977)}, + }, + }, + CanonicalBlockHash: generateTestHash(1977), + }, result.ConfirmationMap) + assert.Equal(t, uint64(5), result.TargetConfirmationCount) + + mRPC.AssertExpectations(t) +} + +// Tests of the compareAndUpdateConfirmationQueue function + +func TestCompareAndUpdateConfirmationQueue_EmptyChain(t *testing.T) { + // Setup - create a chain with one block that's older than the transaction + bl := &blockListener{ + canonicalChain: createTestChain(50, 50), // Single block at 50, tx is at 100 + } + ctx := context.Background() + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{}, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(txBlockNumber) + + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(txBlockNumber - 1), + } + targetConfirmationCount := uint64(5) + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert - should return early due to chain being too short + assert.NotNil(t, occ.ConfirmationMap) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 0) + assert.NotNil(t, occ.ConfirmationMap) + assert.False(t, occ.HasNewFork) + assert.False(t, occ.HasNewConfirmation) + assert.False(t, occ.Rebuilt) + assert.False(t, occ.Confirmed) +} + +func TestCompareAndUpdateConfirmationQueue_ChainTooShort(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(50, 99), // Chain ends at 99, tx is at 100 + } + ctx := context.Background() + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{}, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(txBlockNumber) + + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(txBlockNumber - 1), + } + targetConfirmationCount := uint64(5) + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert - should return early due to chain being too short + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 0) + assert.NotNil(t, occ.ConfirmationMap) + assert.False(t, occ.HasNewFork) + assert.False(t, occ.HasNewConfirmation) + assert.False(t, occ.Rebuilt) + assert.False(t, occ.Confirmed) +} + +func TestCompareAndUpdateConfirmationQueue_NilConfirmationMap(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(50, 150), + } + ctx := context.Background() + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: nil, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(txBlockNumber) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(txBlockNumber - 1), + } + targetConfirmationCount := uint64(5) + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.NotNil(t, occ.ConfirmationMap) + assert.False(t, occ.HasNewFork) + assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.Confirmed) + assert.False(t, occ.Rebuilt) + assert.Equal(t, txBlockHash, occ.ConfirmationMap.CanonicalBlockHash) + // The code builds a full confirmation queue from the canonical chain + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][5].BlockNumber)) + +} + +func TestCompareAndUpdateConfirmationQueue_NilConfirmationMap_ZeroConfirmationCount(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(50, 150), + } + ctx := context.Background() + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: nil, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(txBlockNumber) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(txBlockNumber - 1), + } + targetConfirmationCount := uint64(0) + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.NotNil(t, occ.ConfirmationMap) + assert.False(t, occ.HasNewFork) + assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.Confirmed) + assert.False(t, occ.Rebuilt) + assert.Equal(t, txBlockHash, occ.ConfirmationMap.CanonicalBlockHash) + // The code builds a full confirmation queue from the canonical chain + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 1) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) +} + +func TestCompareAndUpdateConfirmationQueue_NilConfirmationMapUnconfirmed(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(100, 104), + } + ctx := context.Background() + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: nil, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(txBlockNumber) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(txBlockNumber - 1), + } + targetConfirmationCount := uint64(5) + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.NotNil(t, occ.ConfirmationMap) + assert.False(t, occ.HasNewFork) + assert.True(t, occ.HasNewConfirmation) + assert.False(t, occ.Confirmed) + assert.False(t, occ.Rebuilt) + assert.Equal(t, txBlockHash, occ.ConfirmationMap.CanonicalBlockHash) + // The code builds a confirmation queue from the canonical chain up to the available blocks + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 5) // 100, 101, 102, 103, 104 + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][4].BlockNumber)) + +} + +func TestCompareAndUpdateConfirmationQueue_EmptyConfirmationQueue(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(50, 150), + } + ctx := context.Background() + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: make(map[string][]*ffcapi.MinimalBlockInfo), + }, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(txBlockNumber) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(txBlockNumber - 1), + } + targetConfirmationCount := uint64(5) + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.False(t, occ.HasNewFork) + assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.Confirmed) + assert.False(t, occ.Rebuilt) + assert.Equal(t, txBlockHash, occ.ConfirmationMap.CanonicalBlockHash) + // The code builds a full confirmation queue from the canonical chain + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][5].BlockNumber)) +} + +// theoretically, this should never happen because block hash generation has block number as part of the input +func TestCompareAndUpdateConfirmationQueue_DifferentBlockNumber(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(50, 150), + } + ctx := context.Background() + existingQueue := []*ffcapi.MinimalBlockInfo{ + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(99), ParentHash: generateTestHash(98)}, + } + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(100): existingQueue, + }, + }, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(txBlockNumber) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(txBlockNumber - 1), + } + targetConfirmationCount := uint64(5) + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.True(t, occ.HasNewFork) + assert.True(t, occ.Rebuilt) + // The code builds a full confirmation queue from the canonical chain + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][5].BlockNumber)) +} + +func TestCompareAndUpdateConfirmationQueue_MismatchConfirmationBlock(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(103, 150), + } + ctx := context.Background() + existingQueue := []*ffcapi.MinimalBlockInfo{ + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(101)}, // wrong parent hash, so the existing queue should be discarded + {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, + {BlockHash: generateTestHash(103), BlockNumber: fftypes.FFuint64(103), ParentHash: generateTestHash(102)}, + } + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(100): existingQueue, + }, + }, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(5) + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.False(t, occ.HasNewFork) + assert.True(t, occ.Rebuilt) + // The code builds a full confirmation queue from the canonical chain + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 4) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) +} + +func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsTooDistant(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(145, 150), + } + ctx := context.Background() + existingQueue := []*ffcapi.MinimalBlockInfo{ + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, + } + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(100): existingQueue, + }, + }, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(5) + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + // only the tx block and the first block in the canonical chain are in the confirmation queue + // and the transaction is confirmed + assert.False(t, occ.HasNewFork) + assert.True(t, occ.Rebuilt) + assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.Confirmed) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 2) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, bl.canonicalChain.Front().Value.(*ffcapi.MinimalBlockInfo).BlockNumber, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber) +} + +func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmation(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(50, 150), + } + ctx := context.Background() + // Create corrupted confirmation (wrong parent hash) + existingQueue := []*ffcapi.MinimalBlockInfo{ + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: "0xwrongparent"}, + } + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(100): existingQueue, + }, + }, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(5) + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.False(t, occ.HasNewFork) + assert.True(t, occ.Rebuilt) + assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.Confirmed) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, generateTestHash(100), occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].ParentHash) +} + +func TestCompareAndUpdateConfirmationQueue_ConnectionNodeMismatch(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(102, 150), + } + ctx := context.Background() + existingQueue := []*ffcapi.MinimalBlockInfo{ + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: "0xblockwrong", BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, + {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, + {BlockHash: generateTestHash(103), BlockNumber: fftypes.FFuint64(103), ParentHash: generateTestHash(102)}, + } + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(100): existingQueue, + }, + }, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(5) + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.False(t, occ.HasNewFork) + assert.True(t, occ.Rebuilt) + assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.Confirmed) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 5) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, generateTestHash(102), occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockHash) + assert.Equal(t, generateTestHash(102), occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].ParentHash) +} + +func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmationAfterFirstConfirmation(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(100, 150), + } + ctx := context.Background() + existingQueue := []*ffcapi.MinimalBlockInfo{ + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, + {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblockwrong"}, + } + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(100): existingQueue, + }, + }, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(5) + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.False(t, occ.HasNewFork) + assert.True(t, occ.Rebuilt) + assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.Confirmed) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 5) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, generateTestHash(102), occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockHash) + assert.Equal(t, generateTestHash(102), occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].ParentHash) +} + +func TestCompareAndUpdateConfirmationQueue_NewForkAfterFirstConfirmation(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(100, 150), + } + ctx := context.Background() + existingQueue := []*ffcapi.MinimalBlockInfo{ + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, + {BlockHash: "fork1", BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, + } + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(100): existingQueue, + }, + }, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(5) + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.True(t, occ.HasNewFork) + assert.False(t, occ.Rebuilt) + assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.Confirmed) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) +} + +func TestCompareAndUpdateConfirmationQueue_NewForkAfterFirstConfirmation_ZeroConfirmationCount(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(100, 150), + } + ctx := context.Background() + existingQueue := []*ffcapi.MinimalBlockInfo{ + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, + {BlockHash: "fork1", BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, + } + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(100): existingQueue, + }, + }, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(0) + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.False(t, occ.HasNewFork) + assert.False(t, occ.Rebuilt) + assert.False(t, occ.HasNewConfirmation) + assert.True(t, occ.Confirmed) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 1) +} + +func TestCompareAndUpdateConfirmationQueue_NewForkAndNoConnectionToCanonicalChain(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(103, 150), + } + ctx := context.Background() + existingQueue := []*ffcapi.MinimalBlockInfo{ + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: "fork1", BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, + {BlockHash: "fork2", BlockNumber: fftypes.FFuint64(102), ParentHash: "fork1"}, + {BlockHash: "fork3", BlockNumber: fftypes.FFuint64(103), ParentHash: "fork2"}, + } + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(100): existingQueue, + }, + }, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(5) + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.False(t, occ.HasNewFork) + assert.True(t, occ.Rebuilt) + assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.Confirmed) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 4) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) +} + +func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationLaterThanCurrentBlock(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(100, 150), + } + ctx := context.Background() + existingQueue := []*ffcapi.MinimalBlockInfo{ + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: generateTestHash(103), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, + } + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(100): existingQueue, + }, + }, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(5) + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.False(t, occ.HasNewFork) + assert.True(t, occ.Rebuilt) + assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.Confirmed) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][5].BlockNumber)) +} + +func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(103, 150), + } + ctx := context.Background() + // Create confirmations that already meet the target + // and it connects to the canonical chain to validate they are still valid + existingQueue := []*ffcapi.MinimalBlockInfo{ + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, + {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, + {BlockHash: generateTestHash(103), BlockNumber: fftypes.FFuint64(103), ParentHash: generateTestHash(102)}, + + // all blocks after the first block of the canonical chain are discarded in the final confirmation queue + {BlockHash: "0xblock104", BlockNumber: fftypes.FFuint64(104), ParentHash: generateTestHash(103)}, // discarded + {BlockHash: "0xblock105", BlockNumber: fftypes.FFuint64(105), ParentHash: "0xblock104"}, // discarded + } + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(100): existingQueue, + }, + }, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(2) + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.True(t, occ.Confirmed) + assert.False(t, occ.Rebuilt) + assert.False(t, occ.HasNewFork) + assert.False(t, occ.HasNewConfirmation) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 3) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) +} + +func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable_ZeroConfirmationCount(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(103, 150), + } + ctx := context.Background() + // Create confirmations that already meet the target + // and it connects to the canonical chain to validate they are still valid + existingQueue := []*ffcapi.MinimalBlockInfo{ + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, + {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, + {BlockHash: generateTestHash(103), BlockNumber: fftypes.FFuint64(103), ParentHash: generateTestHash(102)}, + + // all blocks after the first block of the canonical chain are discarded in the final confirmation queue + {BlockHash: "0xblock104", BlockNumber: fftypes.FFuint64(104), ParentHash: generateTestHash(103)}, // discarded + {BlockHash: "0xblock105", BlockNumber: fftypes.FFuint64(105), ParentHash: "0xblock104"}, // discarded + } + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(100): existingQueue, + }, + }, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(0) + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.True(t, occ.Confirmed) + assert.False(t, occ.Rebuilt) + assert.False(t, occ.HasNewFork) + assert.False(t, occ.HasNewConfirmation) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 1) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) +} + +func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableConnectable(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(103, 150), + } + ctx := context.Background() + // Create confirmations that already meet the target + // and it connects to the canonical chain to validate they are still valid + existingQueue := []*ffcapi.MinimalBlockInfo{ + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, + {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, + // didn't have block 103, which is the first block of the canonical chain + // but we should still be able to validate the existing confirmations are valid using parent hash + } + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(100): existingQueue, + }, + }, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(1) + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + // The confirmation queue should return the confirmation queue up to the first block of the canonical chain + + assert.True(t, occ.Confirmed) + assert.False(t, occ.Rebuilt) + assert.False(t, occ.HasNewFork) + assert.False(t, occ.HasNewConfirmation) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 2) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) +} + +func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableButAllExistingConfirmationsAreTooHighForTargetConfirmationCount(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(103, 150), + } + ctx := context.Background() + // Create confirmations that already meet the target + // and it connects to the canonical chain to validate they are still valid + existingQueue := []*ffcapi.MinimalBlockInfo{ + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + // gap of 101 is allowed, and is the confirmation required for the transaction with target confirmation count of 1 + {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, + } + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(100): existingQueue, + }, + }, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(1) + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + // The confirmation queue should return the tx block and the first block of the canonical chain + + assert.True(t, occ.Confirmed) + assert.False(t, occ.Rebuilt) + assert.False(t, occ.HasNewFork) + assert.False(t, occ.HasNewConfirmation) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 2) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, uint64(103), uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + +} + +func TestCompareAndUpdateConfirmationQueue_HasSufficientConfirmationsButNoOverlapWithCanonicalChain(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(104, 150), + } + ctx := context.Background() + // Create confirmations that already meet the target + // and it connects to the canonical chain to validate they are still valid + existingQueue := []*ffcapi.MinimalBlockInfo{ + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, + {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, + } + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(100): existingQueue, + }, + }, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(1) + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + // Because the existing confirmations do not have overlap with the canonical chain, + // the confirmation queue should return the tx block and the first block of the canonical chain + assert.True(t, occ.Confirmed) + assert.False(t, occ.HasNewFork) + assert.True(t, occ.Rebuilt) + assert.True(t, occ.HasNewConfirmation) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 2) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, uint64(104), uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + +} + +func TestCompareAndUpdateConfirmationQueue_ValidExistingConfirmations(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(50, 150), + } + ctx := context.Background() + existingQueue := []*ffcapi.MinimalBlockInfo{ + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, + {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, + } + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(100): existingQueue, + }, + }, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(5) + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.False(t, occ.HasNewFork) + assert.False(t, occ.Rebuilt) + assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.Confirmed) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][5].BlockNumber)) +} + +func TestCompareAndUpdateConfirmationQueue_ValidExistingTxBlock(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(50, 150), + } + ctx := context.Background() + existingQueue := []*ffcapi.MinimalBlockInfo{ + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + } + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(100): existingQueue, + }, + }, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(5) + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.False(t, occ.HasNewFork) + assert.False(t, occ.Rebuilt) + assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.Confirmed) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][5].BlockNumber)) +} + +func TestCompareAndUpdateConfirmationQueue_ReachTargetConfirmation(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(50, 150), + } + ctx := context.Background() + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: make(map[string][]*ffcapi.MinimalBlockInfo), + }, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(3) + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.Confirmed) + // The code builds a full confirmation queue from the canonical chain + assert.GreaterOrEqual(t, len(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash]), 4) // tx block + 3 confirmations +} + +func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithGap(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(101, 150), + } + ctx := context.Background() + // Create confirmations with a gap (missing block 102) + existingQueue := []*ffcapi.MinimalBlockInfo{ + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + // no block 101, which is the first block of the canonical chain + {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, + {BlockHash: generateTestHash(103), BlockNumber: fftypes.FFuint64(103), ParentHash: generateTestHash(102)}, + } + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(100): existingQueue, + }, + }, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(5) + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.False(t, occ.HasNewFork) + assert.True(t, occ.Rebuilt) + assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.Confirmed) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) +} + +func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNumber(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(50, 150), + } + ctx := context.Background() + // Create confirmations with a lower block number + existingQueue := []*ffcapi.MinimalBlockInfo{ + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(99), ParentHash: generateTestHash(100)}, // somehow there is a lower block number + } + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(100): existingQueue, + }, + }, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(5) + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.False(t, occ.HasNewFork) + assert.True(t, occ.Rebuilt) + assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.Confirmed) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) +} + +func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNumberAfterFirstConfirmation(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(101, 150), + } + ctx := context.Background() + // Create confirmations with a lower block number + existingQueue := []*ffcapi.MinimalBlockInfo{ + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, + {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(99), ParentHash: generateTestHash(101)}, // somehow there is a lower block number + } + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(100): existingQueue, + }, + }, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(5) + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.False(t, occ.HasNewFork) + assert.True(t, occ.Rebuilt) + assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.Confirmed) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) +} + +// Helper functions + +// generateTestHash creates a predictable hash for testing with consistent prefix and last 4 digits as index +func generateTestHash(index uint64) string { + return fmt.Sprintf("0x%060x", index) +} + +func createTestChain(startBlock, endBlock uint64) *list.List { + chain := list.New() + for i := startBlock; i <= endBlock; i++ { + blockHash := generateTestHash(i) + + var parentHash string + if i > startBlock || i > 0 { + parentHash = generateTestHash(i - 1) + } else { + // For the first block, if it's 0, use a dummy parent hash + parentHash = generateTestHash(9999) // Use a high number to avoid conflicts + } + + blockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(i), + BlockHash: blockHash, + ParentHash: parentHash, + } + chain.PushBack(blockInfo) + } + return chain +} diff --git a/internal/ethereum/event_listener.go b/internal/ethereum/event_listener.go index ea781f6..cc592d7 100644 --- a/internal/ethereum/event_listener.go +++ b/internal/ethereum/event_listener.go @@ -94,20 +94,20 @@ func (cp *listenerCheckpoint) LessThan(b ffcapi.EventListenerCheckpoint) bool { (cp.TransactionIndex == bcp.TransactionIndex && (cp.LogIndex < bcp.LogIndex)))) } -func (l *listener) getInitialBlock(ctx context.Context, fromBlockInstruction string) (int64, error) { +func (l *listener) getInitialBlock(ctx context.Context, fromBlockInstruction string) (uint64, error) { if fromBlockInstruction == ffcapi.FromBlockLatest || fromBlockInstruction == "" { // Get the latest block number of the chain chainHead, ok := l.c.blockListener.getHighestBlock(ctx) if !ok { - return -1, i18n.NewError(ctx, msgs.MsgTimedOutQueryingChainHead) + return 0, i18n.NewError(ctx, msgs.MsgTimedOutQueryingChainHead) } return chainHead, nil } num, ok := new(big.Int).SetString(fromBlockInstruction, 0) if !ok { - return -1, i18n.NewError(ctx, msgs.MsgInvalidFromBlock, fromBlockInstruction) + return 0, i18n.NewError(ctx, msgs.MsgInvalidFromBlock, fromBlockInstruction) } - return num.Int64(), nil + return num.Uint64(), nil } func parseListenerOptions(ctx context.Context, o *fftypes.JSONAny) (*listenerOptions, error) { @@ -131,7 +131,7 @@ func (l *listener) ensureHWM(ctx context.Context) error { return err } // HWM is the configured fromBlock - l.hwmBlock = firstBlock + l.hwmBlock = int64(firstBlock) //nolint:gosec // convert to int64 to match the type of hwmBlock, we should change the type of hwmBlock to uint64 } return nil } diff --git a/internal/ethereum/event_stream.go b/internal/ethereum/event_stream.go index 2fbf36f..5d8da60 100644 --- a/internal/ethereum/event_stream.go +++ b/internal/ethereum/event_stream.go @@ -254,7 +254,7 @@ func (es *eventStream) leadGroupCatchup() bool { } // Check if we're ready to exit catchup mode - headGap := (chainHeadBlock - fromBlock) + headGap := (int64(chainHeadBlock) - fromBlock) //nolint:gosec // convert to int64 to match the type of headGap if headGap < es.c.catchupThreshold { log.L(es.ctx).Infof("Stream head is up to date with chain fromBlock=%d chainHead=%d headGap=%d", fromBlock, chainHeadBlock, headGap) return false @@ -319,7 +319,7 @@ func (es *eventStream) leadGroupSteadyState() bool { // High water mark is a point safely behind the head of the chain in this case, // where re-orgs are not expected. bh, _ := es.c.blockListener.getHighestBlock(es.ctx) /* note we know we're initialized here and will not block */ - hwmBlock := bh - es.c.checkpointBlockGap + hwmBlock := int64(bh) - es.c.checkpointBlockGap //nolint:gosec // convert to int64 to match the type of hwmBlock if hwmBlock < 0 { hwmBlock = 0 } @@ -342,7 +342,7 @@ func (es *eventStream) leadGroupSteadyState() bool { // Check we're not outside of the steady state window, and need to fall back to catchup mode chainHeadBlock, _ := es.c.blockListener.getHighestBlock(es.ctx) /* note we know we're initialized here and will not block */ - blockGapEstimate := (chainHeadBlock - fromBlock) + blockGapEstimate := (int64(chainHeadBlock) - fromBlock) //nolint:gosec // convert to int64 to match the type of blockGapEstimate if blockGapEstimate > es.c.catchupThreshold { log.L(es.ctx).Warnf("Block gap estimate reached %d (above threshold of %d) - reverting to catchup mode", blockGapEstimate, es.c.catchupThreshold) return false @@ -422,8 +422,8 @@ func (es *eventStream) preStartProcessing() { for _, l := range es.listeners { // During initial start we move the "head" block forwards to be the highest of all the initial streams if l.hwmBlock > es.headBlock { - if l.hwmBlock > chainHead { - es.headBlock = chainHead + if l.hwmBlock > int64(chainHead) { //nolint:gosec // convert to int64 to match the type of headBlock + es.headBlock = int64(chainHead) //nolint:gosec // convert to int64 to match the type of headBlock } else { es.headBlock = l.hwmBlock } diff --git a/internal/ethereum/get_block_info.go b/internal/ethereum/get_block_info.go index ae2dd00..12f6159 100644 --- a/internal/ethereum/get_block_info.go +++ b/internal/ethereum/get_block_info.go @@ -26,7 +26,7 @@ import ( func (c *ethConnector) BlockInfoByNumber(ctx context.Context, req *ffcapi.BlockInfoByNumberRequest) (*ffcapi.BlockInfoByNumberResponse, ffcapi.ErrorReason, error) { - blockInfo, reason, err := c.blockListener.getBlockInfoByNumber(ctx, req.BlockNumber.Int64(), req.AllowCache, req.ExpectedParentHash) + blockInfo, reason, err := c.blockListener.getBlockInfoByNumber(ctx, req.BlockNumber.Uint64(), req.AllowCache, req.ExpectedParentHash) if err != nil { return nil, reason, err } diff --git a/internal/ethereum/get_block_info_test.go b/internal/ethereum/get_block_info_test.go index 3f32173..aa3af73 100644 --- a/internal/ethereum/get_block_info_test.go +++ b/internal/ethereum/get_block_info_test.go @@ -115,7 +115,11 @@ func TestGetBlockInfoByNumberBlockNotFoundError(t *testing.T) { defer done() mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByNumber", mock.Anything, false). - Return(&rpcbackend.RPCError{Message: "cannot query unfinalized data"}) + Return(nil). + Run(func(args mock.Arguments) { + err := json.Unmarshal([]byte("null"), args[1]) + assert.NoError(t, err) + }) var req ffcapi.BlockInfoByNumberRequest err := json.Unmarshal([]byte(sampleGetBlockInfoByNumber), &req) diff --git a/internal/msgs/en_error_messages.go b/internal/msgs/en_error_messages.go index e466d8a..56dd6b9 100644 --- a/internal/msgs/en_error_messages.go +++ b/internal/msgs/en_error_messages.go @@ -73,4 +73,6 @@ var ( MsgInvalidProtocolID = ffe("FF23055", "Invalid protocol ID in event log: %s") MsgFailedToRetrieveChainID = ffe("FF23056", "Failed to retrieve chain ID for event enrichment") MsgFailedToRetrieveTransactionInfo = ffe("FF23057", "Failed to retrieve transaction info for transaction hash '%s'") + MsgFailedToQueryReceipt = ffe("FF23058", "Failed to query receipt for transaction %s") + MsgFailedToQueryBlockInfo = ffe("FF23059", "Failed to query block info using hash %s") ) diff --git a/mocks/fftmmocks/manager.go b/mocks/fftmmocks/manager.go index 6134d08..c2661c1 100644 --- a/mocks/fftmmocks/manager.go +++ b/mocks/fftmmocks/manager.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.2. DO NOT EDIT. +// Code generated by mockery v2.53.5. DO NOT EDIT. package fftmmocks @@ -9,6 +9,8 @@ import ( eventapi "github.com/hyperledger/firefly-transaction-manager/pkg/eventapi" + ffcapi "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + mock "github.com/stretchr/testify/mock" mux "github.com/gorilla/mux" @@ -131,6 +133,36 @@ func (_m *Manager) GetTransactionByIDWithStatus(ctx context.Context, txID string return r0, r1 } +// ReconcileConfirmationsForTransaction provides a mock function with given fields: ctx, txHash, confirmMap, targetConfirmationCount +func (_m *Manager) ReconcileConfirmationsForTransaction(ctx context.Context, txHash string, confirmMap *ffcapi.ConfirmationMap, targetConfirmationCount uint64) (*ffcapi.ConfirmationMapUpdateResult, error) { + ret := _m.Called(ctx, txHash, confirmMap, targetConfirmationCount) + + if len(ret) == 0 { + panic("no return value specified for ReconcileConfirmationsForTransaction") + } + + var r0 *ffcapi.ConfirmationMapUpdateResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, *ffcapi.ConfirmationMap, uint64) (*ffcapi.ConfirmationMapUpdateResult, error)); ok { + return rf(ctx, txHash, confirmMap, targetConfirmationCount) + } + if rf, ok := ret.Get(0).(func(context.Context, string, *ffcapi.ConfirmationMap, uint64) *ffcapi.ConfirmationMapUpdateResult); ok { + r0 = rf(ctx, txHash, confirmMap, targetConfirmationCount) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ffcapi.ConfirmationMapUpdateResult) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, *ffcapi.ConfirmationMap, uint64) error); ok { + r1 = rf(ctx, txHash, confirmMap, targetConfirmationCount) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // Start provides a mock function with no fields func (_m *Manager) Start() error { ret := _m.Called() diff --git a/mocks/rpcbackendmocks/backend.go b/mocks/rpcbackendmocks/backend.go index 3af6838..1ba3d6d 100644 --- a/mocks/rpcbackendmocks/backend.go +++ b/mocks/rpcbackendmocks/backend.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.2. DO NOT EDIT. +// Code generated by mockery v2.53.5. DO NOT EDIT. package rpcbackendmocks