Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ unitTests iom knownMigrations =
, testGroup
"rollbacks"
[ test "simple rollback" Rollback.simpleRollback
, test "drepDistr rollback" Rollback.drepDistrRollback
, test "sync bigger chain" Rollback.bigChain
, test "rollback while db-sync is off" Rollback.restartAndRollback
, test "big rollback executed lazily" Rollback.lazyRollback
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{-# LANGUAGE NumericUnderscores #-}
{-# OPTIONS_GHC -Wno-x-partial #-}

module Test.Cardano.Db.Mock.Unit.Conway.Rollback (
simpleRollback,
Expand All @@ -10,24 +11,28 @@ module Test.Cardano.Db.Mock.Unit.Conway.Rollback (
stakeAddressRollback,
rollbackChangeTxOrder,
rollbackFullTx,
drepDistrRollback,
) where

import qualified Cardano.Db as DB
import Cardano.DbSync.Era.Shelley.Generic.Util (unCredentialHash)
import Cardano.Ledger.Coin (Coin (..))
import Cardano.Ledger.Conway.TxCert (ConwayDelegCert (..), Delegatee (..))
import Cardano.Mock.ChainSync.Server (IOManager (), addBlock, rollback)
import Cardano.Mock.Forging.Interpreter (forgeNext)
import qualified Cardano.Mock.Forging.Tx.Conway as Conway
import Cardano.Mock.Forging.Tx.Generic (resolvePool)
import qualified Cardano.Mock.Forging.Tx.Generic as Forging
import Cardano.Mock.Forging.Types (PoolIndex (..), StakeIndex (..), UTxOIndex (..))
import Cardano.Prelude
import Data.Maybe.Strict (StrictMaybe (..))
import Ouroboros.Network.Block (blockPoint)
import Test.Cardano.Db.Mock.Config
import Test.Cardano.Db.Mock.Examples (mockBlock0, mockBlock1, mockBlock2)
import Test.Cardano.Db.Mock.UnifiedApi
import Test.Cardano.Db.Mock.Validate (assertBlockNoBackoff, assertTxCount)
import Test.Cardano.Db.Mock.Validate (assertBlockNoBackoff, assertEqQuery, assertTxCount)
import Test.Tasty.HUnit (Assertion ())
import Prelude (last)
import Prelude (error, head, last)

simpleRollback :: IOManager -> [(Text, Text)] -> Assertion
simpleRollback =
Expand Down Expand Up @@ -55,7 +60,7 @@ simpleRollback =

bigChain :: IOManager -> [(Text, Text)] -> Assertion
bigChain =
withFullConfig conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do
withFullConfigDropDB conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do
-- Forge some blocks
forM_ (replicate 101 mockBlock0) (forgeNextAndSubmit interpreter mockServer)

Expand All @@ -81,7 +86,7 @@ bigChain =

restartAndRollback :: IOManager -> [(Text, Text)] -> Assertion
restartAndRollback =
withFullConfig conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do
withFullConfigDropDB conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do
-- Forge some blocks
forM_ (replicate 101 mockBlock0) (forgeNextAndSubmit interpreter mockServer)

Expand Down Expand Up @@ -109,7 +114,7 @@ restartAndRollback =

lazyRollback :: IOManager -> [(Text, Text)] -> Assertion
lazyRollback =
withFullConfig conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do
withFullConfigDropDB conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do
startDBSync dbSync

-- Create a point to rollback to
Expand All @@ -134,7 +139,7 @@ lazyRollback =

lazyRollbackRestart :: IOManager -> [(Text, Text)] -> Assertion
lazyRollbackRestart =
withFullConfig conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do
withFullConfigDropDB conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do
startDBSync dbSync

-- Create a point to rollback to
Expand Down Expand Up @@ -162,7 +167,7 @@ lazyRollbackRestart =

doubleRollback :: IOManager -> [(Text, Text)] -> Assertion
doubleRollback =
withFullConfig conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do
withFullConfigDropDB conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do
startDBSync dbSync

-- Create points to rollback to
Expand Down Expand Up @@ -197,7 +202,7 @@ doubleRollback =

stakeAddressRollback :: IOManager -> [(Text, Text)] -> Assertion
stakeAddressRollback =
withFullConfig conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do
withFullConfigDropDB conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do
startDBSync dbSync

-- Create a point to rollbackTo
Expand Down Expand Up @@ -231,7 +236,7 @@ stakeAddressRollback =

rollbackChangeTxOrder :: IOManager -> [(Text, Text)] -> Assertion
rollbackChangeTxOrder =
withFullConfig conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do
withFullConfigDropDB conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do
startDBSync dbSync

-- Create a point to rollback to
Expand Down Expand Up @@ -262,7 +267,7 @@ rollbackChangeTxOrder =

rollbackFullTx :: IOManager -> [(Text, Text)] -> Assertion
rollbackFullTx =
withFullConfig conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do
withFullConfigDropDB conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do
startDBSync dbSync

-- Create a point to rollback to
Expand Down Expand Up @@ -291,3 +296,76 @@ rollbackFullTx =
assertTxCount dbSync 14
where
testLabel = "conwayRollbackFullTx"

-- | Test for DrepDistr rollback edge case when rolling back to an epoch boundary.
-- Verifies that DrepDistr records are properly deleted during rollback and replay succeeds
-- without duplicate key constraint violations.
drepDistrRollback :: IOManager -> [(Text, Text)] -> Assertion
drepDistrRollback =
withFullConfigDropDB conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do
startDBSync dbSync

-- Register stake credentials and DReps
void $ registerAllStakeCreds interpreter mockServer
void $ registerDRepsAndDelegateVotes interpreter mockServer

-- Fill the rest of epoch 0 and cross into epoch 1
-- This triggers insertDrepDistr for epoch 1 at the epoch boundary (first block of epoch 1)
epoch0 <- fillUntilNextEpoch interpreter mockServer
assertBlockNoBackoff dbSync (2 + length epoch0)

-- Verify DrepDistr for epoch 1 was inserted
let drepId = Prelude.head Forging.unregisteredDRepIds
assertEqQuery
dbSync
(DB.queryDRepDistrAmount (unCredentialHash drepId) 1)
10_000
"Expected DrepDistr for epoch 1 after crossing boundary"

-- Fill all of epoch 1 and cross into epoch 2
epoch1 <- fillUntilNextEpoch interpreter mockServer
assertBlockNoBackoff dbSync (2 + length epoch0 + length epoch1)

-- Verify DrepDistr for epoch 2 was inserted
assertEqQuery
dbSync
(DB.queryDRepDistrAmount (unCredentialHash drepId) 2)
10_000
"Expected DrepDistr for epoch 2 after crossing boundary"

-- Identify the epoch 2 boundary block (last block of epoch1 list)
rollbackPoint <- case reverse epoch1 of
[] -> error "fillUntilNextEpoch returned empty list for epoch 1"
(epoch2Boundary : _) -> pure $ blockPoint epoch2Boundary

-- Continue a bit into epoch 2 (after DrepDistr insertion at the boundary)
blksAfter <- forgeAndSubmitBlocks interpreter mockServer 3
assertBlockNoBackoff dbSync (2 + length epoch0 + length epoch1 + length blksAfter)

-- Rollback to the epoch 2 boundary (first block of epoch 2)
rollbackTo interpreter mockServer rollbackPoint

-- Create fork - replay through the epoch 2 boundary
-- This will re-insert DrepDistr for epoch 2
-- SUCCESS: No duplicate key constraint violation because epoch 2 was properly deleted
-- (If the fix didn't work, we'd get a unique constraint violation here)
blksFork <- forgeAndSubmitBlocks interpreter mockServer 5

-- Verify DrepDistr for epoch 1 still exists (not affected by rollback)
assertEqQuery
dbSync
(DB.queryDRepDistrAmount (unCredentialHash drepId) 1)
10_000
"DrepDistr for epoch 1 should still exist after rollback"

-- Verify final state
assertBlockNoBackoff dbSync (2 + length epoch0 + length epoch1 + length blksFork)

-- Verify DrepDistr for both epochs exist after replay
assertEqQuery
dbSync
(DB.queryDRepDistrAmount (unCredentialHash drepId) 2)
10_000
"DrepDistr for epoch 2 should be re-inserted after replay through boundary"
where
testLabel = "conwayDrepDistrRollback"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[12,16,18,21,24,30,31,32,33,40,41,42,43,47,52,60,62,70,80,84,86,92,98,100,106,109,110,111,112,127,134,138,146,149,154,166,168,178,183,188,193,194,198,200,202,220,222,223,224,225,231,239,242,247,261,282,283,288,289,301,302,303,308,313,315,316,320,331,334,344,345,363,364,368,369,375,377,381,389,394,407,418,422,425,430,437,438,439,440,447,450,453,454,456,458,461,467,492,499,507,516,524,538,541,544,546,550,567,573,576,577,579,580,586,589,595,597,603,605,609,616,618,619,623,624,634,636,643,644,659,664,665,672,678,692,705,711,712,719,726,730,739,740,743,747,749,751,754,759,762,763,765,767,773,777,786,788,789,794,801,806,807,829,830,832,849,851,853,869,871,874,875,878,882,888,893,895,896,898,899,903,906,908,911,912,913,922,930,932,938,941,944,950,960,963,966,968,972,977,985,986,988,990,991,994,997,1001,1005,1008,1014,1005,1008,1014,1019,1020]
1 change: 1 addition & 0 deletions cardano-chain-gen/test/testfiles/pgpass-testing-macos
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/tmp:5432:testing:*:*
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import Cardano.Prelude
import Cardano.Slotting.Slot (EpochNo (..))

import Cardano.DbSync.Api
import Cardano.DbSync.Api.Types (EpochStatistics (..), SyncEnv (..), InsertOptions (..), UnicodeNullSource, formatUnicodeNullSource)
import Cardano.DbSync.Api.Types (EpochStatistics (..), InsertOptions (..), SyncEnv (..), UnicodeNullSource, formatUnicodeNullSource)
import Cardano.DbSync.Cache.Types (textShowCacheStats)
import Cardano.DbSync.Era.Cardano.Util (insertEpochSyncTime, resetEpochStatistics)
import qualified Cardano.DbSync.Era.Shelley.Generic as Generic
Expand Down Expand Up @@ -54,7 +54,7 @@ insertNewEpochLedgerEvents syncEnv currentEpochNo@(EpochNo curEpoch) =
tracer = getTrace syncEnv
cache = envCache syncEnv
ntw = getNetwork syncEnv
    iopts = getInsertOptions syncEnv
iopts = getInsertOptions syncEnv

subFromCurrentEpoch :: Word64 -> EpochNo
subFromCurrentEpoch m =
Expand Down
19 changes: 16 additions & 3 deletions cardano-db/src/Cardano/Db/Statement/Base.hs
Original file line number Diff line number Diff line change
Expand Up @@ -187,12 +187,18 @@ queryBlockNoAndEpochStmt =
epochNo <- HsqlD.column (HsqlD.nonNullable $ fromIntegral <$> HsqlD.int8)
pure (blockId, epochNo)

-- Get the block ID of the rollback point, but the epoch_no of the previous block.
-- This handles the edge case where rollback is to the first block of a new epoch
-- (where DrepDistr will be inserted). Using the previous block's epoch ensures
-- DrepDistr for the current epoch gets deleted, preventing duplicates
-- when replaying through the epoch boundary.
sql =
TextEnc.encodeUtf8 $
Text.concat
[ "SELECT id, epoch_no"
, " FROM " <> tableName (Proxy @a)
, " WHERE block_no = $1"
[ "SELECT curr.id, prev.epoch_no"
, " FROM " <> tableName (Proxy @a) <> " curr"
, " JOIN " <> tableName (Proxy @a) <> " prev ON prev.block_no = $1 - 1"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this join ever fail, ie having no prev? For example could this query be called when rolling back to genesis?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good question, It would fail if it rolled back to the first block in genesis as it doesn't have a previous block. Do we ever do that?

, " WHERE curr.block_no = $1"
]

queryBlockNoAndEpoch :: Word64 -> DbM (Maybe (Id.BlockId, Word64))
Expand Down Expand Up @@ -686,6 +692,9 @@ deleteBlocksBlockId ::
deleteBlocksBlockId trce txOutVariantType blockId epochN isConsumedTxOut = do
let rb = "Rollback - "

-- Log the epoch being used (comes from previous block's epoch for epoch boundary rollbacks)
liftIO $ logInfo trce $ rb <> "Using epoch " <> textShow epochN <> " (from previous block) for epoch-related deletions"

withProgress (Just trce) 6 rb $ \progressRef -> do
-- Step 0: Initialize
liftIO $ updateProgress (Just trce) progressRef 0 (rb <> "Initializing rollback...")
Expand Down Expand Up @@ -781,13 +790,17 @@ deleteUsingEpochNo trce epochN = do
let epochEncoder = fromIntegral >$< HsqlE.param (HsqlE.nonNullable HsqlE.int8)
epochInt64 = fromIntegral epochN

-- Log which epoch is being used for deletion (this comes from previous block's epoch for boundary rollbacks)
liftIO $ logInfo trce $ "Rollback - Using epoch " <> textShow epochN <> " for deletion (DrepDistr: epoch_no > " <> textShow epochN <> ")"

-- First, count what we're about to delete for progress tracking
totalCounts <- withProgress (Just trce) 5 "Counting epoch records..." $ \progressRef -> do
liftIO $ updateProgress (Just trce) progressRef 0 "Counting Epoch records..."
ec <- runSession mkDbCallStack $ HsqlSes.statement epochN (parameterisedCountWhere @SC.Epoch "no" ">= $1" epochEncoder)

liftIO $ updateProgress (Just trce) progressRef 1 "Counting DrepDistr records..."
dc <- runSession mkDbCallStack $ HsqlSes.statement epochN (parameterisedCountWhere @SC.DrepDistr "epoch_no" "> $1" epochEncoder)
liftIO $ logInfo trce $ "Rollback - Found " <> textShow dc <> " DrepDistr records to delete for epochs > " <> textShow epochN

liftIO $ updateProgress (Just trce) progressRef 2 "Counting RewardRest records..."
rrc <- runSession mkDbCallStack $ HsqlSes.statement epochN (parameterisedCountWhere @SC.RewardRest "spendable_epoch" "> $1" epochEncoder)
Expand Down
1 change: 1 addition & 0 deletions config/pgpass-mainnet-macos
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/tmp:5432:cexplorer:*:*
4 changes: 2 additions & 2 deletions scripts/run-everything-tmux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ tmux send-keys -t 0 "cardano-node run --config $TESTNET_DIR/config.json --databa

# Cardano DB-Sync
tmux send-keys -t 1 "cd $CARDANO_DB_SYNC_DIR/" 'C-m'; sleep 3
tmux send-keys -t 1 "export PGPASSFILE=$CARDANO_DB_SYNC_DIR/config/pgpass-mainnet" 'C-m'; sleep 2
tmux send-keys -t 1 "PGPASSFILE=$CARDANO_DB_SYNC_DIR/config/pgpass-mainnet $dbsync --config $TESTNET_DIR/db-sync-config.json --socket-path $TESTNET_DIR/db/node.socket --state-dir $TESTNET_DIR/ledger-state --schema-dir $CARDANO_DB_SYNC_DIR/schema/" 'C-m'
tmux send-keys -t 1 "export PGPASSFILE=$CARDANO_DB_SYNC_DIR/config/pgpass-mainnet-macos" 'C-m'; sleep 2
tmux send-keys -t 1 "PGPASSFILE=$CARDANO_DB_SYNC_DIR/config/pgpass-mainnet-macos $dbsync --config $TESTNET_DIR/db-sync-config.json --socket-path $TESTNET_DIR/db/node.socket --state-dir $TESTNET_DIR/ledger-state --schema-dir $CARDANO_DB_SYNC_DIR/schema/" 'C-m'
# tmux send-keys -t 1 "$dbsync --config $TESTNET_DIR/db-sync-config.json --socket-path $TESTNET_DIR/db/node.socket --state-dir $TESTNET_DIR/ledger-state --schema-dir $CARDANO_DB_SYNC_DIR/schema/ +RTS -p -hc -L200 -RTS" 'C-m'

tmux -CC attach-session -t $session
Expand Down
Loading