From 4e82ee636888cc251b57a2ef52dd513d1670a431 Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Wed, 10 Sep 2025 01:04:03 +0100 Subject: [PATCH 01/37] adding function to calculate comfirmation map state Signed-off-by: Chengxuan Xing --- internal/ethereum/blocklistener.go | 9 +- internal/ethereum/confirmation_reconciler.go | 317 +++++++ .../ethereum/confirmation_reconciler_test.go | 888 ++++++++++++++++++ internal/msgs/en_error_messages.go | 2 + 4 files changed, 1212 insertions(+), 4 deletions(-) create mode 100644 internal/ethereum/confirmation_reconciler.go create mode 100644 internal/ethereum/confirmation_reconciler_test.go diff --git a/internal/ethereum/blocklistener.go b/internal/ethereum/blocklistener.go index 6cd4675..003e1b9 100644 --- a/internal/ethereum/blocklistener.go +++ b/internal/ethereum/blocklistener.go @@ -57,15 +57,16 @@ type blockListener struct { newHeadsTap chan struct{} newHeadsSub rpcbackend.Subscription highestBlock int64 - mux sync.Mutex + mux sync.RWMutex consumers map[fftypes.UUID]*blockUpdateConsumer blockPollingInterval time.Duration - unstableHeadLength int - canonicalChain *list.List hederaCompatibilityMode bool blockCache *lru.Cache -} + // canonical chain + unstableHeadLength int + canonicalChain *list.List +} type minimalBlockInfo struct { number int64 hash string diff --git a/internal/ethereum/confirmation_reconciler.go b/internal/ethereum/confirmation_reconciler.go new file mode 100644 index 0000000..d34e920 --- /dev/null +++ b/internal/ethereum/confirmation_reconciler.go @@ -0,0 +1,317 @@ +// 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/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" +) + +type ConfirmationMapUpdateResult struct { + *ConfirmationMap + HasNewFork bool `json:"hasNewFork"` // when set to true, it means a fork is detected based on the existing confirmations + Rebuilt bool `json:"rebuilt"` // when set to true, it means all of the existing confirmations are discarded + HasNewConfirmation bool `json:"hasNewConfirmation"` // when set to true, it means new blocks from canonical chain are added to the confirmation queue + Confirmed bool `json:"confirmed"` // when set to true, it means the confirmation queue is complete and all the blocks are confirmed + TargetConfirmationCount int `json:"targetConfirmationCount"` // the target number of confirmations for this event +} + +type ConfirmationMap struct { + // confirmation map is contains a list of possible confirmations for a transaction + // the key is the hash of the first block that contains the transaction hash + // the first block is the block that contains the transaction hash + ConfirmationQueueMap map[string][]*apitypes.Confirmation `json:"confirmationQueueMap,omitempty"` + // which block hash that leads a confirmation queue matches the canonical block hash + CanonicalBlockHash string `json:"canonicalBlockHash,omitempty"` +} + +func (bl *blockListener) BuildConfirmationsForTransaction(ctx context.Context, txHash string, confirmMap *ConfirmationMap, targetConfirmationCount int) (*ConfirmationMapUpdateResult, error) { + // Initialize the output context + occ := &ConfirmationMapUpdateResult{ + ConfirmationMap: confirmMap, + HasNewFork: false, + Rebuilt: false, + HasNewConfirmation: false, + Confirmed: false, + TargetConfirmationCount: targetConfirmationCount, + } + + // Query the chain to find the transaction block + // Note: should consider have an in-memory map of transaction hash to block for faster lookup + // The extra memory usage of the map should be outweighed by the speed of lookup + // But I saw we have a minimalBlockInfo struct that intentionally removes the tx hashes + // so need to figure out the reason first + + res, reason, receiptErr := bl.c.TransactionReceipt(ctx, &ffcapi.TransactionReceiptRequest{ + TransactionHash: txHash, + }) + if receiptErr != nil || res == nil { + if receiptErr != nil && reason != ffcapi.ErrorReasonNotFound { + log.L(ctx).Debugf("Failed to query receipt for transaction %s: %s", txHash, receiptErr) + return nil, i18n.WrapError(ctx, receiptErr, msgs.MsgFailedToQueryReceipt, txHash) + } + log.L(ctx).Debugf("Receipt for transaction %s not yet available: %v", txHash, receiptErr) + return nil, i18n.WrapError(ctx, receiptErr, msgs.MsgFailedToQueryReceipt, txHash) + } + + txBlockHash := res.BlockHash + txBlockNumber := res.BlockNumber.Int64() + // get the parent hash of the transaction block + bi, _, err := bl.getBlockInfoByNumber(ctx, txBlockNumber, true, txBlockHash) + if err != nil { + return nil, i18n.WrapError(ctx, err, msgs.MsgFailedToQueryBlockInfo, txHash) + } + txBlockInfo := &minimalBlockInfo{ + number: bi.Number.BigInt().Int64(), + hash: bi.Hash.String(), + parentHash: bi.ParentHash.String(), + } + + // Compare the existing confirmation queue with the in-memory linked list + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + return occ, nil +} + +// NOTE: this function only build up the confirmation queue uses the in-memory canonical chain +// it does not build up the canonical chain +// compareAndUpdateConfirmationQueue compares the existing confirmation queue with the in-memory linked list + +func (bl *blockListener) compareAndUpdateConfirmationQueue(ctx context.Context, occ *ConfirmationMapUpdateResult, txBlockInfo *minimalBlockInfo, targetConfirmationCount int) { + bl.mux.RLock() + defer bl.mux.RUnlock() + txBlockNumber := txBlockInfo.number + txBlockHash := txBlockInfo.hash + + chainHead := bl.canonicalChain.Front().Value.(*minimalBlockInfo) + chainTail := bl.canonicalChain.Back().Value.(*minimalBlockInfo) + if chainHead == nil || chainTail == nil || chainTail.number < txBlockNumber { + log.L(ctx).Debugf("Canonical chain is waiting for the transaction block %d to be indexed", txBlockNumber) + // we cannot build any useful confirmation information yet, so return the existing confirmation map + return + } + var existingQueue []*apitypes.Confirmation + if occ.ConfirmationMap == nil || len(occ.ConfirmationMap.ConfirmationQueueMap) == 0 { + occ.ConfirmationMap = &ConfirmationMap{ + ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + txBlockHash: {&apitypes.Confirmation{ + BlockHash: txBlockHash, + BlockNumber: fftypes.FFuint64(txBlockNumber), //nolint:gosec // block numbers are always positive + ParentHash: txBlockInfo.parentHash, + }}, + }, + CanonicalBlockHash: txBlockHash, + } + // starting a new confirmation queue + occ.HasNewFork = true + } else { + existingQueue = occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash] + } + + var existingConfirmations []*apitypes.Confirmation + var previousExistingConfirmation *apitypes.Confirmation + + // get the first item of the confirmation queue + // and compare with the transaction block + if len(existingQueue) > 0 { + if existingQueue[0].BlockNumber.Uint64() != uint64(txBlockNumber) { //nolint:gosec // block numbers are always positive + // the existing queue of the current txBlockHash does not have the same block number as the new transaction block + // clear the accumulated confirmation queue + occ.HasNewFork = true + occ.Rebuilt = true + occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash] = []*apitypes.Confirmation{{ + BlockHash: txBlockHash, + BlockNumber: fftypes.FFuint64(txBlockNumber), //nolint:gosec // block numbers are always positive + ParentHash: txBlockInfo.parentHash, + }} + + } else { + existingConfirmations = existingQueue[1:] + if len(existingConfirmations) > 0 { + // check whether the first item in the existing confirmation queue is the next block of the tx block + if existingConfirmations[0].BlockNumber.Uint64() == uint64(txBlockNumber)+1 { //nolint:gosec // block numbers are always positive + // if it is, set the previous existing confirmation to the first item + previousExistingConfirmation = existingQueue[0] + } + // otherwise, don't set the previous existing confirmation + // as we allow gaps between the tx block and the first block in the existing confirmation queue + // NOTE: we don't allow gaps after the first block in the existing confirmation queue + // any gaps, we need to rebuild the confirmations queue + } + } + } + // now start building the new confirmation queue + newQueue := []*apitypes.Confirmation{{ + BlockHash: txBlockHash, + BlockNumber: fftypes.FFuint64(txBlockNumber), //nolint:gosec // block numbers are always positive + ParentHash: txBlockInfo.parentHash, + }} + + // NOTE: the current block might be moved forward as apart of the existing confirmations check + currentBlock := bl.canonicalChain.Front() + // iterate to the tx block if the chain head is earlier than the tx block + if currentBlock != nil && currentBlock.Value.(*minimalBlockInfo).number <= txBlockNumber { + currentBlock = currentBlock.Next() + } + + // NOTE: we assume the first block in the existing confirmation is the lowest block number + // and the last block in the existing confirmation is the highest block number + // check whether the tail of existing confirmations connects to the existing canonical chain + if len(existingConfirmations) > 0 { + lastExistingConfirmation := existingConfirmations[len(existingConfirmations)-1] + if lastExistingConfirmation.BlockNumber.Uint64() < uint64(chainHead.number) && //nolint:gosec // block numbers are always positive + (lastExistingConfirmation.BlockNumber.Uint64() != uint64(chainHead.number-1) || //nolint:gosec // block numbers are always positive + lastExistingConfirmation.BlockHash != chainHead.parentHash) { + // there is no connection between the existing confirmations and the canonical chain + // so we don't need to copy over any of the existing confirmations + // as we won't be able to validate whether they are from the same fork as + // the canonical chain + occ.Rebuilt = true + } else { + // otherwise, the existing confirmations do have overlap with the canonical chain + // so we can give a better view on whether this is a new fork or has new confirmations + queueIndex := 0 + for currentBlock != nil && queueIndex < len(existingConfirmations) { + existingConfirmation := existingConfirmations[queueIndex] + // if any block in the existing confirmation queue is earlier than the tx block + // we need to discard the existing confirmations + if existingConfirmation.BlockNumber.Uint64() <= uint64(txBlockNumber) { //nolint:gosec // block numbers are always positive + occ.Rebuilt = true + newQueue = newQueue[:1] + break + } + // the existing confirmation queue is not tightly controlled by our canonical chain + // so we need to check whether the existing confirmation queue is corrupted + isExistingBlockCorrupted := previousExistingConfirmation != nil && + (previousExistingConfirmation.BlockNumber.Uint64()+1 != existingConfirmation.BlockNumber.Uint64() || + previousExistingConfirmation.BlockHash != existingConfirmation.ParentHash) + currentBlockInfo := currentBlock.Value.(*minimalBlockInfo) + if isExistingBlockCorrupted { + // the existing confirmation queue is corrupted + // so we need to clear out the existing confirmations we copied over + occ.Rebuilt = true + newQueue = newQueue[:1] + break + } + if existingConfirmation.BlockNumber.Uint64() < uint64(currentBlockInfo.number) { //nolint:gosec // block numbers are always positive + // the existing confirmation is earlier than the current block + + // otherwise, we need to add the existing confirmation to the new queue + // NOTE: we are not doing the confirmation count check here + // because we've not reached the current head in the canonical chain to validate + // all the confirmations we copied over are still valid + newQueue = append(newQueue, existingConfirmation) + previousExistingConfirmation = existingConfirmation + queueIndex++ + continue + } else if existingConfirmation.BlockNumber.Uint64() == uint64(currentBlockInfo.number) { //nolint:gosec // block numbers are always positive + // existing confirmation has caught up to the current block + // we also need to check the parent hash matches to decide + // whether the existing confirmations added are still valid + if previousExistingConfirmation.BlockHash != currentBlockInfo.parentHash { + // this indicates a fork that happened before the current block + // so all the previous carried over confirmations are invalid and needs to be cleared out + occ.Rebuilt = true + newQueue = newQueue[:1] + // not adding the current block and do confirmation count check here + // the downstream logic handle all the block addition of the new fork + break + } else if existingConfirmation.BlockHash != currentBlockInfo.hash { + // this indicate the existing confirmation cannot be used, thus all the following confirmations are invalid + occ.HasNewFork = true + break + } + // otherwise, we need to add the existing confirmation to the new queue + newQueue = append(newQueue, existingConfirmation) + + // if the target confirmation count has been reached + if existingConfirmation.BlockNumber.Uint64()-uint64(txBlockNumber) >= uint64(targetConfirmationCount) { //nolint:gosec // block numbers are always positive + // there is a chance the new queue contains more blocks than the target confirmation count + break + } + // move to the next block for checking overlap + currentBlock = currentBlock.Next() + previousExistingConfirmation = existingConfirmation + queueIndex++ + continue + } + // the existing confirmation is later than the current block + // it means we have gaps to fill in, therefore, we need to replace the old confirmations + // and don't bother with what's in the existing confirmation already + occ.Rebuilt = true + newQueue = newQueue[:1] + break + } + // we've just iterated through all the existing confirmations + // now we need to check whether we've got a confirmable queue + lastBlockInNewQueue := newQueue[len(newQueue)-1] // at this point, it's guaranteed to be the highest block number + if lastBlockInNewQueue.BlockNumber.Uint64() >= uint64(txBlockNumber)+uint64(targetConfirmationCount) { //nolint:gosec // block numbers are always positive + // we've got a confirmable so whether the rest of the chain has forked is not longer relevant + // this could happen when user chose a different target confirmation count for the new checks + // but we still need to validate the existing confirmations are connectable to the canonical chain + if lastBlockInNewQueue.BlockNumber.Uint64() >= uint64(chainHead.number) || //nolint:gosec // block numbers are always positive + (lastBlockInNewQueue.BlockNumber.Uint64() == uint64(chainHead.number-1) && //nolint:gosec // block numbers are always positive + lastBlockInNewQueue.BlockHash == chainHead.parentHash) { + occ.HasNewFork = false + occ.HasNewConfirmation = false + occ.Rebuilt = false + occ.Confirmed = true + if lastBlockInNewQueue.BlockNumber.Uint64() == uint64(chainHead.number-1) && //nolint:gosec // block numbers are always positive + lastBlockInNewQueue.BlockHash == chainHead.parentHash { + newQueue = append(newQueue, &apitypes.Confirmation{ + BlockHash: chainHead.hash, + BlockNumber: fftypes.FFuint64(chainHead.number), //nolint:gosec // block numbers are always positive + ParentHash: chainHead.parentHash, + }) + } + // note: we don't trim the queue + occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash] = newQueue + return + } + } + } + } + + // at this point, we know the following: + // 1. the existing confirmations that we copied over are valid + // 2. the new queue is not confirmable just using the existing confirmations + + for currentBlock != nil { + currentBlockInfo := currentBlock.Value.(*minimalBlockInfo) + // only add the blocks that are later than the last block in the newQueue + if currentBlockInfo.number > int64(newQueue[len(newQueue)-1].BlockNumber.Uint64()) { //nolint:gosec // block numbers are always positive + occ.HasNewConfirmation = true + newQueue = append(newQueue, &apitypes.Confirmation{ + BlockHash: currentBlockInfo.hash, + BlockNumber: fftypes.FFuint64(currentBlockInfo.number), //nolint:gosec // block numbers are always positive + ParentHash: currentBlockInfo.parentHash, + }) + if currentBlockInfo.number >= txBlockNumber+int64(targetConfirmationCount) { //nolint:gosec // block numbers are always positive + occ.Confirmed = true + break + } + } + currentBlock = currentBlock.Next() + } + occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash] = newQueue +} diff --git a/internal/ethereum/confirmation_reconciler_test.go b/internal/ethereum/confirmation_reconciler_test.go new file mode 100644 index 0000000..7484002 --- /dev/null +++ b/internal/ethereum/confirmation_reconciler_test.go @@ -0,0 +1,888 @@ +// 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" + "fmt" + "testing" + + "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/stretchr/testify/assert" +) + +func TestCompareAndUpdateConfirmationQueue_EmptyChain(t *testing.T) { + // Setup - create a chain with one block that's older than the transaction + bl := &blockListener{ + canonicalChain: createTestChain(50, 50), // Single block at 50, tx is at 100 + } + ctx := context.Background() + occ := &ConfirmationMapUpdateResult{ + ConfirmationMap: &ConfirmationMap{}, + } + txBlockNumber := int64(100) + txBlockHash := "0xblock100" + + txBlockInfo := &minimalBlockInfo{ + number: txBlockNumber, + hash: txBlockHash, + parentHash: "0xblock99", + } + targetConfirmationCount := 5 + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert - should return early due to chain being too short + assert.NotNil(t, occ.ConfirmationMap) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 0) + assert.NotNil(t, occ.ConfirmationMap) + assert.False(t, occ.HasNewFork) + assert.False(t, occ.HasNewConfirmation) + assert.False(t, occ.Rebuilt) + assert.False(t, occ.Confirmed) +} + +func TestCompareAndUpdateConfirmationQueue_ChainTooShort(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(50, 99), // Chain ends at 99, tx is at 100 + } + ctx := context.Background() + occ := &ConfirmationMapUpdateResult{ + ConfirmationMap: &ConfirmationMap{}, + } + txBlockNumber := int64(100) + txBlockHash := "0xblock100" + + txBlockInfo := &minimalBlockInfo{ + number: txBlockNumber, + hash: txBlockHash, + parentHash: "0xblock99", + } + targetConfirmationCount := 5 + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert - should return early due to chain being too short + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 0) + assert.NotNil(t, occ.ConfirmationMap) + assert.False(t, occ.HasNewFork) + assert.False(t, occ.HasNewConfirmation) + assert.False(t, occ.Rebuilt) + assert.False(t, occ.Confirmed) +} + +func TestCompareAndUpdateConfirmationQueue_NilConfirmationMap(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(50, 150), + } + ctx := context.Background() + occ := &ConfirmationMapUpdateResult{ + ConfirmationMap: nil, + } + txBlockNumber := int64(100) + txBlockHash := "0xblock100" + txBlockInfo := &minimalBlockInfo{ + number: txBlockNumber, + hash: txBlockHash, + parentHash: "0xblock99", + } + targetConfirmationCount := 5 + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.NotNil(t, occ.ConfirmationMap) + assert.True(t, occ.HasNewFork) + assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.Confirmed) + assert.False(t, occ.Rebuilt) + assert.Equal(t, txBlockHash, occ.ConfirmationMap.CanonicalBlockHash) + // The code builds a full confirmation queue from the canonical chain + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) + assert.Equal(t, txBlockNumber, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][5].BlockNumber)) + +} + +func TestCompareAndUpdateConfirmationQueue_NilConfirmationMapUnconfirmed(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(100, 104), + } + ctx := context.Background() + occ := &ConfirmationMapUpdateResult{ + ConfirmationMap: nil, + } + txBlockNumber := int64(100) + txBlockHash := "0xblock100" + txBlockInfo := &minimalBlockInfo{ + number: txBlockNumber, + hash: txBlockHash, + parentHash: "0xblock99", + } + targetConfirmationCount := 5 + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.NotNil(t, occ.ConfirmationMap) + assert.True(t, occ.HasNewFork) + assert.True(t, occ.HasNewConfirmation) + assert.False(t, occ.Confirmed) + assert.False(t, occ.Rebuilt) + assert.Equal(t, txBlockHash, occ.ConfirmationMap.CanonicalBlockHash) + // The code builds a full confirmation queue from the canonical chain + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 5) + assert.Equal(t, txBlockNumber, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][4].BlockNumber)) + +} + +func TestCompareAndUpdateConfirmationQueue_EmptyConfirmationQueue(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(50, 150), + } + ctx := context.Background() + occ := &ConfirmationMapUpdateResult{ + ConfirmationMap: &ConfirmationMap{ + ConfirmationQueueMap: make(map[string][]*apitypes.Confirmation), + }, + } + txBlockNumber := int64(100) + txBlockHash := "0xblock100" + txBlockInfo := &minimalBlockInfo{ + number: txBlockNumber, + hash: txBlockHash, + parentHash: "0xblock99", + } + targetConfirmationCount := 5 + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.True(t, occ.HasNewFork) + assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.Confirmed) + assert.False(t, occ.Rebuilt) + assert.Equal(t, txBlockHash, occ.ConfirmationMap.CanonicalBlockHash) + // The code builds a full confirmation queue from the canonical chain + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) + assert.Equal(t, txBlockNumber, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][5].BlockNumber)) +} + +// theoretically, this should never happen because block hash generation has block number as part of the input +func TestCompareAndUpdateConfirmationQueue_DifferentBlockNumber(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(50, 150), + } + ctx := context.Background() + existingQueue := []*apitypes.Confirmation{ + {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(99), ParentHash: "0xblock99"}, + } + occ := &ConfirmationMapUpdateResult{ + ConfirmationMap: &ConfirmationMap{ + ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + "0xblock100": existingQueue, + }, + }, + } + txBlockNumber := int64(100) + txBlockHash := "0xblock100" + txBlockInfo := &minimalBlockInfo{ + number: txBlockNumber, + hash: txBlockHash, + parentHash: "0xblock99", + } + targetConfirmationCount := 5 + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.True(t, occ.HasNewFork) + assert.True(t, occ.Rebuilt) + // The code builds a full confirmation queue from the canonical chain + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) + assert.Equal(t, txBlockNumber, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][5].BlockNumber)) +} + +func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsTooDistant(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(145, 150), + } + ctx := context.Background() + existingQueue := []*apitypes.Confirmation{ + {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, + {BlockHash: "0xblock101", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xblock100"}, + } + occ := &ConfirmationMapUpdateResult{ + ConfirmationMap: &ConfirmationMap{ + ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + "0xblock100": existingQueue, + }, + }, + } + txBlockNumber := int64(100) + txBlockHash := "0xblock100" + txBlockInfo := &minimalBlockInfo{ + number: txBlockNumber, + hash: txBlockHash, + parentHash: "0xblock99", + } + targetConfirmationCount := 5 + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + // only the tx block and the first block in the canonical chain are in the confirmation queue + // and the transaction is confirmed + assert.False(t, occ.HasNewFork) + assert.True(t, occ.Rebuilt) + assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.Confirmed) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 2) + assert.Equal(t, txBlockNumber, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, bl.canonicalChain.Front().Value.(*minimalBlockInfo).number, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) +} + +func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmation(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(50, 150), + } + ctx := context.Background() + // Create corrupted confirmation (wrong parent hash) + existingQueue := []*apitypes.Confirmation{ + {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, + {BlockHash: "0xblock101", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xwrongparent"}, + } + occ := &ConfirmationMapUpdateResult{ + ConfirmationMap: &ConfirmationMap{ + ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + "0xblock100": existingQueue, + }, + }, + } + txBlockNumber := int64(100) + txBlockHash := "0xblock100" + txBlockInfo := &minimalBlockInfo{ + number: txBlockNumber, + hash: txBlockHash, + parentHash: "0xblock99", + } + targetConfirmationCount := 5 + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.False(t, occ.HasNewFork) + assert.True(t, occ.Rebuilt) + assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.Confirmed) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) + assert.Equal(t, txBlockNumber, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, "0xblock100", occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].ParentHash) +} + +func TestCompareAndUpdateConfirmationQueue_ConnectionNodeMismatch(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(102, 150), + } + ctx := context.Background() + existingQueue := []*apitypes.Confirmation{ + {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, + {BlockHash: "0xblockwrong", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xblock100"}, + {BlockHash: "0xblock102", BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblock101"}, + {BlockHash: "0xblock103", BlockNumber: fftypes.FFuint64(103), ParentHash: "0xblock102"}, + } + occ := &ConfirmationMapUpdateResult{ + ConfirmationMap: &ConfirmationMap{ + ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + "0xblock100": existingQueue, + }, + }, + } + txBlockNumber := int64(100) + txBlockHash := "0xblock100" + txBlockInfo := &minimalBlockInfo{ + number: txBlockNumber, + hash: txBlockHash, + parentHash: "0xblock99", + } + targetConfirmationCount := 5 + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.False(t, occ.HasNewFork) + assert.True(t, occ.Rebuilt) + assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.Confirmed) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 5) + assert.Equal(t, txBlockNumber, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, "0xblock102", occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockHash) + assert.Equal(t, "0xblock102", occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].ParentHash) +} + +func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmationAfterFirstConfirmation(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(100, 150), + } + ctx := context.Background() + existingQueue := []*apitypes.Confirmation{ + {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, + {BlockHash: "0xblock101", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xblock100"}, + {BlockHash: "0xblock102", BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblockwrong"}, + } + occ := &ConfirmationMapUpdateResult{ + ConfirmationMap: &ConfirmationMap{ + ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + "0xblock100": existingQueue, + }, + }, + } + txBlockNumber := int64(100) + txBlockHash := "0xblock100" + txBlockInfo := &minimalBlockInfo{ + number: txBlockNumber, + hash: txBlockHash, + parentHash: "0xblock99", + } + targetConfirmationCount := 5 + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.False(t, occ.HasNewFork) + assert.True(t, occ.Rebuilt) + assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.Confirmed) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 5) + assert.Equal(t, txBlockNumber, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, "0xblock102", occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockHash) + assert.Equal(t, "0xblock102", occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].ParentHash) +} + +func TestCompareAndUpdateConfirmationQueue_NewForkAfterFirstConfirmation(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(100, 150), + } + ctx := context.Background() + existingQueue := []*apitypes.Confirmation{ + {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, + {BlockHash: "0xblock101", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xblock100"}, + {BlockHash: "fork1", BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblock101"}, + } + occ := &ConfirmationMapUpdateResult{ + ConfirmationMap: &ConfirmationMap{ + ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + "0xblock100": existingQueue, + }, + }, + } + txBlockNumber := int64(100) + txBlockHash := "0xblock100" + txBlockInfo := &minimalBlockInfo{ + number: txBlockNumber, + hash: txBlockHash, + parentHash: "0xblock99", + } + targetConfirmationCount := 5 + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.True(t, occ.HasNewFork) + assert.False(t, occ.Rebuilt) + assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.Confirmed) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) +} + +func TestCompareAndUpdateConfirmationQueue_NewFork(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(103, 150), + } + ctx := context.Background() + existingQueue := []*apitypes.Confirmation{ + {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, + {BlockHash: "fork1", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xblock100"}, + {BlockHash: "fork2", BlockNumber: fftypes.FFuint64(102), ParentHash: "fork1"}, + {BlockHash: "fork3", BlockNumber: fftypes.FFuint64(103), ParentHash: "fork2"}, + } + occ := &ConfirmationMapUpdateResult{ + ConfirmationMap: &ConfirmationMap{ + ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + "0xblock100": existingQueue, + }, + }, + } + txBlockNumber := int64(100) + txBlockHash := "0xblock100" + txBlockInfo := &minimalBlockInfo{ + number: txBlockNumber, + hash: txBlockHash, + parentHash: "0xblock99", + } + targetConfirmationCount := 5 + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.False(t, occ.HasNewFork) + assert.True(t, occ.Rebuilt) + assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.Confirmed) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 4) + assert.Equal(t, txBlockNumber, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber+3, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Equal(t, txBlockNumber+4, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) + assert.Equal(t, txBlockNumber+5, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) +} + +func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationLaterThanCurrentBlock(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(100, 150), + } + ctx := context.Background() + existingQueue := []*apitypes.Confirmation{ + {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, + {BlockHash: "0xblock103", BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblock101"}, + } + occ := &ConfirmationMapUpdateResult{ + ConfirmationMap: &ConfirmationMap{ + ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + "0xblock100": existingQueue, + }, + }, + } + txBlockNumber := int64(100) + txBlockHash := "0xblock100" + txBlockInfo := &minimalBlockInfo{ + number: txBlockNumber, + hash: txBlockHash, + parentHash: "0xblock99", + } + targetConfirmationCount := 5 + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.False(t, occ.HasNewFork) + assert.True(t, occ.Rebuilt) + assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.Confirmed) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) + assert.Equal(t, txBlockNumber, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][5].BlockNumber)) +} + +func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(103, 150), + } + ctx := context.Background() + // Create confirmations that already meet the target + // and it connects to the canonical chain to validate they are still valid + existingQueue := []*apitypes.Confirmation{ + {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, + {BlockHash: "0xblock101", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xblock100"}, + {BlockHash: "0xblock102", BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblock101"}, + {BlockHash: "0xblock103", BlockNumber: fftypes.FFuint64(103), ParentHash: "0xblock102"}, + + // all blocks after the first block of the canonical chain are discarded in the final confirmation queue + {BlockHash: "0xblock104", BlockNumber: fftypes.FFuint64(104), ParentHash: "0xblock103"}, // discarded + {BlockHash: "0xblock105", BlockNumber: fftypes.FFuint64(105), ParentHash: "0xblock104"}, // discarded + } + occ := &ConfirmationMapUpdateResult{ + ConfirmationMap: &ConfirmationMap{ + ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + "0xblock100": existingQueue, + }, + }, + } + txBlockNumber := int64(100) + txBlockHash := "0xblock100" + txBlockInfo := &minimalBlockInfo{ + number: txBlockNumber, + hash: txBlockHash, + parentHash: "0xblock99", + } + targetConfirmationCount := 2 + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + // The confirmation queue should return the confirmation queue up to the first block of the canonical chain + + assert.True(t, occ.Confirmed) + assert.False(t, occ.Rebuilt) + assert.False(t, occ.HasNewFork) + assert.False(t, occ.HasNewConfirmation) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 4) + assert.Equal(t, txBlockNumber, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) +} + +func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableConnectable(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(103, 150), + } + ctx := context.Background() + // Create confirmations that already meet the target + // and it connects to the canonical chain to validate they are still valid + existingQueue := []*apitypes.Confirmation{ + {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, + {BlockHash: "0xblock101", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xblock100"}, + {BlockHash: "0xblock102", BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblock101"}, + // didn't have block 103, which is the first block of the canonical chain + // but we should still be able to validate the existing confirmations are valid using parent hash + } + occ := &ConfirmationMapUpdateResult{ + ConfirmationMap: &ConfirmationMap{ + ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + "0xblock100": existingQueue, + }, + }, + } + txBlockNumber := int64(100) + txBlockHash := "0xblock100" + txBlockInfo := &minimalBlockInfo{ + number: txBlockNumber, + hash: txBlockHash, + parentHash: "0xblock99", + } + targetConfirmationCount := 1 + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + // The confirmation queue should return the confirmation queue up to the first block of the canonical chain + + assert.True(t, occ.Confirmed) + assert.False(t, occ.Rebuilt) + assert.False(t, occ.HasNewFork) + assert.False(t, occ.HasNewConfirmation) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 4) + assert.Equal(t, txBlockNumber, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) + +} + +func TestCompareAndUpdateConfirmationQueue_HasSufficientConfirmationsButNoOverlapWithCanonicalChain(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(104, 150), + } + ctx := context.Background() + // Create confirmations that already meet the target + // and it connects to the canonical chain to validate they are still valid + existingQueue := []*apitypes.Confirmation{ + {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, + {BlockHash: "0xblock101", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xblock100"}, + {BlockHash: "0xblock102", BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblock101"}, + } + occ := &ConfirmationMapUpdateResult{ + ConfirmationMap: &ConfirmationMap{ + ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + "0xblock100": existingQueue, + }, + }, + } + txBlockNumber := int64(100) + txBlockHash := "0xblock100" + txBlockInfo := &minimalBlockInfo{ + number: txBlockNumber, + hash: txBlockHash, + parentHash: "0xblock99", + } + targetConfirmationCount := 1 + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + // Because the existing confirmations do not have overlap with the canonical chain, + // the confirmation queue should return the tx block and the first block of the canonical chain + assert.True(t, occ.Confirmed) + assert.False(t, occ.HasNewFork) + assert.True(t, occ.Rebuilt) + assert.True(t, occ.HasNewConfirmation) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 2) + assert.Equal(t, txBlockNumber, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, int64(104), int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + +} + +func TestCompareAndUpdateConfirmationQueue_ValidExistingConfirmations(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(50, 150), + } + ctx := context.Background() + existingQueue := []*apitypes.Confirmation{ + {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, + {BlockHash: "0xblock101", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xblock100"}, + {BlockHash: "0xblock102", BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblock101"}, + } + occ := &ConfirmationMapUpdateResult{ + ConfirmationMap: &ConfirmationMap{ + ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + "0xblock100": existingQueue, + }, + }, + } + txBlockNumber := int64(100) + txBlockHash := "0xblock100" + txBlockInfo := &minimalBlockInfo{ + number: txBlockNumber, + hash: txBlockHash, + parentHash: "0xblock99", + } + targetConfirmationCount := 5 + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.False(t, occ.HasNewFork) + assert.True(t, occ.Rebuilt) + assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.Confirmed) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) + assert.Equal(t, txBlockNumber, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][5].BlockNumber)) +} + +func TestCompareAndUpdateConfirmationQueue_ReachTargetConfirmation(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(50, 150), + } + ctx := context.Background() + occ := &ConfirmationMapUpdateResult{ + ConfirmationMap: &ConfirmationMap{ + ConfirmationQueueMap: make(map[string][]*apitypes.Confirmation), + }, + } + txBlockNumber := int64(100) + txBlockHash := "0xblock100" + txBlockInfo := &minimalBlockInfo{ + number: txBlockNumber, + hash: txBlockHash, + parentHash: "0xblock99", + } + targetConfirmationCount := 3 + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.Confirmed) + // The code builds a full confirmation queue from the canonical chain + assert.GreaterOrEqual(t, len(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash]), 4) // tx block + 3 confirmations +} + +func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithGap(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(101, 150), + } + ctx := context.Background() + // Create confirmations with a gap (missing block 102) + existingQueue := []*apitypes.Confirmation{ + {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, + // no block 101, which is the first block of the canonical chain + {BlockHash: "0xblock102", BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblock101"}, + {BlockHash: "0xblock103", BlockNumber: fftypes.FFuint64(103), ParentHash: "0xblock102"}, + } + occ := &ConfirmationMapUpdateResult{ + ConfirmationMap: &ConfirmationMap{ + ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + "0xblock100": existingQueue, + }, + }, + } + txBlockNumber := int64(100) + txBlockHash := "0xblock100" + txBlockInfo := &minimalBlockInfo{ + number: txBlockNumber, + hash: txBlockHash, + parentHash: "0xblock99", + } + targetConfirmationCount := 5 + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.False(t, occ.HasNewFork) + assert.True(t, occ.Rebuilt) + assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.Confirmed) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) +} + +func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNumber(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(50, 150), + } + ctx := context.Background() + // Create confirmations with a lower block number + existingQueue := []*apitypes.Confirmation{ + {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, + {BlockHash: "0xblock101", BlockNumber: fftypes.FFuint64(99), ParentHash: "0xblock100"}, // somehow there is a lower block number + } + occ := &ConfirmationMapUpdateResult{ + ConfirmationMap: &ConfirmationMap{ + ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + "0xblock100": existingQueue, + }, + }, + } + txBlockNumber := int64(100) + txBlockHash := "0xblock100" + txBlockInfo := &minimalBlockInfo{ + number: txBlockNumber, + hash: txBlockHash, + parentHash: "0xblock99", + } + targetConfirmationCount := 5 + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.False(t, occ.HasNewFork) + assert.True(t, occ.Rebuilt) + assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.Confirmed) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) +} + +func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNumberAfterFirstConfirmation(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(101, 150), + } + ctx := context.Background() + // Create confirmations with a lower block number + existingQueue := []*apitypes.Confirmation{ + {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, + {BlockHash: "0xblock101", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xblock100"}, + {BlockHash: "0xblock102", BlockNumber: fftypes.FFuint64(99), ParentHash: "0xblock101"}, // somehow there is a lower block number + } + occ := &ConfirmationMapUpdateResult{ + ConfirmationMap: &ConfirmationMap{ + ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + "0xblock100": existingQueue, + }, + }, + } + txBlockNumber := int64(100) + txBlockHash := "0xblock100" + txBlockInfo := &minimalBlockInfo{ + number: txBlockNumber, + hash: txBlockHash, + parentHash: "0xblock99", + } + targetConfirmationCount := 5 + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.False(t, occ.HasNewFork) + assert.True(t, occ.Rebuilt) + assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.Confirmed) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) +} + +// Helper functions + +func createTestChain(startBlock, endBlock int64) *list.List { + chain := list.New() + for i := startBlock; i <= endBlock; i++ { + blockInfo := &minimalBlockInfo{ + number: i, + hash: fmt.Sprintf("0xblock%d", i), + parentHash: fmt.Sprintf("0xblock%d", i-1), + } + chain.PushBack(blockInfo) + } + return chain +} diff --git a/internal/msgs/en_error_messages.go b/internal/msgs/en_error_messages.go index e466d8a..56dd6b9 100644 --- a/internal/msgs/en_error_messages.go +++ b/internal/msgs/en_error_messages.go @@ -73,4 +73,6 @@ var ( MsgInvalidProtocolID = ffe("FF23055", "Invalid protocol ID in event log: %s") MsgFailedToRetrieveChainID = ffe("FF23056", "Failed to retrieve chain ID for event enrichment") MsgFailedToRetrieveTransactionInfo = ffe("FF23057", "Failed to retrieve transaction info for transaction hash '%s'") + MsgFailedToQueryReceipt = ffe("FF23058", "Failed to query receipt for transaction %s") + MsgFailedToQueryBlockInfo = ffe("FF23059", "Failed to query block info using hash %s") ) From 2a6447921bafd1de6a7c32886a5d2788a305dde3 Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Wed, 10 Sep 2025 12:16:33 +0100 Subject: [PATCH 02/37] refactor confirmation map reconcile logic Signed-off-by: Chengxuan Xing --- internal/ethereum/blocklistener.go | 9 + internal/ethereum/confirmation_reconciler.go | 338 ++++++++++-------- .../ethereum/confirmation_reconciler_test.go | 95 ++++- 3 files changed, 277 insertions(+), 165 deletions(-) diff --git a/internal/ethereum/blocklistener.go b/internal/ethereum/blocklistener.go index 003e1b9..e95d6ac 100644 --- a/internal/ethereum/blocklistener.go +++ b/internal/ethereum/blocklistener.go @@ -31,6 +31,7 @@ import ( "github.com/hyperledger/firefly-evmconnect/internal/msgs" "github.com/hyperledger/firefly-signer/pkg/ethtypes" "github.com/hyperledger/firefly-signer/pkg/rpcbackend" + "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) @@ -73,6 +74,14 @@ type minimalBlockInfo struct { parentHash string } +func (mbi *minimalBlockInfo) ToConfirmation() *apitypes.Confirmation { + return &apitypes.Confirmation{ + BlockHash: mbi.hash, + BlockNumber: fftypes.FFuint64(mbi.number), //nolint:gosec // block numbers are always positive + ParentHash: mbi.parentHash, + } +} + func newBlockListener(ctx context.Context, c *ethConnector, conf config.Section, wsConf *wsclient.WSConfig) (bl *blockListener, err error) { bl = &blockListener{ ctx: log.WithLogField(ctx, "role", "blocklistener"), diff --git a/internal/ethereum/confirmation_reconciler.go b/internal/ethereum/confirmation_reconciler.go index d34e920..068c520 100644 --- a/internal/ethereum/confirmation_reconciler.go +++ b/internal/ethereum/confirmation_reconciler.go @@ -17,6 +17,7 @@ package ethereum import ( + "container/list" "context" "github.com/hyperledger/firefly-common/pkg/fftypes" @@ -107,198 +108,213 @@ func (bl *blockListener) compareAndUpdateConfirmationQueue(ctx context.Context, chainTail := bl.canonicalChain.Back().Value.(*minimalBlockInfo) if chainHead == nil || chainTail == nil || chainTail.number < txBlockNumber { log.L(ctx).Debugf("Canonical chain is waiting for the transaction block %d to be indexed", txBlockNumber) - // we cannot build any useful confirmation information yet, so return the existing confirmation map return } - var existingQueue []*apitypes.Confirmation + + // Initialize confirmation map and get existing queue + existingQueue := bl.initializeConfirmationMap(occ, txBlockInfo) + + // Validate and process existing confirmations + newQueue, currentBlock := bl.processExistingConfirmations(ctx, occ, txBlockInfo, existingQueue, chainHead, targetConfirmationCount) + + // Build new confirmations from canonical chain only if not already confirmed + if !occ.Confirmed { + newQueue = bl.buildNewConfirmations(occ, newQueue, currentBlock, txBlockNumber, targetConfirmationCount) + } + occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash] = newQueue +} + +func (bl *blockListener) initializeConfirmationMap(occ *ConfirmationMapUpdateResult, txBlockInfo *minimalBlockInfo) []*apitypes.Confirmation { + txBlockHash := txBlockInfo.hash + if occ.ConfirmationMap == nil || len(occ.ConfirmationMap.ConfirmationQueueMap) == 0 { occ.ConfirmationMap = &ConfirmationMap{ ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ - txBlockHash: {&apitypes.Confirmation{ - BlockHash: txBlockHash, - BlockNumber: fftypes.FFuint64(txBlockNumber), //nolint:gosec // block numbers are always positive - ParentHash: txBlockInfo.parentHash, - }}, + txBlockHash: {txBlockInfo.ToConfirmation()}, }, CanonicalBlockHash: txBlockHash, } - // starting a new confirmation queue occ.HasNewFork = true - } else { - existingQueue = occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash] + return nil } - var existingConfirmations []*apitypes.Confirmation - var previousExistingConfirmation *apitypes.Confirmation - - // get the first item of the confirmation queue - // and compare with the transaction block + existingQueue := occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash] if len(existingQueue) > 0 { - if existingQueue[0].BlockNumber.Uint64() != uint64(txBlockNumber) { //nolint:gosec // block numbers are always positive - // the existing queue of the current txBlockHash does not have the same block number as the new transaction block - // clear the accumulated confirmation queue + existingTxBlock := existingQueue[0] + if !isSameBlock(existingTxBlock, txBlockInfo) { + // the tx block in the existing queue does not match the new tx block we queried from the chain + // rebuild a new confirmation queue with the new tx block occ.HasNewFork = true occ.Rebuilt = true - occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash] = []*apitypes.Confirmation{{ - BlockHash: txBlockHash, - BlockNumber: fftypes.FFuint64(txBlockNumber), //nolint:gosec // block numbers are always positive - ParentHash: txBlockInfo.parentHash, - }} - - } else { - existingConfirmations = existingQueue[1:] - if len(existingConfirmations) > 0 { - // check whether the first item in the existing confirmation queue is the next block of the tx block - if existingConfirmations[0].BlockNumber.Uint64() == uint64(txBlockNumber)+1 { //nolint:gosec // block numbers are always positive - // if it is, set the previous existing confirmation to the first item - previousExistingConfirmation = existingQueue[0] - } - // otherwise, don't set the previous existing confirmation - // as we allow gaps between the tx block and the first block in the existing confirmation queue - // NOTE: we don't allow gaps after the first block in the existing confirmation queue - // any gaps, we need to rebuild the confirmations queue - } + occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash] = []*apitypes.Confirmation{txBlockInfo.ToConfirmation()} + return nil } } - // now start building the new confirmation queue - newQueue := []*apitypes.Confirmation{{ - BlockHash: txBlockHash, - BlockNumber: fftypes.FFuint64(txBlockNumber), //nolint:gosec // block numbers are always positive - ParentHash: txBlockInfo.parentHash, - }} - - // NOTE: the current block might be moved forward as apart of the existing confirmations check + + return existingQueue +} + +func (bl *blockListener) processExistingConfirmations(ctx context.Context, occ *ConfirmationMapUpdateResult, txBlockInfo *minimalBlockInfo, existingQueue []*apitypes.Confirmation, chainHead *minimalBlockInfo, targetConfirmationCount int) ([]*apitypes.Confirmation, *list.Element) { + txBlockNumber := txBlockInfo.number + + newQueue := []*apitypes.Confirmation{txBlockInfo.ToConfirmation()} + currentBlock := bl.canonicalChain.Front() // iterate to the tx block if the chain head is earlier than the tx block if currentBlock != nil && currentBlock.Value.(*minimalBlockInfo).number <= txBlockNumber { currentBlock = currentBlock.Next() } - // NOTE: we assume the first block in the existing confirmation is the lowest block number - // and the last block in the existing confirmation is the highest block number - // check whether the tail of existing confirmations connects to the existing canonical chain - if len(existingConfirmations) > 0 { - lastExistingConfirmation := existingConfirmations[len(existingConfirmations)-1] - if lastExistingConfirmation.BlockNumber.Uint64() < uint64(chainHead.number) && //nolint:gosec // block numbers are always positive - (lastExistingConfirmation.BlockNumber.Uint64() != uint64(chainHead.number-1) || //nolint:gosec // block numbers are always positive - lastExistingConfirmation.BlockHash != chainHead.parentHash) { - // there is no connection between the existing confirmations and the canonical chain - // so we don't need to copy over any of the existing confirmations - // as we won't be able to validate whether they are from the same fork as - // the canonical chain + if len(existingQueue) == 0 { + return newQueue, currentBlock + } + + existingConfirmations := existingQueue[1:] + if len(existingConfirmations) == 0 { + return newQueue, currentBlock + } + + return bl.validateExistingConfirmations(ctx, occ, newQueue, existingConfirmations, currentBlock, chainHead, txBlockInfo, targetConfirmationCount) +} + +func (bl *blockListener) validateExistingConfirmations(ctx context.Context, occ *ConfirmationMapUpdateResult, newQueue []*apitypes.Confirmation, existingConfirmations []*apitypes.Confirmation, currentBlock *list.Element, chainHead *minimalBlockInfo, txBlockInfo *minimalBlockInfo, targetConfirmationCount int) ([]*apitypes.Confirmation, *list.Element) { + txBlockNumber := txBlockInfo.number + lastExistingConfirmation := existingConfirmations[len(existingConfirmations)-1] + if lastExistingConfirmation.BlockNumber.Uint64() < uint64(chainHead.number) && //nolint:gosec + // ^^ the highest block number in the existing confirmations is lower than the highest block number in the canonical chain + (lastExistingConfirmation.BlockNumber.Uint64() != uint64(chainHead.number-1) || //nolint:gosec // block numbers are always positive + lastExistingConfirmation.BlockHash != chainHead.parentHash) { + // ^^ and the last existing confirmation is not the parent of the canonical chain head + // Therefore, there is no connection between the existing confirmations and the canonical chain + // so that we cannot validate the existing confirmations are from the same fork as the canonical chain + // so we need to rebuild the confirmations queue + occ.Rebuilt = true + return newQueue, currentBlock + } + + var previousExistingConfirmation *apitypes.Confirmation + queueIndex := 0 + + connectionBlockNumber := currentBlock.Value.(*minimalBlockInfo).number + + for currentBlock != nil && queueIndex < len(existingConfirmations) { + existingConfirmation := existingConfirmations[queueIndex] + if existingConfirmation.BlockNumber.Uint64() <= uint64(txBlockNumber) { //nolint:gosec // block numbers are always positive + log.L(ctx).Debugf("Existing confirmation queue is corrupted, the first block is earlier than the tx block: %d", existingConfirmation.BlockNumber.Uint64()) + // if any block in the existing confirmation queue is earlier than the tx block + // the existing confirmation queue is no valid + // we need to rebuild the confirmations queue occ.Rebuilt = true - } else { - // otherwise, the existing confirmations do have overlap with the canonical chain - // so we can give a better view on whether this is a new fork or has new confirmations - queueIndex := 0 - for currentBlock != nil && queueIndex < len(existingConfirmations) { - existingConfirmation := existingConfirmations[queueIndex] - // if any block in the existing confirmation queue is earlier than the tx block - // we need to discard the existing confirmations - if existingConfirmation.BlockNumber.Uint64() <= uint64(txBlockNumber) { //nolint:gosec // block numbers are always positive - occ.Rebuilt = true - newQueue = newQueue[:1] - break - } - // the existing confirmation queue is not tightly controlled by our canonical chain - // so we need to check whether the existing confirmation queue is corrupted - isExistingBlockCorrupted := previousExistingConfirmation != nil && - (previousExistingConfirmation.BlockNumber.Uint64()+1 != existingConfirmation.BlockNumber.Uint64() || - previousExistingConfirmation.BlockHash != existingConfirmation.ParentHash) - currentBlockInfo := currentBlock.Value.(*minimalBlockInfo) - if isExistingBlockCorrupted { - // the existing confirmation queue is corrupted - // so we need to clear out the existing confirmations we copied over + return newQueue[:1], currentBlock + } + + // the existing confirmation queue is not tightly controlled by our canonical chain + // ^^ even though it supposed to be build by a canonical chain, we cannot rely on it + // because they are stored outside of current system + // Therefore, we need to check whether the existing confirmation queue is corrupted + isCorrupted := previousExistingConfirmation != nil && + (previousExistingConfirmation.BlockNumber.Uint64()+1 != existingConfirmation.BlockNumber.Uint64() || + previousExistingConfirmation.BlockHash != existingConfirmation.ParentHash) || + // check the link between the first confirmation block and the existing tx block + (existingConfirmation.BlockNumber.Uint64() == uint64(txBlockNumber)+1 && //nolint:gosec // block numbers are always positive + existingConfirmation.ParentHash != txBlockInfo.hash) + // we allow gaps between the tx block and the first block in the existing confirmation queue + // NOTE: we don't allow gaps after the first block in the existing confirmation queue + // any gaps, we need to rebuild the confirmations queue + + if isCorrupted { + // any corruption in the existing confirmation queue will cause the confirmation queue to be rebuilt + // we don't keep any of the existing confirmations + occ.Rebuilt = true + return newQueue[:1], currentBlock + } + + currentBlockInfo := currentBlock.Value.(*minimalBlockInfo) + if existingConfirmation.BlockNumber.Uint64() < uint64(currentBlockInfo.number) { //nolint:gosec + // NOTE: we are not doing the confirmation count check here + // because we've not reached the current head in the canonical chain to validate + // all the confirmations we copied over are still valid + newQueue = append(newQueue, existingConfirmation) + previousExistingConfirmation = existingConfirmation + queueIndex++ + continue + } + + if existingConfirmation.BlockNumber.Uint64() == uint64(currentBlockInfo.number) { //nolint:gosec // block numbers are always positive + // existing confirmation has caught up to the current block + // checking the overlaps + + if !isSameBlock(existingConfirmation, currentBlockInfo) { + // we detected a potential fork + if connectionBlockNumber == currentBlockInfo.number && + previousExistingConfirmation.BlockHash != currentBlockInfo.parentHash { + // this is the connection node (first overlap between existing confirmation queue and canonical chain) + // if the first node doesn't chain to to the previous confirmation, it means all the historical confirmation are on a different fork + // therefore, we need to rebuild the confirmations queue occ.Rebuilt = true - newQueue = newQueue[:1] - break - } - if existingConfirmation.BlockNumber.Uint64() < uint64(currentBlockInfo.number) { //nolint:gosec // block numbers are always positive - // the existing confirmation is earlier than the current block - - // otherwise, we need to add the existing confirmation to the new queue - // NOTE: we are not doing the confirmation count check here - // because we've not reached the current head in the canonical chain to validate - // all the confirmations we copied over are still valid - newQueue = append(newQueue, existingConfirmation) - previousExistingConfirmation = existingConfirmation - queueIndex++ - continue - } else if existingConfirmation.BlockNumber.Uint64() == uint64(currentBlockInfo.number) { //nolint:gosec // block numbers are always positive - // existing confirmation has caught up to the current block - // we also need to check the parent hash matches to decide - // whether the existing confirmations added are still valid - if previousExistingConfirmation.BlockHash != currentBlockInfo.parentHash { - // this indicates a fork that happened before the current block - // so all the previous carried over confirmations are invalid and needs to be cleared out - occ.Rebuilt = true - newQueue = newQueue[:1] - // not adding the current block and do confirmation count check here - // the downstream logic handle all the block addition of the new fork - break - } else if existingConfirmation.BlockHash != currentBlockInfo.hash { - // this indicate the existing confirmation cannot be used, thus all the following confirmations are invalid - occ.HasNewFork = true - break - } - // otherwise, we need to add the existing confirmation to the new queue - newQueue = append(newQueue, existingConfirmation) - - // if the target confirmation count has been reached - if existingConfirmation.BlockNumber.Uint64()-uint64(txBlockNumber) >= uint64(targetConfirmationCount) { //nolint:gosec // block numbers are always positive - // there is a chance the new queue contains more blocks than the target confirmation count - break - } - // move to the next block for checking overlap - currentBlock = currentBlock.Next() - previousExistingConfirmation = existingConfirmation - queueIndex++ - continue + return newQueue[:1], currentBlock } - // the existing confirmation is later than the current block - // it means we have gaps to fill in, therefore, we need to replace the old confirmations - // and don't bother with what's in the existing confirmation already - occ.Rebuilt = true - newQueue = newQueue[:1] + + // other scenarios, the historical confirmation are still trustworthy and linked to our canonical chain + occ.HasNewFork = true + return newQueue, currentBlock + } + + newQueue = append(newQueue, existingConfirmation) + if existingConfirmation.BlockNumber.Uint64()-uint64(txBlockNumber) >= uint64(targetConfirmationCount) { //nolint:gosec // block numbers are always positive break } - // we've just iterated through all the existing confirmations - // now we need to check whether we've got a confirmable queue - lastBlockInNewQueue := newQueue[len(newQueue)-1] // at this point, it's guaranteed to be the highest block number - if lastBlockInNewQueue.BlockNumber.Uint64() >= uint64(txBlockNumber)+uint64(targetConfirmationCount) { //nolint:gosec // block numbers are always positive - // we've got a confirmable so whether the rest of the chain has forked is not longer relevant - // this could happen when user chose a different target confirmation count for the new checks - // but we still need to validate the existing confirmations are connectable to the canonical chain - if lastBlockInNewQueue.BlockNumber.Uint64() >= uint64(chainHead.number) || //nolint:gosec // block numbers are always positive - (lastBlockInNewQueue.BlockNumber.Uint64() == uint64(chainHead.number-1) && //nolint:gosec // block numbers are always positive - lastBlockInNewQueue.BlockHash == chainHead.parentHash) { - occ.HasNewFork = false - occ.HasNewConfirmation = false - occ.Rebuilt = false - occ.Confirmed = true - if lastBlockInNewQueue.BlockNumber.Uint64() == uint64(chainHead.number-1) && //nolint:gosec // block numbers are always positive - lastBlockInNewQueue.BlockHash == chainHead.parentHash { - newQueue = append(newQueue, &apitypes.Confirmation{ - BlockHash: chainHead.hash, - BlockNumber: fftypes.FFuint64(chainHead.number), //nolint:gosec // block numbers are always positive - ParentHash: chainHead.parentHash, - }) - } - // note: we don't trim the queue - occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash] = newQueue - return + currentBlock = currentBlock.Next() + previousExistingConfirmation = existingConfirmation + queueIndex++ + continue + } + + occ.Rebuilt = true + return newQueue[:1], currentBlock + } + + // Check if we have enough confirmations + lastBlockInNewQueue := newQueue[len(newQueue)-1] + confirmationBlockNumber := uint64(txBlockNumber) + uint64(targetConfirmationCount) //nolint:gosec // block numbers are always positive + if lastBlockInNewQueue.BlockNumber.Uint64() >= confirmationBlockNumber { + chainHead := bl.canonicalChain.Front().Value.(*minimalBlockInfo) + // we've got a confirmable so whether the rest of the chain has forked is not longer relevant + // this could happen when user chose a different target confirmation count for the new checks + // but we still need to validate the existing confirmations are connectable to the canonical chain + // Check if the queue connects to the canonical chain + if lastBlockInNewQueue.BlockNumber.Uint64() >= uint64(chainHead.number) || //nolint:gosec // block numbers are always positive + (lastBlockInNewQueue.BlockNumber.Uint64() == uint64(chainHead.number-1) && //nolint:gosec // block numbers are always positive + lastBlockInNewQueue.BlockHash == chainHead.parentHash) { + occ.HasNewFork = false + occ.HasNewConfirmation = false + occ.Rebuilt = false + occ.Confirmed = true + + // Trim the queue to only include blocks up to the max confirmation count + trimmedQueue := []*apitypes.Confirmation{} + for _, confirmation := range newQueue { + if confirmation.BlockNumber.Uint64() > confirmationBlockNumber { + break } + trimmedQueue = append(trimmedQueue, confirmation) + } + + // If we've trimmed off all the existing confirmations, we need to add the canonical chain head + // to tell use the head block we used to confirmed the transaction + if len(trimmedQueue) == 1 { + trimmedQueue = append(trimmedQueue, chainHead.ToConfirmation()) } + return trimmedQueue, currentBlock } } - // at this point, we know the following: - // 1. the existing confirmations that we copied over are valid - // 2. the new queue is not confirmable just using the existing confirmations + return newQueue, currentBlock +} +func (bl *blockListener) buildNewConfirmations(occ *ConfirmationMapUpdateResult, newQueue []*apitypes.Confirmation, currentBlock *list.Element, txBlockNumber int64, targetConfirmationCount int) []*apitypes.Confirmation { for currentBlock != nil { currentBlockInfo := currentBlock.Value.(*minimalBlockInfo) - // only add the blocks that are later than the last block in the newQueue if currentBlockInfo.number > int64(newQueue[len(newQueue)-1].BlockNumber.Uint64()) { //nolint:gosec // block numbers are always positive occ.HasNewConfirmation = true newQueue = append(newQueue, &apitypes.Confirmation{ @@ -313,5 +329,11 @@ func (bl *blockListener) compareAndUpdateConfirmationQueue(ctx context.Context, } currentBlock = currentBlock.Next() } - occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash] = newQueue + return newQueue +} + +func isSameBlock(c1 *apitypes.Confirmation, bi *minimalBlockInfo) bool { + return c1.BlockHash == bi.hash && + c1.BlockNumber.Uint64() == uint64(bi.number) && //nolint:gosec // block numbers are always positive + c1.ParentHash == bi.parentHash } diff --git a/internal/ethereum/confirmation_reconciler_test.go b/internal/ethereum/confirmation_reconciler_test.go index 7484002..290531a 100644 --- a/internal/ethereum/confirmation_reconciler_test.go +++ b/internal/ethereum/confirmation_reconciler_test.go @@ -248,6 +248,48 @@ func TestCompareAndUpdateConfirmationQueue_DifferentBlockNumber(t *testing.T) { assert.Equal(t, txBlockNumber+5, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][5].BlockNumber)) } +func TestCompareAndUpdateConfirmationQueue_MismatchConfirmationBlock(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(103, 150), + } + ctx := context.Background() + existingQueue := []*apitypes.Confirmation{ + {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, + {BlockHash: "0xblock101", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xblock101"}, // wrong parent hash, so the existing queue should be discarded + {BlockHash: "0xblock102", BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblock101"}, + {BlockHash: "0xblock103", BlockNumber: fftypes.FFuint64(103), ParentHash: "0xblock102"}, + } + occ := &ConfirmationMapUpdateResult{ + ConfirmationMap: &ConfirmationMap{ + ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + "0xblock100": existingQueue, + }, + }, + } + txBlockNumber := int64(100) + txBlockHash := "0xblock100" + txBlockInfo := &minimalBlockInfo{ + number: txBlockNumber, + hash: txBlockHash, + parentHash: "0xblock99", + } + targetConfirmationCount := 5 + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.False(t, occ.HasNewFork) + assert.True(t, occ.Rebuilt) + // The code builds a full confirmation queue from the canonical chain + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 4) + assert.Equal(t, txBlockNumber, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber+3, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Equal(t, txBlockNumber+4, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) + assert.Equal(t, txBlockNumber+5, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) +} + func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsTooDistant(t *testing.T) { // Setup bl := &blockListener{ @@ -574,17 +616,14 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable(t *testing.T) { bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) // Assert - // The confirmation queue should return the confirmation queue up to the first block of the canonical chain - assert.True(t, occ.Confirmed) assert.False(t, occ.Rebuilt) assert.False(t, occ.HasNewFork) assert.False(t, occ.HasNewConfirmation) - assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 4) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 3) assert.Equal(t, txBlockNumber, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) assert.Equal(t, txBlockNumber+1, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) assert.Equal(t, txBlockNumber+2, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) - assert.Equal(t, txBlockNumber+3, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) } func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableConnectable(t *testing.T) { @@ -628,11 +667,53 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableConnectable(t *test assert.False(t, occ.Rebuilt) assert.False(t, occ.HasNewFork) assert.False(t, occ.HasNewConfirmation) - assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 4) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 2) assert.Equal(t, txBlockNumber, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) assert.Equal(t, txBlockNumber+1, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) - assert.Equal(t, txBlockNumber+2, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) - assert.Equal(t, txBlockNumber+3, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) +} + +func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableButAllExistingConfirmationsAreTooHighForTargetConfirmationCount(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(103, 150), + } + ctx := context.Background() + // Create confirmations that already meet the target + // and it connects to the canonical chain to validate they are still valid + existingQueue := []*apitypes.Confirmation{ + {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, + // gap of 101 is allowed, and is the confirmation required for the transaction with target confirmation count of 1 + {BlockHash: "0xblock102", BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblock101"}, + } + occ := &ConfirmationMapUpdateResult{ + ConfirmationMap: &ConfirmationMap{ + ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + "0xblock100": existingQueue, + }, + }, + } + txBlockNumber := int64(100) + txBlockHash := "0xblock100" + txBlockInfo := &minimalBlockInfo{ + number: txBlockNumber, + hash: txBlockHash, + parentHash: "0xblock99", + } + targetConfirmationCount := 1 + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + // The confirmation queue should return the tx block and the first block of the canonical chain + + assert.True(t, occ.Confirmed) + assert.False(t, occ.Rebuilt) + assert.False(t, occ.HasNewFork) + assert.False(t, occ.HasNewConfirmation) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 2) + assert.Equal(t, txBlockNumber, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, int64(103), int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) } From 8651d54e4d6abc1cbf9d9ebc17bc901ca6e94310 Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Wed, 10 Sep 2025 16:59:37 +0100 Subject: [PATCH 03/37] fixign test and refactor Signed-off-by: Chengxuan Xing --- go.mod | 2 +- go.sum | 8 + internal/ethereum/blocklistener.go | 136 ++--- internal/ethereum/blocklistener_blockquery.go | 39 +- internal/ethereum/blocklistener_test.go | 32 +- internal/ethereum/confirmation_reconciler.go | 164 ++--- .../ethereum/confirmation_reconciler_test.go | 564 +++++++++--------- internal/ethereum/event_listener.go | 10 +- internal/ethereum/event_stream.go | 10 +- internal/ethereum/get_block_info.go | 2 +- mocks/fftmmocks/manager.go | 34 +- mocks/rpcbackendmocks/backend.go | 2 +- 12 files changed, 514 insertions(+), 489 deletions(-) diff --git a/go.mod b/go.mod index 92bec2c..dec7104 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/hashicorp/golang-lru v1.0.2 github.com/hyperledger/firefly-common v1.5.5 github.com/hyperledger/firefly-signer v1.1.21 - github.com/hyperledger/firefly-transaction-manager v1.4.0 + github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250910153533-14142cf9f697 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.0 github.com/stretchr/testify v1.9.0 diff --git a/go.sum b/go.sum index 7fec939..11ca80d 100644 --- a/go.sum +++ b/go.sum @@ -106,6 +106,14 @@ github.com/hyperledger/firefly-signer v1.1.21 h1:r7cTOw6e/6AtiXLf84wZy6Z7zppzlc1 github.com/hyperledger/firefly-signer v1.1.21/go.mod h1:axrlSQeKrd124UdHF5L3MkTjb5DeTcbJxJNCZ3JmcWM= github.com/hyperledger/firefly-transaction-manager v1.4.0 h1:l9DCizLTohKtKec5dewNlydhAeko1/DmTfCRF8le9m0= github.com/hyperledger/firefly-transaction-manager v1.4.0/go.mod h1:mEd9dOH8ds6ajgfPh6nnP3Pd3f8XIZtQRnucqAIJHRs= +github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250910122026-4f65f76eb9eb h1:doSlc4SN1LIA+kMMW/vBDHaud+5Ad5+eWRitbfGBxho= +github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250910122026-4f65f76eb9eb/go.mod h1:KHGvK/QqD2jpRZ04lQoX/k1T2o644NCkRlr3FbvKqnA= +github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250910151057-7bc7bb81591c h1:RNd7cMvH8Mr/wE2Y2B4Vy0+5l0FN4G6UMC4rMqJeFjE= +github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250910151057-7bc7bb81591c/go.mod h1:KHGvK/QqD2jpRZ04lQoX/k1T2o644NCkRlr3FbvKqnA= +github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250910153251-07127ff35b09 h1:1Frz0u69ETPkFbFGcLxPyWJrzUnBv+yVKNdZUfT7wBE= +github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250910153251-07127ff35b09/go.mod h1:KHGvK/QqD2jpRZ04lQoX/k1T2o644NCkRlr3FbvKqnA= +github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250910153533-14142cf9f697 h1:leUNAoiwMidZYwH+F6bmCa7kvN3qcPGH25k2HcMptyg= +github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250910153533-14142cf9f697/go.mod h1:KHGvK/QqD2jpRZ04lQoX/k1T2o644NCkRlr3FbvKqnA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= diff --git a/internal/ethereum/blocklistener.go b/internal/ethereum/blocklistener.go index e95d6ac..a3ac358 100644 --- a/internal/ethereum/blocklistener.go +++ b/internal/ethereum/blocklistener.go @@ -31,7 +31,6 @@ import ( "github.com/hyperledger/firefly-evmconnect/internal/msgs" "github.com/hyperledger/firefly-signer/pkg/ethtypes" "github.com/hyperledger/firefly-signer/pkg/rpcbackend" - "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) @@ -57,7 +56,8 @@ type blockListener struct { initialBlockHeightObtained chan struct{} newHeadsTap chan struct{} newHeadsSub rpcbackend.Subscription - highestBlock int64 + highestBlockSet bool + highestBlock uint64 mux sync.RWMutex consumers map[fftypes.UUID]*blockUpdateConsumer blockPollingInterval time.Duration @@ -68,19 +68,6 @@ type blockListener struct { unstableHeadLength int canonicalChain *list.List } -type minimalBlockInfo struct { - number int64 - hash string - parentHash string -} - -func (mbi *minimalBlockInfo) ToConfirmation() *apitypes.Confirmation { - return &apitypes.Confirmation{ - BlockHash: mbi.hash, - BlockNumber: fftypes.FFuint64(mbi.number), //nolint:gosec // block numbers are always positive - ParentHash: mbi.parentHash, - } -} func newBlockListener(ctx context.Context, c *ethConnector, conf config.Section, wsConf *wsclient.WSConfig) (bl *blockListener, err error) { bl = &blockListener{ @@ -91,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(), @@ -175,10 +163,7 @@ func (bl *blockListener) establishBlockHeightWithRetry() error { return true, rpcErr.Error() } - bl.mux.Lock() - bl.highestBlock = hexBlockHeight.BigInt().Int64() - bl.mux.Unlock() - + bl.setHighestBlock(hexBlockHeight.BigInt().Uint64()) return false, nil }) } @@ -270,7 +255,7 @@ func (bl *blockListener) listenLoop() { default: candidate := bl.reconcileCanonicalChain(bi) // Check this is the lowest position to notify from - if candidate != nil && (notifyPos == nil || candidate.Value.(*minimalBlockInfo).number <= notifyPos.Value.(*minimalBlockInfo).number) { + if candidate != nil && (notifyPos == nil || candidate.Value.(*ffcapi.MinimalBlockInfo).BlockNumber <= notifyPos.Value.(*ffcapi.MinimalBlockInfo).BlockNumber) { notifyPos = candidate } } @@ -278,7 +263,7 @@ func (bl *blockListener) listenLoop() { if notifyPos != nil { // We notify for all hashes from the point of change in the chain onwards for notifyPos != nil { - update.BlockHashes = append(update.BlockHashes, notifyPos.Value.(*minimalBlockInfo).hash) + update.BlockHashes = append(update.BlockHashes, notifyPos.Value.(*ffcapi.MinimalBlockInfo).BlockHash) notifyPos = notifyPos.Next() } @@ -305,16 +290,12 @@ func (bl *blockListener) listenLoop() { // head of the canonical chain we have. If these blocks do not just fit onto the end of the chain, then we // work backwards building a new view and notify about all blocks that are changed in that process. func (bl *blockListener) reconcileCanonicalChain(bi *blockInfoJSONRPC) *list.Element { - mbi := &minimalBlockInfo{ - number: bi.Number.BigInt().Int64(), - hash: bi.Hash.String(), - parentHash: bi.ParentHash.String(), + mbi := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(bi.Number.BigInt().Uint64()), + BlockHash: bi.Hash.String(), + ParentHash: bi.ParentHash.String(), } - bl.mux.Lock() - if mbi.number > bl.highestBlock { - bl.highestBlock = mbi.number - } - bl.mux.Unlock() + bl.checkAndSetHighestBlock(mbi.BlockNumber.Uint64()) // Find the position of this block in the block sequence pos := bl.canonicalChain.Back() @@ -323,15 +304,15 @@ func (bl *blockListener) reconcileCanonicalChain(bi *blockInfoJSONRPC) *list.Ele // We've eliminated all the existing chain (if there was any) return bl.handleNewBlock(mbi, nil) } - posBlock := pos.Value.(*minimalBlockInfo) + posBlock := pos.Value.(*ffcapi.MinimalBlockInfo) switch { - case posBlock.number == mbi.number && posBlock.hash == mbi.hash && posBlock.parentHash == mbi.parentHash: + case posBlock.Equal(mbi): // This is a duplicate - no need to notify of anything return nil - case posBlock.number == mbi.number: + case posBlock.BlockNumber.Uint64() == mbi.BlockNumber.Uint64(): // We are replacing a block in the chain return bl.handleNewBlock(mbi, pos.Prev()) - case posBlock.number < mbi.number: + case posBlock.BlockNumber.Uint64() < mbi.BlockNumber.Uint64(): // We have a position where this block goes return bl.handleNewBlock(mbi, pos) default: @@ -343,14 +324,14 @@ func (bl *blockListener) reconcileCanonicalChain(bi *blockInfoJSONRPC) *list.Ele // handleNewBlock rebuilds the canonical chain around a new block, checking if we need to rebuild our // view of the canonical chain behind it, or trimming anything after it that is invalidated by a new fork. -func (bl *blockListener) handleNewBlock(mbi *minimalBlockInfo, addAfter *list.Element) *list.Element { +func (bl *blockListener) handleNewBlock(mbi *ffcapi.MinimalBlockInfo, addAfter *list.Element) *list.Element { // If we have an existing canonical chain before this point, then we need to check we've not // invalidated that with this block. If we have, then we have to re-verify our whole canonical // chain from the first block. Then notify from the earliest point where it has diverged. if addAfter != nil { - prevBlock := addAfter.Value.(*minimalBlockInfo) - if prevBlock.number != (mbi.number-1) || prevBlock.hash != mbi.parentHash { - log.L(bl.ctx).Infof("Notified of block %d / %s that does not fit after block %d / %s (expected parent: %s)", mbi.number, mbi.hash, prevBlock.number, prevBlock.hash, mbi.parentHash) + prevBlock := addAfter.Value.(*ffcapi.MinimalBlockInfo) + if prevBlock.BlockNumber.Uint64() != (mbi.BlockNumber.Uint64()-1) || prevBlock.BlockHash != mbi.ParentHash { + log.L(bl.ctx).Infof("Notified of block %d / %s that does not fit after block %d / %s (expected parent: %s)", mbi.BlockNumber.Uint64(), mbi.BlockHash, prevBlock.BlockNumber.Uint64(), prevBlock.BlockHash, mbi.ParentHash) return bl.rebuildCanonicalChain() } } @@ -378,7 +359,7 @@ func (bl *blockListener) handleNewBlock(mbi *minimalBlockInfo, addAfter *list.El _ = bl.canonicalChain.Remove(bl.canonicalChain.Front()) } - log.L(bl.ctx).Debugf("Added block %d / %s parent=%s to in-memory canonical chain (new length=%d)", mbi.number, mbi.hash, mbi.parentHash, bl.canonicalChain.Len()) + log.L(bl.ctx).Debugf("Added block %d / %s parent=%s to in-memory canonical chain (new length=%d)", mbi.BlockNumber.Uint64(), mbi.BlockHash, mbi.ParentHash, bl.canonicalChain.Len()) return newElem } @@ -389,18 +370,18 @@ func (bl *blockListener) handleNewBlock(mbi *minimalBlockInfo, addAfter *list.El func (bl *blockListener) rebuildCanonicalChain() *list.Element { // If none of our blocks were valid, start from the first block number we've notified about previously lastValidBlock := bl.trimToLastValidBlock() - var nextBlockNumber int64 + var nextBlockNumber uint64 var expectedParentHash string if lastValidBlock != nil { - nextBlockNumber = lastValidBlock.number + 1 + nextBlockNumber = lastValidBlock.BlockNumber.Uint64() + 1 log.L(bl.ctx).Infof("Canonical chain partially rebuilding from block %d", nextBlockNumber) - expectedParentHash = lastValidBlock.hash + expectedParentHash = lastValidBlock.BlockHash } else { firstBlock := bl.canonicalChain.Front() if firstBlock == nil || firstBlock.Value == nil { return nil } - nextBlockNumber = firstBlock.Value.(*minimalBlockInfo).number + nextBlockNumber = firstBlock.Value.(*ffcapi.MinimalBlockInfo).BlockNumber.Uint64() log.L(bl.ctx).Warnf("Canonical chain re-initialized at block %d", nextBlockNumber) // Clear out the whole chain bl.canonicalChain = bl.canonicalChain.Init() @@ -422,19 +403,19 @@ func (bl *blockListener) rebuildCanonicalChain() *list.Element { log.L(bl.ctx).Infof("Canonical chain rebuilt the chain to the head block %d", nextBlockNumber-1) break } - mbi := &minimalBlockInfo{ - number: bi.Number.BigInt().Int64(), - hash: bi.Hash.String(), - parentHash: bi.ParentHash.String(), + mbi := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(bi.Number.BigInt().Uint64()), + BlockHash: bi.Hash.String(), + ParentHash: bi.ParentHash.String(), } // It's possible the chain will change while we're doing this, and we fall back to the next block notification // to sort that out. - if expectedParentHash != "" && mbi.parentHash != expectedParentHash { - log.L(bl.ctx).Infof("Canonical chain rebuilding stopped at block: %d due to mismatch hash for parent block (%d): %s (expected: %s)", nextBlockNumber, nextBlockNumber-1, mbi.parentHash, expectedParentHash) + if expectedParentHash != "" && mbi.ParentHash != expectedParentHash { + log.L(bl.ctx).Infof("Canonical chain rebuilding stopped at block: %d due to mismatch hash for parent block (%d): %s (expected: %s)", nextBlockNumber, nextBlockNumber-1, mbi.ParentHash, expectedParentHash) break } - expectedParentHash = mbi.hash + expectedParentHash = mbi.BlockHash nextBlockNumber++ // Note we do not trim to a length here, as we need to notify for every block we haven't notified for. @@ -444,33 +425,30 @@ func (bl *blockListener) rebuildCanonicalChain() *list.Element { notifyPos = newElem } - bl.mux.Lock() - if mbi.number > bl.highestBlock { - bl.highestBlock = mbi.number - } - bl.mux.Unlock() + bl.checkAndSetHighestBlock(mbi.BlockNumber.Uint64()) } return notifyPos } -func (bl *blockListener) trimToLastValidBlock() (lastValidBlock *minimalBlockInfo) { +func (bl *blockListener) trimToLastValidBlock() (lastValidBlock *ffcapi.MinimalBlockInfo) { // First remove from the end until we get a block that matches the current un-cached query view from the chain lastElem := bl.canonicalChain.Back() - var startingNumber *int64 + var startingNumber *uint64 for lastElem != nil && lastElem.Value != nil { // Query the block that is no at this blockNumber - currentViewBlock := lastElem.Value.(*minimalBlockInfo) + currentViewBlock := lastElem.Value.(*ffcapi.MinimalBlockInfo) if startingNumber == nil { - startingNumber = ¤tViewBlock.number + currentNumber := currentViewBlock.BlockNumber.Uint64() + startingNumber = ¤tNumber log.L(bl.ctx).Debugf("Canonical chain checking from last block: %d", startingNumber) } var freshBlockInfo *blockInfoJSONRPC var reason ffcapi.ErrorReason err := bl.c.retry.Do(bl.ctx, "rebuild listener canonical chain", func(_ int) (retry bool, err error) { - log.L(bl.ctx).Debugf("Canonical chain validating block: %d", currentViewBlock.number) - freshBlockInfo, reason, err = bl.getBlockInfoByNumber(bl.ctx, currentViewBlock.number, false, "") + log.L(bl.ctx).Debugf("Canonical chain validating block: %d", currentViewBlock.BlockNumber.Uint64()) + freshBlockInfo, reason, err = bl.getBlockInfoByNumber(bl.ctx, currentViewBlock.BlockNumber.Uint64(), false, "") return reason != ffcapi.ErrorReasonNotFound, err }) if err != nil { @@ -479,8 +457,8 @@ func (bl *blockListener) trimToLastValidBlock() (lastValidBlock *minimalBlockInf } } - if freshBlockInfo != nil && freshBlockInfo.Hash.String() == currentViewBlock.hash { - log.L(bl.ctx).Debugf("Canonical chain found last valid block %d", currentViewBlock.number) + if freshBlockInfo != nil && freshBlockInfo.Hash.String() == currentViewBlock.BlockHash { + log.L(bl.ctx).Debugf("Canonical chain found last valid block %d", currentViewBlock.BlockNumber.Uint64()) lastValidBlock = currentViewBlock // Trim everything after this point, as it's invalidated nextElem := lastElem.Next() @@ -494,8 +472,8 @@ func (bl *blockListener) trimToLastValidBlock() (lastValidBlock *minimalBlockInf lastElem = lastElem.Prev() } - if startingNumber != nil && lastValidBlock != nil && *startingNumber != lastValidBlock.number { - log.L(bl.ctx).Debugf("Canonical chain trimmed from block %d to block %d (total number of in memory blocks: %d)", startingNumber, lastValidBlock.number, bl.unstableHeadLength) + if startingNumber != nil && lastValidBlock != nil && *startingNumber != lastValidBlock.BlockNumber.Uint64() { + log.L(bl.ctx).Debugf("Canonical chain trimmed from block %d to block %d (total number of in memory blocks: %d)", startingNumber, lastValidBlock.BlockNumber.Uint64(), bl.unstableHeadLength) } return lastValidBlock } @@ -532,29 +510,45 @@ func (bl *blockListener) addConsumer(ctx context.Context, c *blockUpdateConsumer bl.consumers[*c.id] = c } -func (bl *blockListener) getHighestBlock(ctx context.Context) (int64, bool) { +func (bl *blockListener) getHighestBlock(ctx context.Context) (uint64, bool) { bl.checkAndStartListenerLoop() // block height will be established as the first step of listener startup process // so we don't need to wait for the entire startup process to finish to return the result bl.mux.Lock() - highestBlock := bl.highestBlock + highestBlockSet := bl.highestBlockSet bl.mux.Unlock() // if not yet initialized, wait to be initialized - if highestBlock < 0 { + if !highestBlockSet { select { case <-bl.initialBlockHeightObtained: case <-ctx.Done(): // Inform caller we timed out, or were closed - return -1, false + return 0, false } } bl.mux.Lock() - highestBlock = bl.highestBlock + highestBlock := bl.highestBlock bl.mux.Unlock() log.L(ctx).Debugf("ChainHead=%d", highestBlock) return highestBlock, true } +func (bl *blockListener) setHighestBlock(block uint64) { + bl.mux.Lock() + defer bl.mux.Unlock() + bl.highestBlock = block + bl.highestBlockSet = true +} + +func (bl *blockListener) checkAndSetHighestBlock(block uint64) { + bl.mux.Lock() + defer bl.mux.Unlock() + if block > bl.highestBlock { + bl.highestBlock = block + bl.highestBlockSet = true + } +} + func (bl *blockListener) waitClosed() { bl.mux.Lock() listenLoopDone := bl.listenLoopDone diff --git a/internal/ethereum/blocklistener_blockquery.go b/internal/ethereum/blocklistener_blockquery.go index 3ab9c0b..7694223 100644 --- a/internal/ethereum/blocklistener_blockquery.go +++ b/internal/ethereum/blocklistener_blockquery.go @@ -53,10 +53,43 @@ func (bl *blockListener) addToBlockCache(blockInfo *blockInfoJSONRPC) { bl.blockCache.Add(blockInfo.Number.BigInt().String(), blockInfo) } -func (bl *blockListener) getBlockInfoByNumber(ctx context.Context, blockNumber int64, allowCache bool, expectedHashStr string) (*blockInfoJSONRPC, ffcapi.ErrorReason, error) { +func (bl *blockListener) getBlockInfoContainsTxHash(ctx context.Context, txHash string) (*ffcapi.MinimalBlockInfo, error) { + + // Query the chain to find the transaction block + // Note: should consider have an in-memory map of transaction hash to block for faster lookup + // The extra memory usage of the map should be outweighed by the speed improvement of lookup + // But I saw we have a ffcapi.MinimalBlockInfo struct that intentionally removes the tx hashes + // so need to figure out the reason first + + // TODO: add a cache if map cannot be used + res, _, receiptErr := bl.c.TransactionReceipt(ctx, &ffcapi.TransactionReceiptRequest{ + TransactionHash: txHash, + }) + if receiptErr != nil { + return nil, i18n.WrapError(ctx, receiptErr, msgs.MsgFailedToQueryReceipt, txHash) + } + if res == nil { + return nil, nil + } + txBlockHash := res.BlockHash + txBlockNumber := res.BlockNumber.Uint64() + // get the parent hash of the transaction block + bi, _, err := bl.getBlockInfoByNumber(ctx, txBlockNumber, true, txBlockHash) + if err != nil { + return nil, i18n.WrapError(ctx, err, msgs.MsgFailedToQueryBlockInfo, txHash) + } + + return &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(bi.Number.BigInt().Uint64()), + BlockHash: bi.Hash.String(), + ParentHash: bi.ParentHash.String(), + }, nil +} + +func (bl *blockListener) getBlockInfoByNumber(ctx context.Context, blockNumber uint64, allowCache bool, expectedHashStr string) (*blockInfoJSONRPC, ffcapi.ErrorReason, error) { var blockInfo *blockInfoJSONRPC if allowCache { - cached, ok := bl.blockCache.Get(strconv.FormatInt(blockNumber, 10)) + cached, ok := bl.blockCache.Get(strconv.FormatUint(blockNumber, 10)) if ok { blockInfo = cached.(*blockInfoJSONRPC) if expectedHashStr != "" && blockInfo.ParentHash.String() != expectedHashStr { @@ -67,7 +100,7 @@ func (bl *blockListener) getBlockInfoByNumber(ctx context.Context, blockNumber i } if blockInfo == nil { - rpcErr := bl.backend.CallRPC(ctx, &blockInfo, "eth_getBlockByNumber", ethtypes.NewHexInteger64(blockNumber), false /* only the txn hashes */) + rpcErr := bl.backend.CallRPC(ctx, &blockInfo, "eth_getBlockByNumber", ethtypes.NewHexIntegerU64(blockNumber), false /* only the txn hashes */) if rpcErr != nil { if mapError(blockRPCMethods, rpcErr.Error()) == ffcapi.ErrorReasonNotFound { log.L(ctx).Debugf("Received error signifying 'block not found': '%s'", rpcErr.Message) 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 index 068c520..ba9a66e 100644 --- a/internal/ethereum/confirmation_reconciler.go +++ b/internal/ethereum/confirmation_reconciler.go @@ -21,34 +21,13 @@ 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/apitypes" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) -type ConfirmationMapUpdateResult struct { - *ConfirmationMap - HasNewFork bool `json:"hasNewFork"` // when set to true, it means a fork is detected based on the existing confirmations - Rebuilt bool `json:"rebuilt"` // when set to true, it means all of the existing confirmations are discarded - HasNewConfirmation bool `json:"hasNewConfirmation"` // when set to true, it means new blocks from canonical chain are added to the confirmation queue - Confirmed bool `json:"confirmed"` // when set to true, it means the confirmation queue is complete and all the blocks are confirmed - TargetConfirmationCount int `json:"targetConfirmationCount"` // the target number of confirmations for this event -} - -type ConfirmationMap struct { - // confirmation map is contains a list of possible confirmations for a transaction - // the key is the hash of the first block that contains the transaction hash - // the first block is the block that contains the transaction hash - ConfirmationQueueMap map[string][]*apitypes.Confirmation `json:"confirmationQueueMap,omitempty"` - // which block hash that leads a confirmation queue matches the canonical block hash - CanonicalBlockHash string `json:"canonicalBlockHash,omitempty"` -} - -func (bl *blockListener) BuildConfirmationsForTransaction(ctx context.Context, txHash string, confirmMap *ConfirmationMap, targetConfirmationCount int) (*ConfirmationMapUpdateResult, error) { +func (bl *blockListener) reconcileConfirmationsForTransaction(ctx context.Context, txHash string, confirmMap *ffcapi.ConfirmationMap, targetConfirmationCount uint64) (*ffcapi.ConfirmationMapUpdateResult, error) { // Initialize the output context - occ := &ConfirmationMapUpdateResult{ + occ := &ffcapi.ConfirmationMapUpdateResult{ ConfirmationMap: confirmMap, HasNewFork: false, Rebuilt: false, @@ -57,35 +36,15 @@ func (bl *blockListener) BuildConfirmationsForTransaction(ctx context.Context, t TargetConfirmationCount: targetConfirmationCount, } - // Query the chain to find the transaction block - // Note: should consider have an in-memory map of transaction hash to block for faster lookup - // The extra memory usage of the map should be outweighed by the speed of lookup - // But I saw we have a minimalBlockInfo struct that intentionally removes the tx hashes - // so need to figure out the reason first - - res, reason, receiptErr := bl.c.TransactionReceipt(ctx, &ffcapi.TransactionReceiptRequest{ - TransactionHash: txHash, - }) - if receiptErr != nil || res == nil { - if receiptErr != nil && reason != ffcapi.ErrorReasonNotFound { - log.L(ctx).Debugf("Failed to query receipt for transaction %s: %s", txHash, receiptErr) - return nil, i18n.WrapError(ctx, receiptErr, msgs.MsgFailedToQueryReceipt, txHash) - } - log.L(ctx).Debugf("Receipt for transaction %s not yet available: %v", txHash, receiptErr) - return nil, i18n.WrapError(ctx, receiptErr, msgs.MsgFailedToQueryReceipt, txHash) - } - - txBlockHash := res.BlockHash - txBlockNumber := res.BlockNumber.Int64() - // get the parent hash of the transaction block - bi, _, err := bl.getBlockInfoByNumber(ctx, txBlockNumber, true, txBlockHash) + txBlockInfo, err := bl.getBlockInfoContainsTxHash(ctx, txHash) if err != nil { - return nil, i18n.WrapError(ctx, err, msgs.MsgFailedToQueryBlockInfo, txHash) + log.L(ctx).Errorf("Failed to fetch block info using tx hash %s: %v", txHash, err) + return nil, err } - txBlockInfo := &minimalBlockInfo{ - number: bi.Number.BigInt().Int64(), - hash: bi.Hash.String(), - parentHash: bi.ParentHash.String(), + + if txBlockInfo == nil { + log.L(ctx).Debugf("Transaction %s not found in any block", txHash) + return occ, nil } // Compare the existing confirmation queue with the in-memory linked list @@ -97,16 +56,17 @@ func (bl *blockListener) BuildConfirmationsForTransaction(ctx context.Context, t // NOTE: this function only build up the confirmation queue uses the in-memory canonical chain // it does not build up the canonical chain // compareAndUpdateConfirmationQueue compares the existing confirmation queue with the in-memory linked list +// this function obtains the read lock on the canonical chain, so it should not make any long-running queries -func (bl *blockListener) compareAndUpdateConfirmationQueue(ctx context.Context, occ *ConfirmationMapUpdateResult, txBlockInfo *minimalBlockInfo, targetConfirmationCount int) { +func (bl *blockListener) compareAndUpdateConfirmationQueue(ctx context.Context, occ *ffcapi.ConfirmationMapUpdateResult, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) { bl.mux.RLock() defer bl.mux.RUnlock() - txBlockNumber := txBlockInfo.number - txBlockHash := txBlockInfo.hash + txBlockNumber := txBlockInfo.BlockNumber.Uint64() + txBlockHash := txBlockInfo.BlockHash - chainHead := bl.canonicalChain.Front().Value.(*minimalBlockInfo) - chainTail := bl.canonicalChain.Back().Value.(*minimalBlockInfo) - if chainHead == nil || chainTail == nil || chainTail.number < txBlockNumber { + chainHead := bl.canonicalChain.Front().Value.(*ffcapi.MinimalBlockInfo) + chainTail := bl.canonicalChain.Back().Value.(*ffcapi.MinimalBlockInfo) + if chainHead == nil || chainTail == nil || chainTail.BlockNumber.Uint64() < txBlockNumber { log.L(ctx).Debugf("Canonical chain is waiting for the transaction block %d to be indexed", txBlockNumber) return } @@ -124,13 +84,13 @@ func (bl *blockListener) compareAndUpdateConfirmationQueue(ctx context.Context, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash] = newQueue } -func (bl *blockListener) initializeConfirmationMap(occ *ConfirmationMapUpdateResult, txBlockInfo *minimalBlockInfo) []*apitypes.Confirmation { - txBlockHash := txBlockInfo.hash +func (bl *blockListener) initializeConfirmationMap(occ *ffcapi.ConfirmationMapUpdateResult, txBlockInfo *ffcapi.MinimalBlockInfo) []*ffcapi.MinimalBlockInfo { + txBlockHash := txBlockInfo.BlockHash if occ.ConfirmationMap == nil || len(occ.ConfirmationMap.ConfirmationQueueMap) == 0 { - occ.ConfirmationMap = &ConfirmationMap{ - ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ - txBlockHash: {txBlockInfo.ToConfirmation()}, + occ.ConfirmationMap = &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + txBlockHash: {txBlockInfo}, }, CanonicalBlockHash: txBlockHash, } @@ -141,12 +101,12 @@ func (bl *blockListener) initializeConfirmationMap(occ *ConfirmationMapUpdateRes existingQueue := occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash] if len(existingQueue) > 0 { existingTxBlock := existingQueue[0] - if !isSameBlock(existingTxBlock, txBlockInfo) { + if !existingTxBlock.Equal(txBlockInfo) { // the tx block in the existing queue does not match the new tx block we queried from the chain // rebuild a new confirmation queue with the new tx block occ.HasNewFork = true occ.Rebuilt = true - occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash] = []*apitypes.Confirmation{txBlockInfo.ToConfirmation()} + occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash] = []*ffcapi.MinimalBlockInfo{txBlockInfo} return nil } } @@ -154,14 +114,14 @@ func (bl *blockListener) initializeConfirmationMap(occ *ConfirmationMapUpdateRes return existingQueue } -func (bl *blockListener) processExistingConfirmations(ctx context.Context, occ *ConfirmationMapUpdateResult, txBlockInfo *minimalBlockInfo, existingQueue []*apitypes.Confirmation, chainHead *minimalBlockInfo, targetConfirmationCount int) ([]*apitypes.Confirmation, *list.Element) { - txBlockNumber := txBlockInfo.number +func (bl *blockListener) processExistingConfirmations(ctx context.Context, occ *ffcapi.ConfirmationMapUpdateResult, txBlockInfo *ffcapi.MinimalBlockInfo, existingQueue []*ffcapi.MinimalBlockInfo, chainHead *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) ([]*ffcapi.MinimalBlockInfo, *list.Element) { + txBlockNumber := txBlockInfo.BlockNumber.Uint64() - newQueue := []*apitypes.Confirmation{txBlockInfo.ToConfirmation()} + newQueue := []*ffcapi.MinimalBlockInfo{txBlockInfo} currentBlock := bl.canonicalChain.Front() // iterate to the tx block if the chain head is earlier than the tx block - if currentBlock != nil && currentBlock.Value.(*minimalBlockInfo).number <= txBlockNumber { + if currentBlock != nil && currentBlock.Value.(*ffcapi.MinimalBlockInfo).BlockNumber.Uint64() <= txBlockNumber { currentBlock = currentBlock.Next() } @@ -177,13 +137,13 @@ func (bl *blockListener) processExistingConfirmations(ctx context.Context, occ * return bl.validateExistingConfirmations(ctx, occ, newQueue, existingConfirmations, currentBlock, chainHead, txBlockInfo, targetConfirmationCount) } -func (bl *blockListener) validateExistingConfirmations(ctx context.Context, occ *ConfirmationMapUpdateResult, newQueue []*apitypes.Confirmation, existingConfirmations []*apitypes.Confirmation, currentBlock *list.Element, chainHead *minimalBlockInfo, txBlockInfo *minimalBlockInfo, targetConfirmationCount int) ([]*apitypes.Confirmation, *list.Element) { - txBlockNumber := txBlockInfo.number +func (bl *blockListener) validateExistingConfirmations(ctx context.Context, occ *ffcapi.ConfirmationMapUpdateResult, newQueue []*ffcapi.MinimalBlockInfo, existingConfirmations []*ffcapi.MinimalBlockInfo, currentBlock *list.Element, chainHead *ffcapi.MinimalBlockInfo, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) ([]*ffcapi.MinimalBlockInfo, *list.Element) { + txBlockNumber := txBlockInfo.BlockNumber.Uint64() lastExistingConfirmation := existingConfirmations[len(existingConfirmations)-1] - if lastExistingConfirmation.BlockNumber.Uint64() < uint64(chainHead.number) && //nolint:gosec + if lastExistingConfirmation.BlockNumber.Uint64() < chainHead.BlockNumber.Uint64() && // ^^ the highest block number in the existing confirmations is lower than the highest block number in the canonical chain - (lastExistingConfirmation.BlockNumber.Uint64() != uint64(chainHead.number-1) || //nolint:gosec // block numbers are always positive - lastExistingConfirmation.BlockHash != chainHead.parentHash) { + (lastExistingConfirmation.BlockNumber.Uint64() != chainHead.BlockNumber.Uint64()-1 || + lastExistingConfirmation.BlockHash != chainHead.ParentHash) { // ^^ and the last existing confirmation is not the parent of the canonical chain head // Therefore, there is no connection between the existing confirmations and the canonical chain // so that we cannot validate the existing confirmations are from the same fork as the canonical chain @@ -192,14 +152,14 @@ func (bl *blockListener) validateExistingConfirmations(ctx context.Context, occ return newQueue, currentBlock } - var previousExistingConfirmation *apitypes.Confirmation + var previousExistingConfirmation *ffcapi.MinimalBlockInfo queueIndex := 0 - connectionBlockNumber := currentBlock.Value.(*minimalBlockInfo).number + connectionBlockNumber := currentBlock.Value.(*ffcapi.MinimalBlockInfo).BlockNumber.Uint64() for currentBlock != nil && queueIndex < len(existingConfirmations) { existingConfirmation := existingConfirmations[queueIndex] - if existingConfirmation.BlockNumber.Uint64() <= uint64(txBlockNumber) { //nolint:gosec // block numbers are always positive + if existingConfirmation.BlockNumber.Uint64() <= txBlockNumber { log.L(ctx).Debugf("Existing confirmation queue is corrupted, the first block is earlier than the tx block: %d", existingConfirmation.BlockNumber.Uint64()) // if any block in the existing confirmation queue is earlier than the tx block // the existing confirmation queue is no valid @@ -216,8 +176,8 @@ func (bl *blockListener) validateExistingConfirmations(ctx context.Context, occ (previousExistingConfirmation.BlockNumber.Uint64()+1 != existingConfirmation.BlockNumber.Uint64() || previousExistingConfirmation.BlockHash != existingConfirmation.ParentHash) || // check the link between the first confirmation block and the existing tx block - (existingConfirmation.BlockNumber.Uint64() == uint64(txBlockNumber)+1 && //nolint:gosec // block numbers are always positive - existingConfirmation.ParentHash != txBlockInfo.hash) + (existingConfirmation.BlockNumber.Uint64() == txBlockNumber+1 && + existingConfirmation.ParentHash != txBlockInfo.BlockHash) // we allow gaps between the tx block and the first block in the existing confirmation queue // NOTE: we don't allow gaps after the first block in the existing confirmation queue // any gaps, we need to rebuild the confirmations queue @@ -229,8 +189,8 @@ func (bl *blockListener) validateExistingConfirmations(ctx context.Context, occ return newQueue[:1], currentBlock } - currentBlockInfo := currentBlock.Value.(*minimalBlockInfo) - if existingConfirmation.BlockNumber.Uint64() < uint64(currentBlockInfo.number) { //nolint:gosec + currentBlockInfo := currentBlock.Value.(*ffcapi.MinimalBlockInfo) + if existingConfirmation.BlockNumber.Uint64() < currentBlockInfo.BlockNumber.Uint64() { // NOTE: we are not doing the confirmation count check here // because we've not reached the current head in the canonical chain to validate // all the confirmations we copied over are still valid @@ -240,14 +200,14 @@ func (bl *blockListener) validateExistingConfirmations(ctx context.Context, occ continue } - if existingConfirmation.BlockNumber.Uint64() == uint64(currentBlockInfo.number) { //nolint:gosec // block numbers are always positive + if existingConfirmation.BlockNumber.Uint64() == currentBlockInfo.BlockNumber.Uint64() { // existing confirmation has caught up to the current block // checking the overlaps - if !isSameBlock(existingConfirmation, currentBlockInfo) { + if !existingConfirmation.Equal(currentBlockInfo) { // we detected a potential fork - if connectionBlockNumber == currentBlockInfo.number && - previousExistingConfirmation.BlockHash != currentBlockInfo.parentHash { + if connectionBlockNumber == currentBlockInfo.BlockNumber.Uint64() && + !previousExistingConfirmation.IsParentOf(currentBlockInfo) { // this is the connection node (first overlap between existing confirmation queue and canonical chain) // if the first node doesn't chain to to the previous confirmation, it means all the historical confirmation are on a different fork // therefore, we need to rebuild the confirmations queue @@ -261,7 +221,7 @@ func (bl *blockListener) validateExistingConfirmations(ctx context.Context, occ } newQueue = append(newQueue, existingConfirmation) - if existingConfirmation.BlockNumber.Uint64()-uint64(txBlockNumber) >= uint64(targetConfirmationCount) { //nolint:gosec // block numbers are always positive + if existingConfirmation.BlockNumber.Uint64()-txBlockNumber >= targetConfirmationCount { break } currentBlock = currentBlock.Next() @@ -276,23 +236,23 @@ func (bl *blockListener) validateExistingConfirmations(ctx context.Context, occ // Check if we have enough confirmations lastBlockInNewQueue := newQueue[len(newQueue)-1] - confirmationBlockNumber := uint64(txBlockNumber) + uint64(targetConfirmationCount) //nolint:gosec // block numbers are always positive + confirmationBlockNumber := txBlockNumber + targetConfirmationCount if lastBlockInNewQueue.BlockNumber.Uint64() >= confirmationBlockNumber { - chainHead := bl.canonicalChain.Front().Value.(*minimalBlockInfo) + chainHead := bl.canonicalChain.Front().Value.(*ffcapi.MinimalBlockInfo) // we've got a confirmable so whether the rest of the chain has forked is not longer relevant // this could happen when user chose a different target confirmation count for the new checks // but we still need to validate the existing confirmations are connectable to the canonical chain // Check if the queue connects to the canonical chain - if lastBlockInNewQueue.BlockNumber.Uint64() >= uint64(chainHead.number) || //nolint:gosec // block numbers are always positive - (lastBlockInNewQueue.BlockNumber.Uint64() == uint64(chainHead.number-1) && //nolint:gosec // block numbers are always positive - lastBlockInNewQueue.BlockHash == chainHead.parentHash) { + if lastBlockInNewQueue.BlockNumber.Uint64() >= chainHead.BlockNumber.Uint64() || + (lastBlockInNewQueue.BlockNumber.Uint64() == chainHead.BlockNumber.Uint64()-1 && + lastBlockInNewQueue.BlockHash == chainHead.ParentHash) { occ.HasNewFork = false occ.HasNewConfirmation = false occ.Rebuilt = false occ.Confirmed = true // Trim the queue to only include blocks up to the max confirmation count - trimmedQueue := []*apitypes.Confirmation{} + trimmedQueue := []*ffcapi.MinimalBlockInfo{} for _, confirmation := range newQueue { if confirmation.BlockNumber.Uint64() > confirmationBlockNumber { break @@ -303,7 +263,7 @@ func (bl *blockListener) validateExistingConfirmations(ctx context.Context, occ // If we've trimmed off all the existing confirmations, we need to add the canonical chain head // to tell use the head block we used to confirmed the transaction if len(trimmedQueue) == 1 { - trimmedQueue = append(trimmedQueue, chainHead.ToConfirmation()) + trimmedQueue = append(trimmedQueue, chainHead) } return trimmedQueue, currentBlock } @@ -312,17 +272,17 @@ func (bl *blockListener) validateExistingConfirmations(ctx context.Context, occ return newQueue, currentBlock } -func (bl *blockListener) buildNewConfirmations(occ *ConfirmationMapUpdateResult, newQueue []*apitypes.Confirmation, currentBlock *list.Element, txBlockNumber int64, targetConfirmationCount int) []*apitypes.Confirmation { +func (bl *blockListener) buildNewConfirmations(occ *ffcapi.ConfirmationMapUpdateResult, newQueue []*ffcapi.MinimalBlockInfo, currentBlock *list.Element, txBlockNumber uint64, targetConfirmationCount uint64) []*ffcapi.MinimalBlockInfo { for currentBlock != nil { - currentBlockInfo := currentBlock.Value.(*minimalBlockInfo) - if currentBlockInfo.number > int64(newQueue[len(newQueue)-1].BlockNumber.Uint64()) { //nolint:gosec // block numbers are always positive + currentBlockInfo := currentBlock.Value.(*ffcapi.MinimalBlockInfo) + if currentBlockInfo.BlockNumber.Uint64() > newQueue[len(newQueue)-1].BlockNumber.Uint64() { occ.HasNewConfirmation = true - newQueue = append(newQueue, &apitypes.Confirmation{ - BlockHash: currentBlockInfo.hash, - BlockNumber: fftypes.FFuint64(currentBlockInfo.number), //nolint:gosec // block numbers are always positive - ParentHash: currentBlockInfo.parentHash, + newQueue = append(newQueue, &ffcapi.MinimalBlockInfo{ + BlockHash: currentBlockInfo.BlockHash, + BlockNumber: fftypes.FFuint64(currentBlockInfo.BlockNumber.Uint64()), + ParentHash: currentBlockInfo.ParentHash, }) - if currentBlockInfo.number >= txBlockNumber+int64(targetConfirmationCount) { //nolint:gosec // block numbers are always positive + if currentBlockInfo.BlockNumber.Uint64() >= txBlockNumber+targetConfirmationCount { occ.Confirmed = true break } @@ -332,8 +292,6 @@ func (bl *blockListener) buildNewConfirmations(occ *ConfirmationMapUpdateResult, return newQueue } -func isSameBlock(c1 *apitypes.Confirmation, bi *minimalBlockInfo) bool { - return c1.BlockHash == bi.hash && - c1.BlockNumber.Uint64() == uint64(bi.number) && //nolint:gosec // block numbers are always positive - c1.ParentHash == bi.parentHash +func (c *ethConnector) ReconcileConfirmationsForTransaction(ctx context.Context, txHash string, confirmMap *ffcapi.ConfirmationMap, targetConfirmationCount uint64) (*ffcapi.ConfirmationMapUpdateResult, error) { + return c.blockListener.reconcileConfirmationsForTransaction(ctx, txHash, confirmMap, targetConfirmationCount) } diff --git a/internal/ethereum/confirmation_reconciler_test.go b/internal/ethereum/confirmation_reconciler_test.go index 290531a..c280684 100644 --- a/internal/ethereum/confirmation_reconciler_test.go +++ b/internal/ethereum/confirmation_reconciler_test.go @@ -23,7 +23,7 @@ import ( "testing" "github.com/hyperledger/firefly-common/pkg/fftypes" - "github.com/hyperledger/firefly-transaction-manager/pkg/apitypes" + "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/stretchr/testify/assert" ) @@ -33,18 +33,18 @@ func TestCompareAndUpdateConfirmationQueue_EmptyChain(t *testing.T) { canonicalChain: createTestChain(50, 50), // Single block at 50, tx is at 100 } ctx := context.Background() - occ := &ConfirmationMapUpdateResult{ - ConfirmationMap: &ConfirmationMap{}, + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{}, } - txBlockNumber := int64(100) + txBlockNumber := uint64(100) txBlockHash := "0xblock100" - txBlockInfo := &minimalBlockInfo{ - number: txBlockNumber, - hash: txBlockHash, - parentHash: "0xblock99", + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: "0xblock99", } - targetConfirmationCount := 5 + targetConfirmationCount := uint64(5) // Execute bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) @@ -65,18 +65,18 @@ func TestCompareAndUpdateConfirmationQueue_ChainTooShort(t *testing.T) { canonicalChain: createTestChain(50, 99), // Chain ends at 99, tx is at 100 } ctx := context.Background() - occ := &ConfirmationMapUpdateResult{ - ConfirmationMap: &ConfirmationMap{}, + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{}, } - txBlockNumber := int64(100) + txBlockNumber := uint64(100) txBlockHash := "0xblock100" - txBlockInfo := &minimalBlockInfo{ - number: txBlockNumber, - hash: txBlockHash, - parentHash: "0xblock99", + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: "0xblock99", } - targetConfirmationCount := 5 + targetConfirmationCount := uint64(5) // Execute bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) @@ -96,17 +96,17 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMap(t *testing.T) { canonicalChain: createTestChain(50, 150), } ctx := context.Background() - occ := &ConfirmationMapUpdateResult{ + occ := &ffcapi.ConfirmationMapUpdateResult{ ConfirmationMap: nil, } - txBlockNumber := int64(100) + txBlockNumber := uint64(100) txBlockHash := "0xblock100" - txBlockInfo := &minimalBlockInfo{ - number: txBlockNumber, - hash: txBlockHash, - parentHash: "0xblock99", + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: "0xblock99", } - targetConfirmationCount := 5 + targetConfirmationCount := uint64(5) // Execute bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) @@ -120,12 +120,12 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMap(t *testing.T) { assert.Equal(t, txBlockHash, occ.ConfirmationMap.CanonicalBlockHash) // The code builds a full confirmation queue from the canonical chain assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) - assert.Equal(t, txBlockNumber, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) - assert.Equal(t, txBlockNumber+1, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) - assert.Equal(t, txBlockNumber+2, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) - assert.Equal(t, txBlockNumber+3, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) - assert.Equal(t, txBlockNumber+4, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][4].BlockNumber)) - assert.Equal(t, txBlockNumber+5, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][5].BlockNumber)) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][5].BlockNumber)) } @@ -135,17 +135,17 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMapUnconfirmed(t *test canonicalChain: createTestChain(100, 104), } ctx := context.Background() - occ := &ConfirmationMapUpdateResult{ + occ := &ffcapi.ConfirmationMapUpdateResult{ ConfirmationMap: nil, } - txBlockNumber := int64(100) + txBlockNumber := uint64(100) txBlockHash := "0xblock100" - txBlockInfo := &minimalBlockInfo{ - number: txBlockNumber, - hash: txBlockHash, - parentHash: "0xblock99", + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: "0xblock99", } - targetConfirmationCount := 5 + targetConfirmationCount := uint64(5) // Execute bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) @@ -159,11 +159,11 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMapUnconfirmed(t *test assert.Equal(t, txBlockHash, occ.ConfirmationMap.CanonicalBlockHash) // The code builds a full confirmation queue from the canonical chain assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 5) - assert.Equal(t, txBlockNumber, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) - assert.Equal(t, txBlockNumber+1, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) - assert.Equal(t, txBlockNumber+2, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) - assert.Equal(t, txBlockNumber+3, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) - assert.Equal(t, txBlockNumber+4, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][4].BlockNumber)) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][4].BlockNumber)) } @@ -173,19 +173,19 @@ func TestCompareAndUpdateConfirmationQueue_EmptyConfirmationQueue(t *testing.T) canonicalChain: createTestChain(50, 150), } ctx := context.Background() - occ := &ConfirmationMapUpdateResult{ - ConfirmationMap: &ConfirmationMap{ - ConfirmationQueueMap: make(map[string][]*apitypes.Confirmation), + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: make(map[string][]*ffcapi.MinimalBlockInfo), }, } - txBlockNumber := int64(100) + txBlockNumber := uint64(100) txBlockHash := "0xblock100" - txBlockInfo := &minimalBlockInfo{ - number: txBlockNumber, - hash: txBlockHash, - parentHash: "0xblock99", + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: "0xblock99", } - targetConfirmationCount := 5 + targetConfirmationCount := uint64(5) // Execute bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) @@ -198,12 +198,12 @@ func TestCompareAndUpdateConfirmationQueue_EmptyConfirmationQueue(t *testing.T) assert.Equal(t, txBlockHash, occ.ConfirmationMap.CanonicalBlockHash) // The code builds a full confirmation queue from the canonical chain assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) - assert.Equal(t, txBlockNumber, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) - assert.Equal(t, txBlockNumber+1, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) - assert.Equal(t, txBlockNumber+2, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) - assert.Equal(t, txBlockNumber+3, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) - assert.Equal(t, txBlockNumber+4, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][4].BlockNumber)) - assert.Equal(t, txBlockNumber+5, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][5].BlockNumber)) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][5].BlockNumber)) } // theoretically, this should never happen because block hash generation has block number as part of the input @@ -213,24 +213,24 @@ func TestCompareAndUpdateConfirmationQueue_DifferentBlockNumber(t *testing.T) { canonicalChain: createTestChain(50, 150), } ctx := context.Background() - existingQueue := []*apitypes.Confirmation{ + existingQueue := []*ffcapi.MinimalBlockInfo{ {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(99), ParentHash: "0xblock99"}, } - occ := &ConfirmationMapUpdateResult{ - ConfirmationMap: &ConfirmationMap{ - ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ "0xblock100": existingQueue, }, }, } - txBlockNumber := int64(100) + txBlockNumber := uint64(100) txBlockHash := "0xblock100" - txBlockInfo := &minimalBlockInfo{ - number: txBlockNumber, - hash: txBlockHash, - parentHash: "0xblock99", + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: "0xblock99", } - targetConfirmationCount := 5 + targetConfirmationCount := uint64(5) // Execute bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) @@ -240,12 +240,12 @@ func TestCompareAndUpdateConfirmationQueue_DifferentBlockNumber(t *testing.T) { assert.True(t, occ.Rebuilt) // The code builds a full confirmation queue from the canonical chain assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) - assert.Equal(t, txBlockNumber, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) - assert.Equal(t, txBlockNumber+1, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) - assert.Equal(t, txBlockNumber+2, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) - assert.Equal(t, txBlockNumber+3, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) - assert.Equal(t, txBlockNumber+4, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][4].BlockNumber)) - assert.Equal(t, txBlockNumber+5, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][5].BlockNumber)) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][5].BlockNumber)) } func TestCompareAndUpdateConfirmationQueue_MismatchConfirmationBlock(t *testing.T) { @@ -254,27 +254,27 @@ func TestCompareAndUpdateConfirmationQueue_MismatchConfirmationBlock(t *testing. canonicalChain: createTestChain(103, 150), } ctx := context.Background() - existingQueue := []*apitypes.Confirmation{ + existingQueue := []*ffcapi.MinimalBlockInfo{ {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, {BlockHash: "0xblock101", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xblock101"}, // wrong parent hash, so the existing queue should be discarded {BlockHash: "0xblock102", BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblock101"}, {BlockHash: "0xblock103", BlockNumber: fftypes.FFuint64(103), ParentHash: "0xblock102"}, } - occ := &ConfirmationMapUpdateResult{ - ConfirmationMap: &ConfirmationMap{ - ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ "0xblock100": existingQueue, }, }, } - txBlockNumber := int64(100) + txBlockNumber := uint64(100) txBlockHash := "0xblock100" - txBlockInfo := &minimalBlockInfo{ - number: txBlockNumber, - hash: txBlockHash, - parentHash: "0xblock99", + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: "0xblock99", } - targetConfirmationCount := 5 + targetConfirmationCount := uint64(5) // Execute bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) @@ -284,10 +284,10 @@ func TestCompareAndUpdateConfirmationQueue_MismatchConfirmationBlock(t *testing. assert.True(t, occ.Rebuilt) // The code builds a full confirmation queue from the canonical chain assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 4) - assert.Equal(t, txBlockNumber, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) - assert.Equal(t, txBlockNumber+3, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) - assert.Equal(t, txBlockNumber+4, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) - assert.Equal(t, txBlockNumber+5, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) } func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsTooDistant(t *testing.T) { @@ -296,25 +296,25 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsTooDistant(t *te canonicalChain: createTestChain(145, 150), } ctx := context.Background() - existingQueue := []*apitypes.Confirmation{ + existingQueue := []*ffcapi.MinimalBlockInfo{ {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, {BlockHash: "0xblock101", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xblock100"}, } - occ := &ConfirmationMapUpdateResult{ - ConfirmationMap: &ConfirmationMap{ - ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ "0xblock100": existingQueue, }, }, } - txBlockNumber := int64(100) + txBlockNumber := uint64(100) txBlockHash := "0xblock100" - txBlockInfo := &minimalBlockInfo{ - number: txBlockNumber, - hash: txBlockHash, - parentHash: "0xblock99", + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: "0xblock99", } - targetConfirmationCount := 5 + targetConfirmationCount := uint64(5) // Execute bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) @@ -327,8 +327,8 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsTooDistant(t *te assert.True(t, occ.HasNewConfirmation) assert.True(t, occ.Confirmed) assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 2) - assert.Equal(t, txBlockNumber, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) - assert.Equal(t, bl.canonicalChain.Front().Value.(*minimalBlockInfo).number, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, bl.canonicalChain.Front().Value.(*ffcapi.MinimalBlockInfo).BlockNumber, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber) } func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmation(t *testing.T) { @@ -338,25 +338,25 @@ func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmation(t *test } ctx := context.Background() // Create corrupted confirmation (wrong parent hash) - existingQueue := []*apitypes.Confirmation{ + existingQueue := []*ffcapi.MinimalBlockInfo{ {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, {BlockHash: "0xblock101", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xwrongparent"}, } - occ := &ConfirmationMapUpdateResult{ - ConfirmationMap: &ConfirmationMap{ - ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ "0xblock100": existingQueue, }, }, } - txBlockNumber := int64(100) + txBlockNumber := uint64(100) txBlockHash := "0xblock100" - txBlockInfo := &minimalBlockInfo{ - number: txBlockNumber, - hash: txBlockHash, - parentHash: "0xblock99", + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: "0xblock99", } - targetConfirmationCount := 5 + targetConfirmationCount := uint64(5) // Execute bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) @@ -367,7 +367,7 @@ func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmation(t *test assert.True(t, occ.HasNewConfirmation) assert.True(t, occ.Confirmed) assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) - assert.Equal(t, txBlockNumber, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) assert.Equal(t, "0xblock100", occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].ParentHash) } @@ -377,27 +377,27 @@ func TestCompareAndUpdateConfirmationQueue_ConnectionNodeMismatch(t *testing.T) canonicalChain: createTestChain(102, 150), } ctx := context.Background() - existingQueue := []*apitypes.Confirmation{ + existingQueue := []*ffcapi.MinimalBlockInfo{ {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, {BlockHash: "0xblockwrong", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xblock100"}, {BlockHash: "0xblock102", BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblock101"}, {BlockHash: "0xblock103", BlockNumber: fftypes.FFuint64(103), ParentHash: "0xblock102"}, } - occ := &ConfirmationMapUpdateResult{ - ConfirmationMap: &ConfirmationMap{ - ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ "0xblock100": existingQueue, }, }, } - txBlockNumber := int64(100) + txBlockNumber := uint64(100) txBlockHash := "0xblock100" - txBlockInfo := &minimalBlockInfo{ - number: txBlockNumber, - hash: txBlockHash, - parentHash: "0xblock99", + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: "0xblock99", } - targetConfirmationCount := 5 + targetConfirmationCount := uint64(5) // Execute bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) @@ -408,7 +408,7 @@ func TestCompareAndUpdateConfirmationQueue_ConnectionNodeMismatch(t *testing.T) assert.True(t, occ.HasNewConfirmation) assert.True(t, occ.Confirmed) assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 5) - assert.Equal(t, txBlockNumber, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) assert.Equal(t, "0xblock102", occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockHash) assert.Equal(t, "0xblock102", occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].ParentHash) } @@ -419,26 +419,26 @@ func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmationAfterFir canonicalChain: createTestChain(100, 150), } ctx := context.Background() - existingQueue := []*apitypes.Confirmation{ + existingQueue := []*ffcapi.MinimalBlockInfo{ {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, {BlockHash: "0xblock101", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xblock100"}, {BlockHash: "0xblock102", BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblockwrong"}, } - occ := &ConfirmationMapUpdateResult{ - ConfirmationMap: &ConfirmationMap{ - ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ "0xblock100": existingQueue, }, }, } - txBlockNumber := int64(100) + txBlockNumber := uint64(100) txBlockHash := "0xblock100" - txBlockInfo := &minimalBlockInfo{ - number: txBlockNumber, - hash: txBlockHash, - parentHash: "0xblock99", + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: "0xblock99", } - targetConfirmationCount := 5 + targetConfirmationCount := uint64(5) // Execute bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) @@ -449,7 +449,7 @@ func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmationAfterFir assert.True(t, occ.HasNewConfirmation) assert.True(t, occ.Confirmed) assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 5) - assert.Equal(t, txBlockNumber, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) assert.Equal(t, "0xblock102", occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockHash) assert.Equal(t, "0xblock102", occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].ParentHash) } @@ -460,26 +460,26 @@ func TestCompareAndUpdateConfirmationQueue_NewForkAfterFirstConfirmation(t *test canonicalChain: createTestChain(100, 150), } ctx := context.Background() - existingQueue := []*apitypes.Confirmation{ + existingQueue := []*ffcapi.MinimalBlockInfo{ {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, {BlockHash: "0xblock101", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xblock100"}, {BlockHash: "fork1", BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblock101"}, } - occ := &ConfirmationMapUpdateResult{ - ConfirmationMap: &ConfirmationMap{ - ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ "0xblock100": existingQueue, }, }, } - txBlockNumber := int64(100) + txBlockNumber := uint64(100) txBlockHash := "0xblock100" - txBlockInfo := &minimalBlockInfo{ - number: txBlockNumber, - hash: txBlockHash, - parentHash: "0xblock99", + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: "0xblock99", } - targetConfirmationCount := 5 + targetConfirmationCount := uint64(5) // Execute bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) @@ -492,33 +492,33 @@ func TestCompareAndUpdateConfirmationQueue_NewForkAfterFirstConfirmation(t *test assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) } -func TestCompareAndUpdateConfirmationQueue_NewFork(t *testing.T) { +func TestCompareAndUpdateConfirmationQueue_NewForkAndNoConnectionToCanonicalChain(t *testing.T) { // Setup bl := &blockListener{ canonicalChain: createTestChain(103, 150), } ctx := context.Background() - existingQueue := []*apitypes.Confirmation{ + existingQueue := []*ffcapi.MinimalBlockInfo{ {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, {BlockHash: "fork1", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xblock100"}, {BlockHash: "fork2", BlockNumber: fftypes.FFuint64(102), ParentHash: "fork1"}, {BlockHash: "fork3", BlockNumber: fftypes.FFuint64(103), ParentHash: "fork2"}, } - occ := &ConfirmationMapUpdateResult{ - ConfirmationMap: &ConfirmationMap{ - ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ "0xblock100": existingQueue, }, }, } - txBlockNumber := int64(100) + txBlockNumber := uint64(100) txBlockHash := "0xblock100" - txBlockInfo := &minimalBlockInfo{ - number: txBlockNumber, - hash: txBlockHash, - parentHash: "0xblock99", + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: "0xblock99", } - targetConfirmationCount := 5 + targetConfirmationCount := uint64(5) // Execute bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) @@ -529,10 +529,10 @@ func TestCompareAndUpdateConfirmationQueue_NewFork(t *testing.T) { assert.True(t, occ.HasNewConfirmation) assert.True(t, occ.Confirmed) assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 4) - assert.Equal(t, txBlockNumber, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) - assert.Equal(t, txBlockNumber+3, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) - assert.Equal(t, txBlockNumber+4, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) - assert.Equal(t, txBlockNumber+5, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) } func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationLaterThanCurrentBlock(t *testing.T) { @@ -541,25 +541,25 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationLaterThanCurrentB canonicalChain: createTestChain(100, 150), } ctx := context.Background() - existingQueue := []*apitypes.Confirmation{ + existingQueue := []*ffcapi.MinimalBlockInfo{ {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, {BlockHash: "0xblock103", BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblock101"}, } - occ := &ConfirmationMapUpdateResult{ - ConfirmationMap: &ConfirmationMap{ - ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ "0xblock100": existingQueue, }, }, } - txBlockNumber := int64(100) + txBlockNumber := uint64(100) txBlockHash := "0xblock100" - txBlockInfo := &minimalBlockInfo{ - number: txBlockNumber, - hash: txBlockHash, - parentHash: "0xblock99", + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: "0xblock99", } - targetConfirmationCount := 5 + targetConfirmationCount := uint64(5) // Execute bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) @@ -570,12 +570,12 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationLaterThanCurrentB assert.True(t, occ.HasNewConfirmation) assert.True(t, occ.Confirmed) assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) - assert.Equal(t, txBlockNumber, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) - assert.Equal(t, txBlockNumber+1, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) - assert.Equal(t, txBlockNumber+2, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) - assert.Equal(t, txBlockNumber+3, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) - assert.Equal(t, txBlockNumber+4, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][4].BlockNumber)) - assert.Equal(t, txBlockNumber+5, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][5].BlockNumber)) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][5].BlockNumber)) } func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable(t *testing.T) { @@ -586,7 +586,7 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable(t *testing.T) { ctx := context.Background() // Create confirmations that already meet the target // and it connects to the canonical chain to validate they are still valid - existingQueue := []*apitypes.Confirmation{ + existingQueue := []*ffcapi.MinimalBlockInfo{ {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, {BlockHash: "0xblock101", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xblock100"}, {BlockHash: "0xblock102", BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblock101"}, @@ -596,21 +596,21 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable(t *testing.T) { {BlockHash: "0xblock104", BlockNumber: fftypes.FFuint64(104), ParentHash: "0xblock103"}, // discarded {BlockHash: "0xblock105", BlockNumber: fftypes.FFuint64(105), ParentHash: "0xblock104"}, // discarded } - occ := &ConfirmationMapUpdateResult{ - ConfirmationMap: &ConfirmationMap{ - ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ "0xblock100": existingQueue, }, }, } - txBlockNumber := int64(100) + txBlockNumber := uint64(100) txBlockHash := "0xblock100" - txBlockInfo := &minimalBlockInfo{ - number: txBlockNumber, - hash: txBlockHash, - parentHash: "0xblock99", + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: "0xblock99", } - targetConfirmationCount := 2 + targetConfirmationCount := uint64(2) // Execute bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) @@ -621,9 +621,9 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable(t *testing.T) { assert.False(t, occ.HasNewFork) assert.False(t, occ.HasNewConfirmation) assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 3) - assert.Equal(t, txBlockNumber, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) - assert.Equal(t, txBlockNumber+1, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) - assert.Equal(t, txBlockNumber+2, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) } func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableConnectable(t *testing.T) { @@ -634,28 +634,28 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableConnectable(t *test ctx := context.Background() // Create confirmations that already meet the target // and it connects to the canonical chain to validate they are still valid - existingQueue := []*apitypes.Confirmation{ + existingQueue := []*ffcapi.MinimalBlockInfo{ {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, {BlockHash: "0xblock101", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xblock100"}, {BlockHash: "0xblock102", BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblock101"}, // didn't have block 103, which is the first block of the canonical chain // but we should still be able to validate the existing confirmations are valid using parent hash } - occ := &ConfirmationMapUpdateResult{ - ConfirmationMap: &ConfirmationMap{ - ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ "0xblock100": existingQueue, }, }, } - txBlockNumber := int64(100) + txBlockNumber := uint64(100) txBlockHash := "0xblock100" - txBlockInfo := &minimalBlockInfo{ - number: txBlockNumber, - hash: txBlockHash, - parentHash: "0xblock99", + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: "0xblock99", } - targetConfirmationCount := 1 + targetConfirmationCount := uint64(1) // Execute bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) @@ -668,8 +668,8 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableConnectable(t *test assert.False(t, occ.HasNewFork) assert.False(t, occ.HasNewConfirmation) assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 2) - assert.Equal(t, txBlockNumber, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) - assert.Equal(t, txBlockNumber+1, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) } func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableButAllExistingConfirmationsAreTooHighForTargetConfirmationCount(t *testing.T) { @@ -680,26 +680,26 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableButAllExistingConfi ctx := context.Background() // Create confirmations that already meet the target // and it connects to the canonical chain to validate they are still valid - existingQueue := []*apitypes.Confirmation{ + existingQueue := []*ffcapi.MinimalBlockInfo{ {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, // gap of 101 is allowed, and is the confirmation required for the transaction with target confirmation count of 1 {BlockHash: "0xblock102", BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblock101"}, } - occ := &ConfirmationMapUpdateResult{ - ConfirmationMap: &ConfirmationMap{ - ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ "0xblock100": existingQueue, }, }, } - txBlockNumber := int64(100) + txBlockNumber := uint64(100) txBlockHash := "0xblock100" - txBlockInfo := &minimalBlockInfo{ - number: txBlockNumber, - hash: txBlockHash, - parentHash: "0xblock99", + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: "0xblock99", } - targetConfirmationCount := 1 + targetConfirmationCount := uint64(1) // Execute bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) @@ -712,8 +712,8 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableButAllExistingConfi assert.False(t, occ.HasNewFork) assert.False(t, occ.HasNewConfirmation) assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 2) - assert.Equal(t, txBlockNumber, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) - assert.Equal(t, int64(103), int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, uint64(103), uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) } @@ -725,26 +725,26 @@ func TestCompareAndUpdateConfirmationQueue_HasSufficientConfirmationsButNoOverla ctx := context.Background() // Create confirmations that already meet the target // and it connects to the canonical chain to validate they are still valid - existingQueue := []*apitypes.Confirmation{ + existingQueue := []*ffcapi.MinimalBlockInfo{ {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, {BlockHash: "0xblock101", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xblock100"}, {BlockHash: "0xblock102", BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblock101"}, } - occ := &ConfirmationMapUpdateResult{ - ConfirmationMap: &ConfirmationMap{ - ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ "0xblock100": existingQueue, }, }, } - txBlockNumber := int64(100) + txBlockNumber := uint64(100) txBlockHash := "0xblock100" - txBlockInfo := &minimalBlockInfo{ - number: txBlockNumber, - hash: txBlockHash, - parentHash: "0xblock99", + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: "0xblock99", } - targetConfirmationCount := 1 + targetConfirmationCount := uint64(1) // Execute bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) @@ -757,8 +757,8 @@ func TestCompareAndUpdateConfirmationQueue_HasSufficientConfirmationsButNoOverla assert.True(t, occ.Rebuilt) assert.True(t, occ.HasNewConfirmation) assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 2) - assert.Equal(t, txBlockNumber, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) - assert.Equal(t, int64(104), int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, uint64(104), uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) } @@ -768,26 +768,26 @@ func TestCompareAndUpdateConfirmationQueue_ValidExistingConfirmations(t *testing canonicalChain: createTestChain(50, 150), } ctx := context.Background() - existingQueue := []*apitypes.Confirmation{ + existingQueue := []*ffcapi.MinimalBlockInfo{ {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, {BlockHash: "0xblock101", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xblock100"}, {BlockHash: "0xblock102", BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblock101"}, } - occ := &ConfirmationMapUpdateResult{ - ConfirmationMap: &ConfirmationMap{ - ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ "0xblock100": existingQueue, }, }, } - txBlockNumber := int64(100) + txBlockNumber := uint64(100) txBlockHash := "0xblock100" - txBlockInfo := &minimalBlockInfo{ - number: txBlockNumber, - hash: txBlockHash, - parentHash: "0xblock99", + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: "0xblock99", } - targetConfirmationCount := 5 + targetConfirmationCount := uint64(5) // Execute bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) @@ -798,12 +798,12 @@ func TestCompareAndUpdateConfirmationQueue_ValidExistingConfirmations(t *testing assert.True(t, occ.HasNewConfirmation) assert.True(t, occ.Confirmed) assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) - assert.Equal(t, txBlockNumber, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) - assert.Equal(t, txBlockNumber+1, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) - assert.Equal(t, txBlockNumber+2, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) - assert.Equal(t, txBlockNumber+3, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) - assert.Equal(t, txBlockNumber+4, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][4].BlockNumber)) - assert.Equal(t, txBlockNumber+5, int64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][5].BlockNumber)) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][5].BlockNumber)) } func TestCompareAndUpdateConfirmationQueue_ReachTargetConfirmation(t *testing.T) { @@ -812,19 +812,19 @@ func TestCompareAndUpdateConfirmationQueue_ReachTargetConfirmation(t *testing.T) canonicalChain: createTestChain(50, 150), } ctx := context.Background() - occ := &ConfirmationMapUpdateResult{ - ConfirmationMap: &ConfirmationMap{ - ConfirmationQueueMap: make(map[string][]*apitypes.Confirmation), + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: make(map[string][]*ffcapi.MinimalBlockInfo), }, } - txBlockNumber := int64(100) + txBlockNumber := uint64(100) txBlockHash := "0xblock100" - txBlockInfo := &minimalBlockInfo{ - number: txBlockNumber, - hash: txBlockHash, - parentHash: "0xblock99", + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: "0xblock99", } - targetConfirmationCount := 3 + targetConfirmationCount := uint64(3) // Execute bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) @@ -843,27 +843,27 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithGap(t *testi } ctx := context.Background() // Create confirmations with a gap (missing block 102) - existingQueue := []*apitypes.Confirmation{ + existingQueue := []*ffcapi.MinimalBlockInfo{ {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, // no block 101, which is the first block of the canonical chain {BlockHash: "0xblock102", BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblock101"}, {BlockHash: "0xblock103", BlockNumber: fftypes.FFuint64(103), ParentHash: "0xblock102"}, } - occ := &ConfirmationMapUpdateResult{ - ConfirmationMap: &ConfirmationMap{ - ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ "0xblock100": existingQueue, }, }, } - txBlockNumber := int64(100) + txBlockNumber := uint64(100) txBlockHash := "0xblock100" - txBlockInfo := &minimalBlockInfo{ - number: txBlockNumber, - hash: txBlockHash, - parentHash: "0xblock99", + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: "0xblock99", } - targetConfirmationCount := 5 + targetConfirmationCount := uint64(5) // Execute bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) @@ -883,25 +883,25 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNu } ctx := context.Background() // Create confirmations with a lower block number - existingQueue := []*apitypes.Confirmation{ + existingQueue := []*ffcapi.MinimalBlockInfo{ {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, {BlockHash: "0xblock101", BlockNumber: fftypes.FFuint64(99), ParentHash: "0xblock100"}, // somehow there is a lower block number } - occ := &ConfirmationMapUpdateResult{ - ConfirmationMap: &ConfirmationMap{ - ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ "0xblock100": existingQueue, }, }, } - txBlockNumber := int64(100) + txBlockNumber := uint64(100) txBlockHash := "0xblock100" - txBlockInfo := &minimalBlockInfo{ - number: txBlockNumber, - hash: txBlockHash, - parentHash: "0xblock99", + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: "0xblock99", } - targetConfirmationCount := 5 + targetConfirmationCount := uint64(5) // Execute bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) @@ -921,26 +921,26 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNu } ctx := context.Background() // Create confirmations with a lower block number - existingQueue := []*apitypes.Confirmation{ + existingQueue := []*ffcapi.MinimalBlockInfo{ {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, {BlockHash: "0xblock101", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xblock100"}, {BlockHash: "0xblock102", BlockNumber: fftypes.FFuint64(99), ParentHash: "0xblock101"}, // somehow there is a lower block number } - occ := &ConfirmationMapUpdateResult{ - ConfirmationMap: &ConfirmationMap{ - ConfirmationQueueMap: map[string][]*apitypes.Confirmation{ + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ "0xblock100": existingQueue, }, }, } - txBlockNumber := int64(100) + txBlockNumber := uint64(100) txBlockHash := "0xblock100" - txBlockInfo := &minimalBlockInfo{ - number: txBlockNumber, - hash: txBlockHash, - parentHash: "0xblock99", + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: "0xblock99", } - targetConfirmationCount := 5 + targetConfirmationCount := uint64(5) // Execute bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) @@ -955,13 +955,13 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNu // Helper functions -func createTestChain(startBlock, endBlock int64) *list.List { +func createTestChain(startBlock, endBlock uint64) *list.List { chain := list.New() for i := startBlock; i <= endBlock; i++ { - blockInfo := &minimalBlockInfo{ - number: i, - hash: fmt.Sprintf("0xblock%d", i), - parentHash: fmt.Sprintf("0xblock%d", i-1), + blockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(i), + BlockHash: fmt.Sprintf("0xblock%d", i), + ParentHash: fmt.Sprintf("0xblock%d", i-1), } chain.PushBack(blockInfo) } diff --git a/internal/ethereum/event_listener.go b/internal/ethereum/event_listener.go index ea781f6..cc592d7 100644 --- a/internal/ethereum/event_listener.go +++ b/internal/ethereum/event_listener.go @@ -94,20 +94,20 @@ func (cp *listenerCheckpoint) LessThan(b ffcapi.EventListenerCheckpoint) bool { (cp.TransactionIndex == bcp.TransactionIndex && (cp.LogIndex < bcp.LogIndex)))) } -func (l *listener) getInitialBlock(ctx context.Context, fromBlockInstruction string) (int64, error) { +func (l *listener) getInitialBlock(ctx context.Context, fromBlockInstruction string) (uint64, error) { if fromBlockInstruction == ffcapi.FromBlockLatest || fromBlockInstruction == "" { // Get the latest block number of the chain chainHead, ok := l.c.blockListener.getHighestBlock(ctx) if !ok { - return -1, i18n.NewError(ctx, msgs.MsgTimedOutQueryingChainHead) + return 0, i18n.NewError(ctx, msgs.MsgTimedOutQueryingChainHead) } return chainHead, nil } num, ok := new(big.Int).SetString(fromBlockInstruction, 0) if !ok { - return -1, i18n.NewError(ctx, msgs.MsgInvalidFromBlock, fromBlockInstruction) + return 0, i18n.NewError(ctx, msgs.MsgInvalidFromBlock, fromBlockInstruction) } - return num.Int64(), nil + return num.Uint64(), nil } func parseListenerOptions(ctx context.Context, o *fftypes.JSONAny) (*listenerOptions, error) { @@ -131,7 +131,7 @@ func (l *listener) ensureHWM(ctx context.Context) error { return err } // HWM is the configured fromBlock - l.hwmBlock = firstBlock + l.hwmBlock = int64(firstBlock) //nolint:gosec // convert to int64 to match the type of hwmBlock, we should change the type of hwmBlock to uint64 } return nil } diff --git a/internal/ethereum/event_stream.go b/internal/ethereum/event_stream.go index 2fbf36f..5d8da60 100644 --- a/internal/ethereum/event_stream.go +++ b/internal/ethereum/event_stream.go @@ -254,7 +254,7 @@ func (es *eventStream) leadGroupCatchup() bool { } // Check if we're ready to exit catchup mode - headGap := (chainHeadBlock - fromBlock) + headGap := (int64(chainHeadBlock) - fromBlock) //nolint:gosec // convert to int64 to match the type of headGap if headGap < es.c.catchupThreshold { log.L(es.ctx).Infof("Stream head is up to date with chain fromBlock=%d chainHead=%d headGap=%d", fromBlock, chainHeadBlock, headGap) return false @@ -319,7 +319,7 @@ func (es *eventStream) leadGroupSteadyState() bool { // High water mark is a point safely behind the head of the chain in this case, // where re-orgs are not expected. bh, _ := es.c.blockListener.getHighestBlock(es.ctx) /* note we know we're initialized here and will not block */ - hwmBlock := bh - es.c.checkpointBlockGap + hwmBlock := int64(bh) - es.c.checkpointBlockGap //nolint:gosec // convert to int64 to match the type of hwmBlock if hwmBlock < 0 { hwmBlock = 0 } @@ -342,7 +342,7 @@ func (es *eventStream) leadGroupSteadyState() bool { // Check we're not outside of the steady state window, and need to fall back to catchup mode chainHeadBlock, _ := es.c.blockListener.getHighestBlock(es.ctx) /* note we know we're initialized here and will not block */ - blockGapEstimate := (chainHeadBlock - fromBlock) + blockGapEstimate := (int64(chainHeadBlock) - fromBlock) //nolint:gosec // convert to int64 to match the type of blockGapEstimate if blockGapEstimate > es.c.catchupThreshold { log.L(es.ctx).Warnf("Block gap estimate reached %d (above threshold of %d) - reverting to catchup mode", blockGapEstimate, es.c.catchupThreshold) return false @@ -422,8 +422,8 @@ func (es *eventStream) preStartProcessing() { for _, l := range es.listeners { // During initial start we move the "head" block forwards to be the highest of all the initial streams if l.hwmBlock > es.headBlock { - if l.hwmBlock > chainHead { - es.headBlock = chainHead + if l.hwmBlock > int64(chainHead) { //nolint:gosec // convert to int64 to match the type of headBlock + es.headBlock = int64(chainHead) //nolint:gosec // convert to int64 to match the type of headBlock } else { es.headBlock = l.hwmBlock } diff --git a/internal/ethereum/get_block_info.go b/internal/ethereum/get_block_info.go index ae2dd00..12f6159 100644 --- a/internal/ethereum/get_block_info.go +++ b/internal/ethereum/get_block_info.go @@ -26,7 +26,7 @@ import ( func (c *ethConnector) BlockInfoByNumber(ctx context.Context, req *ffcapi.BlockInfoByNumberRequest) (*ffcapi.BlockInfoByNumberResponse, ffcapi.ErrorReason, error) { - blockInfo, reason, err := c.blockListener.getBlockInfoByNumber(ctx, req.BlockNumber.Int64(), req.AllowCache, req.ExpectedParentHash) + blockInfo, reason, err := c.blockListener.getBlockInfoByNumber(ctx, req.BlockNumber.Uint64(), req.AllowCache, req.ExpectedParentHash) if err != nil { return nil, reason, err } diff --git a/mocks/fftmmocks/manager.go b/mocks/fftmmocks/manager.go index 6134d08..c2661c1 100644 --- a/mocks/fftmmocks/manager.go +++ b/mocks/fftmmocks/manager.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.2. DO NOT EDIT. +// Code generated by mockery v2.53.5. DO NOT EDIT. package fftmmocks @@ -9,6 +9,8 @@ import ( eventapi "github.com/hyperledger/firefly-transaction-manager/pkg/eventapi" + ffcapi "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" + mock "github.com/stretchr/testify/mock" mux "github.com/gorilla/mux" @@ -131,6 +133,36 @@ func (_m *Manager) GetTransactionByIDWithStatus(ctx context.Context, txID string return r0, r1 } +// ReconcileConfirmationsForTransaction provides a mock function with given fields: ctx, txHash, confirmMap, targetConfirmationCount +func (_m *Manager) ReconcileConfirmationsForTransaction(ctx context.Context, txHash string, confirmMap *ffcapi.ConfirmationMap, targetConfirmationCount uint64) (*ffcapi.ConfirmationMapUpdateResult, error) { + ret := _m.Called(ctx, txHash, confirmMap, targetConfirmationCount) + + if len(ret) == 0 { + panic("no return value specified for ReconcileConfirmationsForTransaction") + } + + var r0 *ffcapi.ConfirmationMapUpdateResult + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, *ffcapi.ConfirmationMap, uint64) (*ffcapi.ConfirmationMapUpdateResult, error)); ok { + return rf(ctx, txHash, confirmMap, targetConfirmationCount) + } + if rf, ok := ret.Get(0).(func(context.Context, string, *ffcapi.ConfirmationMap, uint64) *ffcapi.ConfirmationMapUpdateResult); ok { + r0 = rf(ctx, txHash, confirmMap, targetConfirmationCount) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*ffcapi.ConfirmationMapUpdateResult) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, *ffcapi.ConfirmationMap, uint64) error); ok { + r1 = rf(ctx, txHash, confirmMap, targetConfirmationCount) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // Start provides a mock function with no fields func (_m *Manager) Start() error { ret := _m.Called() diff --git a/mocks/rpcbackendmocks/backend.go b/mocks/rpcbackendmocks/backend.go index 3af6838..1ba3d6d 100644 --- a/mocks/rpcbackendmocks/backend.go +++ b/mocks/rpcbackendmocks/backend.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.52.2. DO NOT EDIT. +// Code generated by mockery v2.53.5. DO NOT EDIT. package rpcbackendmocks From 4ccb06d0d98a9c4cefee1843d22cba8322994157 Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Thu, 11 Sep 2025 11:49:43 +0100 Subject: [PATCH 04/37] adding more tests and fixing bugs Signed-off-by: Chengxuan Xing --- internal/ethereum/blocklistener_blockquery.go | 9 +- internal/ethereum/confirmation_reconciler.go | 22 +- .../ethereum/confirmation_reconciler_test.go | 539 ++++++++++++++---- 3 files changed, 445 insertions(+), 125 deletions(-) diff --git a/internal/ethereum/blocklistener_blockquery.go b/internal/ethereum/blocklistener_blockquery.go index 7694223..e75b8a5 100644 --- a/internal/ethereum/blocklistener_blockquery.go +++ b/internal/ethereum/blocklistener_blockquery.go @@ -62,10 +62,10 @@ func (bl *blockListener) getBlockInfoContainsTxHash(ctx context.Context, txHash // so need to figure out the reason first // TODO: add a cache if map cannot be used - res, _, receiptErr := bl.c.TransactionReceipt(ctx, &ffcapi.TransactionReceiptRequest{ + res, reason, receiptErr := bl.c.TransactionReceipt(ctx, &ffcapi.TransactionReceiptRequest{ TransactionHash: txHash, }) - if receiptErr != nil { + if receiptErr != nil && reason != ffcapi.ErrorReasonNotFound { return nil, i18n.WrapError(ctx, receiptErr, msgs.MsgFailedToQueryReceipt, txHash) } if res == nil { @@ -75,9 +75,12 @@ func (bl *blockListener) getBlockInfoContainsTxHash(ctx context.Context, txHash txBlockNumber := res.BlockNumber.Uint64() // get the parent hash of the transaction block bi, _, err := bl.getBlockInfoByNumber(ctx, txBlockNumber, true, txBlockHash) - if err != nil { + if err != nil && reason != ffcapi.ErrorReasonNotFound { // if the block info is not found, then there could be a fork, twe don't throw error in this case and treating it as block not found return nil, i18n.WrapError(ctx, err, msgs.MsgFailedToQueryBlockInfo, txHash) } + if bi == nil { + return nil, nil + } return &ffcapi.MinimalBlockInfo{ BlockNumber: fftypes.FFuint64(bi.Number.BigInt().Uint64()), diff --git a/internal/ethereum/confirmation_reconciler.go b/internal/ethereum/confirmation_reconciler.go index ba9a66e..90e91c4 100644 --- a/internal/ethereum/confirmation_reconciler.go +++ b/internal/ethereum/confirmation_reconciler.go @@ -77,11 +77,21 @@ func (bl *blockListener) compareAndUpdateConfirmationQueue(ctx context.Context, // Validate and process existing confirmations newQueue, currentBlock := bl.processExistingConfirmations(ctx, occ, txBlockInfo, existingQueue, chainHead, targetConfirmationCount) + if currentBlock == nil { + // the tx block is not in the canonical chain + // we should just return the existing block confirmations and wait for future call to correct it + return + } + // Build new confirmations from canonical chain only if not already confirmed if !occ.Confirmed { newQueue = bl.buildNewConfirmations(occ, newQueue, currentBlock, txBlockNumber, targetConfirmationCount) } occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash] = newQueue + + if occ.CanonicalBlockHash != txBlockHash { + occ.CanonicalBlockHash = txBlockHash + } } func (bl *blockListener) initializeConfirmationMap(occ *ffcapi.ConfirmationMapUpdateResult, txBlockInfo *ffcapi.MinimalBlockInfo) []*ffcapi.MinimalBlockInfo { @@ -121,7 +131,17 @@ func (bl *blockListener) processExistingConfirmations(ctx context.Context, occ * currentBlock := bl.canonicalChain.Front() // iterate to the tx block if the chain head is earlier than the tx block - if currentBlock != nil && currentBlock.Value.(*ffcapi.MinimalBlockInfo).BlockNumber.Uint64() <= txBlockNumber { + for currentBlock != nil && currentBlock.Value.(*ffcapi.MinimalBlockInfo).BlockNumber.Uint64() <= txBlockNumber { + if currentBlock.Value.(*ffcapi.MinimalBlockInfo).BlockNumber.Uint64() == txBlockNumber { + // the tx block is already in the canonical chain + // we need to check if the tx block is the same as the chain head + if !currentBlock.Value.(*ffcapi.MinimalBlockInfo).Equal(txBlockInfo) { + // the tx block information is different from the same block number in the canonical chain + // the tx confirmation block is not on the same fork as the canonical chain + // we should just return the existing block confirmations and wait for future call to correct it + return newQueue, nil + } + } currentBlock = currentBlock.Next() } diff --git a/internal/ethereum/confirmation_reconciler_test.go b/internal/ethereum/confirmation_reconciler_test.go index c280684..18cf746 100644 --- a/internal/ethereum/confirmation_reconciler_test.go +++ b/internal/ethereum/confirmation_reconciler_test.go @@ -19,14 +19,254 @@ package ethereum import ( "container/list" "context" + "encoding/json" "fmt" "testing" "github.com/hyperledger/firefly-common/pkg/fftypes" + "github.com/hyperledger/firefly-signer/pkg/ethtypes" + "github.com/hyperledger/firefly-signer/pkg/rpcbackend" "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) +// Tests of the reconcileConfirmationsForTransaction function + +func TestReconcileConfirmationsForTransaction_TransactionNotFound(t *testing.T) { + + _, c, mRPC, _ := newTestConnectorWithNoBlockerFilterDefaultMocks(t) + + // Mock for TransactionReceipt call - return nil to simulate transaction not found + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getTransactionReceipt", generateTestHash(100)).Return(nil).Run(func(args mock.Arguments) { + err := json.Unmarshal([]byte("null"), args[1]) + assert.NoError(t, err) + }) + + // Execute the reconcileConfirmationsForTransaction function + result, err := c.ReconcileConfirmationsForTransaction(context.Background(), generateTestHash(100), nil, 5) + + // Assertions - expect an error when transaction doesn't exist + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.HasNewFork) + assert.False(t, result.Rebuilt) + assert.False(t, result.HasNewConfirmation) + assert.False(t, result.Confirmed) + assert.Nil(t, result.ConfirmationMap) + assert.Equal(t, uint64(5), result.TargetConfirmationCount) + + mRPC.AssertExpectations(t) +} + +func TestReconcileConfirmationsForTransaction_ReceiptRPCCallError(t *testing.T) { + + _, c, mRPC, _ := newTestConnectorWithNoBlockerFilterDefaultMocks(t) + + // Mock for TransactionReceipt call - return error to simulate RPC call error + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getTransactionReceipt", generateTestHash(100)).Return(&rpcbackend.RPCError{Message: "pop"}).Run(func(args mock.Arguments) { + err := json.Unmarshal([]byte("null"), args[1]) + assert.NoError(t, err) + }) + + // Execute the reconcileConfirmationsForTransaction function + result, err := c.ReconcileConfirmationsForTransaction(context.Background(), generateTestHash(100), &ffcapi.ConfirmationMap{}, 5) + + // Assertions - expect an error when RPC call fails + assert.Error(t, err) + assert.Nil(t, result) +} + +func TestReconcileConfirmationsForTransaction_BlockNotFound(t *testing.T) { + + _, c, mRPC, _ := newTestConnectorWithNoBlockerFilterDefaultMocks(t) + + // Mock for TransactionReceipt call + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getTransactionReceipt", + mock.MatchedBy(func(txHash string) bool { + assert.Equal(t, "0x6197ef1a58a2a592bb447efb651f0db7945de21aa8048801b250bd7b7431f9b6", txHash) + return true + })). + Return(nil). + Run(func(args mock.Arguments) { + err := json.Unmarshal([]byte(sampleJSONRPCReceipt), args[1]) + assert.NoError(t, err) + }) + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByNumber", mock.MatchedBy(func(bn *ethtypes.HexInteger) bool { + return bn.BigInt().String() == "1977" + }), false).Return(nil).Run(func(args mock.Arguments) { + err := json.Unmarshal([]byte("null"), args[1]) + assert.NoError(t, err) + }) + + // Execute the reconcileConfirmationsForTransaction function + result, err := c.ReconcileConfirmationsForTransaction(context.Background(), "0x6197ef1a58a2a592bb447efb651f0db7945de21aa8048801b250bd7b7431f9b6", &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(1977): { + {BlockNumber: fftypes.FFuint64(1977), BlockHash: generateTestHash(1977), ParentHash: generateTestHash(1976)}, + }, + }, + }, 5) + + // Assertions - expect an error when transaction doesn't exist + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.HasNewFork) + assert.False(t, result.Rebuilt) + assert.False(t, result.HasNewConfirmation) + assert.False(t, result.Confirmed) + assert.Equal(t, &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(1977): { + {BlockNumber: fftypes.FFuint64(1977), BlockHash: generateTestHash(1977), ParentHash: generateTestHash(1976)}, + }, + }, + }, result.ConfirmationMap) + assert.Equal(t, uint64(5), result.TargetConfirmationCount) + + mRPC.AssertExpectations(t) +} + +func TestReconcileConfirmationsForTransaction_BlockRPCCallError(t *testing.T) { + + _, c, mRPC, _ := newTestConnectorWithNoBlockerFilterDefaultMocks(t) + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getTransactionReceipt", + mock.MatchedBy(func(txHash string) bool { + assert.Equal(t, "0x6197ef1a58a2a592bb447efb651f0db7945de21aa8048801b250bd7b7431f9b6", txHash) + return true + })). + Return(nil). + Run(func(args mock.Arguments) { + err := json.Unmarshal([]byte(sampleJSONRPCReceipt), args[1]) + assert.NoError(t, err) + }) + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByNumber", mock.MatchedBy(func(bn *ethtypes.HexInteger) bool { + return bn.BigInt().String() == "1977" + }), false).Return(&rpcbackend.RPCError{Message: "pop"}) + + // Execute the reconcileConfirmationsForTransaction function + result, err := c.ReconcileConfirmationsForTransaction(context.Background(), "0x6197ef1a58a2a592bb447efb651f0db7945de21aa8048801b250bd7b7431f9b6", &ffcapi.ConfirmationMap{}, 5) + + // Assertions - expect an error when RPC call fails + assert.Error(t, err) + assert.Nil(t, result) +} + +func TestReconcileConfirmationsForTransaction_TxBlockNotInCanonicalChain(t *testing.T) { + + _, c, mRPC, _ := newTestConnectorWithNoBlockerFilterDefaultMocks(t) + bl := c.blockListener + bl.canonicalChain = createTestChain(1976, 1978) // Single block at 50, tx is at 100 + + // Mock for TransactionReceipt call + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getTransactionReceipt", + mock.MatchedBy(func(txHash string) bool { + assert.Equal(t, "0x6197ef1a58a2a592bb447efb651f0db7945de21aa8048801b250bd7b7431f9b6", txHash) + return true + })). + Return(nil). + Run(func(args mock.Arguments) { + err := json.Unmarshal([]byte(sampleJSONRPCReceipt), args[1]) + assert.NoError(t, err) + }) + + fakeParentHash := fftypes.NewRandB32().String() + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByNumber", mock.MatchedBy(func(bn *ethtypes.HexInteger) bool { + return bn.BigInt().String() == "1977" + }), false).Return(nil).Run(func(args mock.Arguments) { + *args[1].(**blockInfoJSONRPC) = &blockInfoJSONRPC{ + Number: ethtypes.NewHexInteger64(1977), + Hash: ethtypes.MustNewHexBytes0xPrefix(generateTestHash(1977)), + ParentHash: ethtypes.MustNewHexBytes0xPrefix(fakeParentHash), + } + }) + + // Execute the reconcileConfirmationsForTransaction function + result, err := c.ReconcileConfirmationsForTransaction(context.Background(), "0x6197ef1a58a2a592bb447efb651f0db7945de21aa8048801b250bd7b7431f9b6", &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(1977): {}, + }, + }, 5) + + // Assertions - expect the existing confirmation queue to be returned because the tx block doesn't match the same block number in the canonical chain + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.HasNewFork) + assert.False(t, result.Rebuilt) + assert.False(t, result.HasNewConfirmation) + assert.False(t, result.Confirmed) + assert.Equal(t, &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(1977): {}, + }, + }, result.ConfirmationMap) + assert.Equal(t, uint64(5), result.TargetConfirmationCount) + + mRPC.AssertExpectations(t) +} + +func TestReconcileConfirmationsForTransaction_NewConfirmation(t *testing.T) { + + _, c, mRPC, _ := newTestConnectorWithNoBlockerFilterDefaultMocks(t) + bl := c.blockListener + bl.canonicalChain = createTestChain(1976, 1978) // Single block at 50, tx is at 100 + + // Mock for TransactionReceipt call + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getTransactionReceipt", + mock.MatchedBy(func(txHash string) bool { + assert.Equal(t, "0x6197ef1a58a2a592bb447efb651f0db7945de21aa8048801b250bd7b7431f9b6", txHash) + return true + })). + Return(nil). + Run(func(args mock.Arguments) { + err := json.Unmarshal([]byte(sampleJSONRPCReceipt), args[1]) + assert.NoError(t, err) + }) + + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByNumber", mock.MatchedBy(func(bn *ethtypes.HexInteger) bool { + return bn.BigInt().String() == "1977" + }), false).Return(nil).Run(func(args mock.Arguments) { + *args[1].(**blockInfoJSONRPC) = &blockInfoJSONRPC{ + Number: ethtypes.NewHexInteger64(1977), + Hash: ethtypes.MustNewHexBytes0xPrefix(generateTestHash(1977)), + ParentHash: ethtypes.MustNewHexBytes0xPrefix(generateTestHash(1976)), + } + }) + + // Execute the reconcileConfirmationsForTransaction function + result, err := c.ReconcileConfirmationsForTransaction(context.Background(), "0x6197ef1a58a2a592bb447efb651f0db7945de21aa8048801b250bd7b7431f9b6", &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(1977): {}, + }, + }, 5) + + // Assertions - expect the existing confirmation queue to be returned because the tx block doesn't match the same block number in the canonical chain + assert.NoError(t, err) + assert.NotNil(t, result) + assert.False(t, result.HasNewFork) + assert.False(t, result.Rebuilt) + assert.True(t, result.HasNewConfirmation) + assert.False(t, result.Confirmed) + assert.Equal(t, &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(1977): { + {BlockNumber: fftypes.FFuint64(1977), BlockHash: generateTestHash(1977), ParentHash: generateTestHash(1976)}, + {BlockNumber: fftypes.FFuint64(1978), BlockHash: generateTestHash(1978), ParentHash: generateTestHash(1977)}, + }, + }, + CanonicalBlockHash: generateTestHash(1977), + }, result.ConfirmationMap) + assert.Equal(t, uint64(5), result.TargetConfirmationCount) + + mRPC.AssertExpectations(t) +} + +// Tests of the compareAndUpdateConfirmationQueue function + func TestCompareAndUpdateConfirmationQueue_EmptyChain(t *testing.T) { // Setup - create a chain with one block that's older than the transaction bl := &blockListener{ @@ -37,12 +277,12 @@ func TestCompareAndUpdateConfirmationQueue_EmptyChain(t *testing.T) { ConfirmationMap: &ffcapi.ConfirmationMap{}, } txBlockNumber := uint64(100) - txBlockHash := "0xblock100" + txBlockHash := generateTestHash(txBlockNumber) txBlockInfo := &ffcapi.MinimalBlockInfo{ BlockNumber: fftypes.FFuint64(txBlockNumber), BlockHash: txBlockHash, - ParentHash: "0xblock99", + ParentHash: generateTestHash(txBlockNumber - 1), } targetConfirmationCount := uint64(5) @@ -69,12 +309,12 @@ func TestCompareAndUpdateConfirmationQueue_ChainTooShort(t *testing.T) { ConfirmationMap: &ffcapi.ConfirmationMap{}, } txBlockNumber := uint64(100) - txBlockHash := "0xblock100" + txBlockHash := generateTestHash(txBlockNumber) txBlockInfo := &ffcapi.MinimalBlockInfo{ BlockNumber: fftypes.FFuint64(txBlockNumber), BlockHash: txBlockHash, - ParentHash: "0xblock99", + ParentHash: generateTestHash(txBlockNumber - 1), } targetConfirmationCount := uint64(5) @@ -100,11 +340,11 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMap(t *testing.T) { ConfirmationMap: nil, } txBlockNumber := uint64(100) - txBlockHash := "0xblock100" + txBlockHash := generateTestHash(txBlockNumber) txBlockInfo := &ffcapi.MinimalBlockInfo{ BlockNumber: fftypes.FFuint64(txBlockNumber), BlockHash: txBlockHash, - ParentHash: "0xblock99", + ParentHash: generateTestHash(txBlockNumber - 1), } targetConfirmationCount := uint64(5) @@ -139,11 +379,11 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMapUnconfirmed(t *test ConfirmationMap: nil, } txBlockNumber := uint64(100) - txBlockHash := "0xblock100" + txBlockHash := generateTestHash(txBlockNumber) txBlockInfo := &ffcapi.MinimalBlockInfo{ BlockNumber: fftypes.FFuint64(txBlockNumber), BlockHash: txBlockHash, - ParentHash: "0xblock99", + ParentHash: generateTestHash(txBlockNumber - 1), } targetConfirmationCount := uint64(5) @@ -157,8 +397,8 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMapUnconfirmed(t *test assert.False(t, occ.Confirmed) assert.False(t, occ.Rebuilt) assert.Equal(t, txBlockHash, occ.ConfirmationMap.CanonicalBlockHash) - // The code builds a full confirmation queue from the canonical chain - assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 5) + // The code builds a confirmation queue from the canonical chain up to the available blocks + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 5) // 100, 101, 102, 103, 104 assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) assert.Equal(t, txBlockNumber+1, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) assert.Equal(t, txBlockNumber+2, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) @@ -179,11 +419,11 @@ func TestCompareAndUpdateConfirmationQueue_EmptyConfirmationQueue(t *testing.T) }, } txBlockNumber := uint64(100) - txBlockHash := "0xblock100" + txBlockHash := generateTestHash(txBlockNumber) txBlockInfo := &ffcapi.MinimalBlockInfo{ BlockNumber: fftypes.FFuint64(txBlockNumber), BlockHash: txBlockHash, - ParentHash: "0xblock99", + ParentHash: generateTestHash(txBlockNumber - 1), } targetConfirmationCount := uint64(5) @@ -214,21 +454,21 @@ func TestCompareAndUpdateConfirmationQueue_DifferentBlockNumber(t *testing.T) { } ctx := context.Background() existingQueue := []*ffcapi.MinimalBlockInfo{ - {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(99), ParentHash: "0xblock99"}, + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(99), ParentHash: generateTestHash(98)}, } occ := &ffcapi.ConfirmationMapUpdateResult{ ConfirmationMap: &ffcapi.ConfirmationMap{ ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - "0xblock100": existingQueue, + generateTestHash(100): existingQueue, }, }, } txBlockNumber := uint64(100) - txBlockHash := "0xblock100" + txBlockHash := generateTestHash(txBlockNumber) txBlockInfo := &ffcapi.MinimalBlockInfo{ BlockNumber: fftypes.FFuint64(txBlockNumber), BlockHash: txBlockHash, - ParentHash: "0xblock99", + ParentHash: generateTestHash(txBlockNumber - 1), } targetConfirmationCount := uint64(5) @@ -255,24 +495,24 @@ func TestCompareAndUpdateConfirmationQueue_MismatchConfirmationBlock(t *testing. } ctx := context.Background() existingQueue := []*ffcapi.MinimalBlockInfo{ - {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, - {BlockHash: "0xblock101", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xblock101"}, // wrong parent hash, so the existing queue should be discarded - {BlockHash: "0xblock102", BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblock101"}, - {BlockHash: "0xblock103", BlockNumber: fftypes.FFuint64(103), ParentHash: "0xblock102"}, + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(101)}, // wrong parent hash, so the existing queue should be discarded + {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, + {BlockHash: generateTestHash(103), BlockNumber: fftypes.FFuint64(103), ParentHash: generateTestHash(102)}, } occ := &ffcapi.ConfirmationMapUpdateResult{ ConfirmationMap: &ffcapi.ConfirmationMap{ ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - "0xblock100": existingQueue, + generateTestHash(100): existingQueue, }, }, } txBlockNumber := uint64(100) - txBlockHash := "0xblock100" + txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ BlockNumber: fftypes.FFuint64(txBlockNumber), BlockHash: txBlockHash, - ParentHash: "0xblock99", + ParentHash: generateTestHash(99), } targetConfirmationCount := uint64(5) @@ -297,22 +537,22 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsTooDistant(t *te } ctx := context.Background() existingQueue := []*ffcapi.MinimalBlockInfo{ - {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, - {BlockHash: "0xblock101", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xblock100"}, + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, } occ := &ffcapi.ConfirmationMapUpdateResult{ ConfirmationMap: &ffcapi.ConfirmationMap{ ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - "0xblock100": existingQueue, + generateTestHash(100): existingQueue, }, }, } txBlockNumber := uint64(100) - txBlockHash := "0xblock100" + txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ BlockNumber: fftypes.FFuint64(txBlockNumber), BlockHash: txBlockHash, - ParentHash: "0xblock99", + ParentHash: generateTestHash(99), } targetConfirmationCount := uint64(5) @@ -339,22 +579,22 @@ func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmation(t *test ctx := context.Background() // Create corrupted confirmation (wrong parent hash) existingQueue := []*ffcapi.MinimalBlockInfo{ - {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, - {BlockHash: "0xblock101", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xwrongparent"}, + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: "0xwrongparent"}, } occ := &ffcapi.ConfirmationMapUpdateResult{ ConfirmationMap: &ffcapi.ConfirmationMap{ ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - "0xblock100": existingQueue, + generateTestHash(100): existingQueue, }, }, } txBlockNumber := uint64(100) - txBlockHash := "0xblock100" + txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ BlockNumber: fftypes.FFuint64(txBlockNumber), BlockHash: txBlockHash, - ParentHash: "0xblock99", + ParentHash: generateTestHash(99), } targetConfirmationCount := uint64(5) @@ -368,7 +608,7 @@ func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmation(t *test assert.True(t, occ.Confirmed) assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) - assert.Equal(t, "0xblock100", occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].ParentHash) + assert.Equal(t, generateTestHash(100), occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].ParentHash) } func TestCompareAndUpdateConfirmationQueue_ConnectionNodeMismatch(t *testing.T) { @@ -378,24 +618,24 @@ func TestCompareAndUpdateConfirmationQueue_ConnectionNodeMismatch(t *testing.T) } ctx := context.Background() existingQueue := []*ffcapi.MinimalBlockInfo{ - {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, - {BlockHash: "0xblockwrong", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xblock100"}, - {BlockHash: "0xblock102", BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblock101"}, - {BlockHash: "0xblock103", BlockNumber: fftypes.FFuint64(103), ParentHash: "0xblock102"}, + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: "0xblockwrong", BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, + {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, + {BlockHash: generateTestHash(103), BlockNumber: fftypes.FFuint64(103), ParentHash: generateTestHash(102)}, } occ := &ffcapi.ConfirmationMapUpdateResult{ ConfirmationMap: &ffcapi.ConfirmationMap{ ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - "0xblock100": existingQueue, + generateTestHash(100): existingQueue, }, }, } txBlockNumber := uint64(100) - txBlockHash := "0xblock100" + txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ BlockNumber: fftypes.FFuint64(txBlockNumber), BlockHash: txBlockHash, - ParentHash: "0xblock99", + ParentHash: generateTestHash(99), } targetConfirmationCount := uint64(5) @@ -409,8 +649,8 @@ func TestCompareAndUpdateConfirmationQueue_ConnectionNodeMismatch(t *testing.T) assert.True(t, occ.Confirmed) assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 5) assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) - assert.Equal(t, "0xblock102", occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockHash) - assert.Equal(t, "0xblock102", occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].ParentHash) + assert.Equal(t, generateTestHash(102), occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockHash) + assert.Equal(t, generateTestHash(102), occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].ParentHash) } func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmationAfterFirstConfirmation(t *testing.T) { @@ -420,23 +660,23 @@ func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmationAfterFir } ctx := context.Background() existingQueue := []*ffcapi.MinimalBlockInfo{ - {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, - {BlockHash: "0xblock101", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xblock100"}, - {BlockHash: "0xblock102", BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblockwrong"}, + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, + {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblockwrong"}, } occ := &ffcapi.ConfirmationMapUpdateResult{ ConfirmationMap: &ffcapi.ConfirmationMap{ ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - "0xblock100": existingQueue, + generateTestHash(100): existingQueue, }, }, } txBlockNumber := uint64(100) - txBlockHash := "0xblock100" + txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ BlockNumber: fftypes.FFuint64(txBlockNumber), BlockHash: txBlockHash, - ParentHash: "0xblock99", + ParentHash: generateTestHash(99), } targetConfirmationCount := uint64(5) @@ -450,8 +690,8 @@ func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmationAfterFir assert.True(t, occ.Confirmed) assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 5) assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) - assert.Equal(t, "0xblock102", occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockHash) - assert.Equal(t, "0xblock102", occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].ParentHash) + assert.Equal(t, generateTestHash(102), occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockHash) + assert.Equal(t, generateTestHash(102), occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].ParentHash) } func TestCompareAndUpdateConfirmationQueue_NewForkAfterFirstConfirmation(t *testing.T) { @@ -461,23 +701,23 @@ func TestCompareAndUpdateConfirmationQueue_NewForkAfterFirstConfirmation(t *test } ctx := context.Background() existingQueue := []*ffcapi.MinimalBlockInfo{ - {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, - {BlockHash: "0xblock101", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xblock100"}, - {BlockHash: "fork1", BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblock101"}, + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, + {BlockHash: "fork1", BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, } occ := &ffcapi.ConfirmationMapUpdateResult{ ConfirmationMap: &ffcapi.ConfirmationMap{ ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - "0xblock100": existingQueue, + generateTestHash(100): existingQueue, }, }, } txBlockNumber := uint64(100) - txBlockHash := "0xblock100" + txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ BlockNumber: fftypes.FFuint64(txBlockNumber), BlockHash: txBlockHash, - ParentHash: "0xblock99", + ParentHash: generateTestHash(99), } targetConfirmationCount := uint64(5) @@ -499,24 +739,24 @@ func TestCompareAndUpdateConfirmationQueue_NewForkAndNoConnectionToCanonicalChai } ctx := context.Background() existingQueue := []*ffcapi.MinimalBlockInfo{ - {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, - {BlockHash: "fork1", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xblock100"}, + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: "fork1", BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, {BlockHash: "fork2", BlockNumber: fftypes.FFuint64(102), ParentHash: "fork1"}, {BlockHash: "fork3", BlockNumber: fftypes.FFuint64(103), ParentHash: "fork2"}, } occ := &ffcapi.ConfirmationMapUpdateResult{ ConfirmationMap: &ffcapi.ConfirmationMap{ ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - "0xblock100": existingQueue, + generateTestHash(100): existingQueue, }, }, } txBlockNumber := uint64(100) - txBlockHash := "0xblock100" + txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ BlockNumber: fftypes.FFuint64(txBlockNumber), BlockHash: txBlockHash, - ParentHash: "0xblock99", + ParentHash: generateTestHash(99), } targetConfirmationCount := uint64(5) @@ -542,22 +782,22 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationLaterThanCurrentB } ctx := context.Background() existingQueue := []*ffcapi.MinimalBlockInfo{ - {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, - {BlockHash: "0xblock103", BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblock101"}, + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: generateTestHash(103), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, } occ := &ffcapi.ConfirmationMapUpdateResult{ ConfirmationMap: &ffcapi.ConfirmationMap{ ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - "0xblock100": existingQueue, + generateTestHash(100): existingQueue, }, }, } txBlockNumber := uint64(100) - txBlockHash := "0xblock100" + txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ BlockNumber: fftypes.FFuint64(txBlockNumber), BlockHash: txBlockHash, - ParentHash: "0xblock99", + ParentHash: generateTestHash(99), } targetConfirmationCount := uint64(5) @@ -587,28 +827,28 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable(t *testing.T) { // Create confirmations that already meet the target // and it connects to the canonical chain to validate they are still valid existingQueue := []*ffcapi.MinimalBlockInfo{ - {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, - {BlockHash: "0xblock101", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xblock100"}, - {BlockHash: "0xblock102", BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblock101"}, - {BlockHash: "0xblock103", BlockNumber: fftypes.FFuint64(103), ParentHash: "0xblock102"}, + {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: "0xblock103"}, // discarded - {BlockHash: "0xblock105", BlockNumber: fftypes.FFuint64(105), ParentHash: "0xblock104"}, // discarded + {BlockHash: "0xblock104", BlockNumber: fftypes.FFuint64(104), ParentHash: generateTestHash(103)}, // discarded + {BlockHash: "0xblock105", BlockNumber: fftypes.FFuint64(105), ParentHash: "0xblock104"}, // discarded } occ := &ffcapi.ConfirmationMapUpdateResult{ ConfirmationMap: &ffcapi.ConfirmationMap{ ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - "0xblock100": existingQueue, + generateTestHash(100): existingQueue, }, }, } txBlockNumber := uint64(100) - txBlockHash := "0xblock100" + txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ BlockNumber: fftypes.FFuint64(txBlockNumber), BlockHash: txBlockHash, - ParentHash: "0xblock99", + ParentHash: generateTestHash(99), } targetConfirmationCount := uint64(2) @@ -635,25 +875,25 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableConnectable(t *test // Create confirmations that already meet the target // and it connects to the canonical chain to validate they are still valid existingQueue := []*ffcapi.MinimalBlockInfo{ - {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, - {BlockHash: "0xblock101", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xblock100"}, - {BlockHash: "0xblock102", BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblock101"}, + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, + {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, // didn't have block 103, which is the first block of the canonical chain // but we should still be able to validate the existing confirmations are valid using parent hash } occ := &ffcapi.ConfirmationMapUpdateResult{ ConfirmationMap: &ffcapi.ConfirmationMap{ ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - "0xblock100": existingQueue, + generateTestHash(100): existingQueue, }, }, } txBlockNumber := uint64(100) - txBlockHash := "0xblock100" + txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ BlockNumber: fftypes.FFuint64(txBlockNumber), BlockHash: txBlockHash, - ParentHash: "0xblock99", + ParentHash: generateTestHash(99), } targetConfirmationCount := uint64(1) @@ -681,23 +921,23 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableButAllExistingConfi // Create confirmations that already meet the target // and it connects to the canonical chain to validate they are still valid existingQueue := []*ffcapi.MinimalBlockInfo{ - {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, // gap of 101 is allowed, and is the confirmation required for the transaction with target confirmation count of 1 - {BlockHash: "0xblock102", BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblock101"}, + {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, } occ := &ffcapi.ConfirmationMapUpdateResult{ ConfirmationMap: &ffcapi.ConfirmationMap{ ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - "0xblock100": existingQueue, + generateTestHash(100): existingQueue, }, }, } txBlockNumber := uint64(100) - txBlockHash := "0xblock100" + txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ BlockNumber: fftypes.FFuint64(txBlockNumber), BlockHash: txBlockHash, - ParentHash: "0xblock99", + ParentHash: generateTestHash(99), } targetConfirmationCount := uint64(1) @@ -726,23 +966,23 @@ func TestCompareAndUpdateConfirmationQueue_HasSufficientConfirmationsButNoOverla // Create confirmations that already meet the target // and it connects to the canonical chain to validate they are still valid existingQueue := []*ffcapi.MinimalBlockInfo{ - {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, - {BlockHash: "0xblock101", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xblock100"}, - {BlockHash: "0xblock102", BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblock101"}, + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, + {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, } occ := &ffcapi.ConfirmationMapUpdateResult{ ConfirmationMap: &ffcapi.ConfirmationMap{ ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - "0xblock100": existingQueue, + generateTestHash(100): existingQueue, }, }, } txBlockNumber := uint64(100) - txBlockHash := "0xblock100" + txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ BlockNumber: fftypes.FFuint64(txBlockNumber), BlockHash: txBlockHash, - ParentHash: "0xblock99", + ParentHash: generateTestHash(99), } targetConfirmationCount := uint64(1) @@ -769,23 +1009,23 @@ func TestCompareAndUpdateConfirmationQueue_ValidExistingConfirmations(t *testing } ctx := context.Background() existingQueue := []*ffcapi.MinimalBlockInfo{ - {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, - {BlockHash: "0xblock101", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xblock100"}, - {BlockHash: "0xblock102", BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblock101"}, + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, + {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, } occ := &ffcapi.ConfirmationMapUpdateResult{ ConfirmationMap: &ffcapi.ConfirmationMap{ ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - "0xblock100": existingQueue, + generateTestHash(100): existingQueue, }, }, } txBlockNumber := uint64(100) - txBlockHash := "0xblock100" + txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ BlockNumber: fftypes.FFuint64(txBlockNumber), BlockHash: txBlockHash, - ParentHash: "0xblock99", + ParentHash: generateTestHash(99), } targetConfirmationCount := uint64(5) @@ -794,7 +1034,49 @@ func TestCompareAndUpdateConfirmationQueue_ValidExistingConfirmations(t *testing // Assert assert.False(t, occ.HasNewFork) - assert.True(t, occ.Rebuilt) + assert.False(t, occ.Rebuilt) + assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.Confirmed) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][5].BlockNumber)) +} + +func TestCompareAndUpdateConfirmationQueue_ValidExistingTxBlock(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(50, 150), + } + ctx := context.Background() + existingQueue := []*ffcapi.MinimalBlockInfo{ + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + } + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(100): existingQueue, + }, + }, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(5) + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.False(t, occ.HasNewFork) + assert.False(t, occ.Rebuilt) assert.True(t, occ.HasNewConfirmation) assert.True(t, occ.Confirmed) assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) @@ -818,11 +1100,11 @@ func TestCompareAndUpdateConfirmationQueue_ReachTargetConfirmation(t *testing.T) }, } txBlockNumber := uint64(100) - txBlockHash := "0xblock100" + txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ BlockNumber: fftypes.FFuint64(txBlockNumber), BlockHash: txBlockHash, - ParentHash: "0xblock99", + ParentHash: generateTestHash(99), } targetConfirmationCount := uint64(3) @@ -844,24 +1126,24 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithGap(t *testi ctx := context.Background() // Create confirmations with a gap (missing block 102) existingQueue := []*ffcapi.MinimalBlockInfo{ - {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, // no block 101, which is the first block of the canonical chain - {BlockHash: "0xblock102", BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblock101"}, - {BlockHash: "0xblock103", BlockNumber: fftypes.FFuint64(103), ParentHash: "0xblock102"}, + {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, + {BlockHash: generateTestHash(103), BlockNumber: fftypes.FFuint64(103), ParentHash: generateTestHash(102)}, } occ := &ffcapi.ConfirmationMapUpdateResult{ ConfirmationMap: &ffcapi.ConfirmationMap{ ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - "0xblock100": existingQueue, + generateTestHash(100): existingQueue, }, }, } txBlockNumber := uint64(100) - txBlockHash := "0xblock100" + txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ BlockNumber: fftypes.FFuint64(txBlockNumber), BlockHash: txBlockHash, - ParentHash: "0xblock99", + ParentHash: generateTestHash(99), } targetConfirmationCount := uint64(5) @@ -884,22 +1166,22 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNu ctx := context.Background() // Create confirmations with a lower block number existingQueue := []*ffcapi.MinimalBlockInfo{ - {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, - {BlockHash: "0xblock101", BlockNumber: fftypes.FFuint64(99), ParentHash: "0xblock100"}, // somehow there is a lower block number + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(99), ParentHash: generateTestHash(100)}, // somehow there is a lower block number } occ := &ffcapi.ConfirmationMapUpdateResult{ ConfirmationMap: &ffcapi.ConfirmationMap{ ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - "0xblock100": existingQueue, + generateTestHash(100): existingQueue, }, }, } txBlockNumber := uint64(100) - txBlockHash := "0xblock100" + txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ BlockNumber: fftypes.FFuint64(txBlockNumber), BlockHash: txBlockHash, - ParentHash: "0xblock99", + ParentHash: generateTestHash(99), } targetConfirmationCount := uint64(5) @@ -922,23 +1204,23 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNu ctx := context.Background() // Create confirmations with a lower block number existingQueue := []*ffcapi.MinimalBlockInfo{ - {BlockHash: "0xblock100", BlockNumber: fftypes.FFuint64(100), ParentHash: "0xblock99"}, - {BlockHash: "0xblock101", BlockNumber: fftypes.FFuint64(101), ParentHash: "0xblock100"}, - {BlockHash: "0xblock102", BlockNumber: fftypes.FFuint64(99), ParentHash: "0xblock101"}, // somehow there is a lower block number + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, + {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(99), ParentHash: generateTestHash(101)}, // somehow there is a lower block number } occ := &ffcapi.ConfirmationMapUpdateResult{ ConfirmationMap: &ffcapi.ConfirmationMap{ ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - "0xblock100": existingQueue, + generateTestHash(100): existingQueue, }, }, } txBlockNumber := uint64(100) - txBlockHash := "0xblock100" + txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ BlockNumber: fftypes.FFuint64(txBlockNumber), BlockHash: txBlockHash, - ParentHash: "0xblock99", + ParentHash: generateTestHash(99), } targetConfirmationCount := uint64(5) @@ -955,13 +1237,28 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNu // 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: fmt.Sprintf("0xblock%d", i), - ParentHash: fmt.Sprintf("0xblock%d", i-1), + BlockHash: blockHash, + ParentHash: parentHash, } chain.PushBack(blockInfo) } From ea0f58d94404dd86362d857a86365abb1712af23 Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Thu, 11 Sep 2025 19:21:50 +0100 Subject: [PATCH 05/37] Apply suggestions from code review Co-authored-by: alexey semenyuk Signed-off-by: Chengxuan Xing --- internal/ethereum/blocklistener_blockquery.go | 2 +- internal/ethereum/confirmation_reconciler.go | 16 +++++++--------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/internal/ethereum/blocklistener_blockquery.go b/internal/ethereum/blocklistener_blockquery.go index e75b8a5..67bbb7d 100644 --- a/internal/ethereum/blocklistener_blockquery.go +++ b/internal/ethereum/blocklistener_blockquery.go @@ -75,7 +75,7 @@ func (bl *blockListener) getBlockInfoContainsTxHash(ctx context.Context, txHash txBlockNumber := res.BlockNumber.Uint64() // get the parent hash of the transaction block bi, _, 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 + if err != nil && reason != ffcapi.ErrorReasonNotFound { // if the block info is not found, then there could be a fork, we don't throw error in this case and treating it as block not found return nil, i18n.WrapError(ctx, err, msgs.MsgFailedToQueryBlockInfo, txHash) } if bi == nil { diff --git a/internal/ethereum/confirmation_reconciler.go b/internal/ethereum/confirmation_reconciler.go index 90e91c4..3f63162 100644 --- a/internal/ethereum/confirmation_reconciler.go +++ b/internal/ethereum/confirmation_reconciler.go @@ -145,16 +145,14 @@ func (bl *blockListener) processExistingConfirmations(ctx context.Context, occ * currentBlock = currentBlock.Next() } - if len(existingQueue) == 0 { - return newQueue, currentBlock - } - - existingConfirmations := existingQueue[1:] - if len(existingConfirmations) == 0 { - return newQueue, currentBlock - } + if len(existingQueue) <= 1 { + return newQueue, currentBlock + } - return bl.validateExistingConfirmations(ctx, occ, newQueue, existingConfirmations, currentBlock, chainHead, txBlockInfo, targetConfirmationCount) + existingConfirmations := existingQueue[1:] + return bl.validateExistingConfirmations( + ctx, occ, newQueue, existingConfirmations, currentBlock, chainHead, txBlockInfo, targetConfirmationCount, +) } func (bl *blockListener) validateExistingConfirmations(ctx context.Context, occ *ffcapi.ConfirmationMapUpdateResult, newQueue []*ffcapi.MinimalBlockInfo, existingConfirmations []*ffcapi.MinimalBlockInfo, currentBlock *list.Element, chainHead *ffcapi.MinimalBlockInfo, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) ([]*ffcapi.MinimalBlockInfo, *list.Element) { From 0a086129a65f74e66caabe0d22997e582371a030 Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Thu, 11 Sep 2025 19:35:39 +0100 Subject: [PATCH 06/37] fixing get_blockInfo not found logic. Signed-off-by: Chengxuan Xing --- internal/ethereum/blocklistener_blockquery.go | 10 +++------- internal/ethereum/get_block_info_test.go | 6 +++++- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/ethereum/blocklistener_blockquery.go b/internal/ethereum/blocklistener_blockquery.go index 67bbb7d..5b5d97f 100644 --- a/internal/ethereum/blocklistener_blockquery.go +++ b/internal/ethereum/blocklistener_blockquery.go @@ -74,8 +74,8 @@ func (bl *blockListener) getBlockInfoContainsTxHash(ctx context.Context, txHash txBlockHash := res.BlockHash txBlockNumber := res.BlockNumber.Uint64() // get the parent hash of the transaction block - bi, _, 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, we don't throw error in this case and treating it as block not found + bi, reason, err := bl.getBlockInfoByNumber(ctx, txBlockNumber, true, txBlockHash) + if err != nil && reason != ffcapi.ErrorReasonNotFound { // if the block info is not found, then there could be a fork, twe don't throw error in this case and treating it as block not found return nil, i18n.WrapError(ctx, err, msgs.MsgFailedToQueryBlockInfo, txHash) } if bi == nil { @@ -105,14 +105,10 @@ func (bl *blockListener) getBlockInfoByNumber(ctx context.Context, blockNumber u if blockInfo == nil { 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/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) From 578db356740c820a73122dbd2bb5323235530d39 Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Fri, 12 Sep 2025 11:32:59 +0100 Subject: [PATCH 07/37] fixing formating Signed-off-by: Chengxuan Xing --- internal/ethereum/confirmation_reconciler.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/ethereum/confirmation_reconciler.go b/internal/ethereum/confirmation_reconciler.go index 3f63162..382f659 100644 --- a/internal/ethereum/confirmation_reconciler.go +++ b/internal/ethereum/confirmation_reconciler.go @@ -146,13 +146,13 @@ func (bl *blockListener) processExistingConfirmations(ctx context.Context, occ * } if len(existingQueue) <= 1 { - return newQueue, currentBlock - } + return newQueue, currentBlock + } - existingConfirmations := existingQueue[1:] - return bl.validateExistingConfirmations( - ctx, occ, newQueue, existingConfirmations, currentBlock, chainHead, txBlockInfo, targetConfirmationCount, -) + existingConfirmations := existingQueue[1:] + return bl.validateExistingConfirmations( + ctx, occ, newQueue, existingConfirmations, currentBlock, chainHead, txBlockInfo, targetConfirmationCount, + ) } func (bl *blockListener) validateExistingConfirmations(ctx context.Context, occ *ffcapi.ConfirmationMapUpdateResult, newQueue []*ffcapi.MinimalBlockInfo, existingConfirmations []*ffcapi.MinimalBlockInfo, currentBlock *list.Element, chainHead *ffcapi.MinimalBlockInfo, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) ([]*ffcapi.MinimalBlockInfo, *list.Element) { From 5b9f1c86f99d1f4776969633eb96a4422be1d254 Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Mon, 15 Sep 2025 10:26:06 +0100 Subject: [PATCH 08/37] don't treat brand new confirmation as a new fork Signed-off-by: Chengxuan Xing --- internal/ethereum/confirmation_reconciler.go | 1 - internal/ethereum/confirmation_reconciler_test.go | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/internal/ethereum/confirmation_reconciler.go b/internal/ethereum/confirmation_reconciler.go index 382f659..ce78a5f 100644 --- a/internal/ethereum/confirmation_reconciler.go +++ b/internal/ethereum/confirmation_reconciler.go @@ -104,7 +104,6 @@ func (bl *blockListener) initializeConfirmationMap(occ *ffcapi.ConfirmationMapUp }, CanonicalBlockHash: txBlockHash, } - occ.HasNewFork = true return nil } diff --git a/internal/ethereum/confirmation_reconciler_test.go b/internal/ethereum/confirmation_reconciler_test.go index 18cf746..136a45a 100644 --- a/internal/ethereum/confirmation_reconciler_test.go +++ b/internal/ethereum/confirmation_reconciler_test.go @@ -353,7 +353,7 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMap(t *testing.T) { // Assert assert.NotNil(t, occ.ConfirmationMap) - assert.True(t, occ.HasNewFork) + assert.False(t, occ.HasNewFork) assert.True(t, occ.HasNewConfirmation) assert.True(t, occ.Confirmed) assert.False(t, occ.Rebuilt) @@ -392,7 +392,7 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMapUnconfirmed(t *test // Assert assert.NotNil(t, occ.ConfirmationMap) - assert.True(t, occ.HasNewFork) + assert.False(t, occ.HasNewFork) assert.True(t, occ.HasNewConfirmation) assert.False(t, occ.Confirmed) assert.False(t, occ.Rebuilt) @@ -431,7 +431,7 @@ func TestCompareAndUpdateConfirmationQueue_EmptyConfirmationQueue(t *testing.T) bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) // Assert - assert.True(t, occ.HasNewFork) + assert.False(t, occ.HasNewFork) assert.True(t, occ.HasNewConfirmation) assert.True(t, occ.Confirmed) assert.False(t, occ.Rebuilt) From b16e6a8a696ff8a770beea389d523c45b8493c44 Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Mon, 15 Sep 2025 16:04:23 +0100 Subject: [PATCH 09/37] fixing 0 confirmations behaviour Signed-off-by: Chengxuan Xing --- internal/ethereum/confirmation_reconciler.go | 14 +++ .../ethereum/confirmation_reconciler_test.go | 117 ++++++++++++++++++ 2 files changed, 131 insertions(+) diff --git a/internal/ethereum/confirmation_reconciler.go b/internal/ethereum/confirmation_reconciler.go index ce78a5f..7d10d66 100644 --- a/internal/ethereum/confirmation_reconciler.go +++ b/internal/ethereum/confirmation_reconciler.go @@ -74,6 +74,19 @@ func (bl *blockListener) compareAndUpdateConfirmationQueue(ctx context.Context, // Initialize confirmation map and get existing queue existingQueue := bl.initializeConfirmationMap(occ, txBlockInfo) + if targetConfirmationCount == 0 { + // if the target confirmation count is 0, we should just return the transaction block + occ.Confirmed = true + // Only return the transaction block for zero confirmation + occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash] = []*ffcapi.MinimalBlockInfo{txBlockInfo} + // For zero confirmation, determine if we processed new confirmations + if occ.Rebuilt || occ.HasNewFork { + // If a fork was detected, we processed new confirmations + occ.HasNewConfirmation = true + } + return + } + // Validate and process existing confirmations newQueue, currentBlock := bl.processExistingConfirmations(ctx, occ, txBlockInfo, existingQueue, chainHead, targetConfirmationCount) @@ -104,6 +117,7 @@ func (bl *blockListener) initializeConfirmationMap(occ *ffcapi.ConfirmationMapUp }, CanonicalBlockHash: txBlockHash, } + occ.HasNewConfirmation = true return nil } diff --git a/internal/ethereum/confirmation_reconciler_test.go b/internal/ethereum/confirmation_reconciler_test.go index 136a45a..982bf47 100644 --- a/internal/ethereum/confirmation_reconciler_test.go +++ b/internal/ethereum/confirmation_reconciler_test.go @@ -369,6 +369,39 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMap(t *testing.T) { } +func TestCompareAndUpdateConfirmationQueue_NilConfirmationMap_ZeroConfirmationCount(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(50, 150), + } + ctx := context.Background() + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: nil, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(txBlockNumber) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(txBlockNumber - 1), + } + targetConfirmationCount := uint64(0) + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.NotNil(t, occ.ConfirmationMap) + assert.False(t, occ.HasNewFork) + assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.Confirmed) + assert.False(t, occ.Rebuilt) + assert.Equal(t, txBlockHash, occ.ConfirmationMap.CanonicalBlockHash) + // The code builds a full confirmation queue from the canonical chain + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 1) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) +} + func TestCompareAndUpdateConfirmationQueue_NilConfirmationMapUnconfirmed(t *testing.T) { // Setup bl := &blockListener{ @@ -732,6 +765,44 @@ func TestCompareAndUpdateConfirmationQueue_NewForkAfterFirstConfirmation(t *test assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) } +func TestCompareAndUpdateConfirmationQueue_NewForkAfterFirstConfirmation_ZeroConfirmationCount(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(100, 150), + } + ctx := context.Background() + existingQueue := []*ffcapi.MinimalBlockInfo{ + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, + {BlockHash: "fork1", BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, + } + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(100): existingQueue, + }, + }, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(0) + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.False(t, occ.HasNewFork) + assert.False(t, occ.Rebuilt) + assert.False(t, occ.HasNewConfirmation) + assert.True(t, occ.Confirmed) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 1) +} + func TestCompareAndUpdateConfirmationQueue_NewForkAndNoConnectionToCanonicalChain(t *testing.T) { // Setup bl := &blockListener{ @@ -866,6 +937,52 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable(t *testing.T) { assert.Equal(t, txBlockNumber+2, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) } +func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable_ZeroConfirmationCount(t *testing.T) { + // Setup + bl := &blockListener{ + canonicalChain: createTestChain(103, 150), + } + ctx := context.Background() + // Create confirmations that already meet the target + // and it connects to the canonical chain to validate they are still valid + existingQueue := []*ffcapi.MinimalBlockInfo{ + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, + {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, + {BlockHash: generateTestHash(103), BlockNumber: fftypes.FFuint64(103), ParentHash: generateTestHash(102)}, + + // all blocks after the first block of the canonical chain are discarded in the final confirmation queue + {BlockHash: "0xblock104", BlockNumber: fftypes.FFuint64(104), ParentHash: generateTestHash(103)}, // discarded + {BlockHash: "0xblock105", BlockNumber: fftypes.FFuint64(105), ParentHash: "0xblock104"}, // discarded + } + occ := &ffcapi.ConfirmationMapUpdateResult{ + ConfirmationMap: &ffcapi.ConfirmationMap{ + ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ + generateTestHash(100): existingQueue, + }, + }, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(0) + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.True(t, occ.Confirmed) + assert.False(t, occ.Rebuilt) + assert.False(t, occ.HasNewFork) + assert.False(t, occ.HasNewConfirmation) + assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 1) + assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) +} + func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableConnectable(t *testing.T) { // Setup bl := &blockListener{ From c3c31f97bc891cce027056aa23e33589a37a0156 Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Wed, 17 Sep 2025 12:52:13 +0100 Subject: [PATCH 10/37] remove redundant check Signed-off-by: Chengxuan Xing --- internal/ethereum/confirmation_reconciler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/ethereum/confirmation_reconciler.go b/internal/ethereum/confirmation_reconciler.go index 7d10d66..ab551bc 100644 --- a/internal/ethereum/confirmation_reconciler.go +++ b/internal/ethereum/confirmation_reconciler.go @@ -66,7 +66,7 @@ func (bl *blockListener) compareAndUpdateConfirmationQueue(ctx context.Context, chainHead := bl.canonicalChain.Front().Value.(*ffcapi.MinimalBlockInfo) chainTail := bl.canonicalChain.Back().Value.(*ffcapi.MinimalBlockInfo) - if chainHead == nil || chainTail == nil || chainTail.BlockNumber.Uint64() < txBlockNumber { + if chainTail == nil || chainTail.BlockNumber.Uint64() < txBlockNumber { log.L(ctx).Debugf("Canonical chain is waiting for the transaction block %d to be indexed", txBlockNumber) return } From 73498c9b8b5107f10ad65491ba715f26ed295ff3 Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Wed, 17 Sep 2025 13:01:01 +0100 Subject: [PATCH 11/37] Apply suggestions from code review Co-authored-by: Enrique Lacal Signed-off-by: Chengxuan Xing --- internal/ethereum/confirmation_reconciler.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/ethereum/confirmation_reconciler.go b/internal/ethereum/confirmation_reconciler.go index ab551bc..200c73d 100644 --- a/internal/ethereum/confirmation_reconciler.go +++ b/internal/ethereum/confirmation_reconciler.go @@ -270,7 +270,7 @@ func (bl *blockListener) validateExistingConfirmations(ctx context.Context, occ confirmationBlockNumber := txBlockNumber + targetConfirmationCount if lastBlockInNewQueue.BlockNumber.Uint64() >= confirmationBlockNumber { chainHead := bl.canonicalChain.Front().Value.(*ffcapi.MinimalBlockInfo) - // we've got a confirmable so whether the rest of the chain has forked is not longer relevant + // we've got a confirmation so whether the rest of the chain has forked is no longer relevant // this could happen when user chose a different target confirmation count for the new checks // but we still need to validate the existing confirmations are connectable to the canonical chain // Check if the queue connects to the canonical chain @@ -292,7 +292,7 @@ func (bl *blockListener) validateExistingConfirmations(ctx context.Context, occ } // If we've trimmed off all the existing confirmations, we need to add the canonical chain head - // to tell use the head block we used to confirmed the transaction + // to tell us the head block we used to confirm the transaction if len(trimmedQueue) == 1 { trimmedQueue = append(trimmedQueue, chainHead) } From fe0fb668d3babeb49daadfc2ecfd7ff5ac5111ab Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Wed, 17 Sep 2025 13:01:35 +0100 Subject: [PATCH 12/37] rename variable Signed-off-by: Chengxuan Xing --- internal/ethereum/confirmation_reconciler.go | 80 ++++++++++---------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/internal/ethereum/confirmation_reconciler.go b/internal/ethereum/confirmation_reconciler.go index 200c73d..39865f0 100644 --- a/internal/ethereum/confirmation_reconciler.go +++ b/internal/ethereum/confirmation_reconciler.go @@ -27,7 +27,7 @@ import ( func (bl *blockListener) reconcileConfirmationsForTransaction(ctx context.Context, txHash string, confirmMap *ffcapi.ConfirmationMap, targetConfirmationCount uint64) (*ffcapi.ConfirmationMapUpdateResult, error) { // Initialize the output context - occ := &ffcapi.ConfirmationMapUpdateResult{ + reconcileResult := &ffcapi.ConfirmationMapUpdateResult{ ConfirmationMap: confirmMap, HasNewFork: false, Rebuilt: false, @@ -44,13 +44,13 @@ func (bl *blockListener) reconcileConfirmationsForTransaction(ctx context.Contex if txBlockInfo == nil { log.L(ctx).Debugf("Transaction %s not found in any block", txHash) - return occ, nil + return reconcileResult, nil } // Compare the existing confirmation queue with the in-memory linked list - bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + bl.compareAndUpdateConfirmationQueue(ctx, reconcileResult, txBlockInfo, targetConfirmationCount) - return occ, nil + return reconcileResult, nil } // NOTE: this function only build up the confirmation queue uses the in-memory canonical chain @@ -58,7 +58,7 @@ func (bl *blockListener) reconcileConfirmationsForTransaction(ctx context.Contex // compareAndUpdateConfirmationQueue compares the existing confirmation queue with the in-memory linked list // this function obtains the read lock on the canonical chain, so it should not make any long-running queries -func (bl *blockListener) compareAndUpdateConfirmationQueue(ctx context.Context, occ *ffcapi.ConfirmationMapUpdateResult, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) { +func (bl *blockListener) compareAndUpdateConfirmationQueue(ctx context.Context, reconcileResult *ffcapi.ConfirmationMapUpdateResult, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) { bl.mux.RLock() defer bl.mux.RUnlock() txBlockNumber := txBlockInfo.BlockNumber.Uint64() @@ -72,23 +72,23 @@ func (bl *blockListener) compareAndUpdateConfirmationQueue(ctx context.Context, } // Initialize confirmation map and get existing queue - existingQueue := bl.initializeConfirmationMap(occ, txBlockInfo) + existingQueue := bl.initializeConfirmationMap(reconcileResult, txBlockInfo) if targetConfirmationCount == 0 { // if the target confirmation count is 0, we should just return the transaction block - occ.Confirmed = true + reconcileResult.Confirmed = true // Only return the transaction block for zero confirmation - occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash] = []*ffcapi.MinimalBlockInfo{txBlockInfo} + reconcileResult.ConfirmationMap.ConfirmationQueueMap[txBlockHash] = []*ffcapi.MinimalBlockInfo{txBlockInfo} // For zero confirmation, determine if we processed new confirmations - if occ.Rebuilt || occ.HasNewFork { + if reconcileResult.Rebuilt || reconcileResult.HasNewFork { // If a fork was detected, we processed new confirmations - occ.HasNewConfirmation = true + reconcileResult.HasNewConfirmation = true } return } // Validate and process existing confirmations - newQueue, currentBlock := bl.processExistingConfirmations(ctx, occ, txBlockInfo, existingQueue, chainHead, targetConfirmationCount) + newQueue, currentBlock := bl.processExistingConfirmations(ctx, reconcileResult, txBlockInfo, existingQueue, chainHead, targetConfirmationCount) if currentBlock == nil { // the tx block is not in the canonical chain @@ -97,39 +97,39 @@ func (bl *blockListener) compareAndUpdateConfirmationQueue(ctx context.Context, } // Build new confirmations from canonical chain only if not already confirmed - if !occ.Confirmed { - newQueue = bl.buildNewConfirmations(occ, newQueue, currentBlock, txBlockNumber, targetConfirmationCount) + if !reconcileResult.Confirmed { + newQueue = bl.buildNewConfirmations(reconcileResult, newQueue, currentBlock, txBlockNumber, targetConfirmationCount) } - occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash] = newQueue + reconcileResult.ConfirmationMap.ConfirmationQueueMap[txBlockHash] = newQueue - if occ.CanonicalBlockHash != txBlockHash { - occ.CanonicalBlockHash = txBlockHash + if reconcileResult.CanonicalBlockHash != txBlockHash { + reconcileResult.CanonicalBlockHash = txBlockHash } } -func (bl *blockListener) initializeConfirmationMap(occ *ffcapi.ConfirmationMapUpdateResult, txBlockInfo *ffcapi.MinimalBlockInfo) []*ffcapi.MinimalBlockInfo { +func (bl *blockListener) initializeConfirmationMap(reconcileResult *ffcapi.ConfirmationMapUpdateResult, txBlockInfo *ffcapi.MinimalBlockInfo) []*ffcapi.MinimalBlockInfo { txBlockHash := txBlockInfo.BlockHash - if occ.ConfirmationMap == nil || len(occ.ConfirmationMap.ConfirmationQueueMap) == 0 { - occ.ConfirmationMap = &ffcapi.ConfirmationMap{ + if reconcileResult.ConfirmationMap == nil || len(reconcileResult.ConfirmationMap.ConfirmationQueueMap) == 0 { + reconcileResult.ConfirmationMap = &ffcapi.ConfirmationMap{ ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ txBlockHash: {txBlockInfo}, }, CanonicalBlockHash: txBlockHash, } - occ.HasNewConfirmation = true + reconcileResult.HasNewConfirmation = true return nil } - existingQueue := occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash] + existingQueue := reconcileResult.ConfirmationMap.ConfirmationQueueMap[txBlockHash] if len(existingQueue) > 0 { existingTxBlock := existingQueue[0] if !existingTxBlock.Equal(txBlockInfo) { // the tx block in the existing queue does not match the new tx block we queried from the chain // rebuild a new confirmation queue with the new tx block - occ.HasNewFork = true - occ.Rebuilt = true - occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash] = []*ffcapi.MinimalBlockInfo{txBlockInfo} + reconcileResult.HasNewFork = true + reconcileResult.Rebuilt = true + reconcileResult.ConfirmationMap.ConfirmationQueueMap[txBlockHash] = []*ffcapi.MinimalBlockInfo{txBlockInfo} return nil } } @@ -137,7 +137,7 @@ func (bl *blockListener) initializeConfirmationMap(occ *ffcapi.ConfirmationMapUp return existingQueue } -func (bl *blockListener) processExistingConfirmations(ctx context.Context, occ *ffcapi.ConfirmationMapUpdateResult, txBlockInfo *ffcapi.MinimalBlockInfo, existingQueue []*ffcapi.MinimalBlockInfo, chainHead *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) ([]*ffcapi.MinimalBlockInfo, *list.Element) { +func (bl *blockListener) processExistingConfirmations(ctx context.Context, reconcileResult *ffcapi.ConfirmationMapUpdateResult, txBlockInfo *ffcapi.MinimalBlockInfo, existingQueue []*ffcapi.MinimalBlockInfo, chainHead *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) ([]*ffcapi.MinimalBlockInfo, *list.Element) { txBlockNumber := txBlockInfo.BlockNumber.Uint64() newQueue := []*ffcapi.MinimalBlockInfo{txBlockInfo} @@ -164,11 +164,11 @@ func (bl *blockListener) processExistingConfirmations(ctx context.Context, occ * existingConfirmations := existingQueue[1:] return bl.validateExistingConfirmations( - ctx, occ, newQueue, existingConfirmations, currentBlock, chainHead, txBlockInfo, targetConfirmationCount, + ctx, reconcileResult, newQueue, existingConfirmations, currentBlock, chainHead, txBlockInfo, targetConfirmationCount, ) } -func (bl *blockListener) validateExistingConfirmations(ctx context.Context, occ *ffcapi.ConfirmationMapUpdateResult, newQueue []*ffcapi.MinimalBlockInfo, existingConfirmations []*ffcapi.MinimalBlockInfo, currentBlock *list.Element, chainHead *ffcapi.MinimalBlockInfo, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) ([]*ffcapi.MinimalBlockInfo, *list.Element) { +func (bl *blockListener) validateExistingConfirmations(ctx context.Context, reconcileResult *ffcapi.ConfirmationMapUpdateResult, newQueue []*ffcapi.MinimalBlockInfo, existingConfirmations []*ffcapi.MinimalBlockInfo, currentBlock *list.Element, chainHead *ffcapi.MinimalBlockInfo, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) ([]*ffcapi.MinimalBlockInfo, *list.Element) { txBlockNumber := txBlockInfo.BlockNumber.Uint64() lastExistingConfirmation := existingConfirmations[len(existingConfirmations)-1] if lastExistingConfirmation.BlockNumber.Uint64() < chainHead.BlockNumber.Uint64() && @@ -179,7 +179,7 @@ func (bl *blockListener) validateExistingConfirmations(ctx context.Context, occ // Therefore, there is no connection between the existing confirmations and the canonical chain // so that we cannot validate the existing confirmations are from the same fork as the canonical chain // so we need to rebuild the confirmations queue - occ.Rebuilt = true + reconcileResult.Rebuilt = true return newQueue, currentBlock } @@ -195,7 +195,7 @@ func (bl *blockListener) validateExistingConfirmations(ctx context.Context, occ // if any block in the existing confirmation queue is earlier than the tx block // the existing confirmation queue is no valid // we need to rebuild the confirmations queue - occ.Rebuilt = true + reconcileResult.Rebuilt = true return newQueue[:1], currentBlock } @@ -216,7 +216,7 @@ func (bl *blockListener) validateExistingConfirmations(ctx context.Context, occ if isCorrupted { // any corruption in the existing confirmation queue will cause the confirmation queue to be rebuilt // we don't keep any of the existing confirmations - occ.Rebuilt = true + reconcileResult.Rebuilt = true return newQueue[:1], currentBlock } @@ -242,12 +242,12 @@ func (bl *blockListener) validateExistingConfirmations(ctx context.Context, occ // this is the connection node (first overlap between existing confirmation queue and canonical chain) // if the first node doesn't chain to to the previous confirmation, it means all the historical confirmation are on a different fork // therefore, we need to rebuild the confirmations queue - occ.Rebuilt = true + reconcileResult.Rebuilt = true return newQueue[:1], currentBlock } // other scenarios, the historical confirmation are still trustworthy and linked to our canonical chain - occ.HasNewFork = true + reconcileResult.HasNewFork = true return newQueue, currentBlock } @@ -261,7 +261,7 @@ func (bl *blockListener) validateExistingConfirmations(ctx context.Context, occ continue } - occ.Rebuilt = true + reconcileResult.Rebuilt = true return newQueue[:1], currentBlock } @@ -277,10 +277,10 @@ func (bl *blockListener) validateExistingConfirmations(ctx context.Context, occ if lastBlockInNewQueue.BlockNumber.Uint64() >= chainHead.BlockNumber.Uint64() || (lastBlockInNewQueue.BlockNumber.Uint64() == chainHead.BlockNumber.Uint64()-1 && lastBlockInNewQueue.BlockHash == chainHead.ParentHash) { - occ.HasNewFork = false - occ.HasNewConfirmation = false - occ.Rebuilt = false - occ.Confirmed = true + reconcileResult.HasNewFork = false + reconcileResult.HasNewConfirmation = false + reconcileResult.Rebuilt = false + reconcileResult.Confirmed = true // Trim the queue to only include blocks up to the max confirmation count trimmedQueue := []*ffcapi.MinimalBlockInfo{} @@ -303,18 +303,18 @@ func (bl *blockListener) validateExistingConfirmations(ctx context.Context, occ return newQueue, currentBlock } -func (bl *blockListener) buildNewConfirmations(occ *ffcapi.ConfirmationMapUpdateResult, newQueue []*ffcapi.MinimalBlockInfo, currentBlock *list.Element, txBlockNumber uint64, targetConfirmationCount uint64) []*ffcapi.MinimalBlockInfo { +func (bl *blockListener) buildNewConfirmations(reconcileResult *ffcapi.ConfirmationMapUpdateResult, newQueue []*ffcapi.MinimalBlockInfo, currentBlock *list.Element, txBlockNumber uint64, targetConfirmationCount uint64) []*ffcapi.MinimalBlockInfo { for currentBlock != nil { currentBlockInfo := currentBlock.Value.(*ffcapi.MinimalBlockInfo) if currentBlockInfo.BlockNumber.Uint64() > newQueue[len(newQueue)-1].BlockNumber.Uint64() { - occ.HasNewConfirmation = true + reconcileResult.HasNewConfirmation = true newQueue = append(newQueue, &ffcapi.MinimalBlockInfo{ BlockHash: currentBlockInfo.BlockHash, BlockNumber: fftypes.FFuint64(currentBlockInfo.BlockNumber.Uint64()), ParentHash: currentBlockInfo.ParentHash, }) if currentBlockInfo.BlockNumber.Uint64() >= txBlockNumber+targetConfirmationCount { - occ.Confirmed = true + reconcileResult.Confirmed = true break } } From 91858a110fffc786cfdecc393ef5f3d248790aaa Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Tue, 30 Sep 2025 12:57:50 +0100 Subject: [PATCH 13/37] return a single confirmations array Signed-off-by: Chengxuan Xing --- go.mod | 2 +- go.sum | 2 + internal/ethereum/confirmation_reconciler.go | 32 +- .../ethereum/confirmation_reconciler_test.go | 408 +++++++----------- mocks/fftmmocks/manager.go | 18 +- 5 files changed, 170 insertions(+), 292 deletions(-) diff --git a/go.mod b/go.mod index 45dd531..a301fb5 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/hashicorp/golang-lru v1.0.2 github.com/hyperledger/firefly-common v1.5.6-0.20250630201730-e234335c0381 github.com/hyperledger/firefly-signer v1.1.21 - github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250910153533-14142cf9f697 + github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250930113938-a1e02b75dbc9 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 58cd774..1a13344 100644 --- a/go.sum +++ b/go.sum @@ -114,6 +114,8 @@ github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250910153251-07127 github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250910153251-07127ff35b09/go.mod h1:KHGvK/QqD2jpRZ04lQoX/k1T2o644NCkRlr3FbvKqnA= github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250910153533-14142cf9f697 h1:leUNAoiwMidZYwH+F6bmCa7kvN3qcPGH25k2HcMptyg= github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250910153533-14142cf9f697/go.mod h1:KHGvK/QqD2jpRZ04lQoX/k1T2o644NCkRlr3FbvKqnA= +github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250930113938-a1e02b75dbc9 h1:9AK0bTWEv9NC2HhLLKC8aKjdaOlC/B1P3yB6KnnLOH4= +github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250930113938-a1e02b75dbc9/go.mod h1:KHGvK/QqD2jpRZ04lQoX/k1T2o644NCkRlr3FbvKqnA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= diff --git a/internal/ethereum/confirmation_reconciler.go b/internal/ethereum/confirmation_reconciler.go index 39865f0..9e8b477 100644 --- a/internal/ethereum/confirmation_reconciler.go +++ b/internal/ethereum/confirmation_reconciler.go @@ -25,10 +25,10 @@ import ( "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) -func (bl *blockListener) reconcileConfirmationsForTransaction(ctx context.Context, txHash string, confirmMap *ffcapi.ConfirmationMap, targetConfirmationCount uint64) (*ffcapi.ConfirmationMapUpdateResult, error) { +func (bl *blockListener) reconcileConfirmationsForTransaction(ctx context.Context, txHash string, existingConfirmations []*ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) (*ffcapi.ConfirmationMapUpdateResult, error) { // Initialize the output context reconcileResult := &ffcapi.ConfirmationMapUpdateResult{ - ConfirmationMap: confirmMap, + Confirmations: existingConfirmations, HasNewFork: false, Rebuilt: false, HasNewConfirmation: false, @@ -62,7 +62,6 @@ func (bl *blockListener) compareAndUpdateConfirmationQueue(ctx context.Context, bl.mux.RLock() defer bl.mux.RUnlock() txBlockNumber := txBlockInfo.BlockNumber.Uint64() - txBlockHash := txBlockInfo.BlockHash chainHead := bl.canonicalChain.Front().Value.(*ffcapi.MinimalBlockInfo) chainTail := bl.canonicalChain.Back().Value.(*ffcapi.MinimalBlockInfo) @@ -78,7 +77,7 @@ func (bl *blockListener) compareAndUpdateConfirmationQueue(ctx context.Context, // if the target confirmation count is 0, we should just return the transaction block reconcileResult.Confirmed = true // Only return the transaction block for zero confirmation - reconcileResult.ConfirmationMap.ConfirmationQueueMap[txBlockHash] = []*ffcapi.MinimalBlockInfo{txBlockInfo} + reconcileResult.Confirmations = []*ffcapi.MinimalBlockInfo{txBlockInfo} // For zero confirmation, determine if we processed new confirmations if reconcileResult.Rebuilt || reconcileResult.HasNewFork { // If a fork was detected, we processed new confirmations @@ -100,28 +99,17 @@ func (bl *blockListener) compareAndUpdateConfirmationQueue(ctx context.Context, if !reconcileResult.Confirmed { newQueue = bl.buildNewConfirmations(reconcileResult, newQueue, currentBlock, txBlockNumber, targetConfirmationCount) } - reconcileResult.ConfirmationMap.ConfirmationQueueMap[txBlockHash] = newQueue - - if reconcileResult.CanonicalBlockHash != txBlockHash { - reconcileResult.CanonicalBlockHash = txBlockHash - } + reconcileResult.Confirmations = newQueue } func (bl *blockListener) initializeConfirmationMap(reconcileResult *ffcapi.ConfirmationMapUpdateResult, txBlockInfo *ffcapi.MinimalBlockInfo) []*ffcapi.MinimalBlockInfo { - txBlockHash := txBlockInfo.BlockHash - - if reconcileResult.ConfirmationMap == nil || len(reconcileResult.ConfirmationMap.ConfirmationQueueMap) == 0 { - reconcileResult.ConfirmationMap = &ffcapi.ConfirmationMap{ - ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - txBlockHash: {txBlockInfo}, - }, - CanonicalBlockHash: txBlockHash, - } + if reconcileResult.Confirmations == nil || len(reconcileResult.Confirmations) == 0 { + reconcileResult.Confirmations = []*ffcapi.MinimalBlockInfo{txBlockInfo} reconcileResult.HasNewConfirmation = true return nil } - existingQueue := reconcileResult.ConfirmationMap.ConfirmationQueueMap[txBlockHash] + existingQueue := reconcileResult.Confirmations if len(existingQueue) > 0 { existingTxBlock := existingQueue[0] if !existingTxBlock.Equal(txBlockInfo) { @@ -129,7 +117,7 @@ func (bl *blockListener) initializeConfirmationMap(reconcileResult *ffcapi.Confi // rebuild a new confirmation queue with the new tx block reconcileResult.HasNewFork = true reconcileResult.Rebuilt = true - reconcileResult.ConfirmationMap.ConfirmationQueueMap[txBlockHash] = []*ffcapi.MinimalBlockInfo{txBlockInfo} + reconcileResult.Confirmations = []*ffcapi.MinimalBlockInfo{txBlockInfo} return nil } } @@ -323,6 +311,6 @@ func (bl *blockListener) buildNewConfirmations(reconcileResult *ffcapi.Confirmat return newQueue } -func (c *ethConnector) ReconcileConfirmationsForTransaction(ctx context.Context, txHash string, confirmMap *ffcapi.ConfirmationMap, targetConfirmationCount uint64) (*ffcapi.ConfirmationMapUpdateResult, error) { - return c.blockListener.reconcileConfirmationsForTransaction(ctx, txHash, confirmMap, targetConfirmationCount) +func (c *ethConnector) ReconcileConfirmationsForTransaction(ctx context.Context, txHash string, existingConfirmations []*ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) (*ffcapi.ConfirmationMapUpdateResult, error) { + return c.blockListener.reconcileConfirmationsForTransaction(ctx, txHash, existingConfirmations, targetConfirmationCount) } diff --git a/internal/ethereum/confirmation_reconciler_test.go b/internal/ethereum/confirmation_reconciler_test.go index 982bf47..f3bcce0 100644 --- a/internal/ethereum/confirmation_reconciler_test.go +++ b/internal/ethereum/confirmation_reconciler_test.go @@ -53,7 +53,7 @@ func TestReconcileConfirmationsForTransaction_TransactionNotFound(t *testing.T) assert.False(t, result.Rebuilt) assert.False(t, result.HasNewConfirmation) assert.False(t, result.Confirmed) - assert.Nil(t, result.ConfirmationMap) + assert.Nil(t, result.Confirmations) assert.Equal(t, uint64(5), result.TargetConfirmationCount) mRPC.AssertExpectations(t) @@ -70,7 +70,7 @@ func TestReconcileConfirmationsForTransaction_ReceiptRPCCallError(t *testing.T) }) // Execute the reconcileConfirmationsForTransaction function - result, err := c.ReconcileConfirmationsForTransaction(context.Background(), generateTestHash(100), &ffcapi.ConfirmationMap{}, 5) + result, err := c.ReconcileConfirmationsForTransaction(context.Background(), generateTestHash(100), []*ffcapi.MinimalBlockInfo{}, 5) // Assertions - expect an error when RPC call fails assert.Error(t, err) @@ -101,12 +101,8 @@ func TestReconcileConfirmationsForTransaction_BlockNotFound(t *testing.T) { }) // Execute the reconcileConfirmationsForTransaction function - result, err := c.ReconcileConfirmationsForTransaction(context.Background(), "0x6197ef1a58a2a592bb447efb651f0db7945de21aa8048801b250bd7b7431f9b6", &ffcapi.ConfirmationMap{ - ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - generateTestHash(1977): { - {BlockNumber: fftypes.FFuint64(1977), BlockHash: generateTestHash(1977), ParentHash: generateTestHash(1976)}, - }, - }, + 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 @@ -116,13 +112,9 @@ func TestReconcileConfirmationsForTransaction_BlockNotFound(t *testing.T) { assert.False(t, result.Rebuilt) assert.False(t, result.HasNewConfirmation) assert.False(t, result.Confirmed) - assert.Equal(t, &ffcapi.ConfirmationMap{ - ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - generateTestHash(1977): { - {BlockNumber: fftypes.FFuint64(1977), BlockHash: generateTestHash(1977), ParentHash: generateTestHash(1976)}, - }, - }, - }, result.ConfirmationMap) + assert.Equal(t, []*ffcapi.MinimalBlockInfo{ + {BlockNumber: fftypes.FFuint64(1977), BlockHash: generateTestHash(1977), ParentHash: generateTestHash(1976)}, + }, result.Confirmations) assert.Equal(t, uint64(5), result.TargetConfirmationCount) mRPC.AssertExpectations(t) @@ -148,7 +140,7 @@ func TestReconcileConfirmationsForTransaction_BlockRPCCallError(t *testing.T) { }), false).Return(&rpcbackend.RPCError{Message: "pop"}) // Execute the reconcileConfirmationsForTransaction function - result, err := c.ReconcileConfirmationsForTransaction(context.Background(), "0x6197ef1a58a2a592bb447efb651f0db7945de21aa8048801b250bd7b7431f9b6", &ffcapi.ConfirmationMap{}, 5) + result, err := c.ReconcileConfirmationsForTransaction(context.Background(), "0x6197ef1a58a2a592bb447efb651f0db7945de21aa8048801b250bd7b7431f9b6", []*ffcapi.MinimalBlockInfo{}, 5) // Assertions - expect an error when RPC call fails assert.Error(t, err) @@ -186,24 +178,17 @@ func TestReconcileConfirmationsForTransaction_TxBlockNotInCanonicalChain(t *test }) // Execute the reconcileConfirmationsForTransaction function - result, err := c.ReconcileConfirmationsForTransaction(context.Background(), "0x6197ef1a58a2a592bb447efb651f0db7945de21aa8048801b250bd7b7431f9b6", &ffcapi.ConfirmationMap{ - ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - generateTestHash(1977): {}, - }, - }, 5) + 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 + // Assertions - expect the transaction block to be added even though it's not in canonical chain assert.NoError(t, err) assert.NotNil(t, result) assert.False(t, result.HasNewFork) assert.False(t, result.Rebuilt) - assert.False(t, result.HasNewConfirmation) + assert.True(t, result.HasNewConfirmation) assert.False(t, result.Confirmed) - assert.Equal(t, &ffcapi.ConfirmationMap{ - ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - generateTestHash(1977): {}, - }, - }, result.ConfirmationMap) + assert.Len(t, result.Confirmations, 1) + assert.Equal(t, uint64(1977), uint64(result.Confirmations[0].BlockNumber)) assert.Equal(t, uint64(5), result.TargetConfirmationCount) mRPC.AssertExpectations(t) @@ -238,11 +223,7 @@ func TestReconcileConfirmationsForTransaction_NewConfirmation(t *testing.T) { }) // Execute the reconcileConfirmationsForTransaction function - result, err := c.ReconcileConfirmationsForTransaction(context.Background(), "0x6197ef1a58a2a592bb447efb651f0db7945de21aa8048801b250bd7b7431f9b6", &ffcapi.ConfirmationMap{ - ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - generateTestHash(1977): {}, - }, - }, 5) + 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) @@ -251,15 +232,10 @@ func TestReconcileConfirmationsForTransaction_NewConfirmation(t *testing.T) { assert.False(t, result.Rebuilt) assert.True(t, result.HasNewConfirmation) assert.False(t, result.Confirmed) - assert.Equal(t, &ffcapi.ConfirmationMap{ - ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - generateTestHash(1977): { - {BlockNumber: fftypes.FFuint64(1977), BlockHash: generateTestHash(1977), ParentHash: generateTestHash(1976)}, - {BlockNumber: fftypes.FFuint64(1978), BlockHash: generateTestHash(1978), ParentHash: generateTestHash(1977)}, - }, - }, - CanonicalBlockHash: generateTestHash(1977), - }, result.ConfirmationMap) + assert.Equal(t, []*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) mRPC.AssertExpectations(t) @@ -274,7 +250,7 @@ func TestCompareAndUpdateConfirmationQueue_EmptyChain(t *testing.T) { } ctx := context.Background() occ := &ffcapi.ConfirmationMapUpdateResult{ - ConfirmationMap: &ffcapi.ConfirmationMap{}, + Confirmations: []*ffcapi.MinimalBlockInfo{}, } txBlockNumber := uint64(100) txBlockHash := generateTestHash(txBlockNumber) @@ -290,9 +266,9 @@ func TestCompareAndUpdateConfirmationQueue_EmptyChain(t *testing.T) { bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) // Assert - should return early due to chain being too short - assert.NotNil(t, occ.ConfirmationMap) - assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 0) - assert.NotNil(t, occ.ConfirmationMap) + assert.NotNil(t, occ.Confirmations) + assert.Len(t, occ.Confirmations, 0) + assert.NotNil(t, occ.Confirmations) assert.False(t, occ.HasNewFork) assert.False(t, occ.HasNewConfirmation) assert.False(t, occ.Rebuilt) @@ -306,7 +282,7 @@ func TestCompareAndUpdateConfirmationQueue_ChainTooShort(t *testing.T) { } ctx := context.Background() occ := &ffcapi.ConfirmationMapUpdateResult{ - ConfirmationMap: &ffcapi.ConfirmationMap{}, + Confirmations: []*ffcapi.MinimalBlockInfo{}, } txBlockNumber := uint64(100) txBlockHash := generateTestHash(txBlockNumber) @@ -322,8 +298,8 @@ func TestCompareAndUpdateConfirmationQueue_ChainTooShort(t *testing.T) { bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) // Assert - should return early due to chain being too short - assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 0) - assert.NotNil(t, occ.ConfirmationMap) + assert.Len(t, occ.Confirmations, 0) + assert.NotNil(t, occ.Confirmations) assert.False(t, occ.HasNewFork) assert.False(t, occ.HasNewConfirmation) assert.False(t, occ.Rebuilt) @@ -337,7 +313,7 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMap(t *testing.T) { } ctx := context.Background() occ := &ffcapi.ConfirmationMapUpdateResult{ - ConfirmationMap: nil, + Confirmations: nil, } txBlockNumber := uint64(100) txBlockHash := generateTestHash(txBlockNumber) @@ -352,20 +328,19 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMap(t *testing.T) { bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) // Assert - assert.NotNil(t, occ.ConfirmationMap) + assert.NotNil(t, occ.Confirmations) assert.False(t, occ.HasNewFork) assert.True(t, occ.HasNewConfirmation) assert.True(t, occ.Confirmed) assert.False(t, occ.Rebuilt) - assert.Equal(t, txBlockHash, occ.ConfirmationMap.CanonicalBlockHash) // The code builds a full confirmation queue from the canonical chain - assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) - assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) - assert.Equal(t, txBlockNumber+1, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) - assert.Equal(t, txBlockNumber+2, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) - assert.Equal(t, txBlockNumber+3, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) - assert.Equal(t, txBlockNumber+4, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][4].BlockNumber)) - assert.Equal(t, txBlockNumber+5, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][5].BlockNumber)) + assert.Len(t, occ.Confirmations, 6) + assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(occ.Confirmations[1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(occ.Confirmations[2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(occ.Confirmations[3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(occ.Confirmations[4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(occ.Confirmations[5].BlockNumber)) } @@ -376,7 +351,7 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMap_ZeroConfirmationCo } ctx := context.Background() occ := &ffcapi.ConfirmationMapUpdateResult{ - ConfirmationMap: nil, + Confirmations: nil, } txBlockNumber := uint64(100) txBlockHash := generateTestHash(txBlockNumber) @@ -391,15 +366,14 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMap_ZeroConfirmationCo bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) // Assert - assert.NotNil(t, occ.ConfirmationMap) + assert.NotNil(t, occ.Confirmations) assert.False(t, occ.HasNewFork) assert.True(t, occ.HasNewConfirmation) assert.True(t, occ.Confirmed) assert.False(t, occ.Rebuilt) - assert.Equal(t, txBlockHash, occ.ConfirmationMap.CanonicalBlockHash) // The code builds a full confirmation queue from the canonical chain - assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 1) - assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Len(t, occ.Confirmations, 1) + assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) } func TestCompareAndUpdateConfirmationQueue_NilConfirmationMapUnconfirmed(t *testing.T) { @@ -409,7 +383,7 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMapUnconfirmed(t *test } ctx := context.Background() occ := &ffcapi.ConfirmationMapUpdateResult{ - ConfirmationMap: nil, + Confirmations: nil, } txBlockNumber := uint64(100) txBlockHash := generateTestHash(txBlockNumber) @@ -424,19 +398,18 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMapUnconfirmed(t *test bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) // Assert - assert.NotNil(t, occ.ConfirmationMap) + assert.NotNil(t, occ.Confirmations) assert.False(t, occ.HasNewFork) assert.True(t, occ.HasNewConfirmation) assert.False(t, occ.Confirmed) assert.False(t, occ.Rebuilt) - assert.Equal(t, txBlockHash, occ.ConfirmationMap.CanonicalBlockHash) // The code builds a confirmation queue from the canonical chain up to the available blocks - assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 5) // 100, 101, 102, 103, 104 - assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) - assert.Equal(t, txBlockNumber+1, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) - assert.Equal(t, txBlockNumber+2, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) - assert.Equal(t, txBlockNumber+3, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) - assert.Equal(t, txBlockNumber+4, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][4].BlockNumber)) + assert.Len(t, occ.Confirmations, 5) // 100, 101, 102, 103, 104 + assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(occ.Confirmations[1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(occ.Confirmations[2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(occ.Confirmations[3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(occ.Confirmations[4].BlockNumber)) } @@ -447,9 +420,7 @@ func TestCompareAndUpdateConfirmationQueue_EmptyConfirmationQueue(t *testing.T) } ctx := context.Background() occ := &ffcapi.ConfirmationMapUpdateResult{ - ConfirmationMap: &ffcapi.ConfirmationMap{ - ConfirmationQueueMap: make(map[string][]*ffcapi.MinimalBlockInfo), - }, + Confirmations: []*ffcapi.MinimalBlockInfo{}, } txBlockNumber := uint64(100) txBlockHash := generateTestHash(txBlockNumber) @@ -468,15 +439,14 @@ func TestCompareAndUpdateConfirmationQueue_EmptyConfirmationQueue(t *testing.T) assert.True(t, occ.HasNewConfirmation) assert.True(t, occ.Confirmed) assert.False(t, occ.Rebuilt) - assert.Equal(t, txBlockHash, occ.ConfirmationMap.CanonicalBlockHash) // The code builds a full confirmation queue from the canonical chain - assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) - assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) - assert.Equal(t, txBlockNumber+1, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) - assert.Equal(t, txBlockNumber+2, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) - assert.Equal(t, txBlockNumber+3, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) - assert.Equal(t, txBlockNumber+4, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][4].BlockNumber)) - assert.Equal(t, txBlockNumber+5, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][5].BlockNumber)) + assert.Len(t, occ.Confirmations, 6) + assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(occ.Confirmations[1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(occ.Confirmations[2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(occ.Confirmations[3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(occ.Confirmations[4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(occ.Confirmations[5].BlockNumber)) } // theoretically, this should never happen because block hash generation has block number as part of the input @@ -490,11 +460,7 @@ func TestCompareAndUpdateConfirmationQueue_DifferentBlockNumber(t *testing.T) { {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(99), ParentHash: generateTestHash(98)}, } occ := &ffcapi.ConfirmationMapUpdateResult{ - ConfirmationMap: &ffcapi.ConfirmationMap{ - ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - generateTestHash(100): existingQueue, - }, - }, + Confirmations: existingQueue, } txBlockNumber := uint64(100) txBlockHash := generateTestHash(txBlockNumber) @@ -512,13 +478,13 @@ func TestCompareAndUpdateConfirmationQueue_DifferentBlockNumber(t *testing.T) { assert.True(t, occ.HasNewFork) assert.True(t, occ.Rebuilt) // The code builds a full confirmation queue from the canonical chain - assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) - assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) - assert.Equal(t, txBlockNumber+1, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) - assert.Equal(t, txBlockNumber+2, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) - assert.Equal(t, txBlockNumber+3, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) - assert.Equal(t, txBlockNumber+4, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][4].BlockNumber)) - assert.Equal(t, txBlockNumber+5, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][5].BlockNumber)) + assert.Len(t, occ.Confirmations, 6) + assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(occ.Confirmations[1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(occ.Confirmations[2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(occ.Confirmations[3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(occ.Confirmations[4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(occ.Confirmations[5].BlockNumber)) } func TestCompareAndUpdateConfirmationQueue_MismatchConfirmationBlock(t *testing.T) { @@ -534,11 +500,7 @@ func TestCompareAndUpdateConfirmationQueue_MismatchConfirmationBlock(t *testing. {BlockHash: generateTestHash(103), BlockNumber: fftypes.FFuint64(103), ParentHash: generateTestHash(102)}, } occ := &ffcapi.ConfirmationMapUpdateResult{ - ConfirmationMap: &ffcapi.ConfirmationMap{ - ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - generateTestHash(100): existingQueue, - }, - }, + Confirmations: existingQueue, } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) @@ -556,11 +518,11 @@ func TestCompareAndUpdateConfirmationQueue_MismatchConfirmationBlock(t *testing. assert.False(t, occ.HasNewFork) assert.True(t, occ.Rebuilt) // The code builds a full confirmation queue from the canonical chain - assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 4) - assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) - assert.Equal(t, txBlockNumber+3, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) - assert.Equal(t, txBlockNumber+4, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) - assert.Equal(t, txBlockNumber+5, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) + assert.Len(t, occ.Confirmations, 4) + assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(occ.Confirmations[1].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(occ.Confirmations[2].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(occ.Confirmations[3].BlockNumber)) } func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsTooDistant(t *testing.T) { @@ -574,11 +536,7 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsTooDistant(t *te {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, } occ := &ffcapi.ConfirmationMapUpdateResult{ - ConfirmationMap: &ffcapi.ConfirmationMap{ - ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - generateTestHash(100): existingQueue, - }, - }, + Confirmations: existingQueue, } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) @@ -599,9 +557,9 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsTooDistant(t *te assert.True(t, occ.Rebuilt) assert.True(t, occ.HasNewConfirmation) assert.True(t, occ.Confirmed) - assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 2) - assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) - assert.Equal(t, bl.canonicalChain.Front().Value.(*ffcapi.MinimalBlockInfo).BlockNumber, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber) + assert.Len(t, occ.Confirmations, 2) + assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) + assert.Equal(t, bl.canonicalChain.Front().Value.(*ffcapi.MinimalBlockInfo).BlockNumber, occ.Confirmations[1].BlockNumber) } func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmation(t *testing.T) { @@ -616,11 +574,7 @@ func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmation(t *test {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: "0xwrongparent"}, } occ := &ffcapi.ConfirmationMapUpdateResult{ - ConfirmationMap: &ffcapi.ConfirmationMap{ - ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - generateTestHash(100): existingQueue, - }, - }, + Confirmations: existingQueue, } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) @@ -639,9 +593,9 @@ func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmation(t *test assert.True(t, occ.Rebuilt) assert.True(t, occ.HasNewConfirmation) assert.True(t, occ.Confirmed) - assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) - assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) - assert.Equal(t, generateTestHash(100), occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].ParentHash) + assert.Len(t, occ.Confirmations, 6) + assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) + assert.Equal(t, generateTestHash(100), occ.Confirmations[1].ParentHash) } func TestCompareAndUpdateConfirmationQueue_ConnectionNodeMismatch(t *testing.T) { @@ -657,11 +611,7 @@ func TestCompareAndUpdateConfirmationQueue_ConnectionNodeMismatch(t *testing.T) {BlockHash: generateTestHash(103), BlockNumber: fftypes.FFuint64(103), ParentHash: generateTestHash(102)}, } occ := &ffcapi.ConfirmationMapUpdateResult{ - ConfirmationMap: &ffcapi.ConfirmationMap{ - ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - generateTestHash(100): existingQueue, - }, - }, + Confirmations: existingQueue, } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) @@ -680,10 +630,10 @@ func TestCompareAndUpdateConfirmationQueue_ConnectionNodeMismatch(t *testing.T) assert.True(t, occ.Rebuilt) assert.True(t, occ.HasNewConfirmation) assert.True(t, occ.Confirmed) - assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 5) - assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) - assert.Equal(t, generateTestHash(102), occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockHash) - assert.Equal(t, generateTestHash(102), occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].ParentHash) + assert.Len(t, occ.Confirmations, 5) + assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) + assert.Equal(t, generateTestHash(102), occ.Confirmations[1].BlockHash) + assert.Equal(t, generateTestHash(102), occ.Confirmations[2].ParentHash) } func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmationAfterFirstConfirmation(t *testing.T) { @@ -698,11 +648,7 @@ func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmationAfterFir {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblockwrong"}, } occ := &ffcapi.ConfirmationMapUpdateResult{ - ConfirmationMap: &ffcapi.ConfirmationMap{ - ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - generateTestHash(100): existingQueue, - }, - }, + Confirmations: existingQueue, } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) @@ -721,10 +667,10 @@ func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmationAfterFir assert.True(t, occ.Rebuilt) assert.True(t, occ.HasNewConfirmation) assert.True(t, occ.Confirmed) - assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 5) - assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) - assert.Equal(t, generateTestHash(102), occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockHash) - assert.Equal(t, generateTestHash(102), occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].ParentHash) + assert.Len(t, occ.Confirmations, 5) + assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) + assert.Equal(t, generateTestHash(102), occ.Confirmations[1].BlockHash) + assert.Equal(t, generateTestHash(102), occ.Confirmations[2].ParentHash) } func TestCompareAndUpdateConfirmationQueue_NewForkAfterFirstConfirmation(t *testing.T) { @@ -739,11 +685,7 @@ func TestCompareAndUpdateConfirmationQueue_NewForkAfterFirstConfirmation(t *test {BlockHash: "fork1", BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, } occ := &ffcapi.ConfirmationMapUpdateResult{ - ConfirmationMap: &ffcapi.ConfirmationMap{ - ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - generateTestHash(100): existingQueue, - }, - }, + Confirmations: existingQueue, } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) @@ -762,7 +704,7 @@ func TestCompareAndUpdateConfirmationQueue_NewForkAfterFirstConfirmation(t *test assert.False(t, occ.Rebuilt) assert.True(t, occ.HasNewConfirmation) assert.True(t, occ.Confirmed) - assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) + assert.Len(t, occ.Confirmations, 6) } func TestCompareAndUpdateConfirmationQueue_NewForkAfterFirstConfirmation_ZeroConfirmationCount(t *testing.T) { @@ -777,11 +719,7 @@ func TestCompareAndUpdateConfirmationQueue_NewForkAfterFirstConfirmation_ZeroCon {BlockHash: "fork1", BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, } occ := &ffcapi.ConfirmationMapUpdateResult{ - ConfirmationMap: &ffcapi.ConfirmationMap{ - ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - generateTestHash(100): existingQueue, - }, - }, + Confirmations: existingQueue, } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) @@ -800,7 +738,7 @@ func TestCompareAndUpdateConfirmationQueue_NewForkAfterFirstConfirmation_ZeroCon assert.False(t, occ.Rebuilt) assert.False(t, occ.HasNewConfirmation) assert.True(t, occ.Confirmed) - assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 1) + assert.Len(t, occ.Confirmations, 1) } func TestCompareAndUpdateConfirmationQueue_NewForkAndNoConnectionToCanonicalChain(t *testing.T) { @@ -816,11 +754,7 @@ func TestCompareAndUpdateConfirmationQueue_NewForkAndNoConnectionToCanonicalChai {BlockHash: "fork3", BlockNumber: fftypes.FFuint64(103), ParentHash: "fork2"}, } occ := &ffcapi.ConfirmationMapUpdateResult{ - ConfirmationMap: &ffcapi.ConfirmationMap{ - ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - generateTestHash(100): existingQueue, - }, - }, + Confirmations: existingQueue, } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) @@ -839,11 +773,11 @@ func TestCompareAndUpdateConfirmationQueue_NewForkAndNoConnectionToCanonicalChai assert.True(t, occ.Rebuilt) assert.True(t, occ.HasNewConfirmation) assert.True(t, occ.Confirmed) - assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 4) - assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) - assert.Equal(t, txBlockNumber+3, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) - assert.Equal(t, txBlockNumber+4, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) - assert.Equal(t, txBlockNumber+5, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) + assert.Len(t, occ.Confirmations, 4) + assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(occ.Confirmations[1].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(occ.Confirmations[2].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(occ.Confirmations[3].BlockNumber)) } func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationLaterThanCurrentBlock(t *testing.T) { @@ -857,11 +791,7 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationLaterThanCurrentB {BlockHash: generateTestHash(103), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, } occ := &ffcapi.ConfirmationMapUpdateResult{ - ConfirmationMap: &ffcapi.ConfirmationMap{ - ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - generateTestHash(100): existingQueue, - }, - }, + Confirmations: existingQueue, } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) @@ -880,13 +810,13 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationLaterThanCurrentB assert.True(t, occ.Rebuilt) assert.True(t, occ.HasNewConfirmation) assert.True(t, occ.Confirmed) - assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) - assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) - assert.Equal(t, txBlockNumber+1, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) - assert.Equal(t, txBlockNumber+2, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) - assert.Equal(t, txBlockNumber+3, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) - assert.Equal(t, txBlockNumber+4, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][4].BlockNumber)) - assert.Equal(t, txBlockNumber+5, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][5].BlockNumber)) + assert.Len(t, occ.Confirmations, 6) + assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(occ.Confirmations[1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(occ.Confirmations[2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(occ.Confirmations[3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(occ.Confirmations[4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(occ.Confirmations[5].BlockNumber)) } func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable(t *testing.T) { @@ -908,11 +838,7 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable(t *testing.T) { {BlockHash: "0xblock105", BlockNumber: fftypes.FFuint64(105), ParentHash: "0xblock104"}, // discarded } occ := &ffcapi.ConfirmationMapUpdateResult{ - ConfirmationMap: &ffcapi.ConfirmationMap{ - ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - generateTestHash(100): existingQueue, - }, - }, + Confirmations: existingQueue, } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) @@ -931,10 +857,10 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable(t *testing.T) { assert.False(t, occ.Rebuilt) assert.False(t, occ.HasNewFork) assert.False(t, occ.HasNewConfirmation) - assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 3) - assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) - assert.Equal(t, txBlockNumber+1, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) - assert.Equal(t, txBlockNumber+2, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) + assert.Len(t, occ.Confirmations, 3) + assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(occ.Confirmations[1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(occ.Confirmations[2].BlockNumber)) } func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable_ZeroConfirmationCount(t *testing.T) { @@ -956,11 +882,7 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable_ZeroConfirmationCo {BlockHash: "0xblock105", BlockNumber: fftypes.FFuint64(105), ParentHash: "0xblock104"}, // discarded } occ := &ffcapi.ConfirmationMapUpdateResult{ - ConfirmationMap: &ffcapi.ConfirmationMap{ - ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - generateTestHash(100): existingQueue, - }, - }, + Confirmations: existingQueue, } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) @@ -979,8 +901,8 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable_ZeroConfirmationCo assert.False(t, occ.Rebuilt) assert.False(t, occ.HasNewFork) assert.False(t, occ.HasNewConfirmation) - assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 1) - assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) + assert.Len(t, occ.Confirmations, 1) + assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) } func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableConnectable(t *testing.T) { @@ -999,11 +921,7 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableConnectable(t *test // but we should still be able to validate the existing confirmations are valid using parent hash } occ := &ffcapi.ConfirmationMapUpdateResult{ - ConfirmationMap: &ffcapi.ConfirmationMap{ - ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - generateTestHash(100): existingQueue, - }, - }, + Confirmations: existingQueue, } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) @@ -1024,9 +942,9 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableConnectable(t *test assert.False(t, occ.Rebuilt) assert.False(t, occ.HasNewFork) assert.False(t, occ.HasNewConfirmation) - assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 2) - assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) - assert.Equal(t, txBlockNumber+1, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Len(t, occ.Confirmations, 2) + assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(occ.Confirmations[1].BlockNumber)) } func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableButAllExistingConfirmationsAreTooHighForTargetConfirmationCount(t *testing.T) { @@ -1043,11 +961,7 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableButAllExistingConfi {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, } occ := &ffcapi.ConfirmationMapUpdateResult{ - ConfirmationMap: &ffcapi.ConfirmationMap{ - ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - generateTestHash(100): existingQueue, - }, - }, + Confirmations: existingQueue, } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) @@ -1068,9 +982,9 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableButAllExistingConfi assert.False(t, occ.Rebuilt) assert.False(t, occ.HasNewFork) assert.False(t, occ.HasNewConfirmation) - assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 2) - assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) - assert.Equal(t, uint64(103), uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Len(t, occ.Confirmations, 2) + assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) + assert.Equal(t, uint64(103), uint64(occ.Confirmations[1].BlockNumber)) } @@ -1088,11 +1002,7 @@ func TestCompareAndUpdateConfirmationQueue_HasSufficientConfirmationsButNoOverla {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, } occ := &ffcapi.ConfirmationMapUpdateResult{ - ConfirmationMap: &ffcapi.ConfirmationMap{ - ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - generateTestHash(100): existingQueue, - }, - }, + Confirmations: existingQueue, } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) @@ -1113,9 +1023,9 @@ func TestCompareAndUpdateConfirmationQueue_HasSufficientConfirmationsButNoOverla assert.False(t, occ.HasNewFork) assert.True(t, occ.Rebuilt) assert.True(t, occ.HasNewConfirmation) - assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 2) - assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) - assert.Equal(t, uint64(104), uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) + assert.Len(t, occ.Confirmations, 2) + assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) + assert.Equal(t, uint64(104), uint64(occ.Confirmations[1].BlockNumber)) } @@ -1131,11 +1041,7 @@ func TestCompareAndUpdateConfirmationQueue_ValidExistingConfirmations(t *testing {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, } occ := &ffcapi.ConfirmationMapUpdateResult{ - ConfirmationMap: &ffcapi.ConfirmationMap{ - ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - generateTestHash(100): existingQueue, - }, - }, + Confirmations: existingQueue, } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) @@ -1154,13 +1060,13 @@ func TestCompareAndUpdateConfirmationQueue_ValidExistingConfirmations(t *testing assert.False(t, occ.Rebuilt) assert.True(t, occ.HasNewConfirmation) assert.True(t, occ.Confirmed) - assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) - assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) - assert.Equal(t, txBlockNumber+1, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) - assert.Equal(t, txBlockNumber+2, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) - assert.Equal(t, txBlockNumber+3, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) - assert.Equal(t, txBlockNumber+4, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][4].BlockNumber)) - assert.Equal(t, txBlockNumber+5, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][5].BlockNumber)) + assert.Len(t, occ.Confirmations, 6) + assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(occ.Confirmations[1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(occ.Confirmations[2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(occ.Confirmations[3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(occ.Confirmations[4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(occ.Confirmations[5].BlockNumber)) } func TestCompareAndUpdateConfirmationQueue_ValidExistingTxBlock(t *testing.T) { @@ -1173,11 +1079,7 @@ func TestCompareAndUpdateConfirmationQueue_ValidExistingTxBlock(t *testing.T) { {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, } occ := &ffcapi.ConfirmationMapUpdateResult{ - ConfirmationMap: &ffcapi.ConfirmationMap{ - ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - generateTestHash(100): existingQueue, - }, - }, + Confirmations: existingQueue, } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) @@ -1196,13 +1098,13 @@ func TestCompareAndUpdateConfirmationQueue_ValidExistingTxBlock(t *testing.T) { assert.False(t, occ.Rebuilt) assert.True(t, occ.HasNewConfirmation) assert.True(t, occ.Confirmed) - assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) - assert.Equal(t, txBlockNumber, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][0].BlockNumber)) - assert.Equal(t, txBlockNumber+1, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][1].BlockNumber)) - assert.Equal(t, txBlockNumber+2, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][2].BlockNumber)) - assert.Equal(t, txBlockNumber+3, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][3].BlockNumber)) - assert.Equal(t, txBlockNumber+4, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][4].BlockNumber)) - assert.Equal(t, txBlockNumber+5, uint64(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash][5].BlockNumber)) + assert.Len(t, occ.Confirmations, 6) + assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(occ.Confirmations[1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(occ.Confirmations[2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(occ.Confirmations[3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(occ.Confirmations[4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(occ.Confirmations[5].BlockNumber)) } func TestCompareAndUpdateConfirmationQueue_ReachTargetConfirmation(t *testing.T) { @@ -1212,9 +1114,7 @@ func TestCompareAndUpdateConfirmationQueue_ReachTargetConfirmation(t *testing.T) } ctx := context.Background() occ := &ffcapi.ConfirmationMapUpdateResult{ - ConfirmationMap: &ffcapi.ConfirmationMap{ - ConfirmationQueueMap: make(map[string][]*ffcapi.MinimalBlockInfo), - }, + Confirmations: []*ffcapi.MinimalBlockInfo{}, } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) @@ -1232,7 +1132,7 @@ func TestCompareAndUpdateConfirmationQueue_ReachTargetConfirmation(t *testing.T) assert.True(t, occ.HasNewConfirmation) assert.True(t, occ.Confirmed) // The code builds a full confirmation queue from the canonical chain - assert.GreaterOrEqual(t, len(occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash]), 4) // tx block + 3 confirmations + assert.GreaterOrEqual(t, len(occ.Confirmations), 4) // tx block + 3 confirmations } func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithGap(t *testing.T) { @@ -1249,11 +1149,7 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithGap(t *testi {BlockHash: generateTestHash(103), BlockNumber: fftypes.FFuint64(103), ParentHash: generateTestHash(102)}, } occ := &ffcapi.ConfirmationMapUpdateResult{ - ConfirmationMap: &ffcapi.ConfirmationMap{ - ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - generateTestHash(100): existingQueue, - }, - }, + Confirmations: existingQueue, } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) @@ -1272,7 +1168,7 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithGap(t *testi assert.True(t, occ.Rebuilt) assert.True(t, occ.HasNewConfirmation) assert.True(t, occ.Confirmed) - assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) + assert.Len(t, occ.Confirmations, 6) } func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNumber(t *testing.T) { @@ -1287,11 +1183,7 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNu {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(99), ParentHash: generateTestHash(100)}, // somehow there is a lower block number } occ := &ffcapi.ConfirmationMapUpdateResult{ - ConfirmationMap: &ffcapi.ConfirmationMap{ - ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - generateTestHash(100): existingQueue, - }, - }, + Confirmations: existingQueue, } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) @@ -1310,7 +1202,7 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNu assert.True(t, occ.Rebuilt) assert.True(t, occ.HasNewConfirmation) assert.True(t, occ.Confirmed) - assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) + assert.Len(t, occ.Confirmations, 6) } func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNumberAfterFirstConfirmation(t *testing.T) { @@ -1326,11 +1218,7 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNu {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(99), ParentHash: generateTestHash(101)}, // somehow there is a lower block number } occ := &ffcapi.ConfirmationMapUpdateResult{ - ConfirmationMap: &ffcapi.ConfirmationMap{ - ConfirmationQueueMap: map[string][]*ffcapi.MinimalBlockInfo{ - generateTestHash(100): existingQueue, - }, - }, + Confirmations: existingQueue, } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) @@ -1349,7 +1237,7 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNu assert.True(t, occ.Rebuilt) assert.True(t, occ.HasNewConfirmation) assert.True(t, occ.Confirmed) - assert.Len(t, occ.ConfirmationMap.ConfirmationQueueMap[txBlockHash], 6) + assert.Len(t, occ.Confirmations, 6) } // Helper functions diff --git a/mocks/fftmmocks/manager.go b/mocks/fftmmocks/manager.go index c2661c1..be458d8 100644 --- a/mocks/fftmmocks/manager.go +++ b/mocks/fftmmocks/manager.go @@ -133,9 +133,9 @@ func (_m *Manager) GetTransactionByIDWithStatus(ctx context.Context, txID string return r0, r1 } -// ReconcileConfirmationsForTransaction provides a mock function with given fields: ctx, txHash, confirmMap, targetConfirmationCount -func (_m *Manager) ReconcileConfirmationsForTransaction(ctx context.Context, txHash string, confirmMap *ffcapi.ConfirmationMap, targetConfirmationCount uint64) (*ffcapi.ConfirmationMapUpdateResult, error) { - ret := _m.Called(ctx, txHash, confirmMap, targetConfirmationCount) +// 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.ConfirmationMapUpdateResult, error) { + ret := _m.Called(ctx, txHash, existingConfirmations, targetConfirmationCount) if len(ret) == 0 { panic("no return value specified for ReconcileConfirmationsForTransaction") @@ -143,19 +143,19 @@ func (_m *Manager) ReconcileConfirmationsForTransaction(ctx context.Context, txH var r0 *ffcapi.ConfirmationMapUpdateResult var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, *ffcapi.ConfirmationMap, uint64) (*ffcapi.ConfirmationMapUpdateResult, error)); ok { - return rf(ctx, txHash, confirmMap, targetConfirmationCount) + if rf, ok := ret.Get(0).(func(context.Context, string, []*ffcapi.MinimalBlockInfo, uint64) (*ffcapi.ConfirmationMapUpdateResult, error)); ok { + return rf(ctx, txHash, existingConfirmations, targetConfirmationCount) } - if rf, ok := ret.Get(0).(func(context.Context, string, *ffcapi.ConfirmationMap, uint64) *ffcapi.ConfirmationMapUpdateResult); ok { - r0 = rf(ctx, txHash, confirmMap, targetConfirmationCount) + if rf, ok := ret.Get(0).(func(context.Context, string, []*ffcapi.MinimalBlockInfo, uint64) *ffcapi.ConfirmationMapUpdateResult); ok { + r0 = rf(ctx, txHash, existingConfirmations, targetConfirmationCount) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*ffcapi.ConfirmationMapUpdateResult) } } - if rf, ok := ret.Get(1).(func(context.Context, string, *ffcapi.ConfirmationMap, uint64) error); ok { - r1 = rf(ctx, txHash, confirmMap, targetConfirmationCount) + 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) } From 01fac014068cce614b77f651981d74e18920dc6b Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Tue, 30 Sep 2025 12:58:25 +0100 Subject: [PATCH 14/37] remove redundant check Signed-off-by: Chengxuan Xing --- internal/ethereum/confirmation_reconciler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/ethereum/confirmation_reconciler.go b/internal/ethereum/confirmation_reconciler.go index 9e8b477..e6aaa44 100644 --- a/internal/ethereum/confirmation_reconciler.go +++ b/internal/ethereum/confirmation_reconciler.go @@ -103,7 +103,7 @@ func (bl *blockListener) compareAndUpdateConfirmationQueue(ctx context.Context, } func (bl *blockListener) initializeConfirmationMap(reconcileResult *ffcapi.ConfirmationMapUpdateResult, txBlockInfo *ffcapi.MinimalBlockInfo) []*ffcapi.MinimalBlockInfo { - if reconcileResult.Confirmations == nil || len(reconcileResult.Confirmations) == 0 { + if len(reconcileResult.Confirmations) == 0 { reconcileResult.Confirmations = []*ffcapi.MinimalBlockInfo{txBlockInfo} reconcileResult.HasNewConfirmation = true return nil From 90b49e5d4a0cfb5dc713282c57c5c79af76e5bec Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Fri, 3 Oct 2025 15:16:26 +0100 Subject: [PATCH 15/37] implement retrospective confirmations building Signed-off-by: Chengxuan Xing --- go.mod | 2 +- go.sum | 2 + internal/ethereum/blocklistener.go | 4 +- internal/ethereum/blocklistener_blockquery.go | 8 +- internal/ethereum/confirmation_reconciler.go | 309 +++------ .../ethereum/confirmation_reconciler_test.go | 614 +++++++++++------- internal/ethereum/get_block_info.go | 2 +- internal/msgs/en_error_messages.go | 1 + 8 files changed, 511 insertions(+), 431 deletions(-) diff --git a/go.mod b/go.mod index a301fb5..61d6633 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/hashicorp/golang-lru v1.0.2 github.com/hyperledger/firefly-common v1.5.6-0.20250630201730-e234335c0381 github.com/hyperledger/firefly-signer v1.1.21 - github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250930113938-a1e02b75dbc9 + github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20251003113149-46c9271ca66e 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 1a13344..b27e93b 100644 --- a/go.sum +++ b/go.sum @@ -116,6 +116,8 @@ github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250910153533-14142 github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250910153533-14142cf9f697/go.mod h1:KHGvK/QqD2jpRZ04lQoX/k1T2o644NCkRlr3FbvKqnA= github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250930113938-a1e02b75dbc9 h1:9AK0bTWEv9NC2HhLLKC8aKjdaOlC/B1P3yB6KnnLOH4= github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250930113938-a1e02b75dbc9/go.mod h1:KHGvK/QqD2jpRZ04lQoX/k1T2o644NCkRlr3FbvKqnA= +github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20251003113149-46c9271ca66e h1:jUumirQuZR8cQCBq9BNmpLM4rKo9ahQPdFzGvf/RYIs= +github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20251003113149-46c9271ca66e/go.mod h1:KHGvK/QqD2jpRZ04lQoX/k1T2o644NCkRlr3FbvKqnA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= diff --git a/internal/ethereum/blocklistener.go b/internal/ethereum/blocklistener.go index a3ac358..b7cd5a0 100644 --- a/internal/ethereum/blocklistener.go +++ b/internal/ethereum/blocklistener.go @@ -391,7 +391,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 { @@ -448,7 +448,7 @@ func (bl *blockListener) trimToLastValidBlock() (lastValidBlock *ffcapi.MinimalB 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.BlockNumber.Uint64()) - freshBlockInfo, reason, err = bl.getBlockInfoByNumber(bl.ctx, currentViewBlock.BlockNumber.Uint64(), false, "") + freshBlockInfo, reason, err = bl.getBlockInfoByNumber(bl.ctx, currentViewBlock.BlockNumber.Uint64(), false, "", "") return reason != ffcapi.ErrorReasonNotFound, err }) if err != nil { diff --git a/internal/ethereum/blocklistener_blockquery.go b/internal/ethereum/blocklistener_blockquery.go index 5b5d97f..0987fc8 100644 --- a/internal/ethereum/blocklistener_blockquery.go +++ b/internal/ethereum/blocklistener_blockquery.go @@ -74,7 +74,7 @@ func (bl *blockListener) getBlockInfoContainsTxHash(ctx context.Context, txHash txBlockHash := res.BlockHash txBlockNumber := res.BlockNumber.Uint64() // get the parent hash of the transaction block - bi, reason, err := bl.getBlockInfoByNumber(ctx, txBlockNumber, true, txBlockHash) + bi, reason, err := bl.getBlockInfoByNumber(ctx, txBlockNumber, true, "", txBlockHash) if err != nil && reason != ffcapi.ErrorReasonNotFound { // if the block info is not found, then there could be a fork, twe don't throw error in this case and treating it as block not found return nil, i18n.WrapError(ctx, err, msgs.MsgFailedToQueryBlockInfo, txHash) } @@ -89,14 +89,14 @@ func (bl *blockListener) getBlockInfoContainsTxHash(ctx context.Context, txHash }, nil } -func (bl *blockListener) getBlockInfoByNumber(ctx context.Context, blockNumber uint64, allowCache bool, expectedHashStr string) (*blockInfoJSONRPC, ffcapi.ErrorReason, error) { +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.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 } } diff --git a/internal/ethereum/confirmation_reconciler.go b/internal/ethereum/confirmation_reconciler.go index e6aaa44..a5033ae 100644 --- a/internal/ethereum/confirmation_reconciler.go +++ b/internal/ethereum/confirmation_reconciler.go @@ -17,11 +17,12 @@ package ethereum import ( - "container/list" "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" ) @@ -29,9 +30,7 @@ func (bl *blockListener) reconcileConfirmationsForTransaction(ctx context.Contex // Initialize the output context reconcileResult := &ffcapi.ConfirmationMapUpdateResult{ Confirmations: existingConfirmations, - HasNewFork: false, - Rebuilt: false, - HasNewConfirmation: false, + NewFork: false, Confirmed: false, TargetConfirmationCount: targetConfirmationCount, } @@ -47,65 +46,84 @@ func (bl *blockListener) reconcileConfirmationsForTransaction(ctx context.Contex return reconcileResult, nil } - // Compare the existing confirmation queue with the in-memory linked list - bl.compareAndUpdateConfirmationQueue(ctx, reconcileResult, txBlockInfo, targetConfirmationCount) + return bl.compareAndUpdateConfirmationQueue(ctx, reconcileResult, txBlockInfo, targetConfirmationCount) +} - return reconcileResult, nil +func (bl *blockListener) compareAndUpdateConfirmationQueue(ctx context.Context, reconcileResult *ffcapi.ConfirmationMapUpdateResult, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) (*ffcapi.ConfirmationMapUpdateResult, error) { + var err error + // Compare the and build the tail part of the confirmation queue using the canonical chain + newConfirmationsWithoutTxBlock, existingConfirmations, returnResult := bl.buildConfirmationQueueUsingCanonicalChain(ctx, reconcileResult, txBlockInfo, targetConfirmationCount) + if returnResult { + return reconcileResult, nil + } + + // Validate and process existing confirmations + // and fill in the gap in the confirmation queue + var confirmations []*ffcapi.MinimalBlockInfo + var newFork bool + confirmations, newFork, err = bl.checkAndFillInGap(ctx, newConfirmationsWithoutTxBlock, existingConfirmations, txBlockInfo, targetConfirmationCount) + if err != nil { + return reconcileResult, err + } + reconcileResult.NewFork = newFork + reconcileResult.Confirmations = confirmations + return reconcileResult, err } // NOTE: this function only build up the confirmation queue uses the in-memory canonical chain // it does not build up the canonical chain -// compareAndUpdateConfirmationQueue compares the existing confirmation queue with the in-memory linked list +// compareAndUpdateConfirmationQueueUsingCanonicalChain compares the existing confirmation queue with the in-memory linked list // this function obtains the read lock on the canonical chain, so it should not make any long-running queries -func (bl *blockListener) compareAndUpdateConfirmationQueue(ctx context.Context, reconcileResult *ffcapi.ConfirmationMapUpdateResult, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) { +func (bl *blockListener) buildConfirmationQueueUsingCanonicalChain(ctx context.Context, reconcileResult *ffcapi.ConfirmationMapUpdateResult, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) (newConfirmationsWithoutTxBlock []*ffcapi.MinimalBlockInfo, existingConfirmations []*ffcapi.MinimalBlockInfo, returnResult bool) { bl.mux.RLock() defer bl.mux.RUnlock() txBlockNumber := txBlockInfo.BlockNumber.Uint64() + targetBlockNumber := txBlockInfo.BlockNumber.Uint64() + targetConfirmationCount - chainHead := bl.canonicalChain.Front().Value.(*ffcapi.MinimalBlockInfo) chainTail := bl.canonicalChain.Back().Value.(*ffcapi.MinimalBlockInfo) if chainTail == nil || chainTail.BlockNumber.Uint64() < txBlockNumber { log.L(ctx).Debugf("Canonical chain is waiting for the transaction block %d to be indexed", txBlockNumber) - return + return nil, nil, true } // Initialize confirmation map and get existing queue - existingQueue := bl.initializeConfirmationMap(reconcileResult, txBlockInfo) + existingConfirmations = bl.initializeConfirmationMap(reconcileResult, txBlockInfo) + // if the target confirmation count is 0, we should just return the transaction block if targetConfirmationCount == 0 { - // if the target confirmation count is 0, we should just return the transaction block reconcileResult.Confirmed = true - // Only return the transaction block for zero confirmation reconcileResult.Confirmations = []*ffcapi.MinimalBlockInfo{txBlockInfo} - // For zero confirmation, determine if we processed new confirmations - if reconcileResult.Rebuilt || reconcileResult.HasNewFork { - // If a fork was detected, we processed new confirmations - reconcileResult.HasNewConfirmation = true - } - return + return nil, existingConfirmations, true } - // Validate and process existing confirmations - newQueue, currentBlock := bl.processExistingConfirmations(ctx, reconcileResult, txBlockInfo, existingQueue, chainHead, targetConfirmationCount) - - if currentBlock == nil { - // the tx block is not in the canonical chain - // we should just return the existing block confirmations and wait for future call to correct it - return - } + // build the tail part of the queue from the canonical chain - // Build new confirmations from canonical chain only if not already confirmed - if !reconcileResult.Confirmed { - newQueue = bl.buildNewConfirmations(reconcileResult, newQueue, currentBlock, txBlockNumber, targetConfirmationCount) + newConfirmationsWithoutTxBlock = []*ffcapi.MinimalBlockInfo{} + currentBlock := bl.canonicalChain.Front() + for currentBlock != nil { + currentBlockInfo := currentBlock.Value.(*ffcapi.MinimalBlockInfo) + if currentBlockInfo.BlockNumber.Uint64() > targetBlockNumber { + reconcileResult.Confirmed = true + break + } + if currentBlockInfo.BlockNumber.Uint64() <= txBlockNumber { + currentBlock = currentBlock.Next() + continue + } + newConfirmationsWithoutTxBlock = append(newConfirmationsWithoutTxBlock, &ffcapi.MinimalBlockInfo{ + BlockHash: currentBlockInfo.BlockHash, + BlockNumber: fftypes.FFuint64(currentBlockInfo.BlockNumber.Uint64()), + ParentHash: currentBlockInfo.ParentHash, + }) + currentBlock = currentBlock.Next() } - reconcileResult.Confirmations = newQueue + return newConfirmationsWithoutTxBlock, existingConfirmations, false } func (bl *blockListener) initializeConfirmationMap(reconcileResult *ffcapi.ConfirmationMapUpdateResult, txBlockInfo *ffcapi.MinimalBlockInfo) []*ffcapi.MinimalBlockInfo { if len(reconcileResult.Confirmations) == 0 { reconcileResult.Confirmations = []*ffcapi.MinimalBlockInfo{txBlockInfo} - reconcileResult.HasNewConfirmation = true return nil } @@ -115,8 +133,7 @@ func (bl *blockListener) initializeConfirmationMap(reconcileResult *ffcapi.Confi if !existingTxBlock.Equal(txBlockInfo) { // the tx block in the existing queue does not match the new tx block we queried from the chain // rebuild a new confirmation queue with the new tx block - reconcileResult.HasNewFork = true - reconcileResult.Rebuilt = true + reconcileResult.NewFork = true reconcileResult.Confirmations = []*ffcapi.MinimalBlockInfo{txBlockInfo} return nil } @@ -125,190 +142,72 @@ func (bl *blockListener) initializeConfirmationMap(reconcileResult *ffcapi.Confi return existingQueue } -func (bl *blockListener) processExistingConfirmations(ctx context.Context, reconcileResult *ffcapi.ConfirmationMapUpdateResult, txBlockInfo *ffcapi.MinimalBlockInfo, existingQueue []*ffcapi.MinimalBlockInfo, chainHead *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) ([]*ffcapi.MinimalBlockInfo, *list.Element) { - txBlockNumber := txBlockInfo.BlockNumber.Uint64() - - newQueue := []*ffcapi.MinimalBlockInfo{txBlockInfo} - - currentBlock := bl.canonicalChain.Front() - // iterate to the tx block if the chain head is earlier than the tx block - for currentBlock != nil && currentBlock.Value.(*ffcapi.MinimalBlockInfo).BlockNumber.Uint64() <= txBlockNumber { - if currentBlock.Value.(*ffcapi.MinimalBlockInfo).BlockNumber.Uint64() == txBlockNumber { - // the tx block is already in the canonical chain - // we need to check if the tx block is the same as the chain head - if !currentBlock.Value.(*ffcapi.MinimalBlockInfo).Equal(txBlockInfo) { - // the tx block information is different from the same block number in the canonical chain - // the tx confirmation block is not on the same fork as the canonical chain - // we should just return the existing block confirmations and wait for future call to correct it - return newQueue, nil +func (bl *blockListener) checkAndFillInGap(ctx context.Context, newConfirmationsWithoutTxBlock []*ffcapi.MinimalBlockInfo, existingConfirmations []*ffcapi.MinimalBlockInfo, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) ([]*ffcapi.MinimalBlockInfo, bool, error) { + var hasNewFork bool + // check whether there are forks in the newConfirmations + for _, confirmation := range newConfirmationsWithoutTxBlock { + for _, existingConfirmation := range existingConfirmations { + if confirmation.BlockNumber.Uint64() == existingConfirmation.BlockNumber.Uint64() && !confirmation.Equal(existingConfirmation) { + hasNewFork = true + break } } - currentBlock = currentBlock.Next() - } - - if len(existingQueue) <= 1 { - return newQueue, currentBlock + if hasNewFork { + break + } } - existingConfirmations := existingQueue[1:] - return bl.validateExistingConfirmations( - ctx, reconcileResult, newQueue, existingConfirmations, currentBlock, chainHead, txBlockInfo, targetConfirmationCount, - ) -} - -func (bl *blockListener) validateExistingConfirmations(ctx context.Context, reconcileResult *ffcapi.ConfirmationMapUpdateResult, newQueue []*ffcapi.MinimalBlockInfo, existingConfirmations []*ffcapi.MinimalBlockInfo, currentBlock *list.Element, chainHead *ffcapi.MinimalBlockInfo, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) ([]*ffcapi.MinimalBlockInfo, *list.Element) { - txBlockNumber := txBlockInfo.BlockNumber.Uint64() - lastExistingConfirmation := existingConfirmations[len(existingConfirmations)-1] - if lastExistingConfirmation.BlockNumber.Uint64() < chainHead.BlockNumber.Uint64() && - // ^^ the highest block number in the existing confirmations is lower than the highest block number in the canonical chain - (lastExistingConfirmation.BlockNumber.Uint64() != chainHead.BlockNumber.Uint64()-1 || - lastExistingConfirmation.BlockHash != chainHead.ParentHash) { - // ^^ and the last existing confirmation is not the parent of the canonical chain head - // Therefore, there is no connection between the existing confirmations and the canonical chain - // so that we cannot validate the existing confirmations are from the same fork as the canonical chain - // so we need to rebuild the confirmations queue - reconcileResult.Rebuilt = true - return newQueue, currentBlock + blockNumberToReach := txBlockInfo.BlockNumber.Uint64() + targetConfirmationCount + var lastValidatedBlock *ffcapi.MinimalBlockInfo + if len(newConfirmationsWithoutTxBlock) > 0 { + blockNumberToReach = newConfirmationsWithoutTxBlock[0].BlockNumber.Uint64() - 1 + lastValidatedBlock = newConfirmationsWithoutTxBlock[0] } - var previousExistingConfirmation *ffcapi.MinimalBlockInfo - queueIndex := 0 - - connectionBlockNumber := currentBlock.Value.(*ffcapi.MinimalBlockInfo).BlockNumber.Uint64() - - for currentBlock != nil && queueIndex < len(existingConfirmations) { - existingConfirmation := existingConfirmations[queueIndex] - if existingConfirmation.BlockNumber.Uint64() <= txBlockNumber { - log.L(ctx).Debugf("Existing confirmation queue is corrupted, the first block is earlier than the tx block: %d", existingConfirmation.BlockNumber.Uint64()) - // if any block in the existing confirmation queue is earlier than the tx block - // the existing confirmation queue is no valid - // we need to rebuild the confirmations queue - reconcileResult.Rebuilt = true - return newQueue[:1], currentBlock - } - - // the existing confirmation queue is not tightly controlled by our canonical chain - // ^^ even though it supposed to be build by a canonical chain, we cannot rely on it - // because they are stored outside of current system - // Therefore, we need to check whether the existing confirmation queue is corrupted - isCorrupted := previousExistingConfirmation != nil && - (previousExistingConfirmation.BlockNumber.Uint64()+1 != existingConfirmation.BlockNumber.Uint64() || - previousExistingConfirmation.BlockHash != existingConfirmation.ParentHash) || - // check the link between the first confirmation block and the existing tx block - (existingConfirmation.BlockNumber.Uint64() == txBlockNumber+1 && - existingConfirmation.ParentHash != txBlockInfo.BlockHash) - // we allow gaps between the tx block and the first block in the existing confirmation queue - // NOTE: we don't allow gaps after the first block in the existing confirmation queue - // any gaps, we need to rebuild the confirmations queue - - if isCorrupted { - // any corruption in the existing confirmation queue will cause the confirmation queue to be rebuilt - // we don't keep any of the existing confirmations - reconcileResult.Rebuilt = true - return newQueue[:1], currentBlock - } - - currentBlockInfo := currentBlock.Value.(*ffcapi.MinimalBlockInfo) - if existingConfirmation.BlockNumber.Uint64() < currentBlockInfo.BlockNumber.Uint64() { - // NOTE: we are not doing the confirmation count check here - // because we've not reached the current head in the canonical chain to validate - // all the confirmations we copied over are still valid - newQueue = append(newQueue, existingConfirmation) - previousExistingConfirmation = existingConfirmation - queueIndex++ - continue - } - - if existingConfirmation.BlockNumber.Uint64() == currentBlockInfo.BlockNumber.Uint64() { - // existing confirmation has caught up to the current block - // checking the overlaps - - if !existingConfirmation.Equal(currentBlockInfo) { - // we detected a potential fork - if connectionBlockNumber == currentBlockInfo.BlockNumber.Uint64() && - !previousExistingConfirmation.IsParentOf(currentBlockInfo) { - // this is the connection node (first overlap between existing confirmation queue and canonical chain) - // if the first node doesn't chain to to the previous confirmation, it means all the historical confirmation are on a different fork - // therefore, we need to rebuild the confirmations queue - reconcileResult.Rebuilt = true - return newQueue[:1], currentBlock + for i := blockNumberToReach; i > txBlockInfo.BlockNumber.Uint64(); i-- { + // first use the block info from the confirmation queue if matches are found + fetchedFromExistingQueue := false + if lastValidatedBlock != nil { + + for _, confirmation := range existingConfirmations { + if confirmation.BlockNumber.Uint64() == i { + if confirmation.IsParentOf(lastValidatedBlock) { + newConfirmationsWithoutTxBlock = append([]*ffcapi.MinimalBlockInfo{confirmation}, newConfirmationsWithoutTxBlock...) + lastValidatedBlock = confirmation + fetchedFromExistingQueue = true + break + } + hasNewFork = true } - - // other scenarios, the historical confirmation are still trustworthy and linked to our canonical chain - reconcileResult.HasNewFork = true - return newQueue, currentBlock - } - - newQueue = append(newQueue, existingConfirmation) - if existingConfirmation.BlockNumber.Uint64()-txBlockNumber >= targetConfirmationCount { - break } - currentBlock = currentBlock.Next() - previousExistingConfirmation = existingConfirmation - queueIndex++ + } + if fetchedFromExistingQueue { continue } - - reconcileResult.Rebuilt = true - return newQueue[:1], currentBlock - } - - // Check if we have enough confirmations - lastBlockInNewQueue := newQueue[len(newQueue)-1] - confirmationBlockNumber := txBlockNumber + targetConfirmationCount - if lastBlockInNewQueue.BlockNumber.Uint64() >= confirmationBlockNumber { - chainHead := bl.canonicalChain.Front().Value.(*ffcapi.MinimalBlockInfo) - // we've got a confirmation so whether the rest of the chain has forked is no longer relevant - // this could happen when user chose a different target confirmation count for the new checks - // but we still need to validate the existing confirmations are connectable to the canonical chain - // Check if the queue connects to the canonical chain - if lastBlockInNewQueue.BlockNumber.Uint64() >= chainHead.BlockNumber.Uint64() || - (lastBlockInNewQueue.BlockNumber.Uint64() == chainHead.BlockNumber.Uint64()-1 && - lastBlockInNewQueue.BlockHash == chainHead.ParentHash) { - reconcileResult.HasNewFork = false - reconcileResult.HasNewConfirmation = false - reconcileResult.Rebuilt = false - reconcileResult.Confirmed = true - - // Trim the queue to only include blocks up to the max confirmation count - trimmedQueue := []*ffcapi.MinimalBlockInfo{} - for _, confirmation := range newQueue { - if confirmation.BlockNumber.Uint64() > confirmationBlockNumber { - break - } - trimmedQueue = append(trimmedQueue, confirmation) - } - - // If we've trimmed off all the existing confirmations, we need to add the canonical chain head - // to tell us the head block we used to confirm the transaction - if len(trimmedQueue) == 1 { - trimmedQueue = append(trimmedQueue, chainHead) - } - return trimmedQueue, currentBlock + // if no match is found, fetch the block info from the chain + freshBlockInfo, _, err := bl.getBlockInfoByNumber(ctx, i, false, "", "") + if err != nil { + return nil, hasNewFork, err + } + fetchedBlock := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(freshBlockInfo.Number.BigInt().Uint64()), + BlockHash: freshBlockInfo.Hash.String(), + ParentHash: freshBlockInfo.ParentHash.String(), + } + if lastValidatedBlock != nil && !fetchedBlock.IsParentOf(lastValidatedBlock) { + // the fetched block is not the parent of the last validated block + // chain is not in a stable stable to build the confirmation queue + return nil, hasNewFork, i18n.NewError(ctx, msgs.MsgFailedToBuildConfirmationQueue) } + newConfirmationsWithoutTxBlock = append([]*ffcapi.MinimalBlockInfo{fetchedBlock}, newConfirmationsWithoutTxBlock...) + lastValidatedBlock = fetchedBlock } - return newQueue, currentBlock -} - -func (bl *blockListener) buildNewConfirmations(reconcileResult *ffcapi.ConfirmationMapUpdateResult, newQueue []*ffcapi.MinimalBlockInfo, currentBlock *list.Element, txBlockNumber uint64, targetConfirmationCount uint64) []*ffcapi.MinimalBlockInfo { - for currentBlock != nil { - currentBlockInfo := currentBlock.Value.(*ffcapi.MinimalBlockInfo) - if currentBlockInfo.BlockNumber.Uint64() > newQueue[len(newQueue)-1].BlockNumber.Uint64() { - reconcileResult.HasNewConfirmation = true - newQueue = append(newQueue, &ffcapi.MinimalBlockInfo{ - BlockHash: currentBlockInfo.BlockHash, - BlockNumber: fftypes.FFuint64(currentBlockInfo.BlockNumber.Uint64()), - ParentHash: currentBlockInfo.ParentHash, - }) - if currentBlockInfo.BlockNumber.Uint64() >= txBlockNumber+targetConfirmationCount { - reconcileResult.Confirmed = true - break - } - } - currentBlock = currentBlock.Next() + // we've rebuilt the confirmations queue, now check the front of the queue still connect to the tx block + if !txBlockInfo.IsParentOf(newConfirmationsWithoutTxBlock[0]) { + return nil, hasNewFork, i18n.NewError(ctx, msgs.MsgFailedToBuildConfirmationQueue) } - return newQueue + return append([]*ffcapi.MinimalBlockInfo{txBlockInfo}, newConfirmationsWithoutTxBlock...), hasNewFork, nil } func (c *ethConnector) ReconcileConfirmationsForTransaction(ctx context.Context, txHash string, existingConfirmations []*ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) (*ffcapi.ConfirmationMapUpdateResult, error) { diff --git a/internal/ethereum/confirmation_reconciler_test.go b/internal/ethereum/confirmation_reconciler_test.go index f3bcce0..952550b 100644 --- a/internal/ethereum/confirmation_reconciler_test.go +++ b/internal/ethereum/confirmation_reconciler_test.go @@ -21,9 +21,12 @@ import ( "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" @@ -49,9 +52,7 @@ func TestReconcileConfirmationsForTransaction_TransactionNotFound(t *testing.T) // Assertions - expect an error when transaction doesn't exist assert.NoError(t, err) assert.NotNil(t, result) - assert.False(t, result.HasNewFork) - assert.False(t, result.Rebuilt) - assert.False(t, result.HasNewConfirmation) + assert.False(t, result.NewFork) assert.False(t, result.Confirmed) assert.Nil(t, result.Confirmations) assert.Equal(t, uint64(5), result.TargetConfirmationCount) @@ -108,9 +109,7 @@ func TestReconcileConfirmationsForTransaction_BlockNotFound(t *testing.T) { // Assertions - expect an error when transaction doesn't exist assert.NoError(t, err) assert.NotNil(t, result) - assert.False(t, result.HasNewFork) - assert.False(t, result.Rebuilt) - assert.False(t, result.HasNewConfirmation) + assert.False(t, result.NewFork) assert.False(t, result.Confirmed) assert.Equal(t, []*ffcapi.MinimalBlockInfo{ {BlockNumber: fftypes.FFuint64(1977), BlockHash: generateTestHash(1977), ParentHash: generateTestHash(1976)}, @@ -180,17 +179,16 @@ func TestReconcileConfirmationsForTransaction_TxBlockNotInCanonicalChain(t *test // Execute the reconcileConfirmationsForTransaction function result, err := c.ReconcileConfirmationsForTransaction(context.Background(), "0x6197ef1a58a2a592bb447efb651f0db7945de21aa8048801b250bd7b7431f9b6", []*ffcapi.MinimalBlockInfo{}, 5) - // Assertions - expect the transaction block to be added even though it's not in canonical chain + // 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.HasNewFork) - assert.False(t, result.Rebuilt) - assert.True(t, result.HasNewConfirmation) + assert.False(t, result.NewFork) assert.False(t, result.Confirmed) - assert.Len(t, result.Confirmations, 1) - assert.Equal(t, uint64(1977), uint64(result.Confirmations[0].BlockNumber)) - assert.Equal(t, uint64(5), result.TargetConfirmationCount) - + assert.Len(t, result.Confirmations, 2) mRPC.AssertExpectations(t) } @@ -228,9 +226,7 @@ func TestReconcileConfirmationsForTransaction_NewConfirmation(t *testing.T) { // Assertions - expect the existing confirmation queue to be returned because the tx block doesn't match the same block number in the canonical chain assert.NoError(t, err) assert.NotNil(t, result) - assert.False(t, result.HasNewFork) - assert.False(t, result.Rebuilt) - assert.True(t, result.HasNewConfirmation) + assert.False(t, result.NewFork) assert.False(t, result.Confirmed) assert.Equal(t, []*ffcapi.MinimalBlockInfo{ {BlockNumber: fftypes.FFuint64(1977), BlockHash: generateTestHash(1977), ParentHash: generateTestHash(1976)}, @@ -245,9 +241,8 @@ func TestReconcileConfirmationsForTransaction_NewConfirmation(t *testing.T) { func TestCompareAndUpdateConfirmationQueue_EmptyChain(t *testing.T) { // Setup - create a chain with one block that's older than the transaction - bl := &blockListener{ - canonicalChain: createTestChain(50, 50), // Single block at 50, tx is at 100 - } + bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 50, []uint64{}) + defer done() ctx := context.Background() occ := &ffcapi.ConfirmationMapUpdateResult{ Confirmations: []*ffcapi.MinimalBlockInfo{}, @@ -269,17 +264,14 @@ func TestCompareAndUpdateConfirmationQueue_EmptyChain(t *testing.T) { assert.NotNil(t, occ.Confirmations) assert.Len(t, occ.Confirmations, 0) assert.NotNil(t, occ.Confirmations) - assert.False(t, occ.HasNewFork) - assert.False(t, occ.HasNewConfirmation) - assert.False(t, occ.Rebuilt) + assert.False(t, occ.NewFork) assert.False(t, occ.Confirmed) } func TestCompareAndUpdateConfirmationQueue_ChainTooShort(t *testing.T) { // Setup - bl := &blockListener{ - canonicalChain: createTestChain(50, 99), // Chain ends at 99, tx is at 100 - } + bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 99, []uint64{}) + defer done() ctx := context.Background() occ := &ffcapi.ConfirmationMapUpdateResult{ Confirmations: []*ffcapi.MinimalBlockInfo{}, @@ -300,17 +292,14 @@ func TestCompareAndUpdateConfirmationQueue_ChainTooShort(t *testing.T) { // Assert - should return early due to chain being too short assert.Len(t, occ.Confirmations, 0) assert.NotNil(t, occ.Confirmations) - assert.False(t, occ.HasNewFork) - assert.False(t, occ.HasNewConfirmation) - assert.False(t, occ.Rebuilt) + assert.False(t, occ.NewFork) assert.False(t, occ.Confirmed) } func TestCompareAndUpdateConfirmationQueue_NilConfirmationMap(t *testing.T) { // Setup - bl := &blockListener{ - canonicalChain: createTestChain(50, 150), - } + bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 150, []uint64{}) + defer done() ctx := context.Background() occ := &ffcapi.ConfirmationMapUpdateResult{ Confirmations: nil, @@ -329,10 +318,8 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMap(t *testing.T) { // Assert assert.NotNil(t, occ.Confirmations) - assert.False(t, occ.HasNewFork) - assert.True(t, occ.HasNewConfirmation) + assert.False(t, occ.NewFork) assert.True(t, occ.Confirmed) - assert.False(t, occ.Rebuilt) // The code builds a full confirmation queue from the canonical chain assert.Len(t, occ.Confirmations, 6) assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) @@ -346,9 +333,8 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMap(t *testing.T) { func TestCompareAndUpdateConfirmationQueue_NilConfirmationMap_ZeroConfirmationCount(t *testing.T) { // Setup - bl := &blockListener{ - canonicalChain: createTestChain(50, 150), - } + bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 150, []uint64{}) + defer done() ctx := context.Background() occ := &ffcapi.ConfirmationMapUpdateResult{ Confirmations: nil, @@ -367,10 +353,8 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMap_ZeroConfirmationCo // Assert assert.NotNil(t, occ.Confirmations) - assert.False(t, occ.HasNewFork) - assert.True(t, occ.HasNewConfirmation) + assert.False(t, occ.NewFork) assert.True(t, occ.Confirmed) - assert.False(t, occ.Rebuilt) // The code builds a full confirmation queue from the canonical chain assert.Len(t, occ.Confirmations, 1) assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) @@ -378,9 +362,8 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMap_ZeroConfirmationCo func TestCompareAndUpdateConfirmationQueue_NilConfirmationMapUnconfirmed(t *testing.T) { // Setup - bl := &blockListener{ - canonicalChain: createTestChain(100, 104), - } + bl, done := newBlockListenerWithTestChain(t, 100, 5, 100, 104, []uint64{}) + defer done() ctx := context.Background() occ := &ffcapi.ConfirmationMapUpdateResult{ Confirmations: nil, @@ -399,10 +382,8 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMapUnconfirmed(t *test // Assert assert.NotNil(t, occ.Confirmations) - assert.False(t, occ.HasNewFork) - assert.True(t, occ.HasNewConfirmation) + assert.False(t, occ.NewFork) assert.False(t, occ.Confirmed) - assert.False(t, occ.Rebuilt) // The code builds a confirmation queue from the canonical chain up to the available blocks assert.Len(t, occ.Confirmations, 5) // 100, 101, 102, 103, 104 assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) @@ -415,9 +396,8 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMapUnconfirmed(t *test func TestCompareAndUpdateConfirmationQueue_EmptyConfirmationQueue(t *testing.T) { // Setup - bl := &blockListener{ - canonicalChain: createTestChain(50, 150), - } + bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 150, []uint64{}) + defer done() ctx := context.Background() occ := &ffcapi.ConfirmationMapUpdateResult{ Confirmations: []*ffcapi.MinimalBlockInfo{}, @@ -435,10 +415,8 @@ func TestCompareAndUpdateConfirmationQueue_EmptyConfirmationQueue(t *testing.T) bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) // Assert - assert.False(t, occ.HasNewFork) - assert.True(t, occ.HasNewConfirmation) + assert.False(t, occ.NewFork) assert.True(t, occ.Confirmed) - assert.False(t, occ.Rebuilt) // The code builds a full confirmation queue from the canonical chain assert.Len(t, occ.Confirmations, 6) assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) @@ -449,25 +427,26 @@ func TestCompareAndUpdateConfirmationQueue_EmptyConfirmationQueue(t *testing.T) assert.Equal(t, txBlockNumber+5, uint64(occ.Confirmations[5].BlockNumber)) } -// theoretically, this should never happen because block hash generation has block number as part of the input -func TestCompareAndUpdateConfirmationQueue_DifferentBlockNumber(t *testing.T) { +func TestCompareAndUpdateConfirmationQueue_MismatchConfirmationBlock(t *testing.T) { // Setup - bl := &blockListener{ - canonicalChain: createTestChain(50, 150), - } + bl, done := newBlockListenerWithTestChain(t, 100, 5, 103, 150, []uint64{101}) + defer done() ctx := context.Background() existingQueue := []*ffcapi.MinimalBlockInfo{ - {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(99), ParentHash: generateTestHash(98)}, + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: generateTestHash(999), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, // wrong hash, so the block should be fetched + {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, + {BlockHash: generateTestHash(103), BlockNumber: fftypes.FFuint64(103), ParentHash: generateTestHash(102)}, } occ := &ffcapi.ConfirmationMapUpdateResult{ Confirmations: existingQueue, } txBlockNumber := uint64(100) - txBlockHash := generateTestHash(txBlockNumber) + txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ BlockNumber: fftypes.FFuint64(txBlockNumber), BlockHash: txBlockHash, - ParentHash: generateTestHash(txBlockNumber - 1), + ParentHash: generateTestHash(99), } targetConfirmationCount := uint64(5) @@ -475,8 +454,7 @@ func TestCompareAndUpdateConfirmationQueue_DifferentBlockNumber(t *testing.T) { bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) // Assert - assert.True(t, occ.HasNewFork) - assert.True(t, occ.Rebuilt) + assert.True(t, occ.NewFork) // The code builds a full confirmation queue from the canonical chain assert.Len(t, occ.Confirmations, 6) assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) @@ -487,17 +465,15 @@ func TestCompareAndUpdateConfirmationQueue_DifferentBlockNumber(t *testing.T) { assert.Equal(t, txBlockNumber+5, uint64(occ.Confirmations[5].BlockNumber)) } -func TestCompareAndUpdateConfirmationQueue_MismatchConfirmationBlock(t *testing.T) { +func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsTooDistant(t *testing.T) { // Setup - bl := &blockListener{ - canonicalChain: createTestChain(103, 150), - } + + 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(101)}, // wrong parent hash, so the existing queue should be discarded - {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, - {BlockHash: generateTestHash(103), BlockNumber: fftypes.FFuint64(103), ParentHash: generateTestHash(102)}, + {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, } occ := &ffcapi.ConfirmationMapUpdateResult{ Confirmations: existingQueue, @@ -514,26 +490,28 @@ func TestCompareAndUpdateConfirmationQueue_MismatchConfirmationBlock(t *testing. // Execute bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) - // Assert - assert.False(t, occ.HasNewFork) - assert.True(t, occ.Rebuilt) - // The code builds a full confirmation queue from the canonical chain - assert.Len(t, occ.Confirmations, 4) + // Assert all confirmations are in the confirmation queue + assert.False(t, occ.NewFork) + assert.True(t, occ.Confirmed) + assert.Len(t, occ.Confirmations, 6) assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) - assert.Equal(t, txBlockNumber+3, uint64(occ.Confirmations[1].BlockNumber)) - assert.Equal(t, txBlockNumber+4, uint64(occ.Confirmations[2].BlockNumber)) - assert.Equal(t, txBlockNumber+5, uint64(occ.Confirmations[3].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(occ.Confirmations[1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(occ.Confirmations[2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(occ.Confirmations[3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(occ.Confirmations[4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(occ.Confirmations[5].BlockNumber)) } -func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsTooDistant(t *testing.T) { +func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmationDoNotAffectConfirmations(t *testing.T) { // Setup - bl := &blockListener{ - canonicalChain: createTestChain(145, 150), - } + 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: generateTestHash(100)}, + {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: "0xwrongparent"}, } occ := &ffcapi.ConfirmationMapUpdateResult{ Confirmations: existingQueue, @@ -551,27 +529,27 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsTooDistant(t *te bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) // Assert - // only the tx block and the first block in the canonical chain are in the confirmation queue - // and the transaction is confirmed - assert.False(t, occ.HasNewFork) - assert.True(t, occ.Rebuilt) - assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.NewFork) assert.True(t, occ.Confirmed) - assert.Len(t, occ.Confirmations, 2) + assert.Len(t, occ.Confirmations, 6) assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) - assert.Equal(t, bl.canonicalChain.Front().Value.(*ffcapi.MinimalBlockInfo).BlockNumber, occ.Confirmations[1].BlockNumber) + assert.Equal(t, txBlockNumber+1, uint64(occ.Confirmations[1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(occ.Confirmations[2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(occ.Confirmations[3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(occ.Confirmations[4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(occ.Confirmations[5].BlockNumber)) } -func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmation(t *testing.T) { +func TestCompareAndUpdateConfirmationQueue_ConnectionNodeMismatch(t *testing.T) { // Setup - bl := &blockListener{ - canonicalChain: createTestChain(50, 150), - } + bl, done := newBlockListenerWithTestChain(t, 100, 5, 102, 150, []uint64{101}) + 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"}, + {BlockHash: "0xblockwrong", BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, + {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, + {BlockHash: generateTestHash(103), BlockNumber: fftypes.FFuint64(103), ParentHash: generateTestHash(102)}, } occ := &ffcapi.ConfirmationMapUpdateResult{ Confirmations: existingQueue, @@ -589,26 +567,27 @@ func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmation(t *test bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) // Assert - assert.False(t, occ.HasNewFork) - assert.True(t, occ.Rebuilt) - assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.NewFork) assert.True(t, occ.Confirmed) assert.Len(t, occ.Confirmations, 6) assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) - assert.Equal(t, generateTestHash(100), occ.Confirmations[1].ParentHash) + assert.Equal(t, txBlockNumber+1, uint64(occ.Confirmations[1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(occ.Confirmations[2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(occ.Confirmations[3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(occ.Confirmations[4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(occ.Confirmations[5].BlockNumber)) } -func TestCompareAndUpdateConfirmationQueue_ConnectionNodeMismatch(t *testing.T) { +func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmationAfterFirstConfirmation(t *testing.T) { // Setup - bl := &blockListener{ - canonicalChain: createTestChain(102, 150), - } + 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: "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)}, + {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, + {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblockwrong"}, } occ := &ffcapi.ConfirmationMapUpdateResult{ Confirmations: existingQueue, @@ -626,21 +605,30 @@ func TestCompareAndUpdateConfirmationQueue_ConnectionNodeMismatch(t *testing.T) bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) // Assert - assert.False(t, occ.HasNewFork) - assert.True(t, occ.Rebuilt) - assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.NewFork) assert.True(t, occ.Confirmed) - assert.Len(t, occ.Confirmations, 5) + assert.Len(t, occ.Confirmations, 6) assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) - assert.Equal(t, generateTestHash(102), occ.Confirmations[1].BlockHash) - assert.Equal(t, generateTestHash(102), occ.Confirmations[2].ParentHash) + assert.Equal(t, txBlockNumber+1, uint64(occ.Confirmations[1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(occ.Confirmations[2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(occ.Confirmations[3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(occ.Confirmations[4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(occ.Confirmations[5].BlockNumber)) } -func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmationAfterFirstConfirmation(t *testing.T) { +func TestCompareAndUpdateConfirmationQueue_FailedToFetchBlockInfo(t *testing.T) { // Setup + mRPC := &rpcbackendmocks.Backend{} bl := &blockListener{ - canonicalChain: createTestChain(100, 150), + 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)}, @@ -663,21 +651,54 @@ func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmationAfterFir bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) // Assert - assert.False(t, occ.HasNewFork) - assert.True(t, occ.Rebuilt) - assert.True(t, occ.HasNewConfirmation) - assert.True(t, occ.Confirmed) - assert.Len(t, occ.Confirmations, 5) - assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) - assert.Equal(t, generateTestHash(102), occ.Confirmations[1].BlockHash) - assert.Equal(t, generateTestHash(102), occ.Confirmations[2].ParentHash) + assert.Len(t, occ.Confirmations, 3) // still use the old confirmation queue + } -func TestCompareAndUpdateConfirmationQueue_NewForkAfterFirstConfirmation(t *testing.T) { +func TestCompareAndUpdateConfirmationQueue_NilBlockInfo(t *testing.T) { // Setup + mRPC := &rpcbackendmocks.Backend{} bl := &blockListener{ - canonicalChain: createTestChain(100, 150), + 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"}, + } + occ := &ffcapi.ConfirmationMapUpdateResult{ + Confirmations: existingQueue, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(5) + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.Len(t, occ.Confirmations, 3) // still use the old confirmation queue +} +func TestCompareAndUpdateConfirmationQueue_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)}, @@ -700,18 +721,15 @@ func TestCompareAndUpdateConfirmationQueue_NewForkAfterFirstConfirmation(t *test bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) // Assert - assert.True(t, occ.HasNewFork) - assert.False(t, occ.Rebuilt) - assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.NewFork) assert.True(t, occ.Confirmed) assert.Len(t, occ.Confirmations, 6) } func TestCompareAndUpdateConfirmationQueue_NewForkAfterFirstConfirmation_ZeroConfirmationCount(t *testing.T) { // Setup - bl := &blockListener{ - canonicalChain: createTestChain(100, 150), - } + 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)}, @@ -734,18 +752,15 @@ func TestCompareAndUpdateConfirmationQueue_NewForkAfterFirstConfirmation_ZeroCon bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) // Assert - assert.False(t, occ.HasNewFork) - assert.False(t, occ.Rebuilt) - assert.False(t, occ.HasNewConfirmation) + assert.False(t, occ.NewFork) assert.True(t, occ.Confirmed) assert.Len(t, occ.Confirmations, 1) } func TestCompareAndUpdateConfirmationQueue_NewForkAndNoConnectionToCanonicalChain(t *testing.T) { // Setup - bl := &blockListener{ - canonicalChain: createTestChain(103, 150), - } + 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)}, @@ -769,26 +784,20 @@ func TestCompareAndUpdateConfirmationQueue_NewForkAndNoConnectionToCanonicalChai bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) // Assert - assert.False(t, occ.HasNewFork) - assert.True(t, occ.Rebuilt) - assert.True(t, occ.HasNewConfirmation) + assert.True(t, occ.NewFork) assert.True(t, occ.Confirmed) - assert.Len(t, occ.Confirmations, 4) - assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) - assert.Equal(t, txBlockNumber+3, uint64(occ.Confirmations[1].BlockNumber)) - assert.Equal(t, txBlockNumber+4, uint64(occ.Confirmations[2].BlockNumber)) - assert.Equal(t, txBlockNumber+5, uint64(occ.Confirmations[3].BlockNumber)) + assert.Len(t, occ.Confirmations, 6) } func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationLaterThanCurrentBlock(t *testing.T) { // Setup - bl := &blockListener{ - canonicalChain: createTestChain(100, 150), - } + 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(103), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, + {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, } occ := &ffcapi.ConfirmationMapUpdateResult{ Confirmations: existingQueue, @@ -806,9 +815,7 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationLaterThanCurrentB bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) // Assert - assert.False(t, occ.HasNewFork) - assert.True(t, occ.Rebuilt) - assert.True(t, occ.HasNewConfirmation) + assert.False(t, occ.NewFork) assert.True(t, occ.Confirmed) assert.Len(t, occ.Confirmations, 6) assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) @@ -819,11 +826,48 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationLaterThanCurrentB assert.Equal(t, txBlockNumber+5, uint64(occ.Confirmations[5].BlockNumber)) } -func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable(t *testing.T) { +func TestCompareAndUpdateConfirmationQueue_ExistingTxBockInfoIsWrong(t *testing.T) { // Setup - bl := &blockListener{ - canonicalChain: createTestChain(103, 150), + bl, done := newBlockListenerWithTestChain(t, 100, 5, 100, 150, []uint64{}) + defer done() + + ctx := context.Background() + existingQueue := []*ffcapi.MinimalBlockInfo{ + {BlockHash: generateTestHash(999), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, // should be corrected + {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, } + occ := &ffcapi.ConfirmationMapUpdateResult{ + Confirmations: existingQueue, + } + txBlockNumber := uint64(100) + txBlockHash := generateTestHash(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: txBlockHash, + ParentHash: generateTestHash(99), + } + targetConfirmationCount := uint64(5) + + // Execute + bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + + // Assert + assert.False(t, occ.NewFork) + assert.True(t, occ.Confirmed) + assert.Len(t, occ.Confirmations, 6) + assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) + assert.Equal(t, generateTestHash(100), occ.Confirmations[0].BlockHash) + assert.Equal(t, txBlockNumber+1, uint64(occ.Confirmations[1].BlockNumber)) + assert.Equal(t, txBlockNumber+2, uint64(occ.Confirmations[2].BlockNumber)) + assert.Equal(t, txBlockNumber+3, uint64(occ.Confirmations[3].BlockNumber)) + assert.Equal(t, txBlockNumber+4, uint64(occ.Confirmations[4].BlockNumber)) + assert.Equal(t, txBlockNumber+5, uint64(occ.Confirmations[5].BlockNumber)) +} + +func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable(t *testing.T) { + // Setup + bl, done := newBlockListenerWithTestChain(t, 100, 5, 103, 150, []uint64{102}) + 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 @@ -854,9 +898,7 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable(t *testing.T) { // Assert assert.True(t, occ.Confirmed) - assert.False(t, occ.Rebuilt) - assert.False(t, occ.HasNewFork) - assert.False(t, occ.HasNewConfirmation) + assert.False(t, occ.NewFork) assert.Len(t, occ.Confirmations, 3) assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) assert.Equal(t, txBlockNumber+1, uint64(occ.Confirmations[1].BlockNumber)) @@ -865,9 +907,8 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable(t *testing.T) { func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable_ZeroConfirmationCount(t *testing.T) { // Setup - bl := &blockListener{ - canonicalChain: createTestChain(103, 150), - } + 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 @@ -898,18 +939,15 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable_ZeroConfirmationCo // Assert assert.True(t, occ.Confirmed) - assert.False(t, occ.Rebuilt) - assert.False(t, occ.HasNewFork) - assert.False(t, occ.HasNewConfirmation) + assert.False(t, occ.NewFork) assert.Len(t, occ.Confirmations, 1) assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) } func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableConnectable(t *testing.T) { // Setup - bl := &blockListener{ - canonicalChain: createTestChain(103, 150), - } + bl, done := newBlockListenerWithTestChain(t, 100, 5, 103, 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 @@ -939,9 +977,7 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableConnectable(t *test // The confirmation queue should return the confirmation queue up to the first block of the canonical chain assert.True(t, occ.Confirmed) - assert.False(t, occ.Rebuilt) - assert.False(t, occ.HasNewFork) - assert.False(t, occ.HasNewConfirmation) + assert.False(t, occ.NewFork) assert.Len(t, occ.Confirmations, 2) assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) assert.Equal(t, txBlockNumber+1, uint64(occ.Confirmations[1].BlockNumber)) @@ -949,9 +985,8 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableConnectable(t *test func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableButAllExistingConfirmationsAreTooHighForTargetConfirmationCount(t *testing.T) { // Setup - bl := &blockListener{ - canonicalChain: createTestChain(103, 150), - } + bl, done := newBlockListenerWithTestChain(t, 100, 5, 103, 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 @@ -979,20 +1014,17 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableButAllExistingConfi // The confirmation queue should return the tx block and the first block of the canonical chain assert.True(t, occ.Confirmed) - assert.False(t, occ.Rebuilt) - assert.False(t, occ.HasNewFork) - assert.False(t, occ.HasNewConfirmation) + assert.False(t, occ.NewFork) assert.Len(t, occ.Confirmations, 2) assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) - assert.Equal(t, uint64(103), uint64(occ.Confirmations[1].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(occ.Confirmations[1].BlockNumber)) } func TestCompareAndUpdateConfirmationQueue_HasSufficientConfirmationsButNoOverlapWithCanonicalChain(t *testing.T) { // Setup - bl := &blockListener{ - canonicalChain: createTestChain(104, 150), - } + 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 @@ -1020,20 +1052,17 @@ func TestCompareAndUpdateConfirmationQueue_HasSufficientConfirmationsButNoOverla // Because the existing confirmations do not have overlap with the canonical chain, // the confirmation queue should return the tx block and the first block of the canonical chain assert.True(t, occ.Confirmed) - assert.False(t, occ.HasNewFork) - assert.True(t, occ.Rebuilt) - assert.True(t, occ.HasNewConfirmation) + assert.False(t, occ.NewFork) assert.Len(t, occ.Confirmations, 2) assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) - assert.Equal(t, uint64(104), uint64(occ.Confirmations[1].BlockNumber)) + assert.Equal(t, txBlockNumber+1, uint64(occ.Confirmations[1].BlockNumber)) } func TestCompareAndUpdateConfirmationQueue_ValidExistingConfirmations(t *testing.T) { // Setup - bl := &blockListener{ - canonicalChain: createTestChain(50, 150), - } + 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)}, @@ -1056,9 +1085,7 @@ func TestCompareAndUpdateConfirmationQueue_ValidExistingConfirmations(t *testing bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) // Assert - assert.False(t, occ.HasNewFork) - assert.False(t, occ.Rebuilt) - assert.True(t, occ.HasNewConfirmation) + assert.False(t, occ.NewFork) assert.True(t, occ.Confirmed) assert.Len(t, occ.Confirmations, 6) assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) @@ -1071,9 +1098,8 @@ func TestCompareAndUpdateConfirmationQueue_ValidExistingConfirmations(t *testing func TestCompareAndUpdateConfirmationQueue_ValidExistingTxBlock(t *testing.T) { // Setup - bl := &blockListener{ - canonicalChain: createTestChain(50, 150), - } + 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)}, @@ -1094,9 +1120,7 @@ func TestCompareAndUpdateConfirmationQueue_ValidExistingTxBlock(t *testing.T) { bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) // Assert - assert.False(t, occ.HasNewFork) - assert.False(t, occ.Rebuilt) - assert.True(t, occ.HasNewConfirmation) + assert.False(t, occ.NewFork) assert.True(t, occ.Confirmed) assert.Len(t, occ.Confirmations, 6) assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) @@ -1109,9 +1133,8 @@ func TestCompareAndUpdateConfirmationQueue_ValidExistingTxBlock(t *testing.T) { func TestCompareAndUpdateConfirmationQueue_ReachTargetConfirmation(t *testing.T) { // Setup - bl := &blockListener{ - canonicalChain: createTestChain(50, 150), - } + bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 150, []uint64{}) + defer done() ctx := context.Background() occ := &ffcapi.ConfirmationMapUpdateResult{ Confirmations: []*ffcapi.MinimalBlockInfo{}, @@ -1129,7 +1152,6 @@ func TestCompareAndUpdateConfirmationQueue_ReachTargetConfirmation(t *testing.T) bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) // Assert - assert.True(t, occ.HasNewConfirmation) assert.True(t, occ.Confirmed) // The code builds a full confirmation queue from the canonical chain assert.GreaterOrEqual(t, len(occ.Confirmations), 4) // tx block + 3 confirmations @@ -1137,9 +1159,8 @@ func TestCompareAndUpdateConfirmationQueue_ReachTargetConfirmation(t *testing.T) func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithGap(t *testing.T) { // Setup - bl := &blockListener{ - canonicalChain: createTestChain(101, 150), - } + bl, done := newBlockListenerWithTestChain(t, 100, 5, 101, 150, []uint64{}) + defer done() ctx := context.Background() // Create confirmations with a gap (missing block 102) existingQueue := []*ffcapi.MinimalBlockInfo{ @@ -1164,18 +1185,15 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithGap(t *testi bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) // Assert - assert.False(t, occ.HasNewFork) - assert.True(t, occ.Rebuilt) - assert.True(t, occ.HasNewConfirmation) + assert.False(t, occ.NewFork) assert.True(t, occ.Confirmed) assert.Len(t, occ.Confirmations, 6) } func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNumber(t *testing.T) { // Setup - bl := &blockListener{ - canonicalChain: createTestChain(50, 150), - } + bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 150, []uint64{}) + defer done() ctx := context.Background() // Create confirmations with a lower block number existingQueue := []*ffcapi.MinimalBlockInfo{ @@ -1198,18 +1216,15 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNu bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) // Assert - assert.False(t, occ.HasNewFork) - assert.True(t, occ.Rebuilt) - assert.True(t, occ.HasNewConfirmation) + assert.False(t, occ.NewFork) assert.True(t, occ.Confirmed) assert.Len(t, occ.Confirmations, 6) } func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNumberAfterFirstConfirmation(t *testing.T) { // Setup - bl := &blockListener{ - canonicalChain: createTestChain(101, 150), - } + bl, done := newBlockListenerWithTestChain(t, 100, 5, 101, 150, []uint64{}) + defer done() ctx := context.Background() // Create confirmations with a lower block number existingQueue := []*ffcapi.MinimalBlockInfo{ @@ -1233,9 +1248,7 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNu bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) // Assert - assert.False(t, occ.HasNewFork) - assert.True(t, occ.Rebuilt) - assert.True(t, occ.HasNewConfirmation) + assert.False(t, occ.NewFork) assert.True(t, occ.Confirmed) assert.Len(t, occ.Confirmations, 6) } @@ -1269,3 +1282,168 @@ func createTestChain(startBlock, endBlock uint64) *list.List { } 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) + } +} + +func TestCheckAndFillInGap_GetBlockInfoError(t *testing.T) { + // Setup + _, c, mRPC, _ := newTestConnectorWithNoBlockerFilterDefaultMocks(t) + ctx := context.Background() + txBlockNumber := uint64(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: generateTestHash(txBlockNumber), + ParentHash: generateTestHash(txBlockNumber - 1), + } + targetConfirmationCount := uint64(2) + newConfirmationsWithoutTxBlock := []*ffcapi.MinimalBlockInfo{} + existingConfirmations := []*ffcapi.MinimalBlockInfo{} + + // Mock RPC to return an error for the gap blocks (blockNumberToReach = 100+2 = 102, then loops 102 down to 101) + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByNumber", mock.MatchedBy(func(bn *ethtypes.HexInteger) bool { + return bn.BigInt().String() == strconv.FormatUint(102, 10) + }), false).Return(&rpcbackend.RPCError{Message: "pop"}) + + // Execute + result, hasNewFork, err := c.blockListener.checkAndFillInGap(ctx, newConfirmationsWithoutTxBlock, existingConfirmations, txBlockInfo, targetConfirmationCount) + + // Assertions + assert.Error(t, err) + assert.Contains(t, err.Error(), "pop") + assert.False(t, hasNewFork) + assert.Nil(t, result) + mRPC.AssertExpectations(t) +} + +func TestCheckAndFillInGap_BlockNotAvailable(t *testing.T) { + // Setup + _, c, mRPC, _ := newTestConnectorWithNoBlockerFilterDefaultMocks(t) + ctx := context.Background() + txBlockNumber := uint64(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: generateTestHash(txBlockNumber), + ParentHash: generateTestHash(txBlockNumber - 1), + } + targetConfirmationCount := uint64(2) + newConfirmationsWithoutTxBlock := []*ffcapi.MinimalBlockInfo{} + existingConfirmations := []*ffcapi.MinimalBlockInfo{} + + // Setup RPC calls - return nil to simulate block not available (blockNumberToReach = 100+2 = 102) + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByNumber", mock.MatchedBy(func(bn *ethtypes.HexInteger) bool { + return bn.BigInt().String() == strconv.FormatUint(102, 10) + }), false).Return(nil).Run(func(args mock.Arguments) { + *args[1].(**blockInfoJSONRPC) = nil // Block not available + }) + + // Execute + result, hasNewFork, err := c.blockListener.checkAndFillInGap(ctx, newConfirmationsWithoutTxBlock, existingConfirmations, txBlockInfo, targetConfirmationCount) + + // Assertions + assert.Error(t, err) + assert.Contains(t, err.Error(), "Block not available") + assert.False(t, hasNewFork) + assert.Nil(t, result) + mRPC.AssertExpectations(t) +} + +func TestCheckAndFillInGap_InvalidBlockParentRelationship(t *testing.T) { + // Setup + _, c, mRPC, _ := newTestConnectorWithNoBlockerFilterDefaultMocks(t) + ctx := context.Background() + txBlockNumber := uint64(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: generateTestHash(txBlockNumber), + ParentHash: generateTestHash(txBlockNumber - 1), + } + targetConfirmationCount := uint64(2) + + // Setup test scenario where we have a previous block but fetch a block that doesn't connect + block102 := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(102), + BlockHash: generateTestHash(102), + ParentHash: "wrong_parent_hash", // Wrong parent - should cause validation to fail + } + + newConfirmationsWithoutTxBlock := []*ffcapi.MinimalBlockInfo{block102} // Start with block 102 + existingConfirmations := []*ffcapi.MinimalBlockInfo{} + + // Mock getBlockInfoByNumber calls for gap block 101 (blockNumberToReach = block102.BlockNumber - 1 = 101) + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByNumber", mock.MatchedBy(func(bn *ethtypes.HexInteger) bool { + return bn.BigInt().String() == strconv.FormatUint(101, 10) + }), false).Return(nil).Run(func(args mock.Arguments) { + *args[1].(**blockInfoJSONRPC) = &blockInfoJSONRPC{ + Number: ethtypes.NewHexInteger64(101), + Hash: ethtypes.MustNewHexBytes0xPrefix(generateTestHash(101)), + ParentHash: ethtypes.MustNewHexBytes0xPrefix(generateTestHash(99)), // Wrong parent - should cause validation to fail + } + }) + + // Execute + result, hasNewFork, err := c.blockListener.checkAndFillInGap(ctx, newConfirmationsWithoutTxBlock, existingConfirmations, txBlockInfo, targetConfirmationCount) + + // Assertions - should fail because block 102 has wrong parent hash and doesn't connect to block 101 + assert.Error(t, err) + assert.Contains(t, err.Error(), "Failed to build confirmation queue") + assert.False(t, hasNewFork) + assert.Nil(t, result) + mRPC.AssertExpectations(t) +} + +func TestCheckAndFillInGap_TxBlockNotParentOfFirstConfirmation(t *testing.T) { + // Setup + _, c, mRPC, _ := newTestConnectorWithNoBlockerFilterDefaultMocks(t) + ctx := context.Background() + txBlockNumber := uint64(100) + txBlockInfo := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(txBlockNumber), + BlockHash: generateTestHash(txBlockNumber), + ParentHash: generateTestHash(txBlockNumber - 1), + } + targetConfirmationCount := uint64(1) + + // Setup test scenario where txBlockInfo is NOT parent of the first confirmation block + wrongParentConfirmation := &ffcapi.MinimalBlockInfo{ + BlockNumber: fftypes.FFuint64(101), + BlockHash: generateTestHash(101), + ParentHash: "wrong_parent_hash", // This doesn't match txBlockInfo.BlockHash + } + + newConfirmationsWithoutTxBlock := []*ffcapi.MinimalBlockInfo{wrongParentConfirmation} + existingConfirmations := []*ffcapi.MinimalBlockInfo{} + + // Execute + result, hasNewFork, err := c.blockListener.checkAndFillInGap(ctx, newConfirmationsWithoutTxBlock, existingConfirmations, txBlockInfo, targetConfirmationCount) + + // Assertions + assert.Error(t, err) + assert.Contains(t, err.Error(), "Failed to build confirmation queue") + assert.False(t, hasNewFork) + assert.Nil(t, result) + mRPC.AssertExpectations(t) +} diff --git a/internal/ethereum/get_block_info.go b/internal/ethereum/get_block_info.go index 12f6159..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.Uint64(), 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/msgs/en_error_messages.go b/internal/msgs/en_error_messages.go index 56dd6b9..7e2c098 100644 --- a/internal/msgs/en_error_messages.go +++ b/internal/msgs/en_error_messages.go @@ -75,4 +75,5 @@ var ( 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 queue") ) From 885471d2db5bba17a58db2361abf9ebdd77173d9 Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Fri, 3 Oct 2025 18:49:05 +0100 Subject: [PATCH 16/37] tidy up Signed-off-by: Chengxuan Xing --- internal/ethereum/blocklistener_blockquery.go | 6 -- internal/ethereum/confirmation_reconciler.go | 81 ++++++++++++++----- 2 files changed, 59 insertions(+), 28 deletions(-) diff --git a/internal/ethereum/blocklistener_blockquery.go b/internal/ethereum/blocklistener_blockquery.go index 0987fc8..7e45729 100644 --- a/internal/ethereum/blocklistener_blockquery.go +++ b/internal/ethereum/blocklistener_blockquery.go @@ -56,12 +56,6 @@ func (bl *blockListener) addToBlockCache(blockInfo *blockInfoJSONRPC) { func (bl *blockListener) getBlockInfoContainsTxHash(ctx context.Context, txHash string) (*ffcapi.MinimalBlockInfo, error) { // Query the chain to find the transaction block - // Note: should consider have an in-memory map of transaction hash to block for faster lookup - // The extra memory usage of the map should be outweighed by the speed improvement of lookup - // But I saw we have a ffcapi.MinimalBlockInfo struct that intentionally removes the tx hashes - // so need to figure out the reason first - - // TODO: add a cache if map cannot be used res, reason, receiptErr := bl.c.TransactionReceipt(ctx, &ffcapi.TransactionReceiptRequest{ TransactionHash: txHash, }) diff --git a/internal/ethereum/confirmation_reconciler.go b/internal/ethereum/confirmation_reconciler.go index a5033ae..f4dbf43 100644 --- a/internal/ethereum/confirmation_reconciler.go +++ b/internal/ethereum/confirmation_reconciler.go @@ -14,6 +14,11 @@ // See the License for the specific language governing permissions and // limitations under the License. +// The confirmation reconciler manages transaction confirmation queues by: +// - Copying blocks from the canonical chain and the existing confirmation queue +// - Detecting blockchain forks and rebuilding confirmation queues when necessary +// - Filling gaps in confirmation queues by fetching missing blocks +// - Determining when transactions have reached the target confirmation count package ethereum import ( @@ -26,8 +31,9 @@ import ( "github.com/hyperledger/firefly-transaction-manager/pkg/ffcapi" ) +// reconcileConfirmationsForTransaction reconciles the confirmation queue for a transaction func (bl *blockListener) reconcileConfirmationsForTransaction(ctx context.Context, txHash string, existingConfirmations []*ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) (*ffcapi.ConfirmationMapUpdateResult, error) { - // Initialize the output context + // Initialize the result with existing confirmations reconcileResult := &ffcapi.ConfirmationMapUpdateResult{ Confirmations: existingConfirmations, NewFork: false, @@ -35,6 +41,7 @@ func (bl *blockListener) reconcileConfirmationsForTransaction(ctx context.Contex TargetConfirmationCount: targetConfirmationCount, } + // Fetch the block containing the transaction txBlockInfo, err := bl.getBlockInfoContainsTxHash(ctx, txHash) if err != nil { log.L(ctx).Errorf("Failed to fetch block info using tx hash %s: %v", txHash, err) @@ -49,16 +56,17 @@ func (bl *blockListener) reconcileConfirmationsForTransaction(ctx context.Contex return bl.compareAndUpdateConfirmationQueue(ctx, reconcileResult, txBlockInfo, targetConfirmationCount) } +// compareAndUpdateConfirmationQueue orchestrates the confirmation reconciliation process. +// It builds new confirmations from the canonical chain and fills gaps in the confirmation queue. func (bl *blockListener) compareAndUpdateConfirmationQueue(ctx context.Context, reconcileResult *ffcapi.ConfirmationMapUpdateResult, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) (*ffcapi.ConfirmationMapUpdateResult, error) { var err error - // Compare the and build the tail part of the confirmation queue using the canonical chain + // Build new confirmations from the canonical chain and get existing confirmations newConfirmationsWithoutTxBlock, existingConfirmations, returnResult := bl.buildConfirmationQueueUsingCanonicalChain(ctx, reconcileResult, txBlockInfo, targetConfirmationCount) if returnResult { return reconcileResult, nil } - // Validate and process existing confirmations - // and fill in the gap in the confirmation queue + // Validate existing confirmations and fill gaps in the confirmation queue var confirmations []*ffcapi.MinimalBlockInfo var newFork bool confirmations, newFork, err = bl.checkAndFillInGap(ctx, newConfirmationsWithoutTxBlock, existingConfirmations, txBlockInfo, targetConfirmationCount) @@ -70,47 +78,52 @@ func (bl *blockListener) compareAndUpdateConfirmationQueue(ctx context.Context, return reconcileResult, err } -// NOTE: this function only build up the confirmation queue uses the in-memory canonical chain -// it does not build up the canonical chain -// compareAndUpdateConfirmationQueueUsingCanonicalChain compares the existing confirmation queue with the in-memory linked list -// this function obtains the read lock on the canonical chain, so it should not make any long-running queries - +// buildConfirmationQueueUsingCanonicalChain builds the confirmation queue using the in-memory canonical chain. +// It does not modify the canonical chain itself, only reads from it. +// This function holds a read lock on the canonical chain, so it should not make long-running queries. func (bl *blockListener) buildConfirmationQueueUsingCanonicalChain(ctx context.Context, reconcileResult *ffcapi.ConfirmationMapUpdateResult, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) (newConfirmationsWithoutTxBlock []*ffcapi.MinimalBlockInfo, existingConfirmations []*ffcapi.MinimalBlockInfo, returnResult bool) { bl.mux.RLock() defer bl.mux.RUnlock() txBlockNumber := txBlockInfo.BlockNumber.Uint64() targetBlockNumber := txBlockInfo.BlockNumber.Uint64() + targetConfirmationCount + // Check if the canonical chain has caught up to the transaction block chainTail := bl.canonicalChain.Back().Value.(*ffcapi.MinimalBlockInfo) if chainTail == nil || chainTail.BlockNumber.Uint64() < txBlockNumber { log.L(ctx).Debugf("Canonical chain is waiting for the transaction block %d to be indexed", txBlockNumber) return nil, nil, true } - // Initialize confirmation map and get existing queue + // Initialize confirmation map and get existing confirmations existingConfirmations = bl.initializeConfirmationMap(reconcileResult, txBlockInfo) - // if the target confirmation count is 0, we should just return the transaction block + // Special case: if targetConfirmationCount is 0, transaction is immediately confirmed if targetConfirmationCount == 0 { reconcileResult.Confirmed = true reconcileResult.Confirmations = []*ffcapi.MinimalBlockInfo{txBlockInfo} return nil, existingConfirmations, true } - // build the tail part of the queue from the canonical chain + // Build new confirmations from blocks after the transaction block newConfirmationsWithoutTxBlock = []*ffcapi.MinimalBlockInfo{} currentBlock := bl.canonicalChain.Front() for currentBlock != nil { currentBlockInfo := currentBlock.Value.(*ffcapi.MinimalBlockInfo) + + // If we've reached the target confirmation count, mark as confirmed if currentBlockInfo.BlockNumber.Uint64() > targetBlockNumber { reconcileResult.Confirmed = true break } + + // Skip blocks at or before the transaction block if currentBlockInfo.BlockNumber.Uint64() <= txBlockNumber { currentBlock = currentBlock.Next() continue } + + // Add blocks after the transaction block to confirmations newConfirmationsWithoutTxBlock = append(newConfirmationsWithoutTxBlock, &ffcapi.MinimalBlockInfo{ BlockHash: currentBlockInfo.BlockHash, BlockNumber: fftypes.FFuint64(currentBlockInfo.BlockNumber.Uint64()), @@ -121,18 +134,22 @@ func (bl *blockListener) buildConfirmationQueueUsingCanonicalChain(ctx context.C return newConfirmationsWithoutTxBlock, existingConfirmations, false } +// initializeConfirmationMap initializes the confirmation map with the transaction block +// and validates existing confirmations against the current transaction block. +// Returns existing confirmations if valid, or nil if a fork is detected. func (bl *blockListener) initializeConfirmationMap(reconcileResult *ffcapi.ConfirmationMapUpdateResult, txBlockInfo *ffcapi.MinimalBlockInfo) []*ffcapi.MinimalBlockInfo { + // If no existing confirmations, initialize with the transaction block if len(reconcileResult.Confirmations) == 0 { reconcileResult.Confirmations = []*ffcapi.MinimalBlockInfo{txBlockInfo} return nil } + // Validate existing confirmations against the current transaction block existingQueue := reconcileResult.Confirmations if len(existingQueue) > 0 { existingTxBlock := existingQueue[0] if !existingTxBlock.Equal(txBlockInfo) { - // the tx block in the existing queue does not match the new tx block we queried from the chain - // rebuild a new confirmation queue with the new tx block + // Transaction block mismatch indicates a fork - rebuild confirmation queue reconcileResult.NewFork = true reconcileResult.Confirmations = []*ffcapi.MinimalBlockInfo{txBlockInfo} return nil @@ -142,9 +159,13 @@ func (bl *blockListener) initializeConfirmationMap(reconcileResult *ffcapi.Confi return existingQueue } +// checkAndFillInGap validates existing confirmations, detects forks, and fills gaps +// in the confirmation queue using existing confirmations or fetching missing blocks from the blockchain. +// It ensures the confirmation chain is valid and connected to the transaction block. func (bl *blockListener) checkAndFillInGap(ctx context.Context, newConfirmationsWithoutTxBlock []*ffcapi.MinimalBlockInfo, existingConfirmations []*ffcapi.MinimalBlockInfo, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) ([]*ffcapi.MinimalBlockInfo, bool, error) { var hasNewFork bool - // check whether there are forks in the newConfirmations + + // Detect forks by comparing new confirmations with existing ones for _, confirmation := range newConfirmationsWithoutTxBlock { for _, existingConfirmation := range existingConfirmations { if confirmation.BlockNumber.Uint64() == existingConfirmation.BlockNumber.Uint64() && !confirmation.Equal(existingConfirmation) { @@ -157,59 +178,75 @@ func (bl *blockListener) checkAndFillInGap(ctx context.Context, newConfirmations } } + // Determine the range of blocks to validate and fill gaps blockNumberToReach := txBlockInfo.BlockNumber.Uint64() + targetConfirmationCount var lastValidatedBlock *ffcapi.MinimalBlockInfo if len(newConfirmationsWithoutTxBlock) > 0 { + // Start from the block before the first new confirmation blockNumberToReach = newConfirmationsWithoutTxBlock[0].BlockNumber.Uint64() - 1 lastValidatedBlock = newConfirmationsWithoutTxBlock[0] } + // Fill gaps by validating blocks from target down to transaction block for i := blockNumberToReach; i > txBlockInfo.BlockNumber.Uint64(); i-- { - // first use the block info from the confirmation queue if matches are found fetchedFromExistingQueue := false - if lastValidatedBlock != nil { + // First, try to use existing confirmations if they match + if lastValidatedBlock != nil { for _, confirmation := range existingConfirmations { if confirmation.BlockNumber.Uint64() == i { if confirmation.IsParentOf(lastValidatedBlock) { + // Valid existing confirmation - prepend to queue newConfirmationsWithoutTxBlock = append([]*ffcapi.MinimalBlockInfo{confirmation}, newConfirmationsWithoutTxBlock...) lastValidatedBlock = confirmation fetchedFromExistingQueue = true break } + // Block number matches but parent relationship is invalid - fork detected hasNewFork = true } } } + if fetchedFromExistingQueue { continue } - // if no match is found, fetch the block info from the chain + + // Fetch block from blockchain if not found in existing confirmations freshBlockInfo, _, err := bl.getBlockInfoByNumber(ctx, i, false, "", "") if err != nil { return nil, hasNewFork, err } + if freshBlockInfo == nil { + return nil, hasNewFork, i18n.NewError(ctx, msgs.MsgBlockNotAvailable) + } + fetchedBlock := &ffcapi.MinimalBlockInfo{ BlockNumber: fftypes.FFuint64(freshBlockInfo.Number.BigInt().Uint64()), BlockHash: freshBlockInfo.Hash.String(), ParentHash: freshBlockInfo.ParentHash.String(), } + + // Validate parent-child relationship if lastValidatedBlock != nil && !fetchedBlock.IsParentOf(lastValidatedBlock) { - // the fetched block is not the parent of the last validated block - // chain is not in a stable stable to build the confirmation queue return nil, hasNewFork, i18n.NewError(ctx, msgs.MsgFailedToBuildConfirmationQueue) } + + // Prepend fetched block to confirmation queue newConfirmationsWithoutTxBlock = append([]*ffcapi.MinimalBlockInfo{fetchedBlock}, newConfirmationsWithoutTxBlock...) lastValidatedBlock = fetchedBlock } - // we've rebuilt the confirmations queue, now check the front of the queue still connect to the tx block - if !txBlockInfo.IsParentOf(newConfirmationsWithoutTxBlock[0]) { + // Final validation: ensure the confirmation chain connects to the transaction block + if len(newConfirmationsWithoutTxBlock) > 0 && !txBlockInfo.IsParentOf(newConfirmationsWithoutTxBlock[0]) { return nil, hasNewFork, i18n.NewError(ctx, msgs.MsgFailedToBuildConfirmationQueue) } + return append([]*ffcapi.MinimalBlockInfo{txBlockInfo}, newConfirmationsWithoutTxBlock...), hasNewFork, nil } +// 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.ConfirmationMapUpdateResult, error) { return c.blockListener.reconcileConfirmationsForTransaction(ctx, txHash, existingConfirmations, targetConfirmationCount) } From c49d54e96a4e59ff97ed9565f6b1b4515f0f51ad Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Mon, 6 Oct 2025 09:30:27 +0100 Subject: [PATCH 17/37] tidy up go sum Signed-off-by: Chengxuan Xing --- go.sum | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/go.sum b/go.sum index b27e93b..d0a77a0 100644 --- a/go.sum +++ b/go.sum @@ -104,18 +104,6 @@ github.com/hyperledger/firefly-common v1.5.6-0.20250630201730-e234335c0381 h1:4m github.com/hyperledger/firefly-common v1.5.6-0.20250630201730-e234335c0381/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.1-0.20250910122026-4f65f76eb9eb h1:doSlc4SN1LIA+kMMW/vBDHaud+5Ad5+eWRitbfGBxho= -github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250910122026-4f65f76eb9eb/go.mod h1:KHGvK/QqD2jpRZ04lQoX/k1T2o644NCkRlr3FbvKqnA= -github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250910151057-7bc7bb81591c h1:RNd7cMvH8Mr/wE2Y2B4Vy0+5l0FN4G6UMC4rMqJeFjE= -github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250910151057-7bc7bb81591c/go.mod h1:KHGvK/QqD2jpRZ04lQoX/k1T2o644NCkRlr3FbvKqnA= -github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250910153251-07127ff35b09 h1:1Frz0u69ETPkFbFGcLxPyWJrzUnBv+yVKNdZUfT7wBE= -github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250910153251-07127ff35b09/go.mod h1:KHGvK/QqD2jpRZ04lQoX/k1T2o644NCkRlr3FbvKqnA= -github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250910153533-14142cf9f697 h1:leUNAoiwMidZYwH+F6bmCa7kvN3qcPGH25k2HcMptyg= -github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250910153533-14142cf9f697/go.mod h1:KHGvK/QqD2jpRZ04lQoX/k1T2o644NCkRlr3FbvKqnA= -github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250930113938-a1e02b75dbc9 h1:9AK0bTWEv9NC2HhLLKC8aKjdaOlC/B1P3yB6KnnLOH4= -github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20250930113938-a1e02b75dbc9/go.mod h1:KHGvK/QqD2jpRZ04lQoX/k1T2o644NCkRlr3FbvKqnA= github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20251003113149-46c9271ca66e h1:jUumirQuZR8cQCBq9BNmpLM4rKo9ahQPdFzGvf/RYIs= github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20251003113149-46c9271ca66e/go.mod h1:KHGvK/QqD2jpRZ04lQoX/k1T2o644NCkRlr3FbvKqnA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= From f4722e277b3391bd3b0ead3afbe129116c5ed2e8 Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Mon, 6 Oct 2025 10:31:38 +0100 Subject: [PATCH 18/37] avoid extra call when the next block is in canonical chain Signed-off-by: Chengxuan Xing --- internal/ethereum/confirmation_reconciler.go | 39 +++++++++++-------- .../ethereum/confirmation_reconciler_test.go | 14 +++---- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/internal/ethereum/confirmation_reconciler.go b/internal/ethereum/confirmation_reconciler.go index f4dbf43..2c9f4a3 100644 --- a/internal/ethereum/confirmation_reconciler.go +++ b/internal/ethereum/confirmation_reconciler.go @@ -61,15 +61,27 @@ func (bl *blockListener) reconcileConfirmationsForTransaction(ctx context.Contex func (bl *blockListener) compareAndUpdateConfirmationQueue(ctx context.Context, reconcileResult *ffcapi.ConfirmationMapUpdateResult, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) (*ffcapi.ConfirmationMapUpdateResult, error) { var err error // Build new confirmations from the canonical chain and get existing confirmations - newConfirmationsWithoutTxBlock, existingConfirmations, returnResult := bl.buildConfirmationQueueUsingCanonicalChain(ctx, reconcileResult, txBlockInfo, targetConfirmationCount) + newConfirmationsWithoutTxBlock, lastValidatedBlock, returnResult := bl.buildConfirmationQueueUsingCanonicalChain(ctx, reconcileResult, txBlockInfo, targetConfirmationCount) if returnResult { return reconcileResult, nil } + // Initialize confirmation map and get existing confirmations + // the init must happen after the canonical chain check to avoid + // confirming blocks that are not yet validated in the canonical chain + existingConfirmations := bl.initializeConfirmationMap(reconcileResult, txBlockInfo) + + // Special case: if targetConfirmationCount is 0, transaction is immediately confirmed + if targetConfirmationCount == 0 { + reconcileResult.Confirmed = true + reconcileResult.Confirmations = []*ffcapi.MinimalBlockInfo{txBlockInfo} + return reconcileResult, nil + } + // Validate existing confirmations and fill gaps in the confirmation queue var confirmations []*ffcapi.MinimalBlockInfo var newFork bool - confirmations, newFork, err = bl.checkAndFillInGap(ctx, newConfirmationsWithoutTxBlock, existingConfirmations, txBlockInfo, targetConfirmationCount) + confirmations, newFork, err = bl.checkAndFillInGap(ctx, newConfirmationsWithoutTxBlock, existingConfirmations, txBlockInfo, targetConfirmationCount, lastValidatedBlock) if err != nil { return reconcileResult, err } @@ -81,7 +93,7 @@ func (bl *blockListener) compareAndUpdateConfirmationQueue(ctx context.Context, // buildConfirmationQueueUsingCanonicalChain builds the confirmation queue using the in-memory canonical chain. // It does not modify the canonical chain itself, only reads from it. // This function holds a read lock on the canonical chain, so it should not make long-running queries. -func (bl *blockListener) buildConfirmationQueueUsingCanonicalChain(ctx context.Context, reconcileResult *ffcapi.ConfirmationMapUpdateResult, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) (newConfirmationsWithoutTxBlock []*ffcapi.MinimalBlockInfo, existingConfirmations []*ffcapi.MinimalBlockInfo, returnResult bool) { +func (bl *blockListener) buildConfirmationQueueUsingCanonicalChain(ctx context.Context, reconcileResult *ffcapi.ConfirmationMapUpdateResult, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) (newConfirmationsWithoutTxBlock []*ffcapi.MinimalBlockInfo, lastValidatedBlock *ffcapi.MinimalBlockInfo, returnResult bool) { bl.mux.RLock() defer bl.mux.RUnlock() txBlockNumber := txBlockInfo.BlockNumber.Uint64() @@ -94,16 +106,6 @@ func (bl *blockListener) buildConfirmationQueueUsingCanonicalChain(ctx context.C return nil, nil, true } - // Initialize confirmation map and get existing confirmations - existingConfirmations = bl.initializeConfirmationMap(reconcileResult, txBlockInfo) - - // Special case: if targetConfirmationCount is 0, transaction is immediately confirmed - if targetConfirmationCount == 0 { - reconcileResult.Confirmed = true - reconcileResult.Confirmations = []*ffcapi.MinimalBlockInfo{txBlockInfo} - return nil, existingConfirmations, true - } - // Build new confirmations from blocks after the transaction block newConfirmationsWithoutTxBlock = []*ffcapi.MinimalBlockInfo{} @@ -114,6 +116,12 @@ func (bl *blockListener) buildConfirmationQueueUsingCanonicalChain(ctx context.C // If we've reached the target confirmation count, mark as confirmed if currentBlockInfo.BlockNumber.Uint64() > targetBlockNumber { reconcileResult.Confirmed = true + // if the canonical chain contains the next block after the target block number, + // and the new confirmations queue is empty, + // we set the last validated block to the next block, so the downstream function can use it validate blocks before it + if len(newConfirmationsWithoutTxBlock) == 0 && currentBlockInfo.BlockNumber.Uint64() == targetBlockNumber+1 { + lastValidatedBlock = currentBlockInfo + } break } @@ -131,7 +139,7 @@ func (bl *blockListener) buildConfirmationQueueUsingCanonicalChain(ctx context.C }) currentBlock = currentBlock.Next() } - return newConfirmationsWithoutTxBlock, existingConfirmations, false + return newConfirmationsWithoutTxBlock, lastValidatedBlock, false } // initializeConfirmationMap initializes the confirmation map with the transaction block @@ -162,7 +170,7 @@ func (bl *blockListener) initializeConfirmationMap(reconcileResult *ffcapi.Confi // checkAndFillInGap validates existing confirmations, detects forks, and fills gaps // in the confirmation queue using existing confirmations or fetching missing blocks from the blockchain. // It ensures the confirmation chain is valid and connected to the transaction block. -func (bl *blockListener) checkAndFillInGap(ctx context.Context, newConfirmationsWithoutTxBlock []*ffcapi.MinimalBlockInfo, existingConfirmations []*ffcapi.MinimalBlockInfo, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) ([]*ffcapi.MinimalBlockInfo, bool, error) { +func (bl *blockListener) checkAndFillInGap(ctx context.Context, newConfirmationsWithoutTxBlock []*ffcapi.MinimalBlockInfo, existingConfirmations []*ffcapi.MinimalBlockInfo, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64, lastValidatedBlock *ffcapi.MinimalBlockInfo) ([]*ffcapi.MinimalBlockInfo, bool, error) { var hasNewFork bool // Detect forks by comparing new confirmations with existing ones @@ -180,7 +188,6 @@ func (bl *blockListener) checkAndFillInGap(ctx context.Context, newConfirmations // Determine the range of blocks to validate and fill gaps blockNumberToReach := txBlockInfo.BlockNumber.Uint64() + targetConfirmationCount - var lastValidatedBlock *ffcapi.MinimalBlockInfo if len(newConfirmationsWithoutTxBlock) > 0 { // Start from the block before the first new confirmation blockNumberToReach = newConfirmationsWithoutTxBlock[0].BlockNumber.Uint64() - 1 diff --git a/internal/ethereum/confirmation_reconciler_test.go b/internal/ethereum/confirmation_reconciler_test.go index 952550b..27b7087 100644 --- a/internal/ethereum/confirmation_reconciler_test.go +++ b/internal/ethereum/confirmation_reconciler_test.go @@ -866,7 +866,7 @@ func TestCompareAndUpdateConfirmationQueue_ExistingTxBockInfoIsWrong(t *testing. func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable(t *testing.T) { // Setup - bl, done := newBlockListenerWithTestChain(t, 100, 5, 103, 150, []uint64{102}) + bl, done := newBlockListenerWithTestChain(t, 100, 5, 103, 150, []uint64{}) defer done() ctx := context.Background() // Create confirmations that already meet the target @@ -992,7 +992,7 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableButAllExistingConfi // and it connects to the canonical chain to validate they are still valid existingQueue := []*ffcapi.MinimalBlockInfo{ {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, - // gap of 101 is allowed, and is the confirmation required for the transaction with target confirmation count of 1 + // 101 will be fetched from the JSON-RPC endpoint to fill the gap {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, } occ := &ffcapi.ConfirmationMapUpdateResult{ @@ -1165,7 +1165,7 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithGap(t *testi // Create confirmations with a gap (missing block 102) existingQueue := []*ffcapi.MinimalBlockInfo{ {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, - // no block 101, which is the first block of the canonical chain + // no block 101, which is the first block of the canonical chain, so no fetch to JSON-RPC endpoint is needed {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, {BlockHash: generateTestHash(103), BlockNumber: fftypes.FFuint64(103), ParentHash: generateTestHash(102)}, } @@ -1329,7 +1329,7 @@ func TestCheckAndFillInGap_GetBlockInfoError(t *testing.T) { }), false).Return(&rpcbackend.RPCError{Message: "pop"}) // Execute - result, hasNewFork, err := c.blockListener.checkAndFillInGap(ctx, newConfirmationsWithoutTxBlock, existingConfirmations, txBlockInfo, targetConfirmationCount) + result, hasNewFork, err := c.blockListener.checkAndFillInGap(ctx, newConfirmationsWithoutTxBlock, existingConfirmations, txBlockInfo, targetConfirmationCount, nil) // Assertions assert.Error(t, err) @@ -1361,7 +1361,7 @@ func TestCheckAndFillInGap_BlockNotAvailable(t *testing.T) { }) // Execute - result, hasNewFork, err := c.blockListener.checkAndFillInGap(ctx, newConfirmationsWithoutTxBlock, existingConfirmations, txBlockInfo, targetConfirmationCount) + result, hasNewFork, err := c.blockListener.checkAndFillInGap(ctx, newConfirmationsWithoutTxBlock, existingConfirmations, txBlockInfo, targetConfirmationCount, nil) // Assertions assert.Error(t, err) @@ -1405,7 +1405,7 @@ func TestCheckAndFillInGap_InvalidBlockParentRelationship(t *testing.T) { }) // Execute - result, hasNewFork, err := c.blockListener.checkAndFillInGap(ctx, newConfirmationsWithoutTxBlock, existingConfirmations, txBlockInfo, targetConfirmationCount) + result, hasNewFork, err := c.blockListener.checkAndFillInGap(ctx, newConfirmationsWithoutTxBlock, existingConfirmations, txBlockInfo, targetConfirmationCount, nil) // Assertions - should fail because block 102 has wrong parent hash and doesn't connect to block 101 assert.Error(t, err) @@ -1438,7 +1438,7 @@ func TestCheckAndFillInGap_TxBlockNotParentOfFirstConfirmation(t *testing.T) { existingConfirmations := []*ffcapi.MinimalBlockInfo{} // Execute - result, hasNewFork, err := c.blockListener.checkAndFillInGap(ctx, newConfirmationsWithoutTxBlock, existingConfirmations, txBlockInfo, targetConfirmationCount) + result, hasNewFork, err := c.blockListener.checkAndFillInGap(ctx, newConfirmationsWithoutTxBlock, existingConfirmations, txBlockInfo, targetConfirmationCount, nil) // Assertions assert.Error(t, err) From 1791f53814135fc70cad0f0bc64eeb021ef1fad7 Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Fri, 17 Oct 2025 16:58:43 +0100 Subject: [PATCH 19/37] address review comments Signed-off-by: Chengxuan Xing --- go.mod | 2 +- go.sum | 2 + internal/ethereum/confirmation_reconciler.go | 122 +++++++++--------- .../ethereum/confirmation_reconciler_test.go | 78 +++++------ internal/msgs/en_error_messages.go | 2 + mocks/fftmmocks/manager.go | 10 +- 6 files changed, 104 insertions(+), 112 deletions(-) diff --git a/go.mod b/go.mod index 61d6633..b00e211 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/hashicorp/golang-lru v1.0.2 github.com/hyperledger/firefly-common v1.5.6-0.20250630201730-e234335c0381 github.com/hyperledger/firefly-signer v1.1.21 - github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20251003113149-46c9271ca66e + github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20251017153149-d6f944cab6b6 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 d0a77a0..f9e6d6b 100644 --- a/go.sum +++ b/go.sum @@ -106,6 +106,8 @@ github.com/hyperledger/firefly-signer v1.1.21 h1:r7cTOw6e/6AtiXLf84wZy6Z7zppzlc1 github.com/hyperledger/firefly-signer v1.1.21/go.mod h1:axrlSQeKrd124UdHF5L3MkTjb5DeTcbJxJNCZ3JmcWM= github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20251003113149-46c9271ca66e h1:jUumirQuZR8cQCBq9BNmpLM4rKo9ahQPdFzGvf/RYIs= github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20251003113149-46c9271ca66e/go.mod h1:KHGvK/QqD2jpRZ04lQoX/k1T2o644NCkRlr3FbvKqnA= +github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20251017153149-d6f944cab6b6 h1:FGTXGnOoAaFkdA3n3SMYDXmHePrpX425P5YyljnaSyU= +github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20251017153149-d6f944cab6b6/go.mod h1:KHGvK/QqD2jpRZ04lQoX/k1T2o644NCkRlr3FbvKqnA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= diff --git a/internal/ethereum/confirmation_reconciler.go b/internal/ethereum/confirmation_reconciler.go index 2c9f4a3..abe7c3b 100644 --- a/internal/ethereum/confirmation_reconciler.go +++ b/internal/ethereum/confirmation_reconciler.go @@ -15,7 +15,7 @@ // limitations under the License. // The confirmation reconciler manages transaction confirmation queues by: -// - Copying blocks from the canonical chain and the existing confirmation queue +// - Copying blocks from the in-memory partial chain and the existing confirmation queue // - Detecting blockchain forks and rebuilding confirmation queues when necessary // - Filling gaps in confirmation queues by fetching missing blocks // - Determining when transactions have reached the target confirmation count @@ -32,9 +32,9 @@ import ( ) // reconcileConfirmationsForTransaction reconciles the confirmation queue for a transaction -func (bl *blockListener) reconcileConfirmationsForTransaction(ctx context.Context, txHash string, existingConfirmations []*ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) (*ffcapi.ConfirmationMapUpdateResult, error) { +func (bl *blockListener) reconcileConfirmationsForTransaction(ctx context.Context, txHash string, existingConfirmations []*ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) (*ffcapi.ConfirmationUpdateResult, error) { // Initialize the result with existing confirmations - reconcileResult := &ffcapi.ConfirmationMapUpdateResult{ + reconcileResult := &ffcapi.ConfirmationUpdateResult{ Confirmations: existingConfirmations, NewFork: false, Confirmed: false, @@ -50,31 +50,52 @@ func (bl *blockListener) reconcileConfirmationsForTransaction(ctx context.Contex if txBlockInfo == nil { log.L(ctx).Debugf("Transaction %s not found in any block", txHash) - return reconcileResult, nil + return nil, i18n.NewError(ctx, msgs.MsgTransactionNotFound, txHash) } return bl.compareAndUpdateConfirmationQueue(ctx, reconcileResult, txBlockInfo, targetConfirmationCount) } // compareAndUpdateConfirmationQueue orchestrates the confirmation reconciliation process. -// It builds new confirmations from the canonical chain and fills gaps in the confirmation queue. -func (bl *blockListener) compareAndUpdateConfirmationQueue(ctx context.Context, reconcileResult *ffcapi.ConfirmationMapUpdateResult, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) (*ffcapi.ConfirmationMapUpdateResult, error) { +// It builds new confirmations from the in-memory partial chain and fills gaps in the confirmation queue. +func (bl *blockListener) compareAndUpdateConfirmationQueue(ctx context.Context, reconcileResult *ffcapi.ConfirmationUpdateResult, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) (*ffcapi.ConfirmationUpdateResult, error) { var err error - // Build new confirmations from the canonical chain and get existing confirmations - newConfirmationsWithoutTxBlock, lastValidatedBlock, returnResult := bl.buildConfirmationQueueUsingCanonicalChain(ctx, reconcileResult, txBlockInfo, targetConfirmationCount) - if returnResult { - return reconcileResult, nil + // Build new confirmations from the in-memory partial chain and get existing confirmations + newConfirmationsWithoutTxBlock, lastValidatedBlock, err := bl.buildConfirmationQueueUsingInMemoryPartialChain(ctx, txBlockInfo, targetConfirmationCount) + if err != nil { + return nil, err } // Initialize confirmation map and get existing confirmations - // the init must happen after the canonical chain check to avoid - // confirming blocks that are not yet validated in the canonical chain - existingConfirmations := bl.initializeConfirmationMap(reconcileResult, txBlockInfo) + // the init must happen after the in-memory partial chain check to avoid + // confirming blocks that are not yet validated in the in-memory partial chain + var existingConfirmations []*ffcapi.MinimalBlockInfo + // If no existing confirmations, initialize with the transaction block + if len(reconcileResult.Confirmations) == 0 { + reconcileResult.Confirmations = []*ffcapi.MinimalBlockInfo{txBlockInfo} + existingConfirmations = nil + } else { + // Validate existing confirmations against the current transaction block + existingQueue := reconcileResult.Confirmations + if len(existingQueue) > 0 { + existingTxBlock := existingQueue[0] + if !existingTxBlock.Equal(txBlockInfo) { + // Transaction block mismatch indicates a fork - rebuild confirmation queue + reconcileResult.NewFork = true + reconcileResult.Confirmations = []*ffcapi.MinimalBlockInfo{txBlockInfo} + existingConfirmations = nil + } else { + existingConfirmations = existingQueue + } + } else { + existingConfirmations = existingQueue + } + } // Special case: if targetConfirmationCount is 0, transaction is immediately confirmed if targetConfirmationCount == 0 { - reconcileResult.Confirmed = true reconcileResult.Confirmations = []*ffcapi.MinimalBlockInfo{txBlockInfo} + reconcileResult.Confirmed = true return reconcileResult, nil } @@ -83,88 +104,63 @@ func (bl *blockListener) compareAndUpdateConfirmationQueue(ctx context.Context, var newFork bool confirmations, newFork, err = bl.checkAndFillInGap(ctx, newConfirmationsWithoutTxBlock, existingConfirmations, txBlockInfo, targetConfirmationCount, lastValidatedBlock) if err != nil { - return reconcileResult, err + return nil, err } reconcileResult.NewFork = newFork reconcileResult.Confirmations = confirmations + reconcileResult.Confirmed = uint64(len(confirmations)) >= targetConfirmationCount+1 return reconcileResult, err } -// buildConfirmationQueueUsingCanonicalChain builds the confirmation queue using the in-memory canonical chain. -// It does not modify the canonical chain itself, only reads from it. -// This function holds a read lock on the canonical chain, so it should not make long-running queries. -func (bl *blockListener) buildConfirmationQueueUsingCanonicalChain(ctx context.Context, reconcileResult *ffcapi.ConfirmationMapUpdateResult, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) (newConfirmationsWithoutTxBlock []*ffcapi.MinimalBlockInfo, lastValidatedBlock *ffcapi.MinimalBlockInfo, returnResult bool) { +// buildConfirmationQueueUsingInMemoryPartialChain builds the confirmation queue 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, lastValidatedBlock *ffcapi.MinimalBlockInfo, err error) { bl.mux.RLock() defer bl.mux.RUnlock() txBlockNumber := txBlockInfo.BlockNumber.Uint64() targetBlockNumber := txBlockInfo.BlockNumber.Uint64() + targetConfirmationCount - // Check if the canonical chain has caught up to the transaction block + // Check if the in-memory partial chain has caught up to the transaction block chainTail := bl.canonicalChain.Back().Value.(*ffcapi.MinimalBlockInfo) if chainTail == nil || chainTail.BlockNumber.Uint64() < txBlockNumber { - log.L(ctx).Debugf("Canonical chain is waiting for the transaction block %d to be indexed", txBlockNumber) - return nil, nil, true + log.L(ctx).Debugf("in-memory partial chain is waiting for the transaction block %d to be indexed", txBlockNumber) + return nil, nil, i18n.NewError(ctx, msgs.MsgInMemoryPartialChainNotCaughtUp, txBlockNumber, txBlockInfo.BlockHash) } // Build new confirmations from blocks after the transaction block newConfirmationsWithoutTxBlock = []*ffcapi.MinimalBlockInfo{} - currentBlock := bl.canonicalChain.Front() - for currentBlock != nil { - currentBlockInfo := currentBlock.Value.(*ffcapi.MinimalBlockInfo) + nextInMemoryBlock := bl.canonicalChain.Front() + for nextInMemoryBlock != nil { + nextInMemoryBlockInfo := nextInMemoryBlock.Value.(*ffcapi.MinimalBlockInfo) // If we've reached the target confirmation count, mark as confirmed - if currentBlockInfo.BlockNumber.Uint64() > targetBlockNumber { - reconcileResult.Confirmed = true - // if the canonical chain contains the next block after the target block number, + if nextInMemoryBlockInfo.BlockNumber.Uint64() > targetBlockNumber { + // if the in-memory partial chain contains the next block after the target block number, // and the new confirmations queue is empty, // we set the last validated block to the next block, so the downstream function can use it validate blocks before it - if len(newConfirmationsWithoutTxBlock) == 0 && currentBlockInfo.BlockNumber.Uint64() == targetBlockNumber+1 { - lastValidatedBlock = currentBlockInfo + if len(newConfirmationsWithoutTxBlock) == 0 && nextInMemoryBlockInfo.BlockNumber.Uint64() == targetBlockNumber+1 { + lastValidatedBlock = nextInMemoryBlockInfo } break } // Skip blocks at or before the transaction block - if currentBlockInfo.BlockNumber.Uint64() <= txBlockNumber { - currentBlock = currentBlock.Next() + if nextInMemoryBlockInfo.BlockNumber.Uint64() <= txBlockNumber { + nextInMemoryBlock = nextInMemoryBlock.Next() continue } // Add blocks after the transaction block to confirmations newConfirmationsWithoutTxBlock = append(newConfirmationsWithoutTxBlock, &ffcapi.MinimalBlockInfo{ - BlockHash: currentBlockInfo.BlockHash, - BlockNumber: fftypes.FFuint64(currentBlockInfo.BlockNumber.Uint64()), - ParentHash: currentBlockInfo.ParentHash, + BlockHash: nextInMemoryBlockInfo.BlockHash, + BlockNumber: fftypes.FFuint64(nextInMemoryBlockInfo.BlockNumber.Uint64()), + ParentHash: nextInMemoryBlockInfo.ParentHash, }) - currentBlock = currentBlock.Next() + nextInMemoryBlock = nextInMemoryBlock.Next() } - return newConfirmationsWithoutTxBlock, lastValidatedBlock, false -} - -// initializeConfirmationMap initializes the confirmation map with the transaction block -// and validates existing confirmations against the current transaction block. -// Returns existing confirmations if valid, or nil if a fork is detected. -func (bl *blockListener) initializeConfirmationMap(reconcileResult *ffcapi.ConfirmationMapUpdateResult, txBlockInfo *ffcapi.MinimalBlockInfo) []*ffcapi.MinimalBlockInfo { - // If no existing confirmations, initialize with the transaction block - if len(reconcileResult.Confirmations) == 0 { - reconcileResult.Confirmations = []*ffcapi.MinimalBlockInfo{txBlockInfo} - return nil - } - - // Validate existing confirmations against the current transaction block - existingQueue := reconcileResult.Confirmations - if len(existingQueue) > 0 { - existingTxBlock := existingQueue[0] - if !existingTxBlock.Equal(txBlockInfo) { - // Transaction block mismatch indicates a fork - rebuild confirmation queue - reconcileResult.NewFork = true - reconcileResult.Confirmations = []*ffcapi.MinimalBlockInfo{txBlockInfo} - return nil - } - } - - return existingQueue + return newConfirmationsWithoutTxBlock, lastValidatedBlock, nil } // checkAndFillInGap validates existing confirmations, detects forks, and fills gaps @@ -254,6 +250,6 @@ func (bl *blockListener) checkAndFillInGap(ctx context.Context, newConfirmations // 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.ConfirmationMapUpdateResult, error) { +func (c *ethConnector) ReconcileConfirmationsForTransaction(ctx context.Context, txHash string, existingConfirmations []*ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) (*ffcapi.ConfirmationUpdateResult, error) { return c.blockListener.reconcileConfirmationsForTransaction(ctx, txHash, existingConfirmations, targetConfirmationCount) } diff --git a/internal/ethereum/confirmation_reconciler_test.go b/internal/ethereum/confirmation_reconciler_test.go index 27b7087..2c0c549 100644 --- a/internal/ethereum/confirmation_reconciler_test.go +++ b/internal/ethereum/confirmation_reconciler_test.go @@ -50,12 +50,9 @@ func TestReconcileConfirmationsForTransaction_TransactionNotFound(t *testing.T) result, err := c.ReconcileConfirmationsForTransaction(context.Background(), generateTestHash(100), nil, 5) // Assertions - expect an error when transaction doesn't exist - assert.NoError(t, err) - assert.NotNil(t, result) - assert.False(t, result.NewFork) - assert.False(t, result.Confirmed) - assert.Nil(t, result.Confirmations) - assert.Equal(t, uint64(5), result.TargetConfirmationCount) + assert.Error(t, err) + assert.Regexp(t, "FF23061", err) + assert.Nil(t, result) mRPC.AssertExpectations(t) } @@ -107,14 +104,9 @@ func TestReconcileConfirmationsForTransaction_BlockNotFound(t *testing.T) { }, 5) // Assertions - expect an error when transaction doesn't exist - 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)}, - }, result.Confirmations) - assert.Equal(t, uint64(5), result.TargetConfirmationCount) + assert.Error(t, err) + assert.Regexp(t, "FF23061", err) + assert.Nil(t, result) mRPC.AssertExpectations(t) } @@ -244,7 +236,7 @@ func TestCompareAndUpdateConfirmationQueue_EmptyChain(t *testing.T) { bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 50, []uint64{}) defer done() ctx := context.Background() - occ := &ffcapi.ConfirmationMapUpdateResult{ + occ := &ffcapi.ConfirmationUpdateResult{ Confirmations: []*ffcapi.MinimalBlockInfo{}, } txBlockNumber := uint64(100) @@ -273,7 +265,7 @@ func TestCompareAndUpdateConfirmationQueue_ChainTooShort(t *testing.T) { bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 99, []uint64{}) defer done() ctx := context.Background() - occ := &ffcapi.ConfirmationMapUpdateResult{ + occ := &ffcapi.ConfirmationUpdateResult{ Confirmations: []*ffcapi.MinimalBlockInfo{}, } txBlockNumber := uint64(100) @@ -301,7 +293,7 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMap(t *testing.T) { bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 150, []uint64{}) defer done() ctx := context.Background() - occ := &ffcapi.ConfirmationMapUpdateResult{ + occ := &ffcapi.ConfirmationUpdateResult{ Confirmations: nil, } txBlockNumber := uint64(100) @@ -336,7 +328,7 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMap_ZeroConfirmationCo bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 150, []uint64{}) defer done() ctx := context.Background() - occ := &ffcapi.ConfirmationMapUpdateResult{ + occ := &ffcapi.ConfirmationUpdateResult{ Confirmations: nil, } txBlockNumber := uint64(100) @@ -365,7 +357,7 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMapUnconfirmed(t *test bl, done := newBlockListenerWithTestChain(t, 100, 5, 100, 104, []uint64{}) defer done() ctx := context.Background() - occ := &ffcapi.ConfirmationMapUpdateResult{ + occ := &ffcapi.ConfirmationUpdateResult{ Confirmations: nil, } txBlockNumber := uint64(100) @@ -399,7 +391,7 @@ func TestCompareAndUpdateConfirmationQueue_EmptyConfirmationQueue(t *testing.T) bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 150, []uint64{}) defer done() ctx := context.Background() - occ := &ffcapi.ConfirmationMapUpdateResult{ + occ := &ffcapi.ConfirmationUpdateResult{ Confirmations: []*ffcapi.MinimalBlockInfo{}, } txBlockNumber := uint64(100) @@ -438,7 +430,7 @@ func TestCompareAndUpdateConfirmationQueue_MismatchConfirmationBlock(t *testing. {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, {BlockHash: generateTestHash(103), BlockNumber: fftypes.FFuint64(103), ParentHash: generateTestHash(102)}, } - occ := &ffcapi.ConfirmationMapUpdateResult{ + occ := &ffcapi.ConfirmationUpdateResult{ Confirmations: existingQueue, } txBlockNumber := uint64(100) @@ -475,7 +467,7 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsTooDistant(t *te {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, } - occ := &ffcapi.ConfirmationMapUpdateResult{ + occ := &ffcapi.ConfirmationUpdateResult{ Confirmations: existingQueue, } txBlockNumber := uint64(100) @@ -513,7 +505,7 @@ func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmationDoNotAff {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: "0xwrongparent"}, } - occ := &ffcapi.ConfirmationMapUpdateResult{ + occ := &ffcapi.ConfirmationUpdateResult{ Confirmations: existingQueue, } txBlockNumber := uint64(100) @@ -551,7 +543,7 @@ func TestCompareAndUpdateConfirmationQueue_ConnectionNodeMismatch(t *testing.T) {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, {BlockHash: generateTestHash(103), BlockNumber: fftypes.FFuint64(103), ParentHash: generateTestHash(102)}, } - occ := &ffcapi.ConfirmationMapUpdateResult{ + occ := &ffcapi.ConfirmationUpdateResult{ Confirmations: existingQueue, } txBlockNumber := uint64(100) @@ -589,7 +581,7 @@ func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmationAfterFir {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblockwrong"}, } - occ := &ffcapi.ConfirmationMapUpdateResult{ + occ := &ffcapi.ConfirmationUpdateResult{ Confirmations: existingQueue, } txBlockNumber := uint64(100) @@ -635,7 +627,7 @@ func TestCompareAndUpdateConfirmationQueue_FailedToFetchBlockInfo(t *testing.T) {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblockwrong"}, } - occ := &ffcapi.ConfirmationMapUpdateResult{ + occ := &ffcapi.ConfirmationUpdateResult{ Confirmations: existingQueue, } txBlockNumber := uint64(100) @@ -676,7 +668,7 @@ func TestCompareAndUpdateConfirmationQueue_NilBlockInfo(t *testing.T) { {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblockwrong"}, } - occ := &ffcapi.ConfirmationMapUpdateResult{ + occ := &ffcapi.ConfirmationUpdateResult{ Confirmations: existingQueue, } txBlockNumber := uint64(100) @@ -705,7 +697,7 @@ func TestCompareAndUpdateConfirmationQueue_NewForkAfterFirstConfirmation(t *test {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, {BlockHash: "fork1", BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, } - occ := &ffcapi.ConfirmationMapUpdateResult{ + occ := &ffcapi.ConfirmationUpdateResult{ Confirmations: existingQueue, } txBlockNumber := uint64(100) @@ -736,7 +728,7 @@ func TestCompareAndUpdateConfirmationQueue_NewForkAfterFirstConfirmation_ZeroCon {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, {BlockHash: "fork1", BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, } - occ := &ffcapi.ConfirmationMapUpdateResult{ + occ := &ffcapi.ConfirmationUpdateResult{ Confirmations: existingQueue, } txBlockNumber := uint64(100) @@ -768,7 +760,7 @@ func TestCompareAndUpdateConfirmationQueue_NewForkAndNoConnectionToCanonicalChai {BlockHash: "fork2", BlockNumber: fftypes.FFuint64(102), ParentHash: "fork1"}, {BlockHash: "fork3", BlockNumber: fftypes.FFuint64(103), ParentHash: "fork2"}, } - occ := &ffcapi.ConfirmationMapUpdateResult{ + occ := &ffcapi.ConfirmationUpdateResult{ Confirmations: existingQueue, } txBlockNumber := uint64(100) @@ -799,7 +791,7 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationLaterThanCurrentB {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, } - occ := &ffcapi.ConfirmationMapUpdateResult{ + occ := &ffcapi.ConfirmationUpdateResult{ Confirmations: existingQueue, } txBlockNumber := uint64(100) @@ -836,7 +828,7 @@ func TestCompareAndUpdateConfirmationQueue_ExistingTxBockInfoIsWrong(t *testing. {BlockHash: generateTestHash(999), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, // should be corrected {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, } - occ := &ffcapi.ConfirmationMapUpdateResult{ + occ := &ffcapi.ConfirmationUpdateResult{ Confirmations: existingQueue, } txBlockNumber := uint64(100) @@ -881,7 +873,7 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable(t *testing.T) { {BlockHash: "0xblock104", BlockNumber: fftypes.FFuint64(104), ParentHash: generateTestHash(103)}, // discarded {BlockHash: "0xblock105", BlockNumber: fftypes.FFuint64(105), ParentHash: "0xblock104"}, // discarded } - occ := &ffcapi.ConfirmationMapUpdateResult{ + occ := &ffcapi.ConfirmationUpdateResult{ Confirmations: existingQueue, } txBlockNumber := uint64(100) @@ -922,7 +914,7 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable_ZeroConfirmationCo {BlockHash: "0xblock104", BlockNumber: fftypes.FFuint64(104), ParentHash: generateTestHash(103)}, // discarded {BlockHash: "0xblock105", BlockNumber: fftypes.FFuint64(105), ParentHash: "0xblock104"}, // discarded } - occ := &ffcapi.ConfirmationMapUpdateResult{ + occ := &ffcapi.ConfirmationUpdateResult{ Confirmations: existingQueue, } txBlockNumber := uint64(100) @@ -958,7 +950,7 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableConnectable(t *test // didn't have block 103, which is the first block of the canonical chain // but we should still be able to validate the existing confirmations are valid using parent hash } - occ := &ffcapi.ConfirmationMapUpdateResult{ + occ := &ffcapi.ConfirmationUpdateResult{ Confirmations: existingQueue, } txBlockNumber := uint64(100) @@ -995,7 +987,7 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableButAllExistingConfi // 101 will be fetched from the JSON-RPC endpoint to fill the gap {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, } - occ := &ffcapi.ConfirmationMapUpdateResult{ + occ := &ffcapi.ConfirmationUpdateResult{ Confirmations: existingQueue, } txBlockNumber := uint64(100) @@ -1033,7 +1025,7 @@ func TestCompareAndUpdateConfirmationQueue_HasSufficientConfirmationsButNoOverla {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, } - occ := &ffcapi.ConfirmationMapUpdateResult{ + occ := &ffcapi.ConfirmationUpdateResult{ Confirmations: existingQueue, } txBlockNumber := uint64(100) @@ -1069,7 +1061,7 @@ func TestCompareAndUpdateConfirmationQueue_ValidExistingConfirmations(t *testing {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, } - occ := &ffcapi.ConfirmationMapUpdateResult{ + occ := &ffcapi.ConfirmationUpdateResult{ Confirmations: existingQueue, } txBlockNumber := uint64(100) @@ -1104,7 +1096,7 @@ func TestCompareAndUpdateConfirmationQueue_ValidExistingTxBlock(t *testing.T) { existingQueue := []*ffcapi.MinimalBlockInfo{ {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, } - occ := &ffcapi.ConfirmationMapUpdateResult{ + occ := &ffcapi.ConfirmationUpdateResult{ Confirmations: existingQueue, } txBlockNumber := uint64(100) @@ -1136,7 +1128,7 @@ func TestCompareAndUpdateConfirmationQueue_ReachTargetConfirmation(t *testing.T) bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 150, []uint64{}) defer done() ctx := context.Background() - occ := &ffcapi.ConfirmationMapUpdateResult{ + occ := &ffcapi.ConfirmationUpdateResult{ Confirmations: []*ffcapi.MinimalBlockInfo{}, } txBlockNumber := uint64(100) @@ -1169,7 +1161,7 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithGap(t *testi {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, {BlockHash: generateTestHash(103), BlockNumber: fftypes.FFuint64(103), ParentHash: generateTestHash(102)}, } - occ := &ffcapi.ConfirmationMapUpdateResult{ + occ := &ffcapi.ConfirmationUpdateResult{ Confirmations: existingQueue, } txBlockNumber := uint64(100) @@ -1200,7 +1192,7 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNu {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(99), ParentHash: generateTestHash(100)}, // somehow there is a lower block number } - occ := &ffcapi.ConfirmationMapUpdateResult{ + occ := &ffcapi.ConfirmationUpdateResult{ Confirmations: existingQueue, } txBlockNumber := uint64(100) @@ -1232,7 +1224,7 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNu {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(99), ParentHash: generateTestHash(101)}, // somehow there is a lower block number } - occ := &ffcapi.ConfirmationMapUpdateResult{ + occ := &ffcapi.ConfirmationUpdateResult{ Confirmations: existingQueue, } txBlockNumber := uint64(100) diff --git a/internal/msgs/en_error_messages.go b/internal/msgs/en_error_messages.go index 7e2c098..9bf26c4 100644 --- a/internal/msgs/en_error_messages.go +++ b/internal/msgs/en_error_messages.go @@ -76,4 +76,6 @@ var ( 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 queue") + 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") ) diff --git a/mocks/fftmmocks/manager.go b/mocks/fftmmocks/manager.go index be458d8..40e8c7a 100644 --- a/mocks/fftmmocks/manager.go +++ b/mocks/fftmmocks/manager.go @@ -134,23 +134,23 @@ func (_m *Manager) GetTransactionByIDWithStatus(ctx context.Context, txID string } // 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.ConfirmationMapUpdateResult, error) { +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.ConfirmationMapUpdateResult + var r0 *ffcapi.ConfirmationUpdateResult var r1 error - if rf, ok := ret.Get(0).(func(context.Context, string, []*ffcapi.MinimalBlockInfo, uint64) (*ffcapi.ConfirmationMapUpdateResult, error)); ok { + 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.ConfirmationMapUpdateResult); ok { + 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.ConfirmationMapUpdateResult) + r0 = ret.Get(0).(*ffcapi.ConfirmationUpdateResult) } } From 5b82747d05fa653362ff60826c2d4f0314d4d05d Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Fri, 17 Oct 2025 18:09:29 +0100 Subject: [PATCH 20/37] change order and update test Signed-off-by: Chengxuan Xing --- internal/ethereum/confirmation_reconciler.go | 13 +- .../ethereum/confirmation_reconciler_test.go | 457 +++++++++--------- 2 files changed, 231 insertions(+), 239 deletions(-) diff --git a/internal/ethereum/confirmation_reconciler.go b/internal/ethereum/confirmation_reconciler.go index abe7c3b..129c7a8 100644 --- a/internal/ethereum/confirmation_reconciler.go +++ b/internal/ethereum/confirmation_reconciler.go @@ -59,12 +59,6 @@ func (bl *blockListener) reconcileConfirmationsForTransaction(ctx context.Contex // compareAndUpdateConfirmationQueue orchestrates the confirmation reconciliation process. // It builds new confirmations from the in-memory partial chain and fills gaps in the confirmation queue. func (bl *blockListener) compareAndUpdateConfirmationQueue(ctx context.Context, reconcileResult *ffcapi.ConfirmationUpdateResult, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) (*ffcapi.ConfirmationUpdateResult, error) { - var err error - // Build new confirmations from the in-memory partial chain and get existing confirmations - newConfirmationsWithoutTxBlock, lastValidatedBlock, err := bl.buildConfirmationQueueUsingInMemoryPartialChain(ctx, txBlockInfo, targetConfirmationCount) - if err != nil { - return nil, err - } // Initialize confirmation map and get existing confirmations // the init must happen after the in-memory partial chain check to avoid @@ -92,6 +86,13 @@ func (bl *blockListener) compareAndUpdateConfirmationQueue(ctx context.Context, } } + var err error + // Build new confirmations from the in-memory partial chain and get existing confirmations + newConfirmationsWithoutTxBlock, lastValidatedBlock, err := bl.buildConfirmationQueueUsingInMemoryPartialChain(ctx, txBlockInfo, targetConfirmationCount) + if err != nil { + return nil, err + } + // Special case: if targetConfirmationCount is 0, transaction is immediately confirmed if targetConfirmationCount == 0 { reconcileResult.Confirmations = []*ffcapi.MinimalBlockInfo{txBlockInfo} diff --git a/internal/ethereum/confirmation_reconciler_test.go b/internal/ethereum/confirmation_reconciler_test.go index 2c0c549..f585ac2 100644 --- a/internal/ethereum/confirmation_reconciler_test.go +++ b/internal/ethereum/confirmation_reconciler_test.go @@ -250,14 +250,11 @@ func TestCompareAndUpdateConfirmationQueue_EmptyChain(t *testing.T) { targetConfirmationCount := uint64(5) // Execute - bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) - // Assert - should return early due to chain being too short - assert.NotNil(t, occ.Confirmations) - assert.Len(t, occ.Confirmations, 0) - assert.NotNil(t, occ.Confirmations) - assert.False(t, occ.NewFork) - assert.False(t, occ.Confirmed) + confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + assert.Error(t, err) + assert.Regexp(t, "FF23062", err.Error()) + assert.Nil(t, confirmationUpdateResult) } func TestCompareAndUpdateConfirmationQueue_ChainTooShort(t *testing.T) { @@ -279,13 +276,9 @@ func TestCompareAndUpdateConfirmationQueue_ChainTooShort(t *testing.T) { targetConfirmationCount := uint64(5) // Execute - bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) - - // Assert - should return early due to chain being too short - assert.Len(t, occ.Confirmations, 0) - assert.NotNil(t, occ.Confirmations) - assert.False(t, occ.NewFork) - assert.False(t, occ.Confirmed) + confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + assert.Error(t, err) + assert.Nil(t, confirmationUpdateResult) } func TestCompareAndUpdateConfirmationQueue_NilConfirmationMap(t *testing.T) { @@ -306,20 +299,19 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMap(t *testing.T) { targetConfirmationCount := uint64(5) // Execute - bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) - + confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) // Assert - assert.NotNil(t, occ.Confirmations) - assert.False(t, occ.NewFork) - assert.True(t, occ.Confirmed) - // The code builds a full confirmation queue from the canonical chain - assert.Len(t, occ.Confirmations, 6) - assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) - assert.Equal(t, txBlockNumber+1, uint64(occ.Confirmations[1].BlockNumber)) - assert.Equal(t, txBlockNumber+2, uint64(occ.Confirmations[2].BlockNumber)) - assert.Equal(t, txBlockNumber+3, uint64(occ.Confirmations[3].BlockNumber)) - assert.Equal(t, txBlockNumber+4, uint64(occ.Confirmations[4].BlockNumber)) - assert.Equal(t, txBlockNumber+5, uint64(occ.Confirmations[5].BlockNumber)) + 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)) } @@ -341,15 +333,15 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMap_ZeroConfirmationCo targetConfirmationCount := uint64(0) // Execute - bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) - + confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) // Assert - assert.NotNil(t, occ.Confirmations) - assert.False(t, occ.NewFork) - assert.True(t, occ.Confirmed) + 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, occ.Confirmations, 1) - assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) + assert.Len(t, confirmationUpdateResult.Confirmations, 1) + assert.Equal(t, txBlockNumber, uint64(confirmationUpdateResult.Confirmations[0].BlockNumber)) } func TestCompareAndUpdateConfirmationQueue_NilConfirmationMapUnconfirmed(t *testing.T) { @@ -370,19 +362,19 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMapUnconfirmed(t *test targetConfirmationCount := uint64(5) // Execute - bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) - + confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) // Assert - assert.NotNil(t, occ.Confirmations) - assert.False(t, occ.NewFork) - assert.False(t, occ.Confirmed) + 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, occ.Confirmations, 5) // 100, 101, 102, 103, 104 - assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) - assert.Equal(t, txBlockNumber+1, uint64(occ.Confirmations[1].BlockNumber)) - assert.Equal(t, txBlockNumber+2, uint64(occ.Confirmations[2].BlockNumber)) - assert.Equal(t, txBlockNumber+3, uint64(occ.Confirmations[3].BlockNumber)) - assert.Equal(t, txBlockNumber+4, uint64(occ.Confirmations[4].BlockNumber)) + 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)) } @@ -404,19 +396,19 @@ func TestCompareAndUpdateConfirmationQueue_EmptyConfirmationQueue(t *testing.T) targetConfirmationCount := uint64(5) // Execute - bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) - + confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) // Assert - assert.False(t, occ.NewFork) - assert.True(t, occ.Confirmed) + assert.False(t, confirmationUpdateResult.NewFork) + assert.True(t, confirmationUpdateResult.Confirmed) // The code builds a full confirmation queue from the canonical chain - assert.Len(t, occ.Confirmations, 6) - assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) - assert.Equal(t, txBlockNumber+1, uint64(occ.Confirmations[1].BlockNumber)) - assert.Equal(t, txBlockNumber+2, uint64(occ.Confirmations[2].BlockNumber)) - assert.Equal(t, txBlockNumber+3, uint64(occ.Confirmations[3].BlockNumber)) - assert.Equal(t, txBlockNumber+4, uint64(occ.Confirmations[4].BlockNumber)) - assert.Equal(t, txBlockNumber+5, uint64(occ.Confirmations[5].BlockNumber)) + 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 TestCompareAndUpdateConfirmationQueue_MismatchConfirmationBlock(t *testing.T) { @@ -443,18 +435,18 @@ func TestCompareAndUpdateConfirmationQueue_MismatchConfirmationBlock(t *testing. targetConfirmationCount := uint64(5) // Execute - bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) - + confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) // Assert - assert.True(t, occ.NewFork) + assert.True(t, confirmationUpdateResult.NewFork) // The code builds a full confirmation queue from the canonical chain - assert.Len(t, occ.Confirmations, 6) - assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) - assert.Equal(t, txBlockNumber+1, uint64(occ.Confirmations[1].BlockNumber)) - assert.Equal(t, txBlockNumber+2, uint64(occ.Confirmations[2].BlockNumber)) - assert.Equal(t, txBlockNumber+3, uint64(occ.Confirmations[3].BlockNumber)) - assert.Equal(t, txBlockNumber+4, uint64(occ.Confirmations[4].BlockNumber)) - assert.Equal(t, txBlockNumber+5, uint64(occ.Confirmations[5].BlockNumber)) + 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 TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsTooDistant(t *testing.T) { @@ -480,18 +472,18 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsTooDistant(t *te targetConfirmationCount := uint64(5) // Execute - bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) - + confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) // Assert all confirmations are in the confirmation queue - assert.False(t, occ.NewFork) - assert.True(t, occ.Confirmed) - assert.Len(t, occ.Confirmations, 6) - assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) - assert.Equal(t, txBlockNumber+1, uint64(occ.Confirmations[1].BlockNumber)) - assert.Equal(t, txBlockNumber+2, uint64(occ.Confirmations[2].BlockNumber)) - assert.Equal(t, txBlockNumber+3, uint64(occ.Confirmations[3].BlockNumber)) - assert.Equal(t, txBlockNumber+4, uint64(occ.Confirmations[4].BlockNumber)) - assert.Equal(t, txBlockNumber+5, uint64(occ.Confirmations[5].BlockNumber)) + 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 TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmationDoNotAffectConfirmations(t *testing.T) { @@ -518,18 +510,18 @@ func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmationDoNotAff targetConfirmationCount := uint64(5) // Execute - bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) - + confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) // Assert - assert.True(t, occ.NewFork) - assert.True(t, occ.Confirmed) - assert.Len(t, occ.Confirmations, 6) - assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) - assert.Equal(t, txBlockNumber+1, uint64(occ.Confirmations[1].BlockNumber)) - assert.Equal(t, txBlockNumber+2, uint64(occ.Confirmations[2].BlockNumber)) - assert.Equal(t, txBlockNumber+3, uint64(occ.Confirmations[3].BlockNumber)) - assert.Equal(t, txBlockNumber+4, uint64(occ.Confirmations[4].BlockNumber)) - assert.Equal(t, txBlockNumber+5, uint64(occ.Confirmations[5].BlockNumber)) + 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 TestCompareAndUpdateConfirmationQueue_ConnectionNodeMismatch(t *testing.T) { @@ -556,18 +548,18 @@ func TestCompareAndUpdateConfirmationQueue_ConnectionNodeMismatch(t *testing.T) targetConfirmationCount := uint64(5) // Execute - bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) - + confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) // Assert - assert.True(t, occ.NewFork) - assert.True(t, occ.Confirmed) - assert.Len(t, occ.Confirmations, 6) - assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) - assert.Equal(t, txBlockNumber+1, uint64(occ.Confirmations[1].BlockNumber)) - assert.Equal(t, txBlockNumber+2, uint64(occ.Confirmations[2].BlockNumber)) - assert.Equal(t, txBlockNumber+3, uint64(occ.Confirmations[3].BlockNumber)) - assert.Equal(t, txBlockNumber+4, uint64(occ.Confirmations[4].BlockNumber)) - assert.Equal(t, txBlockNumber+5, uint64(occ.Confirmations[5].BlockNumber)) + 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 TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmationAfterFirstConfirmation(t *testing.T) { @@ -594,18 +586,18 @@ func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmationAfterFir targetConfirmationCount := uint64(5) // Execute - bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) - + confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) // Assert - assert.True(t, occ.NewFork) - assert.True(t, occ.Confirmed) - assert.Len(t, occ.Confirmations, 6) - assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) - assert.Equal(t, txBlockNumber+1, uint64(occ.Confirmations[1].BlockNumber)) - assert.Equal(t, txBlockNumber+2, uint64(occ.Confirmations[2].BlockNumber)) - assert.Equal(t, txBlockNumber+3, uint64(occ.Confirmations[3].BlockNumber)) - assert.Equal(t, txBlockNumber+4, uint64(occ.Confirmations[4].BlockNumber)) - assert.Equal(t, txBlockNumber+5, uint64(occ.Confirmations[5].BlockNumber)) + 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 TestCompareAndUpdateConfirmationQueue_FailedToFetchBlockInfo(t *testing.T) { @@ -640,11 +632,10 @@ func TestCompareAndUpdateConfirmationQueue_FailedToFetchBlockInfo(t *testing.T) targetConfirmationCount := uint64(5) // Execute - bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) - - // Assert - assert.Len(t, occ.Confirmations, 3) // still use the old confirmation queue - + confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + assert.Error(t, err) + assert.Regexp(t, "pop", err.Error()) + assert.Nil(t, confirmationUpdateResult) } func TestCompareAndUpdateConfirmationQueue_NilBlockInfo(t *testing.T) { @@ -681,10 +672,10 @@ func TestCompareAndUpdateConfirmationQueue_NilBlockInfo(t *testing.T) { targetConfirmationCount := uint64(5) // Execute - bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) - - // Assert - assert.Len(t, occ.Confirmations, 3) // still use the old confirmation queue + confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + assert.Error(t, err) + assert.Regexp(t, "FF23011", err.Error()) + assert.Nil(t, confirmationUpdateResult) } func TestCompareAndUpdateConfirmationQueue_NewForkAfterFirstConfirmation(t *testing.T) { // Setup @@ -710,12 +701,12 @@ func TestCompareAndUpdateConfirmationQueue_NewForkAfterFirstConfirmation(t *test targetConfirmationCount := uint64(5) // Execute - bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) - + confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) // Assert - assert.True(t, occ.NewFork) - assert.True(t, occ.Confirmed) - assert.Len(t, occ.Confirmations, 6) + assert.True(t, confirmationUpdateResult.NewFork) + assert.True(t, confirmationUpdateResult.Confirmed) + assert.Len(t, confirmationUpdateResult.Confirmations, 6) } func TestCompareAndUpdateConfirmationQueue_NewForkAfterFirstConfirmation_ZeroConfirmationCount(t *testing.T) { @@ -741,12 +732,12 @@ func TestCompareAndUpdateConfirmationQueue_NewForkAfterFirstConfirmation_ZeroCon targetConfirmationCount := uint64(0) // Execute - bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) - + confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) // Assert - assert.False(t, occ.NewFork) - assert.True(t, occ.Confirmed) - assert.Len(t, occ.Confirmations, 1) + assert.False(t, confirmationUpdateResult.NewFork) + assert.True(t, confirmationUpdateResult.Confirmed) + assert.Len(t, confirmationUpdateResult.Confirmations, 1) } func TestCompareAndUpdateConfirmationQueue_NewForkAndNoConnectionToCanonicalChain(t *testing.T) { @@ -773,12 +764,12 @@ func TestCompareAndUpdateConfirmationQueue_NewForkAndNoConnectionToCanonicalChai targetConfirmationCount := uint64(5) // Execute - bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) - + confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) // Assert - assert.True(t, occ.NewFork) - assert.True(t, occ.Confirmed) - assert.Len(t, occ.Confirmations, 6) + assert.True(t, confirmationUpdateResult.NewFork) + assert.True(t, confirmationUpdateResult.Confirmed) + assert.Len(t, confirmationUpdateResult.Confirmations, 6) } func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationLaterThanCurrentBlock(t *testing.T) { @@ -804,18 +795,18 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationLaterThanCurrentB targetConfirmationCount := uint64(5) // Execute - bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) - + confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) // Assert - assert.False(t, occ.NewFork) - assert.True(t, occ.Confirmed) - assert.Len(t, occ.Confirmations, 6) - assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) - assert.Equal(t, txBlockNumber+1, uint64(occ.Confirmations[1].BlockNumber)) - assert.Equal(t, txBlockNumber+2, uint64(occ.Confirmations[2].BlockNumber)) - assert.Equal(t, txBlockNumber+3, uint64(occ.Confirmations[3].BlockNumber)) - assert.Equal(t, txBlockNumber+4, uint64(occ.Confirmations[4].BlockNumber)) - assert.Equal(t, txBlockNumber+5, uint64(occ.Confirmations[5].BlockNumber)) + 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 TestCompareAndUpdateConfirmationQueue_ExistingTxBockInfoIsWrong(t *testing.T) { @@ -841,19 +832,19 @@ func TestCompareAndUpdateConfirmationQueue_ExistingTxBockInfoIsWrong(t *testing. targetConfirmationCount := uint64(5) // Execute - bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) - + confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) // Assert - assert.False(t, occ.NewFork) - assert.True(t, occ.Confirmed) - assert.Len(t, occ.Confirmations, 6) - assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) - assert.Equal(t, generateTestHash(100), occ.Confirmations[0].BlockHash) - assert.Equal(t, txBlockNumber+1, uint64(occ.Confirmations[1].BlockNumber)) - assert.Equal(t, txBlockNumber+2, uint64(occ.Confirmations[2].BlockNumber)) - assert.Equal(t, txBlockNumber+3, uint64(occ.Confirmations[3].BlockNumber)) - assert.Equal(t, txBlockNumber+4, uint64(occ.Confirmations[4].BlockNumber)) - assert.Equal(t, txBlockNumber+5, uint64(occ.Confirmations[5].BlockNumber)) + 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, generateTestHash(100), confirmationUpdateResult.Confirmations[0].BlockHash) + 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 TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable(t *testing.T) { @@ -886,15 +877,15 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable(t *testing.T) { targetConfirmationCount := uint64(2) // Execute - bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) - + confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) // Assert - assert.True(t, occ.Confirmed) - assert.False(t, occ.NewFork) - assert.Len(t, occ.Confirmations, 3) - assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) - assert.Equal(t, txBlockNumber+1, uint64(occ.Confirmations[1].BlockNumber)) - assert.Equal(t, txBlockNumber+2, uint64(occ.Confirmations[2].BlockNumber)) + 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 TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable_ZeroConfirmationCount(t *testing.T) { @@ -927,13 +918,13 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable_ZeroConfirmationCo targetConfirmationCount := uint64(0) // Execute - bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) - + confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) // Assert - assert.True(t, occ.Confirmed) - assert.False(t, occ.NewFork) - assert.Len(t, occ.Confirmations, 1) - assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) + 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 TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableConnectable(t *testing.T) { @@ -963,16 +954,16 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableConnectable(t *test targetConfirmationCount := uint64(1) // Execute - bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) - + confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, 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, occ.Confirmed) - assert.False(t, occ.NewFork) - assert.Len(t, occ.Confirmations, 2) - assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) - assert.Equal(t, txBlockNumber+1, uint64(occ.Confirmations[1].BlockNumber)) + 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 TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableButAllExistingConfirmationsAreTooHighForTargetConfirmationCount(t *testing.T) { @@ -1000,16 +991,16 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableButAllExistingConfi targetConfirmationCount := uint64(1) // Execute - bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) - + confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) // Assert // The confirmation queue should return the tx block and the first block of the canonical chain - assert.True(t, occ.Confirmed) - assert.False(t, occ.NewFork) - assert.Len(t, occ.Confirmations, 2) - assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) - assert.Equal(t, txBlockNumber+1, uint64(occ.Confirmations[1].BlockNumber)) + 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)) } @@ -1038,16 +1029,16 @@ func TestCompareAndUpdateConfirmationQueue_HasSufficientConfirmationsButNoOverla targetConfirmationCount := uint64(1) // Execute - bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) - + confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, 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, occ.Confirmed) - assert.False(t, occ.NewFork) - assert.Len(t, occ.Confirmations, 2) - assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) - assert.Equal(t, txBlockNumber+1, uint64(occ.Confirmations[1].BlockNumber)) + 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)) } @@ -1074,18 +1065,18 @@ func TestCompareAndUpdateConfirmationQueue_ValidExistingConfirmations(t *testing targetConfirmationCount := uint64(5) // Execute - bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) - + confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) // Assert - assert.False(t, occ.NewFork) - assert.True(t, occ.Confirmed) - assert.Len(t, occ.Confirmations, 6) - assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) - assert.Equal(t, txBlockNumber+1, uint64(occ.Confirmations[1].BlockNumber)) - assert.Equal(t, txBlockNumber+2, uint64(occ.Confirmations[2].BlockNumber)) - assert.Equal(t, txBlockNumber+3, uint64(occ.Confirmations[3].BlockNumber)) - assert.Equal(t, txBlockNumber+4, uint64(occ.Confirmations[4].BlockNumber)) - assert.Equal(t, txBlockNumber+5, uint64(occ.Confirmations[5].BlockNumber)) + 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 TestCompareAndUpdateConfirmationQueue_ValidExistingTxBlock(t *testing.T) { @@ -1109,18 +1100,18 @@ func TestCompareAndUpdateConfirmationQueue_ValidExistingTxBlock(t *testing.T) { targetConfirmationCount := uint64(5) // Execute - bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) - + confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) // Assert - assert.False(t, occ.NewFork) - assert.True(t, occ.Confirmed) - assert.Len(t, occ.Confirmations, 6) - assert.Equal(t, txBlockNumber, uint64(occ.Confirmations[0].BlockNumber)) - assert.Equal(t, txBlockNumber+1, uint64(occ.Confirmations[1].BlockNumber)) - assert.Equal(t, txBlockNumber+2, uint64(occ.Confirmations[2].BlockNumber)) - assert.Equal(t, txBlockNumber+3, uint64(occ.Confirmations[3].BlockNumber)) - assert.Equal(t, txBlockNumber+4, uint64(occ.Confirmations[4].BlockNumber)) - assert.Equal(t, txBlockNumber+5, uint64(occ.Confirmations[5].BlockNumber)) + 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 TestCompareAndUpdateConfirmationQueue_ReachTargetConfirmation(t *testing.T) { @@ -1141,12 +1132,12 @@ func TestCompareAndUpdateConfirmationQueue_ReachTargetConfirmation(t *testing.T) targetConfirmationCount := uint64(3) // Execute - bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) - + confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) // Assert - assert.True(t, occ.Confirmed) + assert.True(t, confirmationUpdateResult.Confirmed) // The code builds a full confirmation queue from the canonical chain - assert.GreaterOrEqual(t, len(occ.Confirmations), 4) // tx block + 3 confirmations + assert.GreaterOrEqual(t, len(confirmationUpdateResult.Confirmations), 4) // tx block + 3 confirmations } func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithGap(t *testing.T) { @@ -1174,12 +1165,12 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithGap(t *testi targetConfirmationCount := uint64(5) // Execute - bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) - + confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) // Assert - assert.False(t, occ.NewFork) - assert.True(t, occ.Confirmed) - assert.Len(t, occ.Confirmations, 6) + assert.False(t, confirmationUpdateResult.NewFork) + assert.True(t, confirmationUpdateResult.Confirmed) + assert.Len(t, confirmationUpdateResult.Confirmations, 6) } func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNumber(t *testing.T) { @@ -1205,12 +1196,12 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNu targetConfirmationCount := uint64(5) // Execute - bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) - + confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) // Assert - assert.False(t, occ.NewFork) - assert.True(t, occ.Confirmed) - assert.Len(t, occ.Confirmations, 6) + assert.False(t, confirmationUpdateResult.NewFork) + assert.True(t, confirmationUpdateResult.Confirmed) + assert.Len(t, confirmationUpdateResult.Confirmations, 6) } func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNumberAfterFirstConfirmation(t *testing.T) { @@ -1237,12 +1228,12 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNu targetConfirmationCount := uint64(5) // Execute - bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) - + confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + assert.NoError(t, err) // Assert - assert.False(t, occ.NewFork) - assert.True(t, occ.Confirmed) - assert.Len(t, occ.Confirmations, 6) + assert.False(t, confirmationUpdateResult.NewFork) + assert.True(t, confirmationUpdateResult.Confirmed) + assert.Len(t, confirmationUpdateResult.Confirmations, 6) } // Helper functions From de86f2028df6ad7dc2d69afd131e2cbdf7b7dc30 Mon Sep 17 00:00:00 2001 From: John Hosie Date: Wed, 22 Oct 2025 22:54:21 +0100 Subject: [PATCH 21/37] refactor confirmations algorithm Signed-off-by: John Hosie --- internal/ethereum/confirmation_reconciler.go | 432 ++++++++++++------ .../ethereum/confirmation_reconciler_test.go | 303 ++++++------ 2 files changed, 438 insertions(+), 297 deletions(-) diff --git a/internal/ethereum/confirmation_reconciler.go b/internal/ethereum/confirmation_reconciler.go index 129c7a8..6701e5a 100644 --- a/internal/ethereum/confirmation_reconciler.go +++ b/internal/ethereum/confirmation_reconciler.go @@ -31,17 +31,24 @@ import ( "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) { + + // Before we start, make sure that the existing confirmations queue is valid and consistent with itself + err := validateExistingConfirmations(ctx, existingConfirmations) + if err != nil { + return nil, err + } + + // 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) { - // Initialize the result with existing confirmations - reconcileResult := &ffcapi.ConfirmationUpdateResult{ - Confirmations: existingConfirmations, - NewFork: false, - Confirmed: false, - TargetConfirmationCount: targetConfirmationCount, - } - // Fetch the block containing the transaction + // Fetch the block containing the transaction then start the algorithm proper txBlockInfo, err := bl.getBlockInfoContainsTxHash(ctx, txHash) if err != nil { log.L(ctx).Errorf("Failed to fetch block info using tx hash %s: %v", txHash, err) @@ -52,71 +59,236 @@ func (bl *blockListener) reconcileConfirmationsForTransaction(ctx context.Contex log.L(ctx).Debugf("Transaction %s not found in any block", txHash) return nil, i18n.NewError(ctx, msgs.MsgTransactionNotFound, txHash) } + return bl.buildConfirmationList(ctx, existingConfirmations, txBlockInfo, targetConfirmationCount) +} - return bl.compareAndUpdateConfirmationQueue(ctx, reconcileResult, txBlockInfo, targetConfirmationCount) +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 canonical chain allows. + // Secondary objective is to report whether any fork was detected (and corrected) during this analysis + // Initialize the result with existing confirmations + reconcileResult := &ffcapi.ConfirmationUpdateResult{ + TargetConfirmationCount: targetConfirmationCount, + } + + // before we get into the main algorithm, a couple of special cases to optimize for that can save us some work + reconcileResult, err := bl.handleSpecialCases(ctx, existingConfirmations, txBlockInfo, targetConfirmationCount, reconcileResult) + if reconcileResult.Confirmed || err != nil { + return reconcileResult, err + } + + // 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(ctx, existingConfirmations, txBlockInfo, reconcileResult) + lateList, err := createLateList(ctx, txBlockInfo, targetConfirmationCount, reconcileResult, 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 from the spliced list + + if !splicedList.containsTransactionBlock() { + // It contained the transaction when we first spliced the lists together + // so it must have gotten removed because of a broken link + // if this happens, it must mean that the chain is currently unstable and we need to start over + reconcileResult.NewFork = true + break + } + + // inner loop to fill any gaps + for splicedList.hasGap() { + err = splicedList.fillGap(ctx, bl) + if err != nil { + return nil, err + } + } + + if confirmations := splicedList.link(); 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 } -// compareAndUpdateConfirmationQueue orchestrates the confirmation reconciliation process. -// It builds new confirmations from the in-memory partial chain and fills gaps in the confirmation queue. -func (bl *blockListener) compareAndUpdateConfirmationQueue(ctx context.Context, reconcileResult *ffcapi.ConfirmationUpdateResult, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) (*ffcapi.ConfirmationUpdateResult, error) { +// 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 +} - // Initialize confirmation map and get existing confirmations - // the init must happen after the in-memory partial chain check to avoid - // confirming blocks that are not yet validated in the in-memory partial chain - var existingConfirmations []*ffcapi.MinimalBlockInfo - // If no existing confirmations, initialize with the transaction block - if len(reconcileResult.Confirmations) == 0 { - reconcileResult.Confirmations = []*ffcapi.MinimalBlockInfo{txBlockInfo} - existingConfirmations = nil - } else { - // Validate existing confirmations against the current transaction block - existingQueue := reconcileResult.Confirmations - if len(existingQueue) > 0 { - existingTxBlock := existingQueue[0] - if !existingTxBlock.Equal(txBlockInfo) { - // Transaction block mismatch indicates a fork - rebuild confirmation queue - reconcileResult.NewFork = true - reconcileResult.Confirmations = []*ffcapi.MinimalBlockInfo{txBlockInfo} - existingConfirmations = nil - } else { - existingConfirmations = existingQueue +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, + } + if len(s.earlyList) == 0 || len(s.lateList) == 0 { + return s, false + } + 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 !discardedEarlyListBlocks[i].Equal(s.lateList[i]) { + detectedFork = true + break } - } else { - existingConfirmations = existingQueue } + + 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) containsTransactionBlock() bool { + // we haven't removed the first block from the early list + return len(s.earlyList) > 0 +} + +func (s *splice) fillGap(ctx context.Context, blockListener *blockListener) error { + if !s.hasGap() { + // no gap to fill + return nil } - var err error - // Build new confirmations from the in-memory partial chain and get existing confirmations - newConfirmationsWithoutTxBlock, lastValidatedBlock, err := bl.buildConfirmationQueueUsingInMemoryPartialChain(ctx, txBlockInfo, targetConfirmationCount) + // 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 nil, err + return err + } + if freshBlockInfo == nil { + return i18n.NewError(ctx, msgs.MsgBlockNotAvailable) } - // Special case: if targetConfirmationCount is 0, transaction is immediately confirmed - if targetConfirmationCount == 0 { - reconcileResult.Confirmations = []*ffcapi.MinimalBlockInfo{txBlockInfo} - reconcileResult.Confirmed = true - return reconcileResult, nil + 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 + return i18n.NewError(ctx, msgs.MsgFailedToBuildConfirmationQueue) + } + + // Prepend fetched block to confirmation queue + s.lateList = append([]*ffcapi.MinimalBlockInfo{fetchedBlock}, s.lateList...) + return nil +} + +func (s *splice) removeBrokenLink() { + if s.hasGap() { + // nothing to remove if there is a gap + return + } + // 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) link() []*ffcapi.MinimalBlockInfo { + if s.hasGap() { + return nil + } + if len(s.earlyList) == 0 { + return s.lateList + } + if len(s.lateList) == 0 { + return s.earlyList + } + 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 + +} + +func createEarlyList(ctx context.Context, existingConfirmations []*ffcapi.MinimalBlockInfo, txBlockInfo *ffcapi.MinimalBlockInfo, reconcileResult *ffcapi.ConfirmationUpdateResult) (earlyList []*ffcapi.MinimalBlockInfo) { + if len(existingConfirmations) > 0 && !existingConfirmations[0].Equal(txBlockInfo) { + // otherwise we discard the existing confirmations queue + reconcileResult.NewFork = true + } else { + earlyList = existingConfirmations + } + + if len(existingConfirmations) == 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 +} - // Validate existing confirmations and fill gaps in the confirmation queue - var confirmations []*ffcapi.MinimalBlockInfo - var newFork bool - confirmations, newFork, err = bl.checkAndFillInGap(ctx, newConfirmationsWithoutTxBlock, existingConfirmations, txBlockInfo, targetConfirmationCount, lastValidatedBlock) +func createLateList(ctx context.Context, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64, reconcileResult *ffcapi.ConfirmationUpdateResult, blockListener *blockListener) (lateList []*ffcapi.MinimalBlockInfo, err error) { + lateList, err = blockListener.buildConfirmationQueueUsingInMemoryPartialChain(ctx, txBlockInfo, targetConfirmationCount) if err != nil { return nil, err } - reconcileResult.NewFork = newFork - reconcileResult.Confirmations = confirmations - reconcileResult.Confirmed = uint64(len(confirmations)) >= targetConfirmationCount+1 - return reconcileResult, 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 + } + if targetBlockInfo == nil { + return nil, i18n.NewError(ctx, msgs.MsgBlockNotAvailable) + } + + lateList = []*ffcapi.MinimalBlockInfo{ + { + BlockNumber: fftypes.FFuint64(targetBlockInfo.Number.BigInt().Uint64()), + BlockHash: targetBlockInfo.Hash.String(), + ParentHash: targetBlockInfo.ParentHash.String(), + }, + } + } + return lateList, nil } // buildConfirmationQueueUsingInMemoryPartialChain builds the confirmation queue 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, lastValidatedBlock *ffcapi.MinimalBlockInfo, err error) { +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() @@ -126,7 +298,7 @@ func (bl *blockListener) buildConfirmationQueueUsingInMemoryPartialChain(ctx con chainTail := bl.canonicalChain.Back().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 to be indexed", txBlockNumber) - return nil, nil, i18n.NewError(ctx, msgs.MsgInMemoryPartialChainNotCaughtUp, txBlockNumber, txBlockInfo.BlockHash) + return nil, i18n.NewError(ctx, msgs.MsgInMemoryPartialChainNotCaughtUp, txBlockNumber, txBlockInfo.BlockHash) } // Build new confirmations from blocks after the transaction block @@ -138,12 +310,6 @@ func (bl *blockListener) buildConfirmationQueueUsingInMemoryPartialChain(ctx con // If we've reached the target confirmation count, mark as confirmed if nextInMemoryBlockInfo.BlockNumber.Uint64() > targetBlockNumber { - // if the in-memory partial chain contains the next block after the target block number, - // and the new confirmations queue is empty, - // we set the last validated block to the next block, so the downstream function can use it validate blocks before it - if len(newConfirmationsWithoutTxBlock) == 0 && nextInMemoryBlockInfo.BlockNumber.Uint64() == targetBlockNumber+1 { - lastValidatedBlock = nextInMemoryBlockInfo - } break } @@ -161,96 +327,90 @@ func (bl *blockListener) buildConfirmationQueueUsingInMemoryPartialChain(ctx con }) nextInMemoryBlock = nextInMemoryBlock.Next() } - return newConfirmationsWithoutTxBlock, lastValidatedBlock, nil + return newConfirmationsWithoutTxBlock, nil } -// checkAndFillInGap validates existing confirmations, detects forks, and fills gaps -// in the confirmation queue using existing confirmations or fetching missing blocks from the blockchain. -// It ensures the confirmation chain is valid and connected to the transaction block. -func (bl *blockListener) checkAndFillInGap(ctx context.Context, newConfirmationsWithoutTxBlock []*ffcapi.MinimalBlockInfo, existingConfirmations []*ffcapi.MinimalBlockInfo, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64, lastValidatedBlock *ffcapi.MinimalBlockInfo) ([]*ffcapi.MinimalBlockInfo, bool, error) { - var hasNewFork bool - - // Detect forks by comparing new confirmations with existing ones - for _, confirmation := range newConfirmationsWithoutTxBlock { - for _, existingConfirmation := range existingConfirmations { - if confirmation.BlockNumber.Uint64() == existingConfirmation.BlockNumber.Uint64() && !confirmation.Equal(existingConfirmation) { - hasNewFork = true - break - } - } - if hasNewFork { - break +func (bl *blockListener) getBlockByNumberFromInMemoryPartialChain(ctx context.Context, blockNumber uint64) *ffcapi.MinimalBlockInfo { + bl.mux.RLock() + defer bl.mux.RUnlock() + if blockNumber < bl.canonicalChain.Front().Value.(*ffcapi.MinimalBlockInfo).BlockNumber.Uint64() { + return nil + } + if blockNumber > bl.canonicalChain.Back().Value.(*ffcapi.MinimalBlockInfo).BlockNumber.Uint64() { + return nil + } + for nextInMemoryBlock := bl.canonicalChain.Front(); nextInMemoryBlock != nil; nextInMemoryBlock = nextInMemoryBlock.Next() { + nextInMemoryBlockInfo := nextInMemoryBlock.Value.(*ffcapi.MinimalBlockInfo) + if nextInMemoryBlockInfo.BlockNumber.Uint64() == blockNumber { + return nextInMemoryBlockInfo } } + return nil +} - // Determine the range of blocks to validate and fill gaps - blockNumberToReach := txBlockInfo.BlockNumber.Uint64() + targetConfirmationCount - if len(newConfirmationsWithoutTxBlock) > 0 { - // Start from the block before the first new confirmation - blockNumberToReach = newConfirmationsWithoutTxBlock[0].BlockNumber.Uint64() - 1 - lastValidatedBlock = newConfirmationsWithoutTxBlock[0] - } - - // Fill gaps by validating blocks from target down to transaction block - for i := blockNumberToReach; i > txBlockInfo.BlockNumber.Uint64(); i-- { - fetchedFromExistingQueue := false - - // First, try to use existing confirmations if they match - if lastValidatedBlock != nil { - for _, confirmation := range existingConfirmations { - if confirmation.BlockNumber.Uint64() == i { - if confirmation.IsParentOf(lastValidatedBlock) { - // Valid existing confirmation - prepend to queue - newConfirmationsWithoutTxBlock = append([]*ffcapi.MinimalBlockInfo{confirmation}, newConfirmationsWithoutTxBlock...) - lastValidatedBlock = confirmation - fetchedFromExistingQueue = true - break - } - // Block number matches but parent relationship is invalid - fork detected - hasNewFork = true - } +func validateExistingConfirmations(ctx context.Context, existingConfirmations []*ffcapi.MinimalBlockInfo) error { + var previousBlock *ffcapi.MinimalBlockInfo + var previousBlockNumber uint64 + for _, existingConfirmation := range existingConfirmations { + if previousBlock != nil { + if existingConfirmation.BlockNumber.Uint64() != previousBlockNumber+1 { + return i18n.NewError(ctx, msgs.MsgFailedToBuildConfirmationQueue) + } + if !previousBlock.IsParentOf(existingConfirmation) { + return i18n.NewError(ctx, msgs.MsgFailedToBuildConfirmationQueue) } } + previousBlock = existingConfirmation + previousBlockNumber = existingConfirmation.BlockNumber.Uint64() - if fetchedFromExistingQueue { - continue - } + } + return nil +} - // Fetch block from blockchain if not found in existing confirmations - freshBlockInfo, _, err := bl.getBlockInfoByNumber(ctx, i, false, "", "") - if err != nil { - return nil, hasNewFork, err - } - if freshBlockInfo == nil { - return nil, hasNewFork, i18n.NewError(ctx, msgs.MsgBlockNotAvailable) - } +func (bl *blockListener) handleSpecialCases(ctx context.Context, existingConfirmations []*ffcapi.MinimalBlockInfo, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64, reconcileResult *ffcapi.ConfirmationUpdateResult) (*ffcapi.ConfirmationUpdateResult, error) { + if targetConfirmationCount == 0 { + // if the target confirmation count is 0, we can immediately return a confirmed result + reconcileResult.Confirmed = true + reconcileResult.Confirmations = []*ffcapi.MinimalBlockInfo{txBlockInfo} + return reconcileResult, nil + } - fetchedBlock := &ffcapi.MinimalBlockInfo{ - BlockNumber: fftypes.FFuint64(freshBlockInfo.Number.BigInt().Uint64()), - BlockHash: freshBlockInfo.Hash.String(), - ParentHash: freshBlockInfo.ParentHash.String(), - } + if uint64(len(existingConfirmations)) > targetConfirmationCount { + if existingConfirmations[0].Equal(txBlockInfo) { + // start of the existing confirmations aligns with our latest view of the chain - // Validate parent-child relationship - if lastValidatedBlock != nil && !fetchedBlock.IsParentOf(lastValidatedBlock) { - return nil, hasNewFork, i18n.NewError(ctx, msgs.MsgFailedToBuildConfirmationQueue) - } + // attempt to validate the rest of the existing confirmations. Starting with the target confirmation block and working forwards - // Prepend fetched block to confirmation queue - newConfirmationsWithoutTxBlock = append([]*ffcapi.MinimalBlockInfo{fetchedBlock}, newConfirmationsWithoutTxBlock...) - lastValidatedBlock = fetchedBlock - } + attemptIndex := targetConfirmationCount - // Final validation: ensure the confirmation chain connects to the transaction block - if len(newConfirmationsWithoutTxBlock) > 0 && !txBlockInfo.IsParentOf(newConfirmationsWithoutTxBlock[0]) { - return nil, hasNewFork, i18n.NewError(ctx, msgs.MsgFailedToBuildConfirmationQueue) - } + for attemptIndex < uint64(len(existingConfirmations))-1 { + existingConfirmationToValidate := existingConfirmations[attemptIndex] + blockInfo := bl.getBlockByNumberFromInMemoryPartialChain(ctx, existingConfirmationToValidate.BlockNumber.Uint64()) + if blockInfo == nil { + attemptIndex++ + continue + } + if blockInfo.BlockHash == existingConfirmationToValidate.BlockHash { + // the last block in the existing confirmations queue is still valid + reconcileResult.Confirmed = true + reconcileResult.Confirmations = existingConfirmations[:targetConfirmationCount+1] + return reconcileResult, nil + } + } - return append([]*ffcapi.MinimalBlockInfo{txBlockInfo}, newConfirmationsWithoutTxBlock...), hasNewFork, nil -} + // if we didn't get a conclusive answer from the in memory cache of the chain, we need to fetch the block from the blockchain + targetBlock := existingConfirmations[targetConfirmationCount] + blockInfo, _, err := bl.getBlockInfoByNumber(ctx, targetBlock.BlockNumber.Uint64(), false, "", "") + if err != nil { + return nil, err + } -// 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) { - return c.blockListener.reconcileConfirmationsForTransaction(ctx, txHash, existingConfirmations, targetConfirmationCount) + if blockInfo.Hash.String() == targetBlock.BlockHash { + reconcileResult.Confirmed = true + reconcileResult.Confirmations = existingConfirmations[:targetConfirmationCount+1] + return reconcileResult, nil + } + } + } + return reconcileResult, nil } diff --git a/internal/ethereum/confirmation_reconciler_test.go b/internal/ethereum/confirmation_reconciler_test.go index f585ac2..7f993b2 100644 --- a/internal/ethereum/confirmation_reconciler_test.go +++ b/internal/ethereum/confirmation_reconciler_test.go @@ -236,9 +236,6 @@ func TestCompareAndUpdateConfirmationQueue_EmptyChain(t *testing.T) { bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 50, []uint64{}) defer done() ctx := context.Background() - occ := &ffcapi.ConfirmationUpdateResult{ - Confirmations: []*ffcapi.MinimalBlockInfo{}, - } txBlockNumber := uint64(100) txBlockHash := generateTestHash(txBlockNumber) @@ -251,7 +248,7 @@ func TestCompareAndUpdateConfirmationQueue_EmptyChain(t *testing.T) { // Execute // Assert - should return early due to chain being too short - confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, []*ffcapi.MinimalBlockInfo{}, txBlockInfo, targetConfirmationCount) assert.Error(t, err) assert.Regexp(t, "FF23062", err.Error()) assert.Nil(t, confirmationUpdateResult) @@ -262,9 +259,7 @@ func TestCompareAndUpdateConfirmationQueue_ChainTooShort(t *testing.T) { bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 99, []uint64{}) defer done() ctx := context.Background() - occ := &ffcapi.ConfirmationUpdateResult{ - Confirmations: []*ffcapi.MinimalBlockInfo{}, - } + txBlockNumber := uint64(100) txBlockHash := generateTestHash(txBlockNumber) @@ -276,7 +271,7 @@ func TestCompareAndUpdateConfirmationQueue_ChainTooShort(t *testing.T) { targetConfirmationCount := uint64(5) // Execute - confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, []*ffcapi.MinimalBlockInfo{}, txBlockInfo, targetConfirmationCount) assert.Error(t, err) assert.Nil(t, confirmationUpdateResult) } @@ -286,9 +281,6 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMap(t *testing.T) { bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 150, []uint64{}) defer done() ctx := context.Background() - occ := &ffcapi.ConfirmationUpdateResult{ - Confirmations: nil, - } txBlockNumber := uint64(100) txBlockHash := generateTestHash(txBlockNumber) txBlockInfo := &ffcapi.MinimalBlockInfo{ @@ -299,7 +291,7 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMap(t *testing.T) { targetConfirmationCount := uint64(5) // Execute - confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, nil, txBlockInfo, targetConfirmationCount) // Assert assert.NoError(t, err) assert.NotNil(t, confirmationUpdateResult) @@ -320,9 +312,6 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMap_ZeroConfirmationCo bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 150, []uint64{}) defer done() ctx := context.Background() - occ := &ffcapi.ConfirmationUpdateResult{ - Confirmations: nil, - } txBlockNumber := uint64(100) txBlockHash := generateTestHash(txBlockNumber) txBlockInfo := &ffcapi.MinimalBlockInfo{ @@ -333,7 +322,7 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMap_ZeroConfirmationCo targetConfirmationCount := uint64(0) // Execute - confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, nil, txBlockInfo, targetConfirmationCount) assert.NoError(t, err) // Assert assert.NotNil(t, confirmationUpdateResult.Confirmations) @@ -349,9 +338,6 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMapUnconfirmed(t *test bl, done := newBlockListenerWithTestChain(t, 100, 5, 100, 104, []uint64{}) defer done() ctx := context.Background() - occ := &ffcapi.ConfirmationUpdateResult{ - Confirmations: nil, - } txBlockNumber := uint64(100) txBlockHash := generateTestHash(txBlockNumber) txBlockInfo := &ffcapi.MinimalBlockInfo{ @@ -362,7 +348,7 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMapUnconfirmed(t *test targetConfirmationCount := uint64(5) // Execute - confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, nil, txBlockInfo, targetConfirmationCount) assert.NoError(t, err) // Assert assert.NotNil(t, confirmationUpdateResult.Confirmations) @@ -383,9 +369,7 @@ func TestCompareAndUpdateConfirmationQueue_EmptyConfirmationQueue(t *testing.T) bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 150, []uint64{}) defer done() ctx := context.Background() - occ := &ffcapi.ConfirmationUpdateResult{ - Confirmations: []*ffcapi.MinimalBlockInfo{}, - } + txBlockNumber := uint64(100) txBlockHash := generateTestHash(txBlockNumber) txBlockInfo := &ffcapi.MinimalBlockInfo{ @@ -396,7 +380,7 @@ func TestCompareAndUpdateConfirmationQueue_EmptyConfirmationQueue(t *testing.T) targetConfirmationCount := uint64(5) // Execute - confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, []*ffcapi.MinimalBlockInfo{}, txBlockInfo, targetConfirmationCount) assert.NoError(t, err) // Assert assert.False(t, confirmationUpdateResult.NewFork) @@ -412,6 +396,7 @@ func TestCompareAndUpdateConfirmationQueue_EmptyConfirmationQueue(t *testing.T) } func TestCompareAndUpdateConfirmationQueue_MismatchConfirmationBlock(t *testing.T) { + t.Skip("Skipping test because I don't understand this behavior") //Trying to understand the reasoning behind supporting an invalid list of existing confirmations. Should this not just be an error? // Setup bl, done := newBlockListenerWithTestChain(t, 100, 5, 103, 150, []uint64{101}) defer done() @@ -422,9 +407,6 @@ func TestCompareAndUpdateConfirmationQueue_MismatchConfirmationBlock(t *testing. {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, {BlockHash: generateTestHash(103), BlockNumber: fftypes.FFuint64(103), ParentHash: generateTestHash(102)}, } - occ := &ffcapi.ConfirmationUpdateResult{ - Confirmations: existingQueue, - } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ @@ -435,7 +417,7 @@ func TestCompareAndUpdateConfirmationQueue_MismatchConfirmationBlock(t *testing. targetConfirmationCount := uint64(5) // Execute - confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) assert.NoError(t, err) // Assert assert.True(t, confirmationUpdateResult.NewFork) @@ -459,9 +441,6 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsTooDistant(t *te {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, } - occ := &ffcapi.ConfirmationUpdateResult{ - Confirmations: existingQueue, - } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ @@ -472,7 +451,7 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsTooDistant(t *te targetConfirmationCount := uint64(5) // Execute - confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + 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) @@ -497,9 +476,6 @@ func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmationDoNotAff {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: "0xwrongparent"}, } - occ := &ffcapi.ConfirmationUpdateResult{ - Confirmations: existingQueue, - } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ @@ -510,7 +486,7 @@ func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmationDoNotAff targetConfirmationCount := uint64(5) // Execute - confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) assert.NoError(t, err) // Assert assert.True(t, confirmationUpdateResult.NewFork) @@ -535,9 +511,6 @@ func TestCompareAndUpdateConfirmationQueue_ConnectionNodeMismatch(t *testing.T) {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, {BlockHash: generateTestHash(103), BlockNumber: fftypes.FFuint64(103), ParentHash: generateTestHash(102)}, } - occ := &ffcapi.ConfirmationUpdateResult{ - Confirmations: existingQueue, - } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ @@ -548,7 +521,7 @@ func TestCompareAndUpdateConfirmationQueue_ConnectionNodeMismatch(t *testing.T) targetConfirmationCount := uint64(5) // Execute - confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) assert.NoError(t, err) // Assert assert.True(t, confirmationUpdateResult.NewFork) @@ -573,9 +546,6 @@ func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmationAfterFir {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblockwrong"}, } - occ := &ffcapi.ConfirmationUpdateResult{ - Confirmations: existingQueue, - } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ @@ -586,7 +556,7 @@ func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmationAfterFir targetConfirmationCount := uint64(5) // Execute - confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) assert.NoError(t, err) // Assert assert.True(t, confirmationUpdateResult.NewFork) @@ -619,9 +589,7 @@ func TestCompareAndUpdateConfirmationQueue_FailedToFetchBlockInfo(t *testing.T) {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblockwrong"}, } - occ := &ffcapi.ConfirmationUpdateResult{ - Confirmations: existingQueue, - } + txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ @@ -632,7 +600,7 @@ func TestCompareAndUpdateConfirmationQueue_FailedToFetchBlockInfo(t *testing.T) targetConfirmationCount := uint64(5) // Execute - confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) assert.Error(t, err) assert.Regexp(t, "pop", err.Error()) assert.Nil(t, confirmationUpdateResult) @@ -659,9 +627,6 @@ func TestCompareAndUpdateConfirmationQueue_NilBlockInfo(t *testing.T) { {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: "0xblockwrong"}, } - occ := &ffcapi.ConfirmationUpdateResult{ - Confirmations: existingQueue, - } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ @@ -672,11 +637,12 @@ func TestCompareAndUpdateConfirmationQueue_NilBlockInfo(t *testing.T) { targetConfirmationCount := uint64(5) // Execute - confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) assert.Error(t, err) assert.Regexp(t, "FF23011", err.Error()) assert.Nil(t, confirmationUpdateResult) } + func TestCompareAndUpdateConfirmationQueue_NewForkAfterFirstConfirmation(t *testing.T) { // Setup bl, done := newBlockListenerWithTestChain(t, 100, 5, 100, 150, []uint64{}) @@ -688,9 +654,6 @@ func TestCompareAndUpdateConfirmationQueue_NewForkAfterFirstConfirmation(t *test {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, {BlockHash: "fork1", BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, } - occ := &ffcapi.ConfirmationUpdateResult{ - Confirmations: existingQueue, - } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ @@ -701,7 +664,7 @@ func TestCompareAndUpdateConfirmationQueue_NewForkAfterFirstConfirmation(t *test targetConfirmationCount := uint64(5) // Execute - confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) assert.NoError(t, err) // Assert assert.True(t, confirmationUpdateResult.NewFork) @@ -719,9 +682,7 @@ func TestCompareAndUpdateConfirmationQueue_NewForkAfterFirstConfirmation_ZeroCon {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, {BlockHash: "fork1", BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, } - occ := &ffcapi.ConfirmationUpdateResult{ - Confirmations: existingQueue, - } + txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ @@ -732,7 +693,7 @@ func TestCompareAndUpdateConfirmationQueue_NewForkAfterFirstConfirmation_ZeroCon targetConfirmationCount := uint64(0) // Execute - confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) assert.NoError(t, err) // Assert assert.False(t, confirmationUpdateResult.NewFork) @@ -751,9 +712,6 @@ func TestCompareAndUpdateConfirmationQueue_NewForkAndNoConnectionToCanonicalChai {BlockHash: "fork2", BlockNumber: fftypes.FFuint64(102), ParentHash: "fork1"}, {BlockHash: "fork3", BlockNumber: fftypes.FFuint64(103), ParentHash: "fork2"}, } - occ := &ffcapi.ConfirmationUpdateResult{ - Confirmations: existingQueue, - } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ @@ -764,7 +722,7 @@ func TestCompareAndUpdateConfirmationQueue_NewForkAndNoConnectionToCanonicalChai targetConfirmationCount := uint64(5) // Execute - confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) assert.NoError(t, err) // Assert assert.True(t, confirmationUpdateResult.NewFork) @@ -773,6 +731,8 @@ func TestCompareAndUpdateConfirmationQueue_NewForkAndNoConnectionToCanonicalChai } func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationLaterThanCurrentBlock(t *testing.T) { + t.Skip("Skipping test because I don't understand this behavior") //Trying to understand the reasoning behind supporting an invalid list of existing confirmations. Should this not just be an error? + // Setup bl, done := newBlockListenerWithTestChain(t, 100, 5, 100, 150, []uint64{}) defer done() @@ -782,9 +742,6 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationLaterThanCurrentB {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, } - occ := &ffcapi.ConfirmationUpdateResult{ - Confirmations: existingQueue, - } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ @@ -795,7 +752,7 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationLaterThanCurrentB targetConfirmationCount := uint64(5) // Execute - confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) assert.NoError(t, err) // Assert assert.False(t, confirmationUpdateResult.NewFork) @@ -811,6 +768,7 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationLaterThanCurrentB func TestCompareAndUpdateConfirmationQueue_ExistingTxBockInfoIsWrong(t *testing.T) { // Setup + t.Skip("Skipping test because I don't understand this behavior") //Trying to understand the reasoning behind supporting an invalid list of existing confirmations. Should this not just be an error? bl, done := newBlockListenerWithTestChain(t, 100, 5, 100, 150, []uint64{}) defer done() @@ -819,9 +777,6 @@ func TestCompareAndUpdateConfirmationQueue_ExistingTxBockInfoIsWrong(t *testing. {BlockHash: generateTestHash(999), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, // should be corrected {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, } - occ := &ffcapi.ConfirmationUpdateResult{ - Confirmations: existingQueue, - } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ @@ -832,7 +787,7 @@ func TestCompareAndUpdateConfirmationQueue_ExistingTxBockInfoIsWrong(t *testing. targetConfirmationCount := uint64(5) // Execute - confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) assert.NoError(t, err) // Assert assert.False(t, confirmationUpdateResult.NewFork) @@ -864,9 +819,6 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable(t *testing.T) { {BlockHash: "0xblock104", BlockNumber: fftypes.FFuint64(104), ParentHash: generateTestHash(103)}, // discarded {BlockHash: "0xblock105", BlockNumber: fftypes.FFuint64(105), ParentHash: "0xblock104"}, // discarded } - occ := &ffcapi.ConfirmationUpdateResult{ - Confirmations: existingQueue, - } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ @@ -877,7 +829,7 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable(t *testing.T) { targetConfirmationCount := uint64(2) // Execute - confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) assert.NoError(t, err) // Assert assert.True(t, confirmationUpdateResult.Confirmed) @@ -905,9 +857,6 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable_ZeroConfirmationCo {BlockHash: "0xblock104", BlockNumber: fftypes.FFuint64(104), ParentHash: generateTestHash(103)}, // discarded {BlockHash: "0xblock105", BlockNumber: fftypes.FFuint64(105), ParentHash: "0xblock104"}, // discarded } - occ := &ffcapi.ConfirmationUpdateResult{ - Confirmations: existingQueue, - } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ @@ -918,7 +867,7 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable_ZeroConfirmationCo targetConfirmationCount := uint64(0) // Execute - confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) assert.NoError(t, err) // Assert assert.True(t, confirmationUpdateResult.Confirmed) @@ -941,9 +890,6 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableConnectable(t *test // didn't have block 103, which is the first block of the canonical chain // but we should still be able to validate the existing confirmations are valid using parent hash } - occ := &ffcapi.ConfirmationUpdateResult{ - Confirmations: existingQueue, - } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ @@ -954,7 +900,7 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableConnectable(t *test targetConfirmationCount := uint64(1) // Execute - confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + 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 @@ -967,6 +913,8 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableConnectable(t *test } func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableButAllExistingConfirmationsAreTooHighForTargetConfirmationCount(t *testing.T) { + t.Skip("Skipping test because I don't understand this behavior") //Trying to understand the reasoning behind supporting an invalid list of existing confirmations. Should this not just be an error? + // Setup bl, done := newBlockListenerWithTestChain(t, 100, 5, 103, 150, []uint64{101}) defer done() @@ -978,9 +926,6 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableButAllExistingConfi // 101 will be fetched from the JSON-RPC endpoint to fill the gap {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, } - occ := &ffcapi.ConfirmationUpdateResult{ - Confirmations: existingQueue, - } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ @@ -991,7 +936,7 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableButAllExistingConfi targetConfirmationCount := uint64(1) // Execute - confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) assert.NoError(t, err) // Assert // The confirmation queue should return the tx block and the first block of the canonical chain @@ -1016,9 +961,7 @@ func TestCompareAndUpdateConfirmationQueue_HasSufficientConfirmationsButNoOverla {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, } - occ := &ffcapi.ConfirmationUpdateResult{ - Confirmations: existingQueue, - } + txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ @@ -1029,7 +972,7 @@ func TestCompareAndUpdateConfirmationQueue_HasSufficientConfirmationsButNoOverla targetConfirmationCount := uint64(1) // Execute - confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + 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, @@ -1052,9 +995,7 @@ func TestCompareAndUpdateConfirmationQueue_ValidExistingConfirmations(t *testing {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, } - occ := &ffcapi.ConfirmationUpdateResult{ - Confirmations: existingQueue, - } + txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ @@ -1065,7 +1006,7 @@ func TestCompareAndUpdateConfirmationQueue_ValidExistingConfirmations(t *testing targetConfirmationCount := uint64(5) // Execute - confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) assert.NoError(t, err) // Assert assert.False(t, confirmationUpdateResult.NewFork) @@ -1087,9 +1028,7 @@ func TestCompareAndUpdateConfirmationQueue_ValidExistingTxBlock(t *testing.T) { existingQueue := []*ffcapi.MinimalBlockInfo{ {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, } - occ := &ffcapi.ConfirmationUpdateResult{ - Confirmations: existingQueue, - } + txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ @@ -1100,7 +1039,7 @@ func TestCompareAndUpdateConfirmationQueue_ValidExistingTxBlock(t *testing.T) { targetConfirmationCount := uint64(5) // Execute - confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) assert.NoError(t, err) // Assert assert.False(t, confirmationUpdateResult.NewFork) @@ -1119,9 +1058,7 @@ func TestCompareAndUpdateConfirmationQueue_ReachTargetConfirmation(t *testing.T) bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 150, []uint64{}) defer done() ctx := context.Background() - occ := &ffcapi.ConfirmationUpdateResult{ - Confirmations: []*ffcapi.MinimalBlockInfo{}, - } + txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ @@ -1132,7 +1069,7 @@ func TestCompareAndUpdateConfirmationQueue_ReachTargetConfirmation(t *testing.T) targetConfirmationCount := uint64(3) // Execute - confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, []*ffcapi.MinimalBlockInfo{}, txBlockInfo, targetConfirmationCount) assert.NoError(t, err) // Assert assert.True(t, confirmationUpdateResult.Confirmed) @@ -1141,6 +1078,8 @@ func TestCompareAndUpdateConfirmationQueue_ReachTargetConfirmation(t *testing.T) } func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithGap(t *testing.T) { + t.Skip("Skipping test because I don't understand this behavior") //Trying to understand the reasoning behind supporting an invalid list of existing confirmations. Should this not just be an error? + // Setup bl, done := newBlockListenerWithTestChain(t, 100, 5, 101, 150, []uint64{}) defer done() @@ -1152,9 +1091,6 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithGap(t *testi {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, {BlockHash: generateTestHash(103), BlockNumber: fftypes.FFuint64(103), ParentHash: generateTestHash(102)}, } - occ := &ffcapi.ConfirmationUpdateResult{ - Confirmations: existingQueue, - } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ @@ -1165,7 +1101,7 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithGap(t *testi targetConfirmationCount := uint64(5) // Execute - confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) assert.NoError(t, err) // Assert assert.False(t, confirmationUpdateResult.NewFork) @@ -1174,6 +1110,8 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithGap(t *testi } func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNumber(t *testing.T) { + t.Skip("Skipping test because I don't understand this behavior") //Trying to understand the reasoning behind supporting an invalid list of existing confirmations. Should this not just be an error? + // Setup bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 150, []uint64{}) defer done() @@ -1183,9 +1121,6 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNu {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(99), ParentHash: generateTestHash(100)}, // somehow there is a lower block number } - occ := &ffcapi.ConfirmationUpdateResult{ - Confirmations: existingQueue, - } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ @@ -1196,7 +1131,7 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNu targetConfirmationCount := uint64(5) // Execute - confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) assert.NoError(t, err) // Assert assert.False(t, confirmationUpdateResult.NewFork) @@ -1205,6 +1140,8 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNu } func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNumberAfterFirstConfirmation(t *testing.T) { + t.Skip("Skipping test because I don't understand this behavior") //Trying to understand the reasoning behind supporting an invalid list of existing confirmations. Should this not just be an error? + // Setup bl, done := newBlockListenerWithTestChain(t, 100, 5, 101, 150, []uint64{}) defer done() @@ -1215,9 +1152,7 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNu {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(99), ParentHash: generateTestHash(101)}, // somehow there is a lower block number } - occ := &ffcapi.ConfirmationUpdateResult{ - Confirmations: existingQueue, - } + txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) txBlockInfo := &ffcapi.MinimalBlockInfo{ @@ -1228,7 +1163,7 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNu targetConfirmationCount := uint64(5) // Execute - confirmationUpdateResult, err := bl.compareAndUpdateConfirmationQueue(ctx, occ, txBlockInfo, targetConfirmationCount) + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) assert.NoError(t, err) // Assert assert.False(t, confirmationUpdateResult.NewFork) @@ -1236,62 +1171,52 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNu assert.Len(t, confirmationUpdateResult.Confirmations, 6) } -// Helper functions +func TestValidateExistingConfirmations_LowerBlockNumber(t *testing.T) { -// 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) + ctx := context.Background() + // Create confirmations with a lower block number + existingQueue := []*ffcapi.MinimalBlockInfo{ + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, + {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(99), ParentHash: generateTestHash(101)}, // somehow there is a lower block number + } + + // Execute + err := validateExistingConfirmations(ctx, existingQueue) + assert.Error(t, err) } -func createTestChain(startBlock, endBlock uint64) *list.List { - chain := list.New() - for i := startBlock; i <= endBlock; i++ { - blockHash := generateTestHash(i) +func TestValidateExistingConfirmations_Gap(t *testing.T) { - 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) + ctx := context.Background() + // Create confirmations with a lower block number + existingQueue := []*ffcapi.MinimalBlockInfo{ + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, + {BlockHash: generateTestHash(103), BlockNumber: fftypes.FFuint64(103), ParentHash: generateTestHash(102)}, } - return chain + + // Execute + err := validateExistingConfirmations(ctx, existingQueue) + assert.Error(t, err) } -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) +func TestValidateExistingConfirmations_BrokenHash(t *testing.T) { - 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) + ctx := context.Background() + // Create confirmations with a lower block number + existingQueue := []*ffcapi.MinimalBlockInfo{ + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, + {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: "broken"}, } + + // Execute + err := validateExistingConfirmations(ctx, existingQueue) + assert.Error(t, err) } +/* func TestCheckAndFillInGap_GetBlockInfoError(t *testing.T) { // Setup _, c, mRPC, _ := newTestConnectorWithNoBlockerFilterDefaultMocks(t) @@ -1430,3 +1355,59 @@ func TestCheckAndFillInGap_TxBlockNotParentOfFirstConfirmation(t *testing.T) { assert.Nil(t, result) mRPC.AssertExpectations(t) } +*/ +// 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) + } +} From 640a37dfd36801d14e18acf426363d720d035188 Mon Sep 17 00:00:00 2001 From: John Hosie Date: Wed, 22 Oct 2025 23:13:37 +0100 Subject: [PATCH 22/37] rename function Signed-off-by: John Hosie --- internal/ethereum/confirmation_reconciler.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/ethereum/confirmation_reconciler.go b/internal/ethereum/confirmation_reconciler.go index 6701e5a..8240786 100644 --- a/internal/ethereum/confirmation_reconciler.go +++ b/internal/ethereum/confirmation_reconciler.go @@ -113,12 +113,14 @@ func (bl *blockListener) buildConfirmationList(ctx context.Context, existingConf } } - if confirmations := splicedList.link(); confirmations != nil { + 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() @@ -224,7 +226,7 @@ func (s *splice) removeBrokenLink() { } -func (s *splice) link() []*ffcapi.MinimalBlockInfo { +func (s *splice) toSingleLinkedList() []*ffcapi.MinimalBlockInfo { if s.hasGap() { return nil } From 95ebe622728479b2b3bf78bc6bf2e6a0cdbae159 Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Fri, 24 Oct 2025 15:05:43 +0100 Subject: [PATCH 23/37] update the new function based on discussion Signed-off-by: Chengxuan Xing --- internal/ethereum/confirmation_reconciler.go | 193 +++--- .../ethereum/confirmation_reconciler_test.go | 564 ++++++------------ internal/msgs/en_error_messages.go | 103 ++-- 3 files changed, 330 insertions(+), 530 deletions(-) diff --git a/internal/ethereum/confirmation_reconciler.go b/internal/ethereum/confirmation_reconciler.go index 8240786..4536eb9 100644 --- a/internal/ethereum/confirmation_reconciler.go +++ b/internal/ethereum/confirmation_reconciler.go @@ -14,11 +14,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -// The confirmation reconciler manages transaction confirmation queues by: -// - Copying blocks from the in-memory partial chain and the existing confirmation queue -// - Detecting blockchain forks and rebuilding confirmation queues when necessary -// - Filling gaps in confirmation queues by fetching missing blocks -// - Determining when transactions have reached the target confirmation count package ethereum import ( @@ -34,7 +29,6 @@ import ( // 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) { - // Before we start, make sure that the existing confirmations queue is valid and consistent with itself err := validateExistingConfirmations(ctx, existingConfirmations) if err != nil { @@ -48,7 +42,7 @@ func (c *ethConnector) ReconcileConfirmationsForTransaction(ctx context.Context, // 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 then start the algorithm proper + // Fetch the block containing the transaction first so that we can use it to build the confirmation list txBlockInfo, err := bl.getBlockInfoContainsTxHash(ctx, txHash) if err != nil { log.L(ctx).Errorf("Failed to fetch block info using tx hash %s: %v", txHash, err) @@ -63,19 +57,20 @@ func (bl *blockListener) reconcileConfirmationsForTransaction(ctx context.Contex } 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 canonical chain allows. + // 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 - // Initialize the result with existing confirmations - reconcileResult := &ffcapi.ConfirmationUpdateResult{ - TargetConfirmationCount: targetConfirmationCount, - } - // before we get into the main algorithm, a couple of special cases to optimize for that can save us some work - reconcileResult, err := bl.handleSpecialCases(ctx, existingConfirmations, txBlockInfo, targetConfirmationCount, reconcileResult) - if reconcileResult.Confirmed || err != nil { + // before we get into the main algorithm, handle a couple of special cases to reduce readability of the main algorithm + reconcileResult, err := bl.handleSpecialCases(ctx, existingConfirmations, txBlockInfo, targetConfirmationCount) + if reconcileResult != nil || err != nil { return reconcileResult, err } + // Initialize the result with the target confirmation count + reconcileResult = &ffcapi.ConfirmationUpdateResult{ + TargetConfirmationCount: targetConfirmationCount, + } + // 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 @@ -83,7 +78,7 @@ func (bl *blockListener) buildConfirmationList(ctx context.Context, existingConf // 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(ctx, existingConfirmations, txBlockInfo, reconcileResult) + earlyList := createEarlyList(existingConfirmations, txBlockInfo, reconcileResult) lateList, err := createLateList(ctx, txBlockInfo, targetConfirmationCount, reconcileResult, bl) if err != nil { return nil, err @@ -95,19 +90,18 @@ func (bl *blockListener) buildConfirmationList(ctx context.Context, existingConf reconcileResult.NewFork = true } for { - // now loop until we can form a contiguous linked list from the spliced list - - if !splicedList.containsTransactionBlock() { - // It contained the transaction when we first spliced the lists together - // so it must have gotten removed because of a broken link - // if this happens, it must mean that the chain is currently unstable and we need to start over - reconcileResult.NewFork = true - break + // 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 + // therefore, we report a fork with no confirmations + return nil, i18n.NewError(ctx, msgs.MsgFailedToBuildConfirmationQueue) } - // inner loop to fill any gaps + // inner loop to fill any gaps between the early list and the late list for splicedList.hasGap() { - err = splicedList.fillGap(ctx, bl) + err = splicedList.fillOneGap(ctx, bl) if err != nil { return nil, err } @@ -154,11 +148,13 @@ func newSplice(earlyList []*ffcapi.MinimalBlockInfo, lateList []*ffcapi.MinimalB 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 @@ -176,12 +172,12 @@ func (s *splice) hasGap() bool { s.earlyList[len(s.earlyList)-1].BlockNumber.Uint64()+1 < s.lateList[0].BlockNumber.Uint64() } -func (s *splice) containsTransactionBlock() bool { +func (s *splice) isEarlyListEmpty() bool { // we haven't removed the first block from the early list - return len(s.earlyList) > 0 + return len(s.earlyList) == 0 } -func (s *splice) fillGap(ctx context.Context, blockListener *blockListener) error { +func (s *splice) fillOneGap(ctx context.Context, blockListener *blockListener) error { if !s.hasGap() { // no gap to fill return nil @@ -211,7 +207,7 @@ func (s *splice) fillGap(ctx context.Context, blockListener *blockListener) erro return i18n.NewError(ctx, msgs.MsgFailedToBuildConfirmationQueue) } - // Prepend fetched block to confirmation queue + // Prepend fetched block to the late list s.lateList = append([]*ffcapi.MinimalBlockInfo{fetchedBlock}, s.lateList...) return nil } @@ -244,7 +240,9 @@ func (s *splice) toSingleLinkedList() []*ffcapi.MinimalBlockInfo { } -func createEarlyList(ctx context.Context, existingConfirmations []*ffcapi.MinimalBlockInfo, txBlockInfo *ffcapi.MinimalBlockInfo, reconcileResult *ffcapi.ConfirmationUpdateResult) (earlyList []*ffcapi.MinimalBlockInfo) { +// 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 && !existingConfirmations[0].Equal(txBlockInfo) { // otherwise we discard the existing confirmations queue reconcileResult.NewFork = true @@ -287,7 +285,7 @@ func createLateList(ctx context.Context, txBlockInfo *ffcapi.MinimalBlockInfo, t return lateList, nil } -// buildConfirmationQueueUsingInMemoryPartialChain builds the confirmation queue using the in-memory partial chain. +// 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) { @@ -332,87 +330,86 @@ func (bl *blockListener) buildConfirmationQueueUsingInMemoryPartialChain(ctx con return newConfirmationsWithoutTxBlock, nil } -func (bl *blockListener) getBlockByNumberFromInMemoryPartialChain(ctx context.Context, blockNumber uint64) *ffcapi.MinimalBlockInfo { - bl.mux.RLock() - defer bl.mux.RUnlock() - if blockNumber < bl.canonicalChain.Front().Value.(*ffcapi.MinimalBlockInfo).BlockNumber.Uint64() { - return nil - } - if blockNumber > bl.canonicalChain.Back().Value.(*ffcapi.MinimalBlockInfo).BlockNumber.Uint64() { - return nil - } - for nextInMemoryBlock := bl.canonicalChain.Front(); nextInMemoryBlock != nil; nextInMemoryBlock = nextInMemoryBlock.Next() { - nextInMemoryBlockInfo := nextInMemoryBlock.Value.(*ffcapi.MinimalBlockInfo) - if nextInMemoryBlockInfo.BlockNumber.Uint64() == blockNumber { - return nextInMemoryBlockInfo - } - } - return nil -} - func validateExistingConfirmations(ctx context.Context, existingConfirmations []*ffcapi.MinimalBlockInfo) error { var previousBlock *ffcapi.MinimalBlockInfo - var previousBlockNumber uint64 for _, existingConfirmation := range existingConfirmations { if previousBlock != nil { - if existingConfirmation.BlockNumber.Uint64() != previousBlockNumber+1 { - return i18n.NewError(ctx, msgs.MsgFailedToBuildConfirmationQueue) - } if !previousBlock.IsParentOf(existingConfirmation) { - return i18n.NewError(ctx, msgs.MsgFailedToBuildConfirmationQueue) + return i18n.NewError(ctx, msgs.MsgFailedToBuildExistingConfirmationInvalid) } } previousBlock = existingConfirmation - previousBlockNumber = existingConfirmation.BlockNumber.Uint64() } return nil } -func (bl *blockListener) handleSpecialCases(ctx context.Context, existingConfirmations []*ffcapi.MinimalBlockInfo, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64, reconcileResult *ffcapi.ConfirmationUpdateResult) (*ffcapi.ConfirmationUpdateResult, error) { - if targetConfirmationCount == 0 { - // if the target confirmation count is 0, we can immediately return a confirmed result - reconcileResult.Confirmed = true - reconcileResult.Confirmations = []*ffcapi.MinimalBlockInfo{txBlockInfo} - return reconcileResult, nil - } - - if uint64(len(existingConfirmations)) > targetConfirmationCount { - if existingConfirmations[0].Equal(txBlockInfo) { - // start of the existing confirmations aligns with our latest view of the chain - - // attempt to validate the rest of the existing confirmations. Starting with the target confirmation block and working forwards - - attemptIndex := targetConfirmationCount - - for attemptIndex < uint64(len(existingConfirmations))-1 { - existingConfirmationToValidate := existingConfirmations[attemptIndex] - blockInfo := bl.getBlockByNumberFromInMemoryPartialChain(ctx, existingConfirmationToValidate.BlockNumber.Uint64()) - if blockInfo == nil { - attemptIndex++ - continue - } - if blockInfo.BlockHash == existingConfirmationToValidate.BlockHash { - // the last block in the existing confirmations queue is still valid - reconcileResult.Confirmed = true - reconcileResult.Confirmations = existingConfirmations[:targetConfirmationCount+1] - return reconcileResult, 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 + chainTail := bl.canonicalChain.Back().Value.(*ffcapi.MinimalBlockInfo) + if chainTail.BlockNumber.Uint64() >= txBlockInfo.BlockNumber.Uint64() { + return &ffcapi.ConfirmationUpdateResult{ + Confirmed: true, + Confirmations: []*ffcapi.MinimalBlockInfo{txBlockInfo}, + }, nil + } + log.L(ctx).Debugf("in-memory partial chain is waiting for the transaction block %d (%s) to be indexed", txBlockInfo.BlockNumber.Uint64(), txBlockInfo.BlockHash) + return nil, i18n.NewError(ctx, msgs.MsgInMemoryPartialChainNotCaughtUp, txBlockInfo.BlockNumber.Uint64(), txBlockInfo.BlockHash) +} - // if we didn't get a conclusive answer from the in memory cache of the chain, we need to fetch the block from the blockchain - targetBlock := existingConfirmations[targetConfirmationCount] - blockInfo, _, err := bl.getBlockInfoByNumber(ctx, targetBlock.BlockNumber.Uint64(), false, "", "") - if err != nil { - return nil, err - } +func (bl *blockListener) handleTargetConfirmationCountLessThanExistingConfirmationsWithOverlap(ctx context.Context, existingConfirmations []*ffcapi.MinimalBlockInfo, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) (*ffcapi.ConfirmationUpdateResult, error) { + 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 { + 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 + if targetConfirmationCount < uint64(len(existingConfirmations)) { + return &ffcapi.ConfirmationUpdateResult{ + Confirmed: true, + Confirmations: existingConfirmations[:targetConfirmationCount+1], + }, nil + } + // only the existing confirmations are not enough, need to fetch more blocks from the in memory partial chain + newList := existingConfirmations + targetBlockNumber := txBlockInfo.BlockNumber.Uint64() + targetConfirmationCount - if blockInfo.Hash.String() == targetBlock.BlockHash { - reconcileResult.Confirmed = true - reconcileResult.Confirmations = existingConfirmations[:targetConfirmationCount+1] - return reconcileResult, nil + for nextInMemoryBlock := bl.canonicalChain.Front(); nextInMemoryBlock != nil; nextInMemoryBlock = nextInMemoryBlock.Next() { + nextInMemoryBlockInfo := nextInMemoryBlock.Value.(*ffcapi.MinimalBlockInfo) + if nextInMemoryBlockInfo.BlockNumber.Uint64() > targetBlockNumber { + break } + newList = append(newList, nextInMemoryBlockInfo) } + return &ffcapi.ConfirmationUpdateResult{ + Confirmed: uint64(len(newList)) > targetConfirmationCount, + Confirmations: newList, + }, nil } - return reconcileResult, nil + return nil, nil +} + +// handleSpecialCases contains the logic that cannot be included in the main algorithm because they reduce the readability of the code +func (bl *blockListener) handleSpecialCases(ctx context.Context, existingConfirmations []*ffcapi.MinimalBlockInfo, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) (*ffcapi.ConfirmationUpdateResult, error) { + if targetConfirmationCount == 0 { + return bl.handleZeroTargetConfirmationCount(ctx, txBlockInfo) + } + + if len(existingConfirmations) > 0 && existingConfirmations[len(existingConfirmations)-1].BlockNumber.Uint64()+1 >= txBlockInfo.BlockNumber.Uint64()+targetConfirmationCount { + return bl.handleTargetConfirmationCountLessThanExistingConfirmationsWithOverlap(ctx, existingConfirmations, txBlockInfo, targetConfirmationCount) + } + return nil, nil } diff --git a/internal/ethereum/confirmation_reconciler_test.go b/internal/ethereum/confirmation_reconciler_test.go index 7f993b2..ba82363 100644 --- a/internal/ethereum/confirmation_reconciler_test.go +++ b/internal/ethereum/confirmation_reconciler_test.go @@ -229,9 +229,139 @@ func TestReconcileConfirmationsForTransaction_NewConfirmation(t *testing.T) { mRPC.AssertExpectations(t) } -// Tests of the compareAndUpdateConfirmationQueue function +func TestBuildConfirmationList_GapInExistingConfirmationsError(t *testing.T) { + // Setup + _, c, mRPC, _ := newTestConnectorWithNoBlockerFilterDefaultMocks(t) + + txHash := generateTestHash(100) + // Mock for TransactionReceipt call - return error to simulate RPC call error + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getTransactionReceipt", txHash).Return(&rpcbackend.RPCError{Message: "pop"}).Run(func(args mock.Arguments) { + err := json.Unmarshal([]byte("null"), args[1]) + assert.NoError(t, err) + }) + ctx := context.Background() + existingQueue := []*ffcapi.MinimalBlockInfo{ + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, + } + + targetConfirmationCount := uint64(5) + + // Execute + confirmationUpdateResult, err := c.ReconcileConfirmationsForTransaction(ctx, txHash, existingQueue, targetConfirmationCount) + assert.Error(t, err) + assert.Nil(t, confirmationUpdateResult) + assert.Regexp(t, "FF23063", err.Error()) + +} + +func TestBuildConfirmationList_MismatchConfirmationBlockError(t *testing.T) { + // Setup + _, c, mRPC, _ := newTestConnectorWithNoBlockerFilterDefaultMocks(t) + + txHash := generateTestHash(100) + // Mock for TransactionReceipt call - return error to simulate RPC call error + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getTransactionReceipt", txHash).Return(&rpcbackend.RPCError{Message: "pop"}).Run(func(args mock.Arguments) { + err := json.Unmarshal([]byte("null"), args[1]) + assert.NoError(t, err) + }) + ctx := context.Background() + 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 + {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, + {BlockHash: generateTestHash(103), BlockNumber: fftypes.FFuint64(103), ParentHash: generateTestHash(102)}, + } + + targetConfirmationCount := uint64(5) + + // Execute + confirmationUpdateResult, err := c.ReconcileConfirmationsForTransaction(ctx, txHash, existingQueue, targetConfirmationCount) + assert.Error(t, err) + assert.Nil(t, confirmationUpdateResult) + assert.Regexp(t, "FF23063", err.Error()) +} + +func TestBuildConfirmationList_ExistingConfirmationLaterThanCurrentBlockError(t *testing.T) { + // Setup + _, c, mRPC, _ := newTestConnectorWithNoBlockerFilterDefaultMocks(t) + + txHash := generateTestHash(100) + // Mock for TransactionReceipt call - return error to simulate RPC call error + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getTransactionReceipt", txHash).Return(&rpcbackend.RPCError{Message: "pop"}).Run(func(args mock.Arguments) { + err := json.Unmarshal([]byte("null"), args[1]) + assert.NoError(t, err) + }) + ctx := context.Background() + existingQueue := []*ffcapi.MinimalBlockInfo{ + + {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, + {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, + } + + targetConfirmationCount := uint64(5) + + // Execute + confirmationUpdateResult, err := c.ReconcileConfirmationsForTransaction(ctx, txHash, existingQueue, targetConfirmationCount) + assert.Error(t, err) + assert.Nil(t, confirmationUpdateResult) + assert.Regexp(t, "FF23063", err.Error()) +} + +func TestBuildConfirmationList_ExistingTxBockInfoIsWrongError(t *testing.T) { + // Setup + _, c, mRPC, _ := newTestConnectorWithNoBlockerFilterDefaultMocks(t) + + txHash := generateTestHash(100) + // Mock for TransactionReceipt call - return error to simulate RPC call error + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getTransactionReceipt", txHash).Return(&rpcbackend.RPCError{Message: "pop"}).Run(func(args mock.Arguments) { + err := json.Unmarshal([]byte("null"), args[1]) + assert.NoError(t, err) + }) + ctx := context.Background() + 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)}, + } + + targetConfirmationCount := uint64(5) + + // Execute + confirmationUpdateResult, err := c.ReconcileConfirmationsForTransaction(ctx, txHash, existingQueue, targetConfirmationCount) + assert.Error(t, err) + assert.Nil(t, confirmationUpdateResult) + assert.Regexp(t, "FF23063", err.Error()) +} + +func TestReconcileConfirmationsForTransaction_ExistingConfirmationsWithLowerBlockNumberError(t *testing.T) { + // Setup + _, c, mRPC, _ := newTestConnectorWithNoBlockerFilterDefaultMocks(t) + + txHash := generateTestHash(100) + // Mock for TransactionReceipt call - return error to simulate RPC call error + mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getTransactionReceipt", txHash).Return(&rpcbackend.RPCError{Message: "pop"}).Run(func(args mock.Arguments) { + err := json.Unmarshal([]byte("null"), args[1]) + assert.NoError(t, err) + }) + ctx := context.Background() + 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 + } + + targetConfirmationCount := uint64(5) + + // Execute + confirmationUpdateResult, err := c.ReconcileConfirmationsForTransaction(ctx, txHash, existingQueue, targetConfirmationCount) + assert.Error(t, err) + assert.Nil(t, confirmationUpdateResult) + assert.Regexp(t, "FF23063", err.Error()) +} -func TestCompareAndUpdateConfirmationQueue_EmptyChain(t *testing.T) { +// 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() @@ -254,7 +384,7 @@ func TestCompareAndUpdateConfirmationQueue_EmptyChain(t *testing.T) { assert.Nil(t, confirmationUpdateResult) } -func TestCompareAndUpdateConfirmationQueue_ChainTooShort(t *testing.T) { +func TestBuildConfirmationList_ChainTooShort(t *testing.T) { // Setup bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 99, []uint64{}) defer done() @@ -276,7 +406,7 @@ func TestCompareAndUpdateConfirmationQueue_ChainTooShort(t *testing.T) { assert.Nil(t, confirmationUpdateResult) } -func TestCompareAndUpdateConfirmationQueue_NilConfirmationMap(t *testing.T) { +func TestBuildConfirmationList_NilConfirmationMap(t *testing.T) { // Setup bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 150, []uint64{}) defer done() @@ -307,7 +437,7 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMap(t *testing.T) { } -func TestCompareAndUpdateConfirmationQueue_NilConfirmationMap_ZeroConfirmationCount(t *testing.T) { +func TestBuildConfirmationList_NilConfirmationMap_ZeroConfirmationCount(t *testing.T) { // Setup bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 150, []uint64{}) defer done() @@ -333,9 +463,9 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMap_ZeroConfirmationCo assert.Equal(t, txBlockNumber, uint64(confirmationUpdateResult.Confirmations[0].BlockNumber)) } -func TestCompareAndUpdateConfirmationQueue_NilConfirmationMapUnconfirmed(t *testing.T) { +func TestBuildConfirmationList_NilConfirmationMap_ZeroConfirmationCountError(t *testing.T) { // Setup - bl, done := newBlockListenerWithTestChain(t, 100, 5, 100, 104, []uint64{}) + bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 99, []uint64{}) defer done() ctx := context.Background() txBlockNumber := uint64(100) @@ -345,31 +475,20 @@ func TestCompareAndUpdateConfirmationQueue_NilConfirmationMapUnconfirmed(t *test BlockHash: txBlockHash, ParentHash: generateTestHash(txBlockNumber - 1), } - targetConfirmationCount := uint64(5) + 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.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)) - + assert.Error(t, err) + assert.Nil(t, confirmationUpdateResult) + assert.Regexp(t, "FF23062", err.Error()) } -func TestCompareAndUpdateConfirmationQueue_EmptyConfirmationQueue(t *testing.T) { +func TestBuildConfirmationList_NilConfirmationMapUnconfirmed(t *testing.T) { // Setup - bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 150, []uint64{}) + bl, done := newBlockListenerWithTestChain(t, 100, 5, 100, 104, []uint64{}) defer done() ctx := context.Background() - txBlockNumber := uint64(100) txBlockHash := generateTestHash(txBlockNumber) txBlockInfo := &ffcapi.MinimalBlockInfo{ @@ -380,47 +499,43 @@ func TestCompareAndUpdateConfirmationQueue_EmptyConfirmationQueue(t *testing.T) targetConfirmationCount := uint64(5) // Execute - confirmationUpdateResult, err := bl.buildConfirmationList(ctx, []*ffcapi.MinimalBlockInfo{}, txBlockInfo, targetConfirmationCount) + 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, 6) + 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)) - assert.Equal(t, txBlockNumber+5, uint64(confirmationUpdateResult.Confirmations[5].BlockNumber)) + } -func TestCompareAndUpdateConfirmationQueue_MismatchConfirmationBlock(t *testing.T) { - t.Skip("Skipping test because I don't understand this behavior") //Trying to understand the reasoning behind supporting an invalid list of existing confirmations. Should this not just be an error? +func TestBuildConfirmationList_EmptyConfirmationQueue(t *testing.T) { // Setup - bl, done := newBlockListenerWithTestChain(t, 100, 5, 103, 150, []uint64{101}) + 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(999), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, // wrong hash, so the block should be fetched - {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) + txBlockHash := generateTestHash(txBlockNumber) txBlockInfo := &ffcapi.MinimalBlockInfo{ BlockNumber: fftypes.FFuint64(txBlockNumber), BlockHash: txBlockHash, - ParentHash: generateTestHash(99), + ParentHash: generateTestHash(txBlockNumber - 1), } targetConfirmationCount := uint64(5) // Execute - confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, []*ffcapi.MinimalBlockInfo{}, txBlockInfo, targetConfirmationCount) assert.NoError(t, err) // Assert - assert.True(t, confirmationUpdateResult.NewFork) + 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)) @@ -431,7 +546,7 @@ func TestCompareAndUpdateConfirmationQueue_MismatchConfirmationBlock(t *testing. assert.Equal(t, txBlockNumber+5, uint64(confirmationUpdateResult.Confirmations[5].BlockNumber)) } -func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsTooDistant(t *testing.T) { +func TestBuildConfirmationList_ExistingConfirmationsTooDistant(t *testing.T) { // Setup bl, done := newBlockListenerWithTestChain(t, 100, 5, 145, 150, []uint64{102, 103, 104, 105}) @@ -465,7 +580,7 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsTooDistant(t *te assert.Equal(t, txBlockNumber+5, uint64(confirmationUpdateResult.Confirmations[5].BlockNumber)) } -func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmationDoNotAffectConfirmations(t *testing.T) { +func TestBuildConfirmationList_CorruptedExistingConfirmationDoNotAffectConfirmations(t *testing.T) { // Setup bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 150, []uint64{}) defer done() @@ -500,7 +615,7 @@ func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmationDoNotAff assert.Equal(t, txBlockNumber+5, uint64(confirmationUpdateResult.Confirmations[5].BlockNumber)) } -func TestCompareAndUpdateConfirmationQueue_ConnectionNodeMismatch(t *testing.T) { +func TestBuildConfirmationList_ConnectionNodeMismatch(t *testing.T) { // Setup bl, done := newBlockListenerWithTestChain(t, 100, 5, 102, 150, []uint64{101}) defer done() @@ -535,7 +650,7 @@ func TestCompareAndUpdateConfirmationQueue_ConnectionNodeMismatch(t *testing.T) assert.Equal(t, txBlockNumber+5, uint64(confirmationUpdateResult.Confirmations[5].BlockNumber)) } -func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmationAfterFirstConfirmation(t *testing.T) { +func TestBuildConfirmationList_CorruptedExistingConfirmationAfterFirstConfirmation(t *testing.T) { // Setup bl, done := newBlockListenerWithTestChain(t, 100, 5, 100, 150, []uint64{}) defer done() @@ -570,7 +685,7 @@ func TestCompareAndUpdateConfirmationQueue_CorruptedExistingConfirmationAfterFir assert.Equal(t, txBlockNumber+5, uint64(confirmationUpdateResult.Confirmations[5].BlockNumber)) } -func TestCompareAndUpdateConfirmationQueue_FailedToFetchBlockInfo(t *testing.T) { +func TestBuildConfirmationList_FailedToFetchBlockInfo(t *testing.T) { // Setup mRPC := &rpcbackendmocks.Backend{} bl := &blockListener{ @@ -606,7 +721,7 @@ func TestCompareAndUpdateConfirmationQueue_FailedToFetchBlockInfo(t *testing.T) assert.Nil(t, confirmationUpdateResult) } -func TestCompareAndUpdateConfirmationQueue_NilBlockInfo(t *testing.T) { +func TestBuildConfirmationList_NilBlockInfo(t *testing.T) { // Setup mRPC := &rpcbackendmocks.Backend{} bl := &blockListener{ @@ -643,7 +758,7 @@ func TestCompareAndUpdateConfirmationQueue_NilBlockInfo(t *testing.T) { assert.Nil(t, confirmationUpdateResult) } -func TestCompareAndUpdateConfirmationQueue_NewForkAfterFirstConfirmation(t *testing.T) { +func TestBuildConfirmationList_NewForkAfterFirstConfirmation(t *testing.T) { // Setup bl, done := newBlockListenerWithTestChain(t, 100, 5, 100, 150, []uint64{}) defer done() @@ -672,7 +787,7 @@ func TestCompareAndUpdateConfirmationQueue_NewForkAfterFirstConfirmation(t *test assert.Len(t, confirmationUpdateResult.Confirmations, 6) } -func TestCompareAndUpdateConfirmationQueue_NewForkAfterFirstConfirmation_ZeroConfirmationCount(t *testing.T) { +func TestBuildConfirmationList_NewForkAfterFirstConfirmation_ZeroConfirmationCount(t *testing.T) { // Setup bl, done := newBlockListenerWithTestChain(t, 100, 5, 100, 150, []uint64{}) defer done() @@ -701,7 +816,7 @@ func TestCompareAndUpdateConfirmationQueue_NewForkAfterFirstConfirmation_ZeroCon assert.Len(t, confirmationUpdateResult.Confirmations, 1) } -func TestCompareAndUpdateConfirmationQueue_NewForkAndNoConnectionToCanonicalChain(t *testing.T) { +func TestBuildConfirmationList_NewForkAndNoConnectionToCanonicalChain(t *testing.T) { // Setup bl, done := newBlockListenerWithTestChain(t, 100, 5, 103, 150, []uint64{101, 102}) defer done() @@ -730,17 +845,16 @@ func TestCompareAndUpdateConfirmationQueue_NewForkAndNoConnectionToCanonicalChai assert.Len(t, confirmationUpdateResult.Confirmations, 6) } -func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationLaterThanCurrentBlock(t *testing.T) { - t.Skip("Skipping test because I don't understand this behavior") //Trying to understand the reasoning behind supporting an invalid list of existing confirmations. Should this not just be an error? - +func TestBuildConfirmationList_ConfirmWithNoFetches(t *testing.T) { // Setup - bl, done := newBlockListenerWithTestChain(t, 100, 5, 100, 150, []uint64{}) + 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(102), BlockNumber: fftypes.FFuint64(102), ParentHash: generateTestHash(101)}, + {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, } txBlockNumber := uint64(100) txBlockHash := generateTestHash(100) @@ -749,60 +863,21 @@ func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationLaterThanCurrentB BlockHash: txBlockHash, ParentHash: generateTestHash(99), } - targetConfirmationCount := uint64(5) + targetConfirmationCount := uint64(2) // 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 TestCompareAndUpdateConfirmationQueue_ExistingTxBockInfoIsWrong(t *testing.T) { - // Setup - t.Skip("Skipping test because I don't understand this behavior") //Trying to understand the reasoning behind supporting an invalid list of existing confirmations. Should this not just be an error? - bl, done := newBlockListenerWithTestChain(t, 100, 5, 100, 150, []uint64{}) - defer done() - - ctx := context.Background() - existingQueue := []*ffcapi.MinimalBlockInfo{ - {BlockHash: generateTestHash(999), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, // should be corrected - {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.Len(t, confirmationUpdateResult.Confirmations, 3) assert.Equal(t, txBlockNumber, uint64(confirmationUpdateResult.Confirmations[0].BlockNumber)) - assert.Equal(t, generateTestHash(100), confirmationUpdateResult.Confirmations[0].BlockHash) 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 TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable(t *testing.T) { +func TestBuildConfirmationList_AlreadyConfirmable(t *testing.T) { // Setup bl, done := newBlockListenerWithTestChain(t, 100, 5, 103, 150, []uint64{}) defer done() @@ -814,10 +889,8 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable(t *testing.T) { {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 + {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) @@ -840,7 +913,7 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable(t *testing.T) { assert.Equal(t, txBlockNumber+2, uint64(confirmationUpdateResult.Confirmations[2].BlockNumber)) } -func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable_ZeroConfirmationCount(t *testing.T) { +func TestBuildConfirmationList_AlreadyConfirmable_ZeroConfirmationCount(t *testing.T) { // Setup bl, done := newBlockListenerWithTestChain(t, 100, 5, 103, 150, []uint64{}) defer done() @@ -876,9 +949,9 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmable_ZeroConfirmationCo assert.Equal(t, txBlockNumber, uint64(confirmationUpdateResult.Confirmations[0].BlockNumber)) } -func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableConnectable(t *testing.T) { +func TestBuildConfirmationList_AlreadyConfirmableConnectable(t *testing.T) { // Setup - bl, done := newBlockListenerWithTestChain(t, 100, 5, 103, 150, []uint64{101}) + bl, done := newBlockListenerWithTestChain(t, 100, 5, 103, 150, []uint64{}) defer done() ctx := context.Background() // Create confirmations that already meet the target @@ -912,44 +985,7 @@ func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableConnectable(t *test assert.Equal(t, txBlockNumber+1, uint64(confirmationUpdateResult.Confirmations[1].BlockNumber)) } -func TestCompareAndUpdateConfirmationQueue_AlreadyConfirmableButAllExistingConfirmationsAreTooHighForTargetConfirmationCount(t *testing.T) { - t.Skip("Skipping test because I don't understand this behavior") //Trying to understand the reasoning behind supporting an invalid list of existing confirmations. Should this not just be an error? - - // Setup - bl, done := newBlockListenerWithTestChain(t, 100, 5, 103, 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)}, - // 101 will be fetched from the JSON-RPC endpoint to fill the gap - {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 - // 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 TestCompareAndUpdateConfirmationQueue_HasSufficientConfirmationsButNoOverlapWithCanonicalChain(t *testing.T) { +func TestBuildConfirmationList_HasSufficientConfirmationsButNoOverlapWithCanonicalChain(t *testing.T) { // Setup bl, done := newBlockListenerWithTestChain(t, 100, 5, 104, 150, []uint64{101}) defer done() @@ -985,7 +1021,7 @@ func TestCompareAndUpdateConfirmationQueue_HasSufficientConfirmationsButNoOverla } -func TestCompareAndUpdateConfirmationQueue_ValidExistingConfirmations(t *testing.T) { +func TestBuildConfirmationList_ValidExistingConfirmations(t *testing.T) { // Setup bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 150, []uint64{}) defer done() @@ -1020,7 +1056,7 @@ func TestCompareAndUpdateConfirmationQueue_ValidExistingConfirmations(t *testing assert.Equal(t, txBlockNumber+5, uint64(confirmationUpdateResult.Confirmations[5].BlockNumber)) } -func TestCompareAndUpdateConfirmationQueue_ValidExistingTxBlock(t *testing.T) { +func TestBuildConfirmationList_ValidExistingTxBlock(t *testing.T) { // Setup bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 150, []uint64{}) defer done() @@ -1053,7 +1089,7 @@ func TestCompareAndUpdateConfirmationQueue_ValidExistingTxBlock(t *testing.T) { assert.Equal(t, txBlockNumber+5, uint64(confirmationUpdateResult.Confirmations[5].BlockNumber)) } -func TestCompareAndUpdateConfirmationQueue_ReachTargetConfirmation(t *testing.T) { +func TestBuildConfirmationList_ReachTargetConfirmation(t *testing.T) { // Setup bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 150, []uint64{}) defer done() @@ -1077,100 +1113,6 @@ func TestCompareAndUpdateConfirmationQueue_ReachTargetConfirmation(t *testing.T) assert.GreaterOrEqual(t, len(confirmationUpdateResult.Confirmations), 4) // tx block + 3 confirmations } -func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithGap(t *testing.T) { - t.Skip("Skipping test because I don't understand this behavior") //Trying to understand the reasoning behind supporting an invalid list of existing confirmations. Should this not just be an error? - - // Setup - bl, done := newBlockListenerWithTestChain(t, 100, 5, 101, 150, []uint64{}) - defer done() - ctx := context.Background() - // Create confirmations with a gap (missing block 102) - existingQueue := []*ffcapi.MinimalBlockInfo{ - {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, - // no block 101, which is the first block of the canonical chain, so no fetch to JSON-RPC endpoint is needed - {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.False(t, confirmationUpdateResult.NewFork) - assert.True(t, confirmationUpdateResult.Confirmed) - assert.Len(t, confirmationUpdateResult.Confirmations, 6) -} - -func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNumber(t *testing.T) { - t.Skip("Skipping test because I don't understand this behavior") //Trying to understand the reasoning behind supporting an invalid list of existing confirmations. Should this not just be an error? - - // Setup - bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 150, []uint64{}) - defer done() - ctx := context.Background() - // Create confirmations with a lower block number - existingQueue := []*ffcapi.MinimalBlockInfo{ - {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, - {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(99), ParentHash: generateTestHash(100)}, // somehow there is a lower block number - } - 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) -} - -func TestCompareAndUpdateConfirmationQueue_ExistingConfirmationsWithLowerBlockNumberAfterFirstConfirmation(t *testing.T) { - t.Skip("Skipping test because I don't understand this behavior") //Trying to understand the reasoning behind supporting an invalid list of existing confirmations. Should this not just be an error? - - // Setup - bl, done := newBlockListenerWithTestChain(t, 100, 5, 101, 150, []uint64{}) - defer done() - ctx := context.Background() - // Create confirmations with a lower block number - existingQueue := []*ffcapi.MinimalBlockInfo{ - {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, - {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, - {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(99), ParentHash: generateTestHash(101)}, // somehow there is a lower block number - } - - 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) -} - func TestValidateExistingConfirmations_LowerBlockNumber(t *testing.T) { ctx := context.Background() @@ -1216,146 +1158,6 @@ func TestValidateExistingConfirmations_BrokenHash(t *testing.T) { assert.Error(t, err) } -/* -func TestCheckAndFillInGap_GetBlockInfoError(t *testing.T) { - // Setup - _, c, mRPC, _ := newTestConnectorWithNoBlockerFilterDefaultMocks(t) - ctx := context.Background() - txBlockNumber := uint64(100) - txBlockInfo := &ffcapi.MinimalBlockInfo{ - BlockNumber: fftypes.FFuint64(txBlockNumber), - BlockHash: generateTestHash(txBlockNumber), - ParentHash: generateTestHash(txBlockNumber - 1), - } - targetConfirmationCount := uint64(2) - newConfirmationsWithoutTxBlock := []*ffcapi.MinimalBlockInfo{} - existingConfirmations := []*ffcapi.MinimalBlockInfo{} - - // Mock RPC to return an error for the gap blocks (blockNumberToReach = 100+2 = 102, then loops 102 down to 101) - mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByNumber", mock.MatchedBy(func(bn *ethtypes.HexInteger) bool { - return bn.BigInt().String() == strconv.FormatUint(102, 10) - }), false).Return(&rpcbackend.RPCError{Message: "pop"}) - - // Execute - result, hasNewFork, err := c.blockListener.checkAndFillInGap(ctx, newConfirmationsWithoutTxBlock, existingConfirmations, txBlockInfo, targetConfirmationCount, nil) - - // Assertions - assert.Error(t, err) - assert.Contains(t, err.Error(), "pop") - assert.False(t, hasNewFork) - assert.Nil(t, result) - mRPC.AssertExpectations(t) -} - -func TestCheckAndFillInGap_BlockNotAvailable(t *testing.T) { - // Setup - _, c, mRPC, _ := newTestConnectorWithNoBlockerFilterDefaultMocks(t) - ctx := context.Background() - txBlockNumber := uint64(100) - txBlockInfo := &ffcapi.MinimalBlockInfo{ - BlockNumber: fftypes.FFuint64(txBlockNumber), - BlockHash: generateTestHash(txBlockNumber), - ParentHash: generateTestHash(txBlockNumber - 1), - } - targetConfirmationCount := uint64(2) - newConfirmationsWithoutTxBlock := []*ffcapi.MinimalBlockInfo{} - existingConfirmations := []*ffcapi.MinimalBlockInfo{} - - // Setup RPC calls - return nil to simulate block not available (blockNumberToReach = 100+2 = 102) - mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByNumber", mock.MatchedBy(func(bn *ethtypes.HexInteger) bool { - return bn.BigInt().String() == strconv.FormatUint(102, 10) - }), false).Return(nil).Run(func(args mock.Arguments) { - *args[1].(**blockInfoJSONRPC) = nil // Block not available - }) - - // Execute - result, hasNewFork, err := c.blockListener.checkAndFillInGap(ctx, newConfirmationsWithoutTxBlock, existingConfirmations, txBlockInfo, targetConfirmationCount, nil) - - // Assertions - assert.Error(t, err) - assert.Contains(t, err.Error(), "Block not available") - assert.False(t, hasNewFork) - assert.Nil(t, result) - mRPC.AssertExpectations(t) -} - -func TestCheckAndFillInGap_InvalidBlockParentRelationship(t *testing.T) { - // Setup - _, c, mRPC, _ := newTestConnectorWithNoBlockerFilterDefaultMocks(t) - ctx := context.Background() - txBlockNumber := uint64(100) - txBlockInfo := &ffcapi.MinimalBlockInfo{ - BlockNumber: fftypes.FFuint64(txBlockNumber), - BlockHash: generateTestHash(txBlockNumber), - ParentHash: generateTestHash(txBlockNumber - 1), - } - targetConfirmationCount := uint64(2) - - // Setup test scenario where we have a previous block but fetch a block that doesn't connect - block102 := &ffcapi.MinimalBlockInfo{ - BlockNumber: fftypes.FFuint64(102), - BlockHash: generateTestHash(102), - ParentHash: "wrong_parent_hash", // Wrong parent - should cause validation to fail - } - - newConfirmationsWithoutTxBlock := []*ffcapi.MinimalBlockInfo{block102} // Start with block 102 - existingConfirmations := []*ffcapi.MinimalBlockInfo{} - - // Mock getBlockInfoByNumber calls for gap block 101 (blockNumberToReach = block102.BlockNumber - 1 = 101) - mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getBlockByNumber", mock.MatchedBy(func(bn *ethtypes.HexInteger) bool { - return bn.BigInt().String() == strconv.FormatUint(101, 10) - }), false).Return(nil).Run(func(args mock.Arguments) { - *args[1].(**blockInfoJSONRPC) = &blockInfoJSONRPC{ - Number: ethtypes.NewHexInteger64(101), - Hash: ethtypes.MustNewHexBytes0xPrefix(generateTestHash(101)), - ParentHash: ethtypes.MustNewHexBytes0xPrefix(generateTestHash(99)), // Wrong parent - should cause validation to fail - } - }) - - // Execute - result, hasNewFork, err := c.blockListener.checkAndFillInGap(ctx, newConfirmationsWithoutTxBlock, existingConfirmations, txBlockInfo, targetConfirmationCount, nil) - - // Assertions - should fail because block 102 has wrong parent hash and doesn't connect to block 101 - assert.Error(t, err) - assert.Contains(t, err.Error(), "Failed to build confirmation queue") - assert.False(t, hasNewFork) - assert.Nil(t, result) - mRPC.AssertExpectations(t) -} - -func TestCheckAndFillInGap_TxBlockNotParentOfFirstConfirmation(t *testing.T) { - // Setup - _, c, mRPC, _ := newTestConnectorWithNoBlockerFilterDefaultMocks(t) - ctx := context.Background() - txBlockNumber := uint64(100) - txBlockInfo := &ffcapi.MinimalBlockInfo{ - BlockNumber: fftypes.FFuint64(txBlockNumber), - BlockHash: generateTestHash(txBlockNumber), - ParentHash: generateTestHash(txBlockNumber - 1), - } - targetConfirmationCount := uint64(1) - - // Setup test scenario where txBlockInfo is NOT parent of the first confirmation block - wrongParentConfirmation := &ffcapi.MinimalBlockInfo{ - BlockNumber: fftypes.FFuint64(101), - BlockHash: generateTestHash(101), - ParentHash: "wrong_parent_hash", // This doesn't match txBlockInfo.BlockHash - } - - newConfirmationsWithoutTxBlock := []*ffcapi.MinimalBlockInfo{wrongParentConfirmation} - existingConfirmations := []*ffcapi.MinimalBlockInfo{} - - // Execute - result, hasNewFork, err := c.blockListener.checkAndFillInGap(ctx, newConfirmationsWithoutTxBlock, existingConfirmations, txBlockInfo, targetConfirmationCount, nil) - - // Assertions - assert.Error(t, err) - assert.Contains(t, err.Error(), "Failed to build confirmation queue") - assert.False(t, hasNewFork) - assert.Nil(t, result) - mRPC.AssertExpectations(t) -} -*/ // Helper functions // generateTestHash creates a predictable hash for testing with consistent prefix and last 4 digits as index diff --git a/internal/msgs/en_error_messages.go b/internal/msgs/en_error_messages.go index 9bf26c4..2bb98ec 100644 --- a/internal/msgs/en_error_messages.go +++ b/internal/msgs/en_error_messages.go @@ -27,55 +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'") - 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 queue") - 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") + 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") ) From ce65dafc9e0856f3ea78b5a3b5334e243f01e8ed Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Fri, 24 Oct 2025 15:11:12 +0100 Subject: [PATCH 24/37] rename Signed-off-by: Chengxuan Xing --- internal/ethereum/confirmation_reconciler.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/ethereum/confirmation_reconciler.go b/internal/ethereum/confirmation_reconciler.go index 4536eb9..d57c5fb 100644 --- a/internal/ethereum/confirmation_reconciler.go +++ b/internal/ethereum/confirmation_reconciler.go @@ -360,7 +360,7 @@ func (bl *blockListener) handleZeroTargetConfirmationCount(ctx context.Context, return nil, i18n.NewError(ctx, msgs.MsgInMemoryPartialChainNotCaughtUp, txBlockInfo.BlockNumber.Uint64(), txBlockInfo.BlockHash) } -func (bl *blockListener) handleTargetConfirmationCountLessThanExistingConfirmationsWithOverlap(ctx context.Context, existingConfirmations []*ffcapi.MinimalBlockInfo, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) (*ffcapi.ConfirmationUpdateResult, error) { +func (bl *blockListener) handleTargetCountLessThanExistingConfirmationLength(ctx context.Context, existingConfirmations []*ffcapi.MinimalBlockInfo, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) (*ffcapi.ConfirmationUpdateResult, error) { bl.mux.RLock() defer bl.mux.RUnlock() nextInMemoryBlock := bl.canonicalChain.Front() @@ -409,7 +409,7 @@ func (bl *blockListener) handleSpecialCases(ctx context.Context, existingConfirm } if len(existingConfirmations) > 0 && existingConfirmations[len(existingConfirmations)-1].BlockNumber.Uint64()+1 >= txBlockInfo.BlockNumber.Uint64()+targetConfirmationCount { - return bl.handleTargetConfirmationCountLessThanExistingConfirmationsWithOverlap(ctx, existingConfirmations, txBlockInfo, targetConfirmationCount) + return bl.handleTargetCountLessThanExistingConfirmationLength(ctx, existingConfirmations, txBlockInfo, targetConfirmationCount) } return nil, nil } From 4dd1b1c185a2a7d252237990bdd657f2f22057d4 Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Fri, 24 Oct 2025 16:31:26 +0100 Subject: [PATCH 25/37] address test coverage Signed-off-by: Chengxuan Xing --- internal/ethereum/confirmation_reconciler.go | 38 ++------- .../ethereum/confirmation_reconciler_test.go | 84 +++++++++++-------- 2 files changed, 53 insertions(+), 69 deletions(-) diff --git a/internal/ethereum/confirmation_reconciler.go b/internal/ethereum/confirmation_reconciler.go index d57c5fb..171531b 100644 --- a/internal/ethereum/confirmation_reconciler.go +++ b/internal/ethereum/confirmation_reconciler.go @@ -79,7 +79,7 @@ func (bl *blockListener) buildConfirmationList(ctx context.Context, existingConf // - 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) - lateList, err := createLateList(ctx, txBlockInfo, targetConfirmationCount, reconcileResult, bl) + lateList, err := createLateList(ctx, txBlockInfo, targetConfirmationCount, bl) if err != nil { return nil, err } @@ -140,9 +140,6 @@ func newSplice(earlyList []*ffcapi.MinimalBlockInfo, lateList []*ffcapi.MinimalB earlyList: earlyList, lateList: lateList, } - if len(s.earlyList) == 0 || len(s.lateList) == 0 { - return s, false - } 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() @@ -178,11 +175,6 @@ func (s *splice) isEarlyListEmpty() bool { } func (s *splice) fillOneGap(ctx context.Context, blockListener *blockListener) error { - if !s.hasGap() { - // no gap to fill - return nil - } - // 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 @@ -191,9 +183,6 @@ func (s *splice) fillOneGap(ctx context.Context, blockListener *blockListener) e if err != nil { return err } - if freshBlockInfo == nil { - return i18n.NewError(ctx, msgs.MsgBlockNotAvailable) - } fetchedBlock := &ffcapi.MinimalBlockInfo{ BlockNumber: fftypes.FFuint64(freshBlockInfo.Number.BigInt().Uint64()), @@ -213,25 +202,12 @@ func (s *splice) fillOneGap(ctx context.Context, blockListener *blockListener) e } func (s *splice) removeBrokenLink() { - if s.hasGap() { - // nothing to remove if there is a gap - return - } // 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.hasGap() { - return nil - } - if len(s.earlyList) == 0 { - return s.lateList - } - if len(s.lateList) == 0 { - return s.earlyList - } if s.earlyList[len(s.earlyList)-1].IsParentOf(s.lateList[0]) { return append(s.earlyList, s.lateList...) } @@ -244,20 +220,20 @@ func (s *splice) toSingleLinkedList() []*ffcapi.MinimalBlockInfo { // 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 && !existingConfirmations[0].Equal(txBlockInfo) { - // otherwise we discard the existing confirmations queue + // otherwise we discard the existing confirmations list reconcileResult.NewFork = true } else { earlyList = existingConfirmations } - if len(existingConfirmations) == 0 { + 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, reconcileResult *ffcapi.ConfirmationUpdateResult, blockListener *blockListener) (lateList []*ffcapi.MinimalBlockInfo, err error) { +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 @@ -270,10 +246,6 @@ func createLateList(ctx context.Context, txBlockInfo *ffcapi.MinimalBlockInfo, t if err != nil { return nil, err } - if targetBlockInfo == nil { - return nil, i18n.NewError(ctx, msgs.MsgBlockNotAvailable) - } - lateList = []*ffcapi.MinimalBlockInfo{ { BlockNumber: fftypes.FFuint64(targetBlockInfo.Number.BigInt().Uint64()), @@ -360,7 +332,7 @@ func (bl *blockListener) handleZeroTargetConfirmationCount(ctx context.Context, return nil, i18n.NewError(ctx, msgs.MsgInMemoryPartialChainNotCaughtUp, txBlockInfo.BlockNumber.Uint64(), txBlockInfo.BlockHash) } -func (bl *blockListener) handleTargetCountLessThanExistingConfirmationLength(ctx context.Context, existingConfirmations []*ffcapi.MinimalBlockInfo, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) (*ffcapi.ConfirmationUpdateResult, error) { +func (bl *blockListener) handleTargetCountLessThanExistingConfirmationLength(_ context.Context, existingConfirmations []*ffcapi.MinimalBlockInfo, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) (*ffcapi.ConfirmationUpdateResult, error) { bl.mux.RLock() defer bl.mux.RUnlock() nextInMemoryBlock := bl.canonicalChain.Front() diff --git a/internal/ethereum/confirmation_reconciler_test.go b/internal/ethereum/confirmation_reconciler_test.go index ba82363..7e46cc7 100644 --- a/internal/ethereum/confirmation_reconciler_test.go +++ b/internal/ethereum/confirmation_reconciler_test.go @@ -229,6 +229,54 @@ func TestReconcileConfirmationsForTransaction_NewConfirmation(t *testing.T) { 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) + + mRPC.AssertExpectations(t) +} + func TestBuildConfirmationList_GapInExistingConfirmationsError(t *testing.T) { // Setup _, c, mRPC, _ := newTestConnectorWithNoBlockerFilterDefaultMocks(t) @@ -281,7 +329,6 @@ func TestBuildConfirmationList_MismatchConfirmationBlockError(t *testing.T) { assert.Nil(t, confirmationUpdateResult) assert.Regexp(t, "FF23063", err.Error()) } - func TestBuildConfirmationList_ExistingConfirmationLaterThanCurrentBlockError(t *testing.T) { // Setup _, c, mRPC, _ := newTestConnectorWithNoBlockerFilterDefaultMocks(t) @@ -650,41 +697,6 @@ func TestBuildConfirmationList_ConnectionNodeMismatch(t *testing.T) { assert.Equal(t, txBlockNumber+5, uint64(confirmationUpdateResult.Confirmations[5].BlockNumber)) } -func TestBuildConfirmationList_CorruptedExistingConfirmationAfterFirstConfirmation(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: 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.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{} From e0ed0c07836204b50583aeb371af73b01d8d430e Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Mon, 27 Oct 2025 13:51:15 +0000 Subject: [PATCH 26/37] address review comment, tolerate invalid input Signed-off-by: Chengxuan Xing --- go.mod | 2 +- go.sum | 6 +- internal/ethereum/confirmation_reconciler.go | 85 +++---- .../ethereum/confirmation_reconciler_test.go | 228 ++++++++---------- 4 files changed, 141 insertions(+), 180 deletions(-) diff --git a/go.mod b/go.mod index b00e211..ab9f650 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/hashicorp/golang-lru v1.0.2 github.com/hyperledger/firefly-common v1.5.6-0.20250630201730-e234335c0381 github.com/hyperledger/firefly-signer v1.1.21 - github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20251017153149-d6f944cab6b6 + github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20251024174118-8e8e3629f7fa 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 f9e6d6b..a91c9ee 100644 --- a/go.sum +++ b/go.sum @@ -104,10 +104,8 @@ github.com/hyperledger/firefly-common v1.5.6-0.20250630201730-e234335c0381 h1:4m github.com/hyperledger/firefly-common v1.5.6-0.20250630201730-e234335c0381/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.1-0.20251003113149-46c9271ca66e h1:jUumirQuZR8cQCBq9BNmpLM4rKo9ahQPdFzGvf/RYIs= -github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20251003113149-46c9271ca66e/go.mod h1:KHGvK/QqD2jpRZ04lQoX/k1T2o644NCkRlr3FbvKqnA= -github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20251017153149-d6f944cab6b6 h1:FGTXGnOoAaFkdA3n3SMYDXmHePrpX425P5YyljnaSyU= -github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20251017153149-d6f944cab6b6/go.mod h1:KHGvK/QqD2jpRZ04lQoX/k1T2o644NCkRlr3FbvKqnA= +github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20251024174118-8e8e3629f7fa h1:OTgB+KnpMS47RhIXs4G7FwSDKozbHpNNgPrbwdngaqk= +github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20251024174118-8e8e3629f7fa/go.mod h1:KHGvK/QqD2jpRZ04lQoX/k1T2o644NCkRlr3FbvKqnA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= diff --git a/internal/ethereum/confirmation_reconciler.go b/internal/ethereum/confirmation_reconciler.go index 171531b..2f029a9 100644 --- a/internal/ethereum/confirmation_reconciler.go +++ b/internal/ethereum/confirmation_reconciler.go @@ -29,12 +29,6 @@ import ( // 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) { - // Before we start, make sure that the existing confirmations queue is valid and consistent with itself - err := validateExistingConfirmations(ctx, existingConfirmations) - if err != nil { - return nil, err - } - // Now we can start the reconciliation process return c.blockListener.reconcileConfirmationsForTransaction(ctx, txHash, existingConfirmations, targetConfirmationCount) } @@ -60,14 +54,16 @@ func (bl *blockListener) buildConfirmationList(ctx context.Context, existingConf // 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 - // before we get into the main algorithm, handle a couple of special cases to reduce readability of the main algorithm - reconcileResult, err := bl.handleSpecialCases(ctx, existingConfirmations, txBlockInfo, targetConfirmationCount) - if reconcileResult != nil || err != nil { - return reconcileResult, err + // 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{ + reconcileResult := &ffcapi.ConfirmationUpdateResult{ TargetConfirmationCount: targetConfirmationCount, } @@ -79,6 +75,15 @@ func (bl *blockListener) buildConfirmationList(ctx context.Context, existingConf // - 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()+1 >= txBlockInfo.BlockNumber.Uint64()+targetConfirmationCount { + reconcileResult := bl.handleTargetCountMetWithEarlyList(earlyList, txBlockInfo, targetConfirmationCount) + if reconcileResult != nil { + return reconcileResult, nil + } + } + lateList, err := createLateList(ctx, txBlockInfo, targetConfirmationCount, bl) if err != nil { return nil, err @@ -121,7 +126,6 @@ func (bl *blockListener) buildConfirmationList(ctx context.Context, existingConf } 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 } @@ -219,11 +223,24 @@ func (s *splice) toSingleLinkedList() []*ffcapi.MinimalBlockInfo { // 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 && !existingConfirmations[0].Equal(txBlockInfo) { - // otherwise we discard the existing confirmations list - reconcileResult.NewFork = true - } else { - earlyList = existingConfirmations + 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 { @@ -302,20 +319,6 @@ func (bl *blockListener) buildConfirmationQueueUsingInMemoryPartialChain(ctx con return newConfirmationsWithoutTxBlock, nil } -func validateExistingConfirmations(ctx context.Context, existingConfirmations []*ffcapi.MinimalBlockInfo) error { - var previousBlock *ffcapi.MinimalBlockInfo - for _, existingConfirmation := range existingConfirmations { - if previousBlock != nil { - if !previousBlock.IsParentOf(existingConfirmation) { - return i18n.NewError(ctx, msgs.MsgFailedToBuildExistingConfirmationInvalid) - } - } - previousBlock = existingConfirmation - - } - return nil -} - func (bl *blockListener) handleZeroTargetConfirmationCount(ctx context.Context, txBlockInfo *ffcapi.MinimalBlockInfo) (*ffcapi.ConfirmationUpdateResult, error) { bl.mux.RLock() defer bl.mux.RUnlock() @@ -332,7 +335,7 @@ func (bl *blockListener) handleZeroTargetConfirmationCount(ctx context.Context, return nil, i18n.NewError(ctx, msgs.MsgInMemoryPartialChainNotCaughtUp, txBlockInfo.BlockNumber.Uint64(), txBlockInfo.BlockHash) } -func (bl *blockListener) handleTargetCountLessThanExistingConfirmationLength(_ context.Context, existingConfirmations []*ffcapi.MinimalBlockInfo, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) (*ffcapi.ConfirmationUpdateResult, error) { +func (bl *blockListener) handleTargetCountMetWithEarlyList(existingConfirmations []*ffcapi.MinimalBlockInfo, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) *ffcapi.ConfirmationUpdateResult { bl.mux.RLock() defer bl.mux.RUnlock() nextInMemoryBlock := bl.canonicalChain.Front() @@ -353,7 +356,7 @@ func (bl *blockListener) handleTargetCountLessThanExistingConfirmationLength(_ c return &ffcapi.ConfirmationUpdateResult{ Confirmed: true, Confirmations: existingConfirmations[:targetConfirmationCount+1], - }, nil + } } // only the existing confirmations are not enough, need to fetch more blocks from the in memory partial chain newList := existingConfirmations @@ -369,19 +372,7 @@ func (bl *blockListener) handleTargetCountLessThanExistingConfirmationLength(_ c return &ffcapi.ConfirmationUpdateResult{ Confirmed: uint64(len(newList)) > targetConfirmationCount, Confirmations: newList, - }, nil - } - return nil, nil -} - -// handleSpecialCases contains the logic that cannot be included in the main algorithm because they reduce the readability of the code -func (bl *blockListener) handleSpecialCases(ctx context.Context, existingConfirmations []*ffcapi.MinimalBlockInfo, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) (*ffcapi.ConfirmationUpdateResult, error) { - if targetConfirmationCount == 0 { - return bl.handleZeroTargetConfirmationCount(ctx, txBlockInfo) - } - - if len(existingConfirmations) > 0 && existingConfirmations[len(existingConfirmations)-1].BlockNumber.Uint64()+1 >= txBlockInfo.BlockNumber.Uint64()+targetConfirmationCount { - return bl.handleTargetCountLessThanExistingConfirmationLength(ctx, existingConfirmations, txBlockInfo, targetConfirmationCount) + } } - return nil, nil + return nil } diff --git a/internal/ethereum/confirmation_reconciler_test.go b/internal/ethereum/confirmation_reconciler_test.go index 7e46cc7..dd448f3 100644 --- a/internal/ethereum/confirmation_reconciler_test.go +++ b/internal/ethereum/confirmation_reconciler_test.go @@ -277,133 +277,150 @@ func TestReconcileConfirmationsForTransaction_DifferentTxBlock(t *testing.T) { mRPC.AssertExpectations(t) } -func TestBuildConfirmationList_GapInExistingConfirmationsError(t *testing.T) { +func TestBuildConfirmationList_GapInExistingConfirmationsShouldBeFilledIn(t *testing.T) { // Setup - _, c, mRPC, _ := newTestConnectorWithNoBlockerFilterDefaultMocks(t) - - txHash := generateTestHash(100) - // Mock for TransactionReceipt call - return error to simulate RPC call error - mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getTransactionReceipt", txHash).Return(&rpcbackend.RPCError{Message: "pop"}).Run(func(args mock.Arguments) { - err := json.Unmarshal([]byte("null"), args[1]) - assert.NoError(t, err) - }) + 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 := c.ReconcileConfirmationsForTransaction(ctx, txHash, existingQueue, targetConfirmationCount) - assert.Error(t, err) - assert.Nil(t, confirmationUpdateResult) - assert.Regexp(t, "FF23063", err.Error()) + 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_MismatchConfirmationBlockError(t *testing.T) { +func TestBuildConfirmationList_MismatchConfirmationBlockShouldBeReplaced(t *testing.T) { // Setup - _, c, mRPC, _ := newTestConnectorWithNoBlockerFilterDefaultMocks(t) - - txHash := generateTestHash(100) - // Mock for TransactionReceipt call - return error to simulate RPC call error - mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getTransactionReceipt", txHash).Return(&rpcbackend.RPCError{Message: "pop"}).Run(func(args mock.Arguments) { - err := json.Unmarshal([]byte("null"), args[1]) - assert.NoError(t, err) - }) + 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 + {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)}, } - - targetConfirmationCount := uint64(5) - - // Execute - confirmationUpdateResult, err := c.ReconcileConfirmationsForTransaction(ctx, txHash, existingQueue, targetConfirmationCount) - assert.Error(t, err) - assert.Nil(t, confirmationUpdateResult) - assert.Regexp(t, "FF23063", err.Error()) -} -func TestBuildConfirmationList_ExistingConfirmationLaterThanCurrentBlockError(t *testing.T) { - // Setup - _, c, mRPC, _ := newTestConnectorWithNoBlockerFilterDefaultMocks(t) - - txHash := generateTestHash(100) - // Mock for TransactionReceipt call - return error to simulate RPC call error - mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getTransactionReceipt", txHash).Return(&rpcbackend.RPCError{Message: "pop"}).Run(func(args mock.Arguments) { - err := json.Unmarshal([]byte("null"), args[1]) - assert.NoError(t, err) - }) - ctx := context.Background() - existingQueue := []*ffcapi.MinimalBlockInfo{ - - {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, - {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 := c.ReconcileConfirmationsForTransaction(ctx, txHash, existingQueue, targetConfirmationCount) - assert.Error(t, err) - assert.Nil(t, confirmationUpdateResult) - assert.Regexp(t, "FF23063", err.Error()) + 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_ExistingTxBockInfoIsWrongError(t *testing.T) { +func TestBuildConfirmationList_ExistingTxBockInfoIsWrongShouldBeIgnored(t *testing.T) { // Setup - _, c, mRPC, _ := newTestConnectorWithNoBlockerFilterDefaultMocks(t) - - txHash := generateTestHash(100) - // Mock for TransactionReceipt call - return error to simulate RPC call error - mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getTransactionReceipt", txHash).Return(&rpcbackend.RPCError{Message: "pop"}).Run(func(args mock.Arguments) { - err := json.Unmarshal([]byte("null"), args[1]) - assert.NoError(t, err) - }) + bl, done := newBlockListenerWithTestChain(t, 100, 5, 102, 150, []uint64{101}) + defer done() ctx := context.Background() - existingQueue := []*ffcapi.MinimalBlockInfo{ + // 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 := c.ReconcileConfirmationsForTransaction(ctx, txHash, existingQueue, targetConfirmationCount) - assert.Error(t, err) - assert.Nil(t, confirmationUpdateResult) - assert.Regexp(t, "FF23063", err.Error()) + 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_ExistingConfirmationsWithLowerBlockNumberError(t *testing.T) { +func TestReconcileConfirmationsForTransaction_ExistingConfirmationsWithLowerBlockNumberShouldBeIgnored(t *testing.T) { // Setup - _, c, mRPC, _ := newTestConnectorWithNoBlockerFilterDefaultMocks(t) - - txHash := generateTestHash(100) - // Mock for TransactionReceipt call - return error to simulate RPC call error - mRPC.On("CallRPC", mock.Anything, mock.Anything, "eth_getTransactionReceipt", txHash).Return(&rpcbackend.RPCError{Message: "pop"}).Run(func(args mock.Arguments) { - err := json.Unmarshal([]byte("null"), args[1]) - assert.NoError(t, err) - }) + 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 := c.ReconcileConfirmationsForTransaction(ctx, txHash, existingQueue, targetConfirmationCount) - assert.Error(t, err) - assert.Nil(t, confirmationUpdateResult) - assert.Regexp(t, "FF23063", err.Error()) + 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 @@ -651,7 +668,7 @@ func TestBuildConfirmationList_CorruptedExistingConfirmationDoNotAffectConfirmat confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingQueue, txBlockInfo, targetConfirmationCount) assert.NoError(t, err) // Assert - assert.True(t, confirmationUpdateResult.NewFork) + 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)) @@ -1125,51 +1142,6 @@ func TestBuildConfirmationList_ReachTargetConfirmation(t *testing.T) { assert.GreaterOrEqual(t, len(confirmationUpdateResult.Confirmations), 4) // tx block + 3 confirmations } -func TestValidateExistingConfirmations_LowerBlockNumber(t *testing.T) { - - ctx := context.Background() - // Create confirmations with a lower block number - existingQueue := []*ffcapi.MinimalBlockInfo{ - {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, - {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, - {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(99), ParentHash: generateTestHash(101)}, // somehow there is a lower block number - } - - // Execute - err := validateExistingConfirmations(ctx, existingQueue) - assert.Error(t, err) -} - -func TestValidateExistingConfirmations_Gap(t *testing.T) { - - ctx := context.Background() - // Create confirmations with a lower block number - existingQueue := []*ffcapi.MinimalBlockInfo{ - {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, - {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, - {BlockHash: generateTestHash(103), BlockNumber: fftypes.FFuint64(103), ParentHash: generateTestHash(102)}, - } - - // Execute - err := validateExistingConfirmations(ctx, existingQueue) - assert.Error(t, err) -} - -func TestValidateExistingConfirmations_BrokenHash(t *testing.T) { - - ctx := context.Background() - // Create confirmations with a lower block number - existingQueue := []*ffcapi.MinimalBlockInfo{ - {BlockHash: generateTestHash(100), BlockNumber: fftypes.FFuint64(100), ParentHash: generateTestHash(99)}, - {BlockHash: generateTestHash(101), BlockNumber: fftypes.FFuint64(101), ParentHash: generateTestHash(100)}, - {BlockHash: generateTestHash(102), BlockNumber: fftypes.FFuint64(102), ParentHash: "broken"}, - } - - // Execute - err := validateExistingConfirmations(ctx, existingQueue) - assert.Error(t, err) -} - // Helper functions // generateTestHash creates a predictable hash for testing with consistent prefix and last 4 digits as index From 2c63ce25ddc387fcbea10f5de4d41960575519cb Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Mon, 27 Oct 2025 20:12:20 +0000 Subject: [PATCH 27/37] fixing nil pointer exception Signed-off-by: Chengxuan Xing --- internal/ethereum/confirmation_reconciler.go | 6 +++- .../ethereum/confirmation_reconciler_test.go | 29 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/internal/ethereum/confirmation_reconciler.go b/internal/ethereum/confirmation_reconciler.go index 2f029a9..fdfa684 100644 --- a/internal/ethereum/confirmation_reconciler.go +++ b/internal/ethereum/confirmation_reconciler.go @@ -284,7 +284,11 @@ func (bl *blockListener) buildConfirmationQueueUsingInMemoryPartialChain(ctx con targetBlockNumber := txBlockInfo.BlockNumber.Uint64() + targetConfirmationCount // Check if the in-memory partial chain has caught up to the transaction block - chainTail := bl.canonicalChain.Back().Value.(*ffcapi.MinimalBlockInfo) + chainTailElement := bl.canonicalChain.Back() + if chainTailElement == nil { + return nil, 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 to be indexed", txBlockNumber) return nil, i18n.NewError(ctx, msgs.MsgInMemoryPartialChainNotCaughtUp, txBlockNumber, txBlockInfo.BlockHash) diff --git a/internal/ethereum/confirmation_reconciler_test.go b/internal/ethereum/confirmation_reconciler_test.go index dd448f3..3a117d6 100644 --- a/internal/ethereum/confirmation_reconciler_test.go +++ b/internal/ethereum/confirmation_reconciler_test.go @@ -448,6 +448,35 @@ func TestBuildConfirmationList_EmptyChain(t *testing.T) { 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 TestBuildConfirmationList_ChainTooShort(t *testing.T) { // Setup bl, done := newBlockListenerWithTestChain(t, 100, 5, 50, 99, []uint64{}) From a4248c0920379c04d26b68dd613bba39e63cd6f8 Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Mon, 27 Oct 2025 20:22:55 +0000 Subject: [PATCH 28/37] fixing other occurences Signed-off-by: Chengxuan Xing --- internal/ethereum/confirmation_reconciler.go | 50 ++++++++++++------- .../ethereum/confirmation_reconciler_test.go | 29 +++++++++++ 2 files changed, 60 insertions(+), 19 deletions(-) diff --git a/internal/ethereum/confirmation_reconciler.go b/internal/ethereum/confirmation_reconciler.go index fdfa684..e107eec 100644 --- a/internal/ethereum/confirmation_reconciler.go +++ b/internal/ethereum/confirmation_reconciler.go @@ -274,6 +274,21 @@ func createLateList(ctx context.Context, txBlockInfo *ffcapi.MinimalBlockInfo, t 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. @@ -284,21 +299,16 @@ func (bl *blockListener) buildConfirmationQueueUsingInMemoryPartialChain(ctx con targetBlockNumber := txBlockInfo.BlockNumber.Uint64() + targetConfirmationCount // Check if the in-memory partial chain has caught up to the transaction block - chainTailElement := bl.canonicalChain.Back() - if chainTailElement == nil { - return nil, 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 to be indexed", txBlockNumber) - return nil, i18n.NewError(ctx, msgs.MsgInMemoryPartialChainNotCaughtUp, txBlockNumber, txBlockInfo.BlockHash) + 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 { + for nextInMemoryBlock != nil && nextInMemoryBlock.Value != nil { nextInMemoryBlockInfo := nextInMemoryBlock.Value.(*ffcapi.MinimalBlockInfo) // If we've reached the target confirmation count, mark as confirmed @@ -328,15 +338,17 @@ func (bl *blockListener) handleZeroTargetConfirmationCount(ctx context.Context, 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 - chainTail := bl.canonicalChain.Back().Value.(*ffcapi.MinimalBlockInfo) - if chainTail.BlockNumber.Uint64() >= txBlockInfo.BlockNumber.Uint64() { - return &ffcapi.ConfirmationUpdateResult{ - Confirmed: true, - Confirmations: []*ffcapi.MinimalBlockInfo{txBlockInfo}, - }, nil + txBlockNumber := txBlockInfo.BlockNumber.Uint64() + err := bl.validateChainCaughtUp(ctx, txBlockInfo, txBlockNumber) + if err != nil { + return nil, err } - log.L(ctx).Debugf("in-memory partial chain is waiting for the transaction block %d (%s) to be indexed", txBlockInfo.BlockNumber.Uint64(), txBlockInfo.BlockHash) - return nil, i18n.NewError(ctx, msgs.MsgInMemoryPartialChainNotCaughtUp, txBlockInfo.BlockNumber.Uint64(), txBlockInfo.BlockHash) + + return &ffcapi.ConfirmationUpdateResult{ + Confirmed: true, + Confirmations: []*ffcapi.MinimalBlockInfo{txBlockInfo}, + }, nil + } func (bl *blockListener) handleTargetCountMetWithEarlyList(existingConfirmations []*ffcapi.MinimalBlockInfo, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) *ffcapi.ConfirmationUpdateResult { @@ -346,7 +358,7 @@ func (bl *blockListener) handleTargetCountMetWithEarlyList(existingConfirmations var nextInMemoryBlockInfo *ffcapi.MinimalBlockInfo lastExistingConfirmation := existingConfirmations[len(existingConfirmations)-1] // iterates to the block that immediately after the last existing confirmation - for nextInMemoryBlock != nil { + for nextInMemoryBlock != nil && nextInMemoryBlock.Value != nil { nextInMemoryBlockInfo = nextInMemoryBlock.Value.(*ffcapi.MinimalBlockInfo) if nextInMemoryBlockInfo.BlockNumber.Uint64() >= lastExistingConfirmation.BlockNumber.Uint64()+1 { break @@ -366,7 +378,7 @@ func (bl *blockListener) handleTargetCountMetWithEarlyList(existingConfirmations newList := existingConfirmations targetBlockNumber := txBlockInfo.BlockNumber.Uint64() + targetConfirmationCount - for nextInMemoryBlock := bl.canonicalChain.Front(); nextInMemoryBlock != nil; nextInMemoryBlock = nextInMemoryBlock.Next() { + for nextInMemoryBlock := bl.canonicalChain.Front(); nextInMemoryBlock != nil && nextInMemoryBlock.Value != nil; nextInMemoryBlock = nextInMemoryBlock.Next() { nextInMemoryBlockInfo := nextInMemoryBlock.Value.(*ffcapi.MinimalBlockInfo) if nextInMemoryBlockInfo.BlockNumber.Uint64() > targetBlockNumber { break diff --git a/internal/ethereum/confirmation_reconciler_test.go b/internal/ethereum/confirmation_reconciler_test.go index 3a117d6..ccc0cd7 100644 --- a/internal/ethereum/confirmation_reconciler_test.go +++ b/internal/ethereum/confirmation_reconciler_test.go @@ -477,6 +477,35 @@ func TestBuildConfirmationQueueUsingInMemoryPartialChain_EmptyCanonicalChain(t * 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{}) From 677fa58698301019e9cd3b2daa05b058976c3f91 Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Mon, 27 Oct 2025 20:24:58 +0000 Subject: [PATCH 29/37] update fftm Signed-off-by: Chengxuan Xing --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index ab9f650..9b4d650 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/hashicorp/golang-lru v1.0.2 github.com/hyperledger/firefly-common v1.5.6-0.20250630201730-e234335c0381 github.com/hyperledger/firefly-signer v1.1.21 - github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20251024174118-8e8e3629f7fa + github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20251027171959-59cd2870a5db 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 a91c9ee..f945e35 100644 --- a/go.sum +++ b/go.sum @@ -104,8 +104,8 @@ github.com/hyperledger/firefly-common v1.5.6-0.20250630201730-e234335c0381 h1:4m github.com/hyperledger/firefly-common v1.5.6-0.20250630201730-e234335c0381/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.1-0.20251024174118-8e8e3629f7fa h1:OTgB+KnpMS47RhIXs4G7FwSDKozbHpNNgPrbwdngaqk= -github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20251024174118-8e8e3629f7fa/go.mod h1:KHGvK/QqD2jpRZ04lQoX/k1T2o644NCkRlr3FbvKqnA= +github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20251027171959-59cd2870a5db h1:N6VP92prFX3D71Wfl24GMcfgmPf/bYOcXCSHE5qtAyE= +github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20251027171959-59cd2870a5db/go.mod h1:KHGvK/QqD2jpRZ04lQoX/k1T2o644NCkRlr3FbvKqnA= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= From 72acb6eabaa42c1368555906243b5be22f5b67ae Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Mon, 27 Oct 2025 20:27:04 +0000 Subject: [PATCH 30/37] update comments Signed-off-by: Chengxuan Xing --- internal/ethereum/confirmation_reconciler.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/ethereum/confirmation_reconciler.go b/internal/ethereum/confirmation_reconciler.go index e107eec..c64bb1d 100644 --- a/internal/ethereum/confirmation_reconciler.go +++ b/internal/ethereum/confirmation_reconciler.go @@ -100,7 +100,7 @@ func (bl *blockListener) buildConfirmationList(ctx context.Context, existingConf // 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 - // therefore, we report a fork with no confirmations + // throw an error to the caller return nil, i18n.NewError(ctx, msgs.MsgFailedToBuildConfirmationQueue) } @@ -197,6 +197,7 @@ func (s *splice) fillOneGap(ctx context.Context, blockListener *blockListener) e // 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) } From 660d8b496e27383895c5082c1597544903cbca6a Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Tue, 28 Oct 2025 19:21:57 +0000 Subject: [PATCH 31/37] pick up fftm v1.4.1 Signed-off-by: Chengxuan Xing --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 9b4d650..b507f4b 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.1-0.20251027171959-59cd2870a5db + github.com/hyperledger/firefly-transaction-manager v1.4.1 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 f945e35..c90be4a 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.1-0.20251027171959-59cd2870a5db h1:N6VP92prFX3D71Wfl24GMcfgmPf/bYOcXCSHE5qtAyE= -github.com/hyperledger/firefly-transaction-manager v1.4.1-0.20251027171959-59cd2870a5db/go.mod h1:KHGvK/QqD2jpRZ04lQoX/k1T2o644NCkRlr3FbvKqnA= +github.com/hyperledger/firefly-transaction-manager v1.4.1 h1:B9yS/b5dqzm1zpDAiKqrDvulMjLC9m/o6U5il2sDnGo= +github.com/hyperledger/firefly-transaction-manager v1.4.1/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= From 94e78977e4bcabc2c76c4512c79cf861f613c8d6 Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Fri, 7 Nov 2025 15:35:23 +0000 Subject: [PATCH 32/37] returning receipt as part of the confirmation result Signed-off-by: Chengxuan Xing --- go.mod | 2 +- go.sum | 4 ++-- internal/ethereum/blocklistener_blockquery.go | 12 ++++++------ internal/ethereum/confirmation_reconciler.go | 8 ++++++-- internal/ethereum/confirmation_reconciler_test.go | 4 +++- 5 files changed, 18 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index b507f4b..c135909 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/hashicorp/golang-lru v1.0.2 github.com/hyperledger/firefly-common v1.5.8 github.com/hyperledger/firefly-signer v1.1.21 - github.com/hyperledger/firefly-transaction-manager v1.4.1 + 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 c90be4a..db2b3f2 100644 --- a/go.sum +++ b/go.sum @@ -104,8 +104,8 @@ github.com/hyperledger/firefly-common v1.5.8 h1:3N59vh81UpqOJu/uX1EOIiEy/sCuzJWY 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.1 h1:B9yS/b5dqzm1zpDAiKqrDvulMjLC9m/o6U5il2sDnGo= -github.com/hyperledger/firefly-transaction-manager v1.4.1/go.mod h1:1kbYt8ofDXqfwC02vwV/HoOjmiv0IuT9UkJ//bbrliE= +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_blockquery.go b/internal/ethereum/blocklistener_blockquery.go index 7e45729..f6debd3 100644 --- a/internal/ethereum/blocklistener_blockquery.go +++ b/internal/ethereum/blocklistener_blockquery.go @@ -53,34 +53,34 @@ func (bl *blockListener) addToBlockCache(blockInfo *blockInfoJSONRPC) { bl.blockCache.Add(blockInfo.Number.BigInt().String(), blockInfo) } -func (bl *blockListener) getBlockInfoContainsTxHash(ctx context.Context, txHash string) (*ffcapi.MinimalBlockInfo, 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, i18n.WrapError(ctx, receiptErr, msgs.MsgFailedToQueryReceipt, txHash) + return nil, nil, i18n.WrapError(ctx, receiptErr, msgs.MsgFailedToQueryReceipt, txHash) } if res == nil { - return nil, 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, i18n.WrapError(ctx, err, msgs.MsgFailedToQueryBlockInfo, txHash) + return nil, nil, i18n.WrapError(ctx, err, msgs.MsgFailedToQueryBlockInfo, txHash) } if bi == nil { - return nil, nil + return nil, nil, nil } return &ffcapi.MinimalBlockInfo{ BlockNumber: fftypes.FFuint64(bi.Number.BigInt().Uint64()), BlockHash: bi.Hash.String(), ParentHash: bi.ParentHash.String(), - }, nil + }, res, nil } func (bl *blockListener) getBlockInfoByNumber(ctx context.Context, blockNumber uint64, allowCache bool, expectedParentHashStr string, expectedBlockHashStr string) (*blockInfoJSONRPC, ffcapi.ErrorReason, error) { diff --git a/internal/ethereum/confirmation_reconciler.go b/internal/ethereum/confirmation_reconciler.go index c64bb1d..3f0a9d0 100644 --- a/internal/ethereum/confirmation_reconciler.go +++ b/internal/ethereum/confirmation_reconciler.go @@ -37,7 +37,7 @@ func (c *ethConnector) ReconcileConfirmationsForTransaction(ctx context.Context, 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, err := bl.getBlockInfoContainsTxHash(ctx, txHash) + 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 @@ -47,7 +47,11 @@ func (bl *blockListener) reconcileConfirmationsForTransaction(ctx context.Contex log.L(ctx).Debugf("Transaction %s not found in any block", txHash) return nil, i18n.NewError(ctx, msgs.MsgTransactionNotFound, txHash) } - return bl.buildConfirmationList(ctx, existingConfirmations, txBlockInfo, targetConfirmationCount) + confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingConfirmations, txBlockInfo, targetConfirmationCount) + if confirmationUpdateResult != nil { + confirmationUpdateResult.Receipt = txReceipt + } + return confirmationUpdateResult, err } func (bl *blockListener) buildConfirmationList(ctx context.Context, existingConfirmations []*ffcapi.MinimalBlockInfo, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) (*ffcapi.ConfirmationUpdateResult, error) { diff --git a/internal/ethereum/confirmation_reconciler_test.go b/internal/ethereum/confirmation_reconciler_test.go index ccc0cd7..75a18df 100644 --- a/internal/ethereum/confirmation_reconciler_test.go +++ b/internal/ethereum/confirmation_reconciler_test.go @@ -181,6 +181,7 @@ func TestReconcileConfirmationsForTransaction_TxBlockNotInCanonicalChain(t *test assert.False(t, result.NewFork) assert.False(t, result.Confirmed) assert.Len(t, result.Confirmations, 2) + assert.NotNil(t, result.Receipt) mRPC.AssertExpectations(t) } @@ -225,6 +226,7 @@ func TestReconcileConfirmationsForTransaction_NewConfirmation(t *testing.T) { {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) } @@ -273,7 +275,7 @@ func TestReconcileConfirmationsForTransaction_DifferentTxBlock(t *testing.T) { {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) } From 4e4cdfdb3738024d58c8c9a01df4b2d9c8de2737 Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Wed, 12 Nov 2025 12:38:45 +0000 Subject: [PATCH 33/37] fixing confirmation bug Signed-off-by: Chengxuan Xing --- internal/ethereum/confirmation_reconciler.go | 23 ++--------- .../ethereum/confirmation_reconciler_test.go | 41 +++++++++++++++++++ 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/internal/ethereum/confirmation_reconciler.go b/internal/ethereum/confirmation_reconciler.go index 3f0a9d0..a8fe2f6 100644 --- a/internal/ethereum/confirmation_reconciler.go +++ b/internal/ethereum/confirmation_reconciler.go @@ -81,7 +81,7 @@ func (bl *blockListener) buildConfirmationList(ctx context.Context, existingConf 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()+1 >= txBlockInfo.BlockNumber.Uint64()+targetConfirmationCount { + if len(earlyList) > 0 && earlyList[len(earlyList)-1].BlockNumber.Uint64() >= txBlockInfo.BlockNumber.Uint64()+targetConfirmationCount { reconcileResult := bl.handleTargetCountMetWithEarlyList(earlyList, txBlockInfo, targetConfirmationCount) if reconcileResult != nil { return reconcileResult, nil @@ -373,26 +373,9 @@ func (bl *blockListener) handleTargetCountMetWithEarlyList(existingConfirmations 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 - if targetConfirmationCount < uint64(len(existingConfirmations)) { - return &ffcapi.ConfirmationUpdateResult{ - Confirmed: true, - Confirmations: existingConfirmations[:targetConfirmationCount+1], - } - } - // only the existing confirmations are not enough, need to fetch more blocks from the in memory partial chain - newList := existingConfirmations - targetBlockNumber := txBlockInfo.BlockNumber.Uint64() + targetConfirmationCount - - for nextInMemoryBlock := bl.canonicalChain.Front(); nextInMemoryBlock != nil && nextInMemoryBlock.Value != nil; nextInMemoryBlock = nextInMemoryBlock.Next() { - nextInMemoryBlockInfo := nextInMemoryBlock.Value.(*ffcapi.MinimalBlockInfo) - if nextInMemoryBlockInfo.BlockNumber.Uint64() > targetBlockNumber { - break - } - newList = append(newList, nextInMemoryBlockInfo) - } return &ffcapi.ConfirmationUpdateResult{ - Confirmed: uint64(len(newList)) > targetConfirmationCount, - Confirmations: newList, + Confirmed: true, + Confirmations: existingConfirmations[:targetConfirmationCount+1], } } return nil diff --git a/internal/ethereum/confirmation_reconciler_test.go b/internal/ethereum/confirmation_reconciler_test.go index 75a18df..debf894 100644 --- a/internal/ethereum/confirmation_reconciler_test.go +++ b/internal/ethereum/confirmation_reconciler_test.go @@ -1110,6 +1110,47 @@ func TestBuildConfirmationList_HasSufficientConfirmationsButNoOverlapWithCanonic } +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{}) From 5a23519578402485266415431a7f0b392b080e6d Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Wed, 12 Nov 2025 12:54:49 +0000 Subject: [PATCH 34/37] remove unused vars Signed-off-by: Chengxuan Xing --- internal/ethereum/confirmation_reconciler.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/ethereum/confirmation_reconciler.go b/internal/ethereum/confirmation_reconciler.go index a8fe2f6..2d7f655 100644 --- a/internal/ethereum/confirmation_reconciler.go +++ b/internal/ethereum/confirmation_reconciler.go @@ -82,7 +82,7 @@ func (bl *blockListener) buildConfirmationList(ctx context.Context, existingConf // 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, txBlockInfo, targetConfirmationCount) + reconcileResult := bl.handleTargetCountMetWithEarlyList(earlyList, targetConfirmationCount) if reconcileResult != nil { return reconcileResult, nil } @@ -356,7 +356,7 @@ func (bl *blockListener) handleZeroTargetConfirmationCount(ctx context.Context, } -func (bl *blockListener) handleTargetCountMetWithEarlyList(existingConfirmations []*ffcapi.MinimalBlockInfo, txBlockInfo *ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) *ffcapi.ConfirmationUpdateResult { +func (bl *blockListener) handleTargetCountMetWithEarlyList(existingConfirmations []*ffcapi.MinimalBlockInfo, targetConfirmationCount uint64) *ffcapi.ConfirmationUpdateResult { bl.mux.RLock() defer bl.mux.RUnlock() nextInMemoryBlock := bl.canonicalChain.Front() From 2d9a3c87a7d4c258a9b291e933e8d59f4d381912 Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Wed, 12 Nov 2025 13:39:47 +0000 Subject: [PATCH 35/37] always return the target confirmation count Signed-off-by: Chengxuan Xing --- internal/ethereum/confirmation_reconciler.go | 5 ++--- internal/ethereum/confirmation_reconciler_test.go | 1 + 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/ethereum/confirmation_reconciler.go b/internal/ethereum/confirmation_reconciler.go index 2d7f655..70b700a 100644 --- a/internal/ethereum/confirmation_reconciler.go +++ b/internal/ethereum/confirmation_reconciler.go @@ -49,6 +49,7 @@ func (bl *blockListener) reconcileConfirmationsForTransaction(ctx context.Contex } confirmationUpdateResult, err := bl.buildConfirmationList(ctx, existingConfirmations, txBlockInfo, targetConfirmationCount) if confirmationUpdateResult != nil { + confirmationUpdateResult.TargetConfirmationCount = targetConfirmationCount confirmationUpdateResult.Receipt = txReceipt } return confirmationUpdateResult, err @@ -67,9 +68,7 @@ func (bl *blockListener) buildConfirmationList(ctx context.Context, existingConf } // Initialize the result with the target confirmation count - reconcileResult := &ffcapi.ConfirmationUpdateResult{ - TargetConfirmationCount: targetConfirmationCount, - } + 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 diff --git a/internal/ethereum/confirmation_reconciler_test.go b/internal/ethereum/confirmation_reconciler_test.go index debf894..c36210f 100644 --- a/internal/ethereum/confirmation_reconciler_test.go +++ b/internal/ethereum/confirmation_reconciler_test.go @@ -181,6 +181,7 @@ func TestReconcileConfirmationsForTransaction_TxBlockNotInCanonicalChain(t *test 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) } From bd6ceb6e949680eb14357d2475e58e45eec5c44f Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Mon, 1 Dec 2025 12:39:25 +0000 Subject: [PATCH 36/37] pick up FFTM v1.4.2 Signed-off-by: Chengxuan Xing --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c135909..c118653 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/hashicorp/golang-lru v1.0.2 github.com/hyperledger/firefly-common v1.5.8 github.com/hyperledger/firefly-signer v1.1.21 - github.com/hyperledger/firefly-transaction-manager v1.4.2-0.20251107152155-813dbc501aa3 + github.com/hyperledger/firefly-transaction-manager v1.4.2 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 db2b3f2..f0bae96 100644 --- a/go.sum +++ b/go.sum @@ -104,8 +104,8 @@ github.com/hyperledger/firefly-common v1.5.8 h1:3N59vh81UpqOJu/uX1EOIiEy/sCuzJWY 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.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/hyperledger/firefly-transaction-manager v1.4.2 h1:RbEY+ieLFKHpCl192OXQdD8WEoYlwY5+MMHr6gnTVes= +github.com/hyperledger/firefly-transaction-manager v1.4.2/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= From 474434867fb78578ad055295adfaac20936780ff Mon Sep 17 00:00:00 2001 From: Chengxuan Xing Date: Mon, 1 Dec 2025 13:19:55 +0000 Subject: [PATCH 37/37] pick up ff-common v1.5.9 Signed-off-by: Chengxuan Xing --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index c118653..fbfff81 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ 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.8 + github.com/hyperledger/firefly-common v1.5.9 github.com/hyperledger/firefly-signer v1.1.21 github.com/hyperledger/firefly-transaction-manager v1.4.2 github.com/sirupsen/logrus v1.9.3 diff --git a/go.sum b/go.sum index f0bae96..c0d17f1 100644 --- a/go.sum +++ b/go.sum @@ -100,8 +100,8 @@ 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.8 h1:3N59vh81UpqOJu/uX1EOIiEy/sCuzJWYAUPMgTRLako= -github.com/hyperledger/firefly-common v1.5.8/go.mod h1:1Xawm5PUhxT7k+CL/Kr3i1LE3cTTzoQwZMLimvlW8rs= +github.com/hyperledger/firefly-common v1.5.9 h1:Z1+SuKNYJ8hPKQ5CvcsMg6r/E4RyW6wb08nGwtcc8Ww= +github.com/hyperledger/firefly-common v1.5.9/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.2 h1:RbEY+ieLFKHpCl192OXQdD8WEoYlwY5+MMHr6gnTVes=