From d72894998f05bb70466a420917a71bb34b8d3e7e Mon Sep 17 00:00:00 2001 From: Artur Wieczorek Date: Mon, 27 Jul 2026 20:36:55 +0200 Subject: [PATCH 1/2] test(db): add failing test for epoch block-tx count by block id queryEpochBlockNumbers returns block_no, but the epoch block-tx check counts transactions with queryBlockTxCount, which keys on block.id. block_no and id diverge on a real chain (an EBB has a null block_no and a positive id), so the check reads the wrong block. Assert the returned identifier counts the right block; the fix follows in the next commit. --- cardano-db/cardano-db.cabal | 3 +- .../test/Test/IO/Cardano/Db/EpochBlockTxs.hs | 40 +++++++++++++++++++ cardano-db/test/test-db.hs | 2 + 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 cardano-db/test/Test/IO/Cardano/Db/EpochBlockTxs.hs diff --git a/cardano-db/cardano-db.cabal b/cardano-db/cardano-db.cabal index e3e9e93cc..e07b6f6c9 100644 --- a/cardano-db/cardano-db.cabal +++ b/cardano-db/cardano-db.cabal @@ -152,7 +152,8 @@ test-suite test-db main-is: test-db.hs hs-source-dirs: test - other-modules: Test.IO.Cardano.Db.EpochCalc + other-modules: Test.IO.Cardano.Db.EpochBlockTxs + Test.IO.Cardano.Db.EpochCalc Test.IO.Cardano.Db.Insert Test.IO.Cardano.Db.Migration Test.IO.Cardano.Db.Rollback diff --git a/cardano-db/test/Test/IO/Cardano/Db/EpochBlockTxs.hs b/cardano-db/test/Test/IO/Cardano/Db/EpochBlockTxs.hs new file mode 100644 index 000000000..3aa17d0a0 --- /dev/null +++ b/cardano-db/test/Test/IO/Cardano/Db/EpochBlockTxs.hs @@ -0,0 +1,40 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Test.IO.Cardano.Db.EpochBlockTxs ( + tests, +) where + +import Cardano.Db +import Control.Monad (void) +import Test.IO.Cardano.Db.Util +import Test.Tasty (TestTree, testGroup) +import Test.Tasty.HUnit (testCase) + +tests :: TestTree +tests = + testGroup + "EpochBlockTxs" + [ testCase "epoch block tx count is looked up by block id" epochBlockTxCountUsesId + ] + +-- block_no and the surrogate block.id diverge on a real chain (an EBB has a null +-- block_no but a positive id). The epoch block-tx check must count transactions +-- by block.id, not by block_no, or it reads the wrong (or no) block. +epochBlockTxCountUsesId :: IO () +epochBlockTxCountUsesId = + runDbStandaloneSilent $ do + deleteAllBlocks + slid <- insertSlotLeader testSlotLeader + let blk = (mkBlock 0 slid) {blockTxCount = 1} + bid <- insertCheckUniqueBlock blk + case mkTxs bid 1 of + (tx : _) -> void $ insertTx tx + [] -> error "mkTxs returned empty list" + rows <- queryEpochBlockNumbers 0 + case rows of + [(blockNo, expected)] -> do + actual <- queryBlockTxCount (BlockId (fromIntegral blockNo)) + assertBool + ("expected tx count " ++ show expected ++ " but got " ++ show actual) + (actual == expected) + _ -> assertBool ("expected one block row, got " ++ show (length rows)) False diff --git a/cardano-db/test/test-db.hs b/cardano-db/test/test-db.hs index 0becc54f4..def310316 100644 --- a/cardano-db/test/test-db.hs +++ b/cardano-db/test/test-db.hs @@ -5,6 +5,7 @@ import Data.Maybe (isNothing) import System.Directory (getCurrentDirectory) import System.Environment (lookupEnv, setEnv) import System.FilePath (()) +import qualified Test.IO.Cardano.Db.EpochBlockTxs import qualified Test.IO.Cardano.Db.EpochCalc import qualified Test.IO.Cardano.Db.Insert import qualified Test.IO.Cardano.Db.Migration @@ -26,6 +27,7 @@ main = do testGroup "Database" [ Test.IO.Cardano.Db.Migration.tests + , Test.IO.Cardano.Db.EpochBlockTxs.tests , Test.IO.Cardano.Db.Insert.tests , Test.IO.Cardano.Db.TotalSupply.tests , Test.IO.Cardano.Db.Rollback.tests From a74ca68bd95a18a6e1866813c0ce14e4b2e7203d Mon Sep 17 00:00:00 2001 From: Artur Wieczorek Date: Tue, 28 Jul 2026 18:47:33 +0200 Subject: [PATCH 2/2] fix(db-tool): count epoch block txs by id and clamp block-range sampling The epoch block-tx check passed block_no to queryBlockTxCount, which keys on block.id; the two diverge on any real chain, so it reported false tx-count mismatches. queryEpochBlockNumbers now also returns the block id and the check counts by it. The block-property checks computed a random start with blkCount - 100000 in Word64, which underflowed to an absurd range on databases with fewer than 100000 blocks. Clamp the sample window to the available block count. --- .../DbTool/Validate/BlockProperties.hs | 21 +++++++++++++------ .../src/Cardano/DbTool/Validate/BlockTxs.hs | 6 +++--- cardano-db/src/Cardano/Db/Statement/DbTool.hs | 9 ++++---- .../test/Test/IO/Cardano/Db/EpochBlockTxs.hs | 4 ++-- 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/cardano-db-tool/src/Cardano/DbTool/Validate/BlockProperties.hs b/cardano-db-tool/src/Cardano/DbTool/Validate/BlockProperties.hs index f5afecfdb..dd641cfde 100644 --- a/cardano-db-tool/src/Cardano/DbTool/Validate/BlockProperties.hs +++ b/cardano-db-tool/src/Cardano/DbTool/Validate/BlockProperties.hs @@ -51,16 +51,24 @@ validateBlockTimesInPast = do showFirst (mEpoch, mBlockNo, time) = mconcat ["epoch ", show mEpoch, " block ", show mBlockNo, " time ", show time] +-- The random start block and how many blocks to sample, clamped so the range +-- never underflows on a database with fewer than testBlocks blocks. +sampleWindow :: Word64 -> Word64 -> (Word64, Word64) +sampleWindow blkCount testBlocks = + let count = min testBlocks blkCount + in (blkCount - count, count) + validataBlockNosContiguous :: Word64 -> IO () validataBlockNosContiguous blkCount = do - startBlock <- Random.randomRIO (0, blkCount - testBlocks) + let (maxStart, count) = sampleWindow blkCount testBlocks + startBlock <- Random.randomRIO (0, maxStart) putStrF $ "Block numbers [" ++ show startBlock ++ " .. " - ++ show (startBlock + testBlocks) + ++ show (startBlock + count) ++ "] are contiguous: " - blockNos <- DB.runDbStandaloneSilent $ DB.queryBlockNoList startBlock testBlocks + blockNos <- DB.runDbStandaloneSilent $ DB.queryBlockNoList startBlock count case checkContinguous blockNos of Nothing -> putStrLn $ greenText "ok" Just xs -> error $ redText "failed: " ++ show xs @@ -79,14 +87,15 @@ validataBlockNosContiguous blkCount = do validateTimestampsOrdered :: Word64 -> IO () validateTimestampsOrdered blkCount = do - startBlock <- Random.randomRIO (0, blkCount - testBlocks) + let (maxStart, count) = sampleWindow blkCount testBlocks + startBlock <- Random.randomRIO (0, maxStart) putStrF $ "Block time stamps for blocks [" ++ show startBlock ++ " .. " - ++ show (startBlock + testBlocks) + ++ show (startBlock + count) ++ "] are ordered: " - ts <- DB.runDbStandaloneSilent $ DB.queryBlockTimestamps startBlock testBlocks + ts <- DB.runDbStandaloneSilent $ DB.queryBlockTimestamps startBlock count if List.nubOrd ts == ts then putStrLn $ greenText "ok" else error $ redText "failed: " ++ show ts diff --git a/cardano-db-tool/src/Cardano/DbTool/Validate/BlockTxs.hs b/cardano-db-tool/src/Cardano/DbTool/Validate/BlockTxs.hs index 520b6eb3d..d1827e673 100644 --- a/cardano-db-tool/src/Cardano/DbTool/Validate/BlockTxs.hs +++ b/cardano-db-tool/src/Cardano/DbTool/Validate/BlockTxs.hs @@ -51,9 +51,9 @@ validateBlockTxs epoch = do ++ show (veTxCountActual ve) ) -validateBlockCount :: (Word64, Word64) -> DB.DbM (Either ValidateError ()) -validateBlockCount (blockNo, txCountExpected) = do - txCountActual <- DB.queryBlockTxCount $ DB.BlockId $ fromIntegral blockNo +validateBlockCount :: (DB.BlockId, Word64, Word64) -> DB.DbM (Either ValidateError ()) +validateBlockCount (blockId, blockNo, txCountExpected) = do + txCountActual <- DB.queryBlockTxCount blockId pure $ if txCountActual == txCountExpected then Right () diff --git a/cardano-db/src/Cardano/Db/Statement/DbTool.hs b/cardano-db/src/Cardano/Db/Statement/DbTool.hs index 854ee79ae..b2de4db0b 100644 --- a/cardano-db/src/Cardano/Db/Statement/DbTool.hs +++ b/cardano-db/src/Cardano/Db/Statement/DbTool.hs @@ -871,7 +871,7 @@ queryOutputsAddress saId = -------------------------------------------------------------------------------- -queryEpochBlockNumbersStmt :: HsqlStmt.Statement Word64 [(Word64, Word64)] +queryEpochBlockNumbersStmt :: HsqlStmt.Statement Word64 [(Id.BlockId, Word64, Word64)] queryEpochBlockNumbersStmt = HsqlStmt.Statement sql encoder decoder True where @@ -880,7 +880,7 @@ queryEpochBlockNumbersStmt = sql = TextEnc.encodeUtf8 $ Text.concat - [ "SELECT COALESCE(block_no, 0), tx_count" + [ "SELECT id, COALESCE(block_no, 0), tx_count" , " FROM " <> blockTableN , " WHERE epoch_no = $1" ] @@ -888,10 +888,11 @@ queryEpochBlockNumbersStmt = encoder = fromIntegral >$< HsqlE.param (HsqlE.nonNullable HsqlE.int8) decoder = HsqlD.rowList $ do + blockId <- Id.idDecoder Id.BlockId blockNo <- HsqlD.column (HsqlD.nonNullable $ fromIntegral <$> HsqlD.int8) txCount <- HsqlD.column (HsqlD.nonNullable $ fromIntegral <$> HsqlD.int8) - pure (blockNo, txCount) + pure (blockId, blockNo, txCount) -queryEpochBlockNumbers :: Word64 -> DbM [(Word64, Word64)] +queryEpochBlockNumbers :: Word64 -> DbM [(Id.BlockId, Word64, Word64)] queryEpochBlockNumbers epoch = runSession mkDbCallStack $ HsqlSes.statement epoch queryEpochBlockNumbersStmt diff --git a/cardano-db/test/Test/IO/Cardano/Db/EpochBlockTxs.hs b/cardano-db/test/Test/IO/Cardano/Db/EpochBlockTxs.hs index 3aa17d0a0..ed038bdbf 100644 --- a/cardano-db/test/Test/IO/Cardano/Db/EpochBlockTxs.hs +++ b/cardano-db/test/Test/IO/Cardano/Db/EpochBlockTxs.hs @@ -32,8 +32,8 @@ epochBlockTxCountUsesId = [] -> error "mkTxs returned empty list" rows <- queryEpochBlockNumbers 0 case rows of - [(blockNo, expected)] -> do - actual <- queryBlockTxCount (BlockId (fromIntegral blockNo)) + [(blockId, _blockNo, expected)] -> do + actual <- queryBlockTxCount blockId assertBool ("expected tx count " ++ show expected ++ " but got " ++ show actual) (actual == expected)