diff --git a/go.mod b/go.mod index 4cddfeb..c135909 100644 --- a/go.mod +++ b/go.mod @@ -5,9 +5,9 @@ go 1.23.0 require ( github.com/gorilla/mux v1.8.1 github.com/hashicorp/golang-lru v1.0.2 - github.com/hyperledger/firefly-common v1.5.6-0.20250630201730-e234335c0381 + github.com/hyperledger/firefly-common v1.5.8 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.2-0.20251107152155-813dbc501aa3 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..db2b3f2 100644 --- a/go.sum +++ b/go.sum @@ -100,12 +100,12 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/hyperledger/firefly-common v1.5.6-0.20250630201730-e234335c0381 h1:4mkvcaVwq9bopPQ7ZNfJqFiN2QA32cPSGUHSyEGSFno= -github.com/hyperledger/firefly-common v1.5.6-0.20250630201730-e234335c0381/go.mod h1:1Xawm5PUhxT7k+CL/Kr3i1LE3cTTzoQwZMLimvlW8rs= +github.com/hyperledger/firefly-common v1.5.8 h1:3N59vh81UpqOJu/uX1EOIiEy/sCuzJWYAUPMgTRLako= +github.com/hyperledger/firefly-common v1.5.8/go.mod h1:1Xawm5PUhxT7k+CL/Kr3i1LE3cTTzoQwZMLimvlW8rs= github.com/hyperledger/firefly-signer v1.1.21 h1:r7cTOw6e/6AtiXLf84wZy6Z7zppzlc191HokW2hv4N4= 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.2-0.20251107152155-813dbc501aa3 h1:ODgCSp2N6EujywKKszU2t+/McZEO/8HyHjmVsvS1YCo= +github.com/hyperledger/firefly-transaction-manager v1.4.2-0.20251107152155-813dbc501aa3/go.mod h1:1kbYt8ofDXqfwC02vwV/HoOjmiv0IuT9UkJ//bbrliE= 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 546b17f..e6ad16e 100644 --- a/internal/ethereum/blocklistener.go +++ b/internal/ethereum/blocklistener.go @@ -55,20 +55,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) (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(), @@ -163,10 +161,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 }) } @@ -258,7 +253,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 } } @@ -266,7 +261,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() } @@ -293,16 +288,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() @@ -311,15 +302,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: @@ -331,14 +322,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() } } @@ -366,7 +357,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 } @@ -377,18 +368,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() @@ -398,7 +389,7 @@ func (bl *blockListener) rebuildCanonicalChain() *list.Element { var bi *blockInfoJSONRPC var reason ffcapi.ErrorReason err := bl.c.retry.Do(bl.ctx, "rebuild listener canonical chain", func(_ int) (retry bool, err error) { - bi, reason, err = bl.getBlockInfoByNumber(bl.ctx, nextBlockNumber, false, "") + bi, reason, err = bl.getBlockInfoByNumber(bl.ctx, nextBlockNumber, false, "", "") return reason != ffcapi.ErrorReasonNotFound, err }) if err != nil { @@ -410,19 +401,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. @@ -432,33 +423,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 { @@ -467,8 +455,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() @@ -482,8 +470,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 } @@ -520,29 +508,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..f6debd3 100644 --- a/internal/ethereum/blocklistener_blockquery.go +++ b/internal/ethereum/blocklistener_blockquery.go @@ -53,30 +53,56 @@ 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, *ffcapi.TransactionReceiptResponse, error) { + + // Query the chain to find the transaction block + res, reason, receiptErr := bl.c.TransactionReceipt(ctx, &ffcapi.TransactionReceiptRequest{ + TransactionHash: txHash, + }) + if receiptErr != nil && reason != ffcapi.ErrorReasonNotFound { + return nil, nil, i18n.WrapError(ctx, receiptErr, msgs.MsgFailedToQueryReceipt, txHash) + } + if res == nil { + return nil, 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, nil, i18n.WrapError(ctx, err, msgs.MsgFailedToQueryBlockInfo, txHash) + } + if bi == nil { + return nil, nil, nil + } + + return &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(bi.Number.BigInt().Uint64()), + BlockHash: bi.Hash.String(), + ParentHash: bi.ParentHash.String(), + }, res, nil +} + +func (bl *blockListener) getBlockInfoByNumber(ctx context.Context, blockNumber uint64, allowCache bool, expectedParentHashStr string, expectedBlockHashStr 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 { - log.L(ctx).Debugf("Block cache miss for block %d due to mismatched parent hash expected=%s found=%s", blockNumber, expectedHashStr, blockInfo.ParentHash) + if (expectedParentHashStr != "" && blockInfo.ParentHash.String() != expectedParentHashStr) || (expectedBlockHashStr != "" && blockInfo.Hash.String() != expectedBlockHashStr) { + log.L(ctx).Debugf("Block cache miss for block %d due to mismatched parent hash expected=%s found=%s", blockNumber, expectedParentHashStr, blockInfo.ParentHash) blockInfo = nil } } } 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..70b700a --- /dev/null +++ b/internal/ethereum/confirmation_reconciler.go @@ -0,0 +1,381 @@ +// 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 ( + "context" + + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-common/pkg/i18n" + "github.com/hyperledger/firefly-common/pkg/log" + "github.com/hyperledger/firefly-evmconnect/internal/msgs" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" +) + +// ReconcileConfirmationsForTransaction is the public API for reconciling transaction confirmations. +// It delegates to the blockListener's internal reconciliation logic. +func (c *ethConnector) ReconcileConfirmationsForTransaction(ctx context.Context, txHash string, existingConfirmations []*ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) (*ffcapi.ConfirmationUpdateResult, error) { + // Now we can start the reconciliation process + return c.blockListener.reconcileConfirmationsForTransaction(ctx, txHash, existingConfirmations, targetConfirmationCount) +} + +// reconcileConfirmationsForTransaction reconciles the confirmation queue for a transaction +func (bl *blockListener) reconcileConfirmationsForTransaction(ctx context.Context, txHash string, existingConfirmations []*ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) (*ffcapi.ConfirmationUpdateResult, error) { + + // Fetch the block containing the transaction first so that we can use it to build the confirmation list + txBlockInfo, txReceipt, 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 nil, i18n.NewError(ctx, msgs.MsgTransactionNotFound, txHash) + } + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingConfirmations, txBlockInfo, targetConfirmationCount) + if confirmationUpdateResult != nil { + confirmationUpdateResult.TargetConfirmationCount = targetConfirmationCount + confirmationUpdateResult.Receipt = txReceipt + } + return confirmationUpdateResult, err +} + +func (bl *blockListener) buildConfirmationList(ctx context.Context, existingConfirmations []*ffcapi.MinimalBlockInfo, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) (*ffcapi.ConfirmationUpdateResult, error) { + // Primary objective of this algorithm is to build a contiguous, linked list of `MinimalBlockInfo` structs, starting from the transaction block and ending as far as our current knowledge of the in-memory partial canonical chain allows. + // Secondary objective is to report whether any fork was detected (and corrected) during this analysis + + // handle confirmation count of 0 as a special case to reduce complexity of the main algorithm + if targetConfirmationCount == 0 { + reconcileResult, err := bl.handleZeroTargetConfirmationCount(ctx, txBlockInfo) + if reconcileResult != nil || err != nil { + return reconcileResult, err + } + } + + // Initialize the result with the target confirmation count + reconcileResult := &ffcapi.ConfirmationUpdateResult{} + + // We start by constructing 2 lists of blocks: + // - The `earlyList`. This is the set of earliest blocks we are interested in. At the least, it starts with the transaction block + // and may also contain some number of existing confirmations i.e. the output from previous call to this function + // Other than the `transactionBlock`, we don't yet know whether any of the early list is still correct as per the current state of the canonical chain. + // The chain may have been re-organized since we discovered the blocks in that list. + // - The `lateList`. This is the most recent set of blocks that we are interesting in and we believe are accurate for the current state of the chain + + earlyList := createEarlyList(existingConfirmations, txBlockInfo, reconcileResult) + + // if early list is sufficient to meet the target confirmation count, we handle this as a special case as well + if len(earlyList) > 0 && earlyList[len(earlyList)-1].BlockNumber.Uint64() >= txBlockInfo.BlockNumber.Uint64()+targetConfirmationCount { + reconcileResult := bl.handleTargetCountMetWithEarlyList(earlyList, targetConfirmationCount) + if reconcileResult != nil { + return reconcileResult, nil + } + } + + lateList, err := createLateList(ctx, txBlockInfo, targetConfirmationCount, bl) + if err != nil { + return nil, err + } + + // These 2 lists may overlap so we splice them together which will remove any overlapping blocks + splicedList, detectedFork := newSplice(earlyList, lateList) + if detectedFork { + reconcileResult.NewFork = true + } + for { + // now loop until we can form a contiguous linked list (by block number) from the spliced list + if splicedList.isEarlyListEmpty() { + // the first block in the early list is transaction block + // if that block is removed, it means the chain is not stable enough for the logic + // to generate a valid confirmation list + // throw an error to the caller + return nil, i18n.NewError(ctx, msgs.MsgFailedToBuildConfirmationQueue) + } + + // inner loop to fill any gaps between the early list and the late list + for splicedList.hasGap() { + err = splicedList.fillOneGap(ctx, bl) + if err != nil { + return nil, err + } + } + + confirmations := splicedList.toSingleLinkedList() + if confirmations != nil { + // we have a contiguous list that starts with the transaction block and ends with the last block in the canonical chain + // so we can return the result + reconcileResult.Confirmations = confirmations + break + } + + // we filled all gaps and still cannot link the 2 lists, must be a fork. Create a gap of one and try again + reconcileResult.NewFork = true + splicedList.removeBrokenLink() + } + + reconcileResult.Confirmed = uint64(len(reconcileResult.Confirmations)) > targetConfirmationCount // do this maths here as a utility so that the consumer doesn't have to do it + return reconcileResult, nil +} + +// splice is the data structure that brings together 2 lists of block info with functions to remove redundant overlaps, to fill gaps and to validate linkability of the 2 lists +type splice struct { + earlyList []*ffcapi.MinimalBlockInfo // beginning of the early list is the earliest block that we are interested in + lateList []*ffcapi.MinimalBlockInfo // late list is assumed to be the most recent view of the network's canonical chain +} + +func newSplice(earlyList []*ffcapi.MinimalBlockInfo, lateList []*ffcapi.MinimalBlockInfo) (*splice, bool) { + // remove any redundant overlaps between the 2 lists + // for now, we are simply looking at block numbers to see if there is any block number for which both lists have a block info + // if there is, we prefer to keep the block info from the late list because in the event that the 2 lists diverge, then the divergence point will be somewhere in the early list ( because we fetched the late list more recently) + // and we will have fewer links to validate if we trim the overlap from the early list + s := &splice{ + earlyList: earlyList, + lateList: lateList, + } + detectedFork := false + // if the early list is bigger than the gap between the transaction block number and the first block in the late list, then we have an overlap + txBlockNumber := s.earlyList[0].BlockNumber.Uint64() + firstLateBlockNumber := s.lateList[0].BlockNumber.Uint64() + if uint64(len(s.earlyList))+txBlockNumber > firstLateBlockNumber { + // there is an overlap so we need to discard the end of the early list but before we do, lets check whether it is equivalent to the equivalent blocks from the late + // list so that we can report whether or not a fork was detected + discardedEarlyListBlocks := s.earlyList[firstLateBlockNumber-txBlockNumber:] + for i := range discardedEarlyListBlocks { + if i >= len(s.lateList) { + break + } + if !discardedEarlyListBlocks[i].Equal(s.lateList[i]) { + detectedFork = true + break + } + } + + s.earlyList = s.earlyList[:firstLateBlockNumber-txBlockNumber] + } + return s, detectedFork +} + +func (s *splice) hasGap() bool { + return len(s.earlyList) > 0 && + len(s.lateList) > 0 && + s.earlyList[len(s.earlyList)-1].BlockNumber.Uint64()+1 < s.lateList[0].BlockNumber.Uint64() +} + +func (s *splice) isEarlyListEmpty() bool { + // we haven't removed the first block from the early list + return len(s.earlyList) == 0 +} + +func (s *splice) fillOneGap(ctx context.Context, blockListener *blockListener) error { + // fill one slot in the gap between the late list and the early list + // always fill from the end of the gap ( i.e. the block before the start of the late list) because + // the late list is our best view of the current canonical chain so working backwards from there will increase the number of blocks that we have a high confidence in + + freshBlockInfo, _, err := blockListener.getBlockInfoByNumber(ctx, s.lateList[0].BlockNumber.Uint64()-1, false, "", "") + if err != nil { + return err + } + + fetchedBlock := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(freshBlockInfo.Number.BigInt().Uint64()), + BlockHash: freshBlockInfo.Hash.String(), + ParentHash: freshBlockInfo.ParentHash.String(), + } + + // Validate parent-child relationship + if !fetchedBlock.IsParentOf(s.lateList[0]) { + // most likely explanation of this is an unstable chain + // throw an error to the caller + return i18n.NewError(ctx, msgs.MsgFailedToBuildConfirmationQueue) + } + + // Prepend fetched block to the late list + s.lateList = append([]*ffcapi.MinimalBlockInfo{fetchedBlock}, s.lateList...) + return nil +} + +func (s *splice) removeBrokenLink() { + // remove the last block from the early list because it is not the parent of the first block in the late list and we have higher confidence in the late list + s.earlyList = s.earlyList[:len(s.earlyList)-1] + +} + +func (s *splice) toSingleLinkedList() []*ffcapi.MinimalBlockInfo { + if s.earlyList[len(s.earlyList)-1].IsParentOf(s.lateList[0]) { + return append(s.earlyList, s.lateList...) + } + // cannot be linked because the last block in the early list is not the parent of the first block in the late list + return nil + +} + +// createEarlyList will return a list of blocks that starts with the latest transaction block and followed by any blocks in the existing confirmations list that are still valid +// any blocks that are not contiguous will be discarded +func createEarlyList(existingConfirmations []*ffcapi.MinimalBlockInfo, txBlockInfo *ffcapi.MinimalBlockInfo, reconcileResult *ffcapi.ConfirmationUpdateResult) (earlyList []*ffcapi.MinimalBlockInfo) { + if len(existingConfirmations) > 0 { + if !existingConfirmations[0].Equal(txBlockInfo) { + // we discard the existing confirmations list if the transaction block doesn't match + reconcileResult.NewFork = true + } else { + // validate and trim the confirmations list to only include linked blocks + + earlyList = []*ffcapi.MinimalBlockInfo{txBlockInfo} + for i := 1; i < len(existingConfirmations); i++ { + if !earlyList[i-1].IsParentOf(existingConfirmations[i]) { + // set rebuilt flag to true to indicate the existing confirmations list is not contiguous + reconcileResult.Rebuilt = true + break + } + earlyList = append(earlyList, existingConfirmations[i]) + } + } + + } + + if len(earlyList) == 0 { + // either because this is the first time we are reconciling this transaction or because we just discarded the existing confirmations queue + earlyList = []*ffcapi.MinimalBlockInfo{txBlockInfo} + } + return earlyList +} + +func createLateList(ctx context.Context, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64, blockListener *blockListener) (lateList []*ffcapi.MinimalBlockInfo, err error) { + lateList, err = blockListener.buildConfirmationQueueUsingInMemoryPartialChain(ctx, txBlockInfo, targetConfirmationCount) + if err != nil { + return nil, err + } + + // If the late list is empty, it may be because the chain has moved on so far and the transaction is so old that + // we no longer have the target block in memory. Lets try to grab the target block from the blockchain and work backwards from there. + if len(lateList) == 0 { + targetBlockInfo, _, err := blockListener.getBlockInfoByNumber(ctx, txBlockInfo.BlockNumber.Uint64()+targetConfirmationCount, false, "", "") + if err != nil { + return nil, err + } + lateList = []*ffcapi.MinimalBlockInfo{ + { + BlockNumber: fftypes.FFuint64(targetBlockInfo.Number.BigInt().Uint64()), + BlockHash: targetBlockInfo.Hash.String(), + ParentHash: targetBlockInfo.ParentHash.String(), + }, + } + } + return lateList, nil +} + +// validateChainCaughtUp checks if the in-memory partial chain has caught up to the transaction block. +// Returns an error if the chain is not initialized or if the chain tail is behind the transaction block. +func (bl *blockListener) validateChainCaughtUp(ctx context.Context, txBlockInfo *ffcapi.MinimalBlockInfo, txBlockNumber uint64) error { + chainTailElement := bl.canonicalChain.Back() + if chainTailElement == nil { + return i18n.NewError(ctx, msgs.MsgInMemoryPartialChainNotCaughtUp, txBlockNumber, txBlockInfo.BlockHash) + } + chainTail := chainTailElement.Value.(*ffcapi.MinimalBlockInfo) + if chainTail == nil || chainTail.BlockNumber.Uint64() < txBlockNumber { + log.L(ctx).Debugf("in-memory partial chain is waiting for the transaction block %d (%s) to be indexed", txBlockNumber, txBlockInfo.BlockHash) + return i18n.NewError(ctx, msgs.MsgInMemoryPartialChainNotCaughtUp, txBlockNumber, txBlockInfo.BlockHash) + } + return nil +} + +// buildConfirmationQueueUsingInMemoryPartialChain builds the late list using the in-memory partial chain. +// It does not modify the in-memory partial chain itself, only reads from it. +// This function holds a read lock on the in-memory partial chain, so it should not make long-running queries. +func (bl *blockListener) buildConfirmationQueueUsingInMemoryPartialChain(ctx context.Context, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) (newConfirmationsWithoutTxBlock []*ffcapi.MinimalBlockInfo, err error) { + bl.mux.RLock() + defer bl.mux.RUnlock() + txBlockNumber := txBlockInfo.BlockNumber.Uint64() + targetBlockNumber := txBlockInfo.BlockNumber.Uint64() + targetConfirmationCount + + // Check if the in-memory partial chain has caught up to the transaction block + err = bl.validateChainCaughtUp(ctx, txBlockInfo, txBlockNumber) + if err != nil { + return nil, err + } + + // Build new confirmations from blocks after the transaction block + + newConfirmationsWithoutTxBlock = []*ffcapi.MinimalBlockInfo{} + nextInMemoryBlock := bl.canonicalChain.Front() + for nextInMemoryBlock != nil && nextInMemoryBlock.Value != nil { + nextInMemoryBlockInfo := nextInMemoryBlock.Value.(*ffcapi.MinimalBlockInfo) + + // If we've reached the target confirmation count, mark as confirmed + if nextInMemoryBlockInfo.BlockNumber.Uint64() > targetBlockNumber { + break + } + + // Skip blocks at or before the transaction block + if nextInMemoryBlockInfo.BlockNumber.Uint64() <= txBlockNumber { + nextInMemoryBlock = nextInMemoryBlock.Next() + continue + } + + // Add blocks after the transaction block to confirmations + newConfirmationsWithoutTxBlock = append(newConfirmationsWithoutTxBlock, &ffcapi.MinimalBlockInfo{ + BlockHash: nextInMemoryBlockInfo.BlockHash, + BlockNumber: fftypes.FFuint64(nextInMemoryBlockInfo.BlockNumber.Uint64()), + ParentHash: nextInMemoryBlockInfo.ParentHash, + }) + nextInMemoryBlock = nextInMemoryBlock.Next() + } + return newConfirmationsWithoutTxBlock, nil +} + +func (bl *blockListener) handleZeroTargetConfirmationCount(ctx context.Context, txBlockInfo *ffcapi.MinimalBlockInfo) (*ffcapi.ConfirmationUpdateResult, error) { + bl.mux.RLock() + defer bl.mux.RUnlock() + // if the target confirmation count is 0, and the transaction blocks is before the last block in the in-memory partial chain, + // we can immediately return a confirmed result + txBlockNumber := txBlockInfo.BlockNumber.Uint64() + err := bl.validateChainCaughtUp(ctx, txBlockInfo, txBlockNumber) + if err != nil { + return nil, err + } + + return &ffcapi.ConfirmationUpdateResult{ + Confirmed: true, + Confirmations: []*ffcapi.MinimalBlockInfo{txBlockInfo}, + }, nil + +} + +func (bl *blockListener) handleTargetCountMetWithEarlyList(existingConfirmations []*ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) *ffcapi.ConfirmationUpdateResult { + bl.mux.RLock() + defer bl.mux.RUnlock() + nextInMemoryBlock := bl.canonicalChain.Front() + var nextInMemoryBlockInfo *ffcapi.MinimalBlockInfo + lastExistingConfirmation := existingConfirmations[len(existingConfirmations)-1] + // iterates to the block that immediately after the last existing confirmation + for nextInMemoryBlock != nil && nextInMemoryBlock.Value != nil { + nextInMemoryBlockInfo = nextInMemoryBlock.Value.(*ffcapi.MinimalBlockInfo) + if nextInMemoryBlockInfo.BlockNumber.Uint64() >= lastExistingConfirmation.BlockNumber.Uint64()+1 { + break + } + nextInMemoryBlock = nextInMemoryBlock.Next() + } + + if nextInMemoryBlockInfo != nil && lastExistingConfirmation.IsParentOf(nextInMemoryBlockInfo) { + // the existing confirmation are connected to the in memory partial chain so we can return them without fetching any more blocks + return &ffcapi.ConfirmationUpdateResult{ + Confirmed: true, + Confirmations: existingConfirmations[:targetConfirmationCount+1], + } + } + return nil +} diff --git a/internal/ethereum/confirmation_reconciler_test.go b/internal/ethereum/confirmation_reconciler_test.go new file mode 100644 index 0000000..c36210f --- /dev/null +++ b/internal/ethereum/confirmation_reconciler_test.go @@ -0,0 +1,1301 @@ +// 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" + "strconv" + "testing" + + lru "github.com/hashicorp/golang-lru" + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-evmconnect/mocks/rpcbackendmocks" + "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.Error(t, err) + assert.Regexp(t, "FF23061", err) + assert.Nil(t, result) + + 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.MinimalBlockInfo{}, 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.MinimalBlockInfo{ + {BlockNumber: fftypes.FFuint64(1977), BlockHash: generateTestHash(1977), ParentHash: generateTestHash(1976)}, + }, 5) + + // Assertions - expect an error when transaction doesn't exist + assert.Error(t, err) + assert.Regexp(t, "FF23061", err) + assert.Nil(t, result) + + 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.MinimalBlockInfo{}, 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.MinimalBlockInfo{}, 5) + + // Assertions - expect the transaction block to be returned + // we trust the block retrieve by getBlockInfoContainsTxHash function more than the canonical chain + // and we allow the canonical chain to be updated at its own pace + // therefore, if the tx block is different from the block of same number in the canonical chain, we should return the tx block for now + // and wait for the canonical chain to be updated + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.NewFork) + assert.False(t, result.Confirmed) + assert.Len(t, result.Confirmations, 2) + assert.Equal(t, uint64(5), result.TargetConfirmationCount) + assert.NotNil(t, result.Receipt) + 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.MinimalBlockInfo{}, 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.NewFork) + assert.False(t, result.Confirmed) + assert.Equal(t, []*ffcapi.MinimalBlockInfo{ + {BlockNumber: fftypes.FFuint64(1977), BlockHash: generateTestHash(1977), ParentHash: generateTestHash(1976)}, + {BlockNumber: fftypes.FFuint64(1978), BlockHash: generateTestHash(1978), ParentHash: generateTestHash(1977)}, + }, result.Confirmations) + assert.Equal(t, uint64(5), result.TargetConfirmationCount) + assert.NotNil(t, result.Receipt) + + mRPC.AssertExpectations(t) +} + +func TestReconcileConfirmationsForTransaction_DifferentTxBlock(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.MinimalBlockInfo{ + {BlockNumber: fftypes.FFuint64(1979), BlockHash: generateTestHash(1979), ParentHash: generateTestHash(1978)}, + {BlockNumber: fftypes.FFuint64(1980), BlockHash: generateTestHash(1980), ParentHash: generateTestHash(1979)}, + }, 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.True(t, result.NewFork) + assert.False(t, result.Confirmed) + assert.Equal(t, []*ffcapi.MinimalBlockInfo{ + {BlockNumber: fftypes.FFuint64(1977), BlockHash: generateTestHash(1977), ParentHash: generateTestHash(1976)}, + {BlockNumber: fftypes.FFuint64(1978), BlockHash: generateTestHash(1978), ParentHash: generateTestHash(1977)}, + }, result.Confirmations) + assert.Equal(t, uint64(5), result.TargetConfirmationCount) + assert.NotNil(t, result.Receipt) + mRPC.AssertExpectations(t) +} + +func TestBuildConfirmationList_GapInExistingConfirmationsShouldBeFilledIn(t *testing.T) { + // Setup + bl, done := newBlockListenerWithTestChain(t, 100, 5, 102, 150, []uint64{101}) + defer done() + ctx := context.Background() + + // Create corrupted confirmation (gap in the existing confirmations list) + existingQueue := []*ffcapi.MinimalBlockInfo{ + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + // gap in the existing confirmations list + {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(5) + + // Execute + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) + + // Assert + assert.False(t, confirmationUpdateResult.NewFork) + assert.True(t, confirmationUpdateResult.Confirmed) + assert.Len(t, confirmationUpdateResult.Confirmations, 6) + assert.Equal(t, txBlockNumber, uint64(confirmationUpdateResult.Confirmations[0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(confirmationUpdateResult.Confirmations[1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(confirmationUpdateResult.Confirmations[2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(confirmationUpdateResult.Confirmations[3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(confirmationUpdateResult.Confirmations[4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(confirmationUpdateResult.Confirmations[5].BlockNumber)) + +} + +func TestBuildConfirmationList_MismatchConfirmationBlockShouldBeReplaced(t *testing.T) { + // Setup + bl, done := newBlockListenerWithTestChain(t, 100, 5, 102, 150, []uint64{101}) + defer done() + ctx := context.Background() + + // Create corrupted confirmation (gap in the existing confirmations list) + existingQueue := []*ffcapi.MinimalBlockInfo{ + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: generateTestHash(999), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, // wrong hash and is a fork + {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, + {BlockHash: generateTestHash(103), BlockNumber: fftypes.FFuint64(103), ParentHash: generateTestHash(102)}, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(5) + + // Execute + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) + + // Assert + assert.True(t, confirmationUpdateResult.NewFork) + assert.True(t, confirmationUpdateResult.Confirmed) + assert.Len(t, confirmationUpdateResult.Confirmations, 6) + assert.Equal(t, txBlockNumber, uint64(confirmationUpdateResult.Confirmations[0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(confirmationUpdateResult.Confirmations[1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(confirmationUpdateResult.Confirmations[2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(confirmationUpdateResult.Confirmations[3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(confirmationUpdateResult.Confirmations[4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(confirmationUpdateResult.Confirmations[5].BlockNumber)) +} + +func TestBuildConfirmationList_ExistingTxBockInfoIsWrongShouldBeIgnored(t *testing.T) { + // Setup + bl, done := newBlockListenerWithTestChain(t, 100, 5, 102, 150, []uint64{101}) + defer done() + ctx := context.Background() + + // Create corrupted confirmation (gap in the existing confirmations list) + existingQueue := []*ffcapi.MinimalBlockInfo{ + {BlockHash: generateTestHash(999), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, // incorrect block number + {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(5) + + // Execute + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) + // Assert + assert.True(t, confirmationUpdateResult.NewFork) + assert.True(t, confirmationUpdateResult.Confirmed) + assert.Len(t, confirmationUpdateResult.Confirmations, 6) + assert.Equal(t, txBlockNumber, uint64(confirmationUpdateResult.Confirmations[0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(confirmationUpdateResult.Confirmations[1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(confirmationUpdateResult.Confirmations[2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(confirmationUpdateResult.Confirmations[3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(confirmationUpdateResult.Confirmations[4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(confirmationUpdateResult.Confirmations[5].BlockNumber)) +} + +func TestReconcileConfirmationsForTransaction_ExistingConfirmationsWithLowerBlockNumberShouldBeIgnored(t *testing.T) { + // Setup + bl, done := newBlockListenerWithTestChain(t, 100, 5, 102, 150, []uint64{101}) + defer done() + ctx := context.Background() + + // Create corrupted confirmation (gap in the existing confirmations list) + 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 + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(5) + + // Execute + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) + // Assert + assert.False(t, confirmationUpdateResult.NewFork) + assert.True(t, confirmationUpdateResult.Confirmed) + assert.Len(t, confirmationUpdateResult.Confirmations, 6) + assert.Equal(t, txBlockNumber, uint64(confirmationUpdateResult.Confirmations[0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(confirmationUpdateResult.Confirmations[1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(confirmationUpdateResult.Confirmations[2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(confirmationUpdateResult.Confirmations[3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(confirmationUpdateResult.Confirmations[4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(confirmationUpdateResult.Confirmations[5].BlockNumber)) +} + +// Tests of the buildConfirmationList function + +func TestBuildConfirmationList_EmptyChain(t *testing.T) { + // Setup - create a chain with one block that's older than the transaction + bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 50, []uint64{}) + defer done() + ctx := context.Background() + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(txBlockNumber) + + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(txBlockNumber - 1), + } + targetConfirmationCount := uint64(5) + + // Execute + // Assert - should return early due to chain being too short + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, []*ffcapi.MinimalBlockInfo{}, txBlockInfo, targetConfirmationCount) + assert.Error(t, err) + assert.Regexp(t, "FF23062", err.Error()) + assert.Nil(t, confirmationUpdateResult) +} + +func TestBuildConfirmationQueueUsingInMemoryPartialChain_EmptyCanonicalChain(t *testing.T) { + // Setup - create a blockListener with an empty canonical chain + mRPC := &rpcbackendmocks.Backend{} + bl := &blockListener{ + canonicalChain: list.New(), // Empty canonical chain + backend: mRPC, + } + bl.blockCache, _ = lru.New(100) + + ctx := context.Background() + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(txBlockNumber) + + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(txBlockNumber - 1), + } + targetConfirmationCount := uint64(5) + + // Execute - should return error when canonical chain is empty + _, err := bl.buildConfirmationQueueUsingInMemoryPartialChain(ctx, txBlockInfo, targetConfirmationCount) + + // Assert - expect error with code FF23062 for empty canonical chain + assert.Error(t, err) + assert.Regexp(t, "FF23062", err.Error()) + mRPC.AssertExpectations(t) +} + +func TestHandleZeroTargetConfirmationCount_EmptyCanonicalChain(t *testing.T) { + // Setup - create a blockListener with an empty canonical chain + mRPC := &rpcbackendmocks.Backend{} + bl := &blockListener{ + canonicalChain: list.New(), // Empty canonical chain + backend: mRPC, + } + bl.blockCache, _ = lru.New(100) + + ctx := context.Background() + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(txBlockNumber) + + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(txBlockNumber - 1), + } + + // Execute - should return error when canonical chain is empty + result, err := bl.handleZeroTargetConfirmationCount(ctx, txBlockInfo) + + // Assert - expect error with code FF23062 for empty canonical chain + assert.Error(t, err) + assert.Regexp(t, "FF23062", err.Error()) + assert.Nil(t, result) + mRPC.AssertExpectations(t) +} + +func TestBuildConfirmationList_ChainTooShort(t *testing.T) { + // Setup + bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 99, []uint64{}) + defer done() + ctx := context.Background() + + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(txBlockNumber) + + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(txBlockNumber - 1), + } + targetConfirmationCount := uint64(5) + + // Execute + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, []*ffcapi.MinimalBlockInfo{}, txBlockInfo, targetConfirmationCount) + assert.Error(t, err) + assert.Nil(t, confirmationUpdateResult) +} + +func TestBuildConfirmationList_NilConfirmationMap(t *testing.T) { + // Setup + bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 150, []uint64{}) + defer done() + ctx := context.Background() + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(txBlockNumber) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(txBlockNumber - 1), + } + targetConfirmationCount := uint64(5) + + // Execute + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, nil, txBlockInfo, targetConfirmationCount) + // Assert + assert.NoError(t, err) + assert.NotNil(t, confirmationUpdateResult) + assert.False(t, confirmationUpdateResult.NewFork) + assert.True(t, confirmationUpdateResult.Confirmed) + assert.Len(t, confirmationUpdateResult.Confirmations, 6) + assert.Equal(t, txBlockNumber, uint64(confirmationUpdateResult.Confirmations[0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(confirmationUpdateResult.Confirmations[1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(confirmationUpdateResult.Confirmations[2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(confirmationUpdateResult.Confirmations[3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(confirmationUpdateResult.Confirmations[4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(confirmationUpdateResult.Confirmations[5].BlockNumber)) + +} + +func TestBuildConfirmationList_NilConfirmationMap_ZeroConfirmationCount(t *testing.T) { + // Setup + bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 150, []uint64{}) + defer done() + ctx := context.Background() + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(txBlockNumber) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(txBlockNumber - 1), + } + targetConfirmationCount := uint64(0) + + // Execute + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, nil, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) + // Assert + assert.NotNil(t, confirmationUpdateResult.Confirmations) + assert.False(t, confirmationUpdateResult.NewFork) + assert.True(t, confirmationUpdateResult.Confirmed) + // The code builds a full confirmation queue from the canonical chain + assert.Len(t, confirmationUpdateResult.Confirmations, 1) + assert.Equal(t, txBlockNumber, uint64(confirmationUpdateResult.Confirmations[0].BlockNumber)) +} + +func TestBuildConfirmationList_NilConfirmationMap_ZeroConfirmationCountError(t *testing.T) { + // Setup + bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 99, []uint64{}) + defer done() + ctx := context.Background() + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(txBlockNumber) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(txBlockNumber - 1), + } + targetConfirmationCount := uint64(0) + + // Execute + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, nil, txBlockInfo, targetConfirmationCount) + assert.Error(t, err) + assert.Nil(t, confirmationUpdateResult) + assert.Regexp(t, "FF23062", err.Error()) +} + +func TestBuildConfirmationList_NilConfirmationMapUnconfirmed(t *testing.T) { + // Setup + bl, done := newBlockListenerWithTestChain(t, 100, 5, 100, 104, []uint64{}) + defer done() + ctx := context.Background() + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(txBlockNumber) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(txBlockNumber - 1), + } + targetConfirmationCount := uint64(5) + + // Execute + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, nil, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) + // Assert + assert.NotNil(t, confirmationUpdateResult.Confirmations) + assert.False(t, confirmationUpdateResult.NewFork) + assert.False(t, confirmationUpdateResult.Confirmed) + // The code builds a confirmation queue from the canonical chain up to the available blocks + assert.Len(t, confirmationUpdateResult.Confirmations, 5) // 100, 101, 102, 103, 104 + assert.Equal(t, txBlockNumber, uint64(confirmationUpdateResult.Confirmations[0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(confirmationUpdateResult.Confirmations[1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(confirmationUpdateResult.Confirmations[2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(confirmationUpdateResult.Confirmations[3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(confirmationUpdateResult.Confirmations[4].BlockNumber)) + +} + +func TestBuildConfirmationList_EmptyConfirmationQueue(t *testing.T) { + // Setup + bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 150, []uint64{}) + defer done() + ctx := context.Background() + + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(txBlockNumber) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(txBlockNumber - 1), + } + targetConfirmationCount := uint64(5) + + // Execute + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, []*ffcapi.MinimalBlockInfo{}, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) + // Assert + assert.False(t, confirmationUpdateResult.NewFork) + assert.True(t, confirmationUpdateResult.Confirmed) + // The code builds a full confirmation queue from the canonical chain + assert.Len(t, confirmationUpdateResult.Confirmations, 6) + assert.Equal(t, txBlockNumber, uint64(confirmationUpdateResult.Confirmations[0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(confirmationUpdateResult.Confirmations[1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(confirmationUpdateResult.Confirmations[2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(confirmationUpdateResult.Confirmations[3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(confirmationUpdateResult.Confirmations[4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(confirmationUpdateResult.Confirmations[5].BlockNumber)) +} + +func TestBuildConfirmationList_ExistingConfirmationsTooDistant(t *testing.T) { + // Setup + + bl, done := newBlockListenerWithTestChain(t, 100, 5, 145, 150, []uint64{102, 103, 104, 105}) + defer done() + 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)}, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(5) + + // Execute + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) + // Assert all confirmations are in the confirmation queue + assert.False(t, confirmationUpdateResult.NewFork) + assert.True(t, confirmationUpdateResult.Confirmed) + assert.Len(t, confirmationUpdateResult.Confirmations, 6) + assert.Equal(t, txBlockNumber, uint64(confirmationUpdateResult.Confirmations[0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(confirmationUpdateResult.Confirmations[1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(confirmationUpdateResult.Confirmations[2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(confirmationUpdateResult.Confirmations[3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(confirmationUpdateResult.Confirmations[4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(confirmationUpdateResult.Confirmations[5].BlockNumber)) +} + +func TestBuildConfirmationList_CorruptedExistingConfirmationDoNotAffectConfirmations(t *testing.T) { + // Setup + bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 150, []uint64{}) + defer done() + + 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"}, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(5) + + // Execute + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) + // Assert + assert.False(t, confirmationUpdateResult.NewFork) + assert.True(t, confirmationUpdateResult.Confirmed) + assert.Len(t, confirmationUpdateResult.Confirmations, 6) + assert.Equal(t, txBlockNumber, uint64(confirmationUpdateResult.Confirmations[0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(confirmationUpdateResult.Confirmations[1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(confirmationUpdateResult.Confirmations[2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(confirmationUpdateResult.Confirmations[3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(confirmationUpdateResult.Confirmations[4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(confirmationUpdateResult.Confirmations[5].BlockNumber)) +} + +func TestBuildConfirmationList_ConnectionNodeMismatch(t *testing.T) { + // Setup + bl, done := newBlockListenerWithTestChain(t, 100, 5, 102, 150, []uint64{101}) + defer done() + 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)}, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(5) + + // Execute + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) + // Assert + assert.True(t, confirmationUpdateResult.NewFork) + assert.True(t, confirmationUpdateResult.Confirmed) + assert.Len(t, confirmationUpdateResult.Confirmations, 6) + assert.Equal(t, txBlockNumber, uint64(confirmationUpdateResult.Confirmations[0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(confirmationUpdateResult.Confirmations[1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(confirmationUpdateResult.Confirmations[2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(confirmationUpdateResult.Confirmations[3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(confirmationUpdateResult.Confirmations[4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(confirmationUpdateResult.Confirmations[5].BlockNumber)) +} + +func TestBuildConfirmationList_FailedToFetchBlockInfo(t *testing.T) { + // Setup + mRPC := &rpcbackendmocks.Backend{} + bl := &blockListener{ + canonicalChain: createTestChain(150, 150), + backend: mRPC, + } + bl.blockCache, _ = lru.New(100) + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByNumber", mock.MatchedBy(func(bn *ethtypes.HexInteger) bool { + return bn.BigInt().String() == strconv.FormatUint(105, 10) + }), false).Return(&rpcbackend.RPCError{Message: "pop"}) + + 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"}, + } + + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(5) + + // Execute + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) + assert.Error(t, err) + assert.Regexp(t, "pop", err.Error()) + assert.Nil(t, confirmationUpdateResult) +} + +func TestBuildConfirmationList_NilBlockInfo(t *testing.T) { + // Setup + mRPC := &rpcbackendmocks.Backend{} + bl := &blockListener{ + canonicalChain: createTestChain(150, 150), + backend: mRPC, + } + bl.blockCache, _ = lru.New(100) + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByNumber", mock.MatchedBy(func(bn *ethtypes.HexInteger) bool { + return bn.BigInt().String() == strconv.FormatUint(105, 10) + }), false).Return(nil).Run(func(args mock.Arguments) { + *args[1].(**blockInfoJSONRPC) = nil + }) + + 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"}, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(5) + + // Execute + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) + assert.Error(t, err) + assert.Regexp(t, "FF23011", err.Error()) + assert.Nil(t, confirmationUpdateResult) +} + +func TestBuildConfirmationList_NewForkAfterFirstConfirmation(t *testing.T) { + // Setup + bl, done := newBlockListenerWithTestChain(t, 100, 5, 100, 150, []uint64{}) + defer done() + + 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)}, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(5) + + // Execute + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) + // Assert + assert.True(t, confirmationUpdateResult.NewFork) + assert.True(t, confirmationUpdateResult.Confirmed) + assert.Len(t, confirmationUpdateResult.Confirmations, 6) +} + +func TestBuildConfirmationList_NewForkAfterFirstConfirmation_ZeroConfirmationCount(t *testing.T) { + // Setup + bl, done := newBlockListenerWithTestChain(t, 100, 5, 100, 150, []uint64{}) + defer done() + 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)}, + } + + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(0) + + // Execute + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) + // Assert + assert.False(t, confirmationUpdateResult.NewFork) + assert.True(t, confirmationUpdateResult.Confirmed) + assert.Len(t, confirmationUpdateResult.Confirmations, 1) +} + +func TestBuildConfirmationList_NewForkAndNoConnectionToCanonicalChain(t *testing.T) { + // Setup + bl, done := newBlockListenerWithTestChain(t, 100, 5, 103, 150, []uint64{101, 102}) + defer done() + 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"}, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(5) + + // Execute + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) + // Assert + assert.True(t, confirmationUpdateResult.NewFork) + assert.True(t, confirmationUpdateResult.Confirmed) + assert.Len(t, confirmationUpdateResult.Confirmations, 6) +} + +func TestBuildConfirmationList_ConfirmWithNoFetches(t *testing.T) { + // Setup + bl, done := newBlockListenerWithTestChain(t, 100, 5, 102, 150, []uint64{}) + defer done() + 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)}, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(2) + + // Execute + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) + // Assert + assert.True(t, confirmationUpdateResult.Confirmed) + assert.False(t, confirmationUpdateResult.NewFork) + assert.Len(t, confirmationUpdateResult.Confirmations, 3) + assert.Equal(t, txBlockNumber, uint64(confirmationUpdateResult.Confirmations[0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(confirmationUpdateResult.Confirmations[1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(confirmationUpdateResult.Confirmations[2].BlockNumber)) +} + +func TestBuildConfirmationList_AlreadyConfirmable(t *testing.T) { + // Setup + bl, done := newBlockListenerWithTestChain(t, 100, 5, 103, 150, []uint64{}) + defer done() + 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)}, + {BlockHash: generateTestHash(104), BlockNumber: fftypes.FFuint64(104), ParentHash: generateTestHash(103)}, + {BlockHash: generateTestHash(105), BlockNumber: fftypes.FFuint64(105), ParentHash: generateTestHash(104)}, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(2) + + // Execute + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) + // Assert + assert.True(t, confirmationUpdateResult.Confirmed) + assert.False(t, confirmationUpdateResult.NewFork) + assert.Len(t, confirmationUpdateResult.Confirmations, 3) + assert.Equal(t, txBlockNumber, uint64(confirmationUpdateResult.Confirmations[0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(confirmationUpdateResult.Confirmations[1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(confirmationUpdateResult.Confirmations[2].BlockNumber)) +} + +func TestBuildConfirmationList_AlreadyConfirmable_ZeroConfirmationCount(t *testing.T) { + // Setup + bl, done := newBlockListenerWithTestChain(t, 100, 5, 103, 150, []uint64{}) + defer done() + 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 + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(0) + + // Execute + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) + // Assert + assert.True(t, confirmationUpdateResult.Confirmed) + assert.False(t, confirmationUpdateResult.NewFork) + assert.Len(t, confirmationUpdateResult.Confirmations, 1) + assert.Equal(t, txBlockNumber, uint64(confirmationUpdateResult.Confirmations[0].BlockNumber)) +} + +func TestBuildConfirmationList_AlreadyConfirmableConnectable(t *testing.T) { + // Setup + bl, done := newBlockListenerWithTestChain(t, 100, 5, 103, 150, []uint64{}) + defer done() + 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 + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(1) + + // Execute + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) + // Assert + // The confirmation queue should return the confirmation queue up to the first block of the canonical chain + + assert.True(t, confirmationUpdateResult.Confirmed) + assert.False(t, confirmationUpdateResult.NewFork) + assert.Len(t, confirmationUpdateResult.Confirmations, 2) + assert.Equal(t, txBlockNumber, uint64(confirmationUpdateResult.Confirmations[0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(confirmationUpdateResult.Confirmations[1].BlockNumber)) +} + +func TestBuildConfirmationList_HasSufficientConfirmationsButNoOverlapWithCanonicalChain(t *testing.T) { + // Setup + bl, done := newBlockListenerWithTestChain(t, 100, 5, 104, 150, []uint64{101}) + defer done() + 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)}, + } + + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(1) + + // Execute + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) + // 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, confirmationUpdateResult.Confirmed) + assert.False(t, confirmationUpdateResult.NewFork) + assert.Len(t, confirmationUpdateResult.Confirmations, 2) + assert.Equal(t, txBlockNumber, uint64(confirmationUpdateResult.Confirmations[0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(confirmationUpdateResult.Confirmations[1].BlockNumber)) + +} + +func TestBuildConfirmationList_ConfirmableWithLateList(t *testing.T) { + // Setup + bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 150, nil) + defer done() + 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)}, + {BlockHash: generateTestHash(104), BlockNumber: fftypes.FFuint64(104), ParentHash: generateTestHash(103)}, + } + + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(5) + + // Execute + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) + // 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, confirmationUpdateResult.Confirmed) + assert.False(t, confirmationUpdateResult.NewFork) + assert.Len(t, confirmationUpdateResult.Confirmations, 6) + assert.Equal(t, txBlockNumber, uint64(confirmationUpdateResult.Confirmations[0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(confirmationUpdateResult.Confirmations[1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(confirmationUpdateResult.Confirmations[2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(confirmationUpdateResult.Confirmations[3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(confirmationUpdateResult.Confirmations[4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(confirmationUpdateResult.Confirmations[5].BlockNumber)) +} + +func TestBuildConfirmationList_ValidExistingConfirmations(t *testing.T) { + // Setup + bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 150, []uint64{}) + defer done() + 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)}, + } + + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(5) + + // Execute + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) + // Assert + assert.False(t, confirmationUpdateResult.NewFork) + assert.True(t, confirmationUpdateResult.Confirmed) + assert.Len(t, confirmationUpdateResult.Confirmations, 6) + assert.Equal(t, txBlockNumber, uint64(confirmationUpdateResult.Confirmations[0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(confirmationUpdateResult.Confirmations[1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(confirmationUpdateResult.Confirmations[2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(confirmationUpdateResult.Confirmations[3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(confirmationUpdateResult.Confirmations[4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(confirmationUpdateResult.Confirmations[5].BlockNumber)) +} + +func TestBuildConfirmationList_ValidExistingTxBlock(t *testing.T) { + // Setup + bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 150, []uint64{}) + defer done() + ctx := context.Background() + existingQueue := []*ffcapi.MinimalBlockInfo{ + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + } + + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(5) + + // Execute + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) + // Assert + assert.False(t, confirmationUpdateResult.NewFork) + assert.True(t, confirmationUpdateResult.Confirmed) + assert.Len(t, confirmationUpdateResult.Confirmations, 6) + assert.Equal(t, txBlockNumber, uint64(confirmationUpdateResult.Confirmations[0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(confirmationUpdateResult.Confirmations[1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(confirmationUpdateResult.Confirmations[2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(confirmationUpdateResult.Confirmations[3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(confirmationUpdateResult.Confirmations[4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(confirmationUpdateResult.Confirmations[5].BlockNumber)) +} + +func TestBuildConfirmationList_ReachTargetConfirmation(t *testing.T) { + // Setup + bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 150, []uint64{}) + defer done() + ctx := context.Background() + + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(3) + + // Execute + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, []*ffcapi.MinimalBlockInfo{}, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) + // Assert + assert.True(t, confirmationUpdateResult.Confirmed) + // The code builds a full confirmation queue from the canonical chain + assert.GreaterOrEqual(t, len(confirmationUpdateResult.Confirmations), 4) // tx block + 3 confirmations +} + +// 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 +} + +func newBlockListenerWithTestChain(t *testing.T, txBlock, confirmationCount, startCanonicalBlock, endCanonicalBlock uint64, blocksToMock []uint64) (*blockListener, func()) { + mRPC := &rpcbackendmocks.Backend{} + bl := &blockListener{ + canonicalChain: createTestChain(startCanonicalBlock, endCanonicalBlock), + backend: mRPC, + } + bl.blockCache, _ = lru.New(100) + + if len(blocksToMock) > 0 { + for _, blockNumber := range blocksToMock { + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByNumber", mock.MatchedBy(func(bn *ethtypes.HexInteger) bool { + return bn.BigInt().String() == strconv.FormatUint(blockNumber, 10) + }), false).Return(nil).Run(func(args mock.Arguments) { + *args[1].(**blockInfoJSONRPC) = &blockInfoJSONRPC{ + Number: ethtypes.NewHexInteger64(int64(blockNumber)), + Hash: ethtypes.MustNewHexBytes0xPrefix(generateTestHash(blockNumber)), + ParentHash: ethtypes.MustNewHexBytes0xPrefix(generateTestHash(blockNumber - 1)), + } + }) + } + } + return bl, func() { + mRPC.AssertExpectations(t) + } +} 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..9fbeda2 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..2bb98ec 100644 --- a/internal/msgs/en_error_messages.go +++ b/internal/msgs/en_error_messages.go @@ -27,50 +27,56 @@ var ffe = func(key, translation string, statusHint ...int) i18n.ErrorMessageKey //revive:disable var ( - MsgRequestTypeNotImplemented = ffe("FF23010", "FFCAPI request '%s' not currently supported") - MsgBlockNotAvailable = ffe("FF23011", "Block not available") - MsgReceiptNotAvailable = ffe("FF23012", "Receipt not available for transaction '%s'") - MsgUnmarshalABIMethodFail = ffe("FF23013", "Failed to parse method ABI: %s") - MsgUnmarshalParamFail = ffe("FF23014", "Failed to parse parameter %d: %s") - MsgGasPriceError = ffe("FF23015", `The gasPrice '%s' could not be parsed. Please supply a numeric string, or an object with 'gasPrice' field, or 'maxFeePerGas'/'maxPriorityFeePerGas' fields (EIP-1559)`) - MsgInvalidOutputType = ffe("FF23016", "Invalid output type: %s") - MsgInvalidGasPrice = ffe("FF23017", "Failed to parse gasPrice '%s': %s") - MsgInvalidTXData = ffe("FF23018", "Failed to parse transaction data as hex '%s': %s") - MsgInvalidFromAddress = ffe("FF23019", "Invalid 'from' address '%s': %s") - MsgInvalidToAddress = ffe("FF23020", "Invalid 'to' address '%s': %s") - MsgReverted = ffe("FF23021", "EVM reverted: %s") - MsgReturnDataInvalid = ffe("FF23023", "EVM return data invalid: %s") - MsgNotInitialized = ffe("FF23024", "Not initialized") - MsgMissingBackendURL = ffe("FF23025", "URL must be set for the backend JSON/RPC endpoint") - MsgBadVersion = ffe("FF23026", "Bad FFCAPI Version '%s': %s") - MsgUnsupportedVersion = ffe("FF23027", "Unsupported FFCAPI Version '%s'") - MsgUnsupportedRequestType = ffe("FF23028", "Unsupported FFCAPI request type '%s'") - MsgMissingRequestID = ffe("FF23029", "Missing FFCAPI request id") - MsgUnknownConnector = ffe("FF23031", "Unknown connector type: '%s'") - MsgBadDataFormat = ffe("FF23032", "Unknown data format option '%s' supported: %s") - MsgInvalidListenerOptions = ffe("FF23033", "Invalid listener options supplied: %v") - MsgInvalidFromBlock = ffe("FF23034", "Invalid fromBlock '%s'") - MsgMissingEventFilter = ffe("FF23035", "Missing event filter - must specify one or more event filters") - MsgInvalidEventFilter = ffe("FF23036", "Invalid event filter: %s") - MsgMissingEventInFilter = ffe("FF23037", "Each filter must have an 'event' child containing the ABI definition of the event") - MsgListenerAlreadyStarted = ffe("FF23038", "Listener already started: %s") - MsgInvalidCheckpoint = ffe("FF23039", "Invalid checkpoint: %s") - MsgCacheInitFail = ffe("FF23040", "Failed to initialize %s cache") - MsgStreamNotStarted = ffe("FF23041", "Event stream %s not started") - MsgStreamAlreadyStarted = ffe("FF23042", "Event stream %s already started") - MsgListenerNotStarted = ffe("FF23043", "Event listener %s not started in event stream %s") - MsgListenerNotInitialized = ffe("FF23044", "Event listener %s not initialized in event stream %s") - MsgStreamNotStopped = ffe("FF23045", "Event stream %s not stopped") - MsgTimedOutQueryingChainHead = ffe("FF23046", "Timed out waiting for chain head block number") - MsgDecodeBytecodeFailed = ffe("FF23047", "Failed to decode 'bytecode' as hex or Base64") - MsgInvalidTXHashReturned = ffe("FF23048", "Received invalid transaction hash from node len=%d") - MsgUnmarshalErrorFail = ffe("FF23049", "Failed to parse error %d: %s") - MsgUnmarshalABIErrorsFail = ffe("FF23050", "Failed to parse errors ABI: %s") - MsgInvalidRegex = ffe("FF23051", "Invalid regular expression for auto-backoff catchup error: %s") - MsgUnableToCallDebug = ffe("FF23052", "Failed to call debug_traceTransaction to get error detail: %s") - MsgReturnValueNotDecoded = ffe("FF23053", "Error return value for custom error: %s") - MsgReturnValueNotAvailable = ffe("FF23054", "Error return value unavailable") - 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'") + MsgRequestTypeNotImplemented = ffe("FF23010", "FFCAPI request '%s' not currently supported") + MsgBlockNotAvailable = ffe("FF23011", "Block not available") + MsgReceiptNotAvailable = ffe("FF23012", "Receipt not available for transaction '%s'") + MsgUnmarshalABIMethodFail = ffe("FF23013", "Failed to parse method ABI: %s") + MsgUnmarshalParamFail = ffe("FF23014", "Failed to parse parameter %d: %s") + MsgGasPriceError = ffe("FF23015", `The gasPrice '%s' could not be parsed. Please supply a numeric string, or an object with 'gasPrice' field, or 'maxFeePerGas'/'maxPriorityFeePerGas' fields (EIP-1559)`) + MsgInvalidOutputType = ffe("FF23016", "Invalid output type: %s") + MsgInvalidGasPrice = ffe("FF23017", "Failed to parse gasPrice '%s': %s") + MsgInvalidTXData = ffe("FF23018", "Failed to parse transaction data as hex '%s': %s") + MsgInvalidFromAddress = ffe("FF23019", "Invalid 'from' address '%s': %s") + MsgInvalidToAddress = ffe("FF23020", "Invalid 'to' address '%s': %s") + MsgReverted = ffe("FF23021", "EVM reverted: %s") + MsgReturnDataInvalid = ffe("FF23023", "EVM return data invalid: %s") + MsgNotInitialized = ffe("FF23024", "Not initialized") + MsgMissingBackendURL = ffe("FF23025", "URL must be set for the backend JSON/RPC endpoint") + MsgBadVersion = ffe("FF23026", "Bad FFCAPI Version '%s': %s") + MsgUnsupportedVersion = ffe("FF23027", "Unsupported FFCAPI Version '%s'") + MsgUnsupportedRequestType = ffe("FF23028", "Unsupported FFCAPI request type '%s'") + MsgMissingRequestID = ffe("FF23029", "Missing FFCAPI request id") + MsgUnknownConnector = ffe("FF23031", "Unknown connector type: '%s'") + MsgBadDataFormat = ffe("FF23032", "Unknown data format option '%s' supported: %s") + MsgInvalidListenerOptions = ffe("FF23033", "Invalid listener options supplied: %v") + MsgInvalidFromBlock = ffe("FF23034", "Invalid fromBlock '%s'") + MsgMissingEventFilter = ffe("FF23035", "Missing event filter - must specify one or more event filters") + MsgInvalidEventFilter = ffe("FF23036", "Invalid event filter: %s") + MsgMissingEventInFilter = ffe("FF23037", "Each filter must have an 'event' child containing the ABI definition of the event") + MsgListenerAlreadyStarted = ffe("FF23038", "Listener already started: %s") + MsgInvalidCheckpoint = ffe("FF23039", "Invalid checkpoint: %s") + MsgCacheInitFail = ffe("FF23040", "Failed to initialize %s cache") + MsgStreamNotStarted = ffe("FF23041", "Event stream %s not started") + MsgStreamAlreadyStarted = ffe("FF23042", "Event stream %s already started") + MsgListenerNotStarted = ffe("FF23043", "Event listener %s not started in event stream %s") + MsgListenerNotInitialized = ffe("FF23044", "Event listener %s not initialized in event stream %s") + MsgStreamNotStopped = ffe("FF23045", "Event stream %s not stopped") + MsgTimedOutQueryingChainHead = ffe("FF23046", "Timed out waiting for chain head block number") + MsgDecodeBytecodeFailed = ffe("FF23047", "Failed to decode 'bytecode' as hex or Base64") + MsgInvalidTXHashReturned = ffe("FF23048", "Received invalid transaction hash from node len=%d") + MsgUnmarshalErrorFail = ffe("FF23049", "Failed to parse error %d: %s") + MsgUnmarshalABIErrorsFail = ffe("FF23050", "Failed to parse errors ABI: %s") + MsgInvalidRegex = ffe("FF23051", "Invalid regular expression for auto-backoff catchup error: %s") + MsgUnableToCallDebug = ffe("FF23052", "Failed to call debug_traceTransaction to get error detail: %s") + MsgReturnValueNotDecoded = ffe("FF23053", "Error return value for custom error: %s") + MsgReturnValueNotAvailable = ffe("FF23054", "Error return value unavailable") + 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") + MsgFailedToBuildConfirmationQueue = ffe("FF23060", "Failed to build confirmation") + MsgTransactionNotFound = ffe("FF23061", "Transaction not found: %s") + MsgInMemoryPartialChainNotCaughtUp = ffe("FF23062", "In-memory partial chain is waiting for the transaction block %d (%s) to be indexed") + MsgFailedToBuildExistingConfirmationInvalid = ffe("FF23063", "Failed to build confirmations, existing confirmations are not valid") ) diff --git a/mocks/fftmmocks/manager.go b/mocks/fftmmocks/manager.go index 6134d08..40e8c7a 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, existingConfirmations, targetConfirmationCount +func (_m *Manager) ReconcileConfirmationsForTransaction(ctx context.Context, txHash string, existingConfirmations []*ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) (*ffcapi.ConfirmationUpdateResult, error) { + ret := _m.Called(ctx, txHash, existingConfirmations, targetConfirmationCount) + + if len(ret) == 0 { + panic("no return value specified for ReconcileConfirmationsForTransaction") + } + + var r0 *ffcapi.ConfirmationUpdateResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, []*ffcapi.MinimalBlockInfo, uint64) (*ffcapi.ConfirmationUpdateResult, error)); ok { + return rf(ctx, txHash, existingConfirmations, targetConfirmationCount) + } + if rf, ok := ret.Get(0).(func(context.Context, string, []*ffcapi.MinimalBlockInfo, uint64) *ffcapi.ConfirmationUpdateResult); ok { + r0 = rf(ctx, txHash, existingConfirmations, targetConfirmationCount) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ffcapi.ConfirmationUpdateResult) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, []*ffcapi.MinimalBlockInfo, uint64) error); ok { + r1 = rf(ctx, txHash, existingConfirmations, 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