Skip to content

Commit a1a5e2f

Browse files
committed
2007 drep_distr rollback error
1 parent 83dde7c commit a1a5e2f

File tree

8 files changed

+112
-17
lines changed

8 files changed

+112
-17
lines changed

cardano-chain-gen/test/Test/Cardano/Db/Mock/Unit/Conway.hs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ unitTests iom knownMigrations =
118118
, testGroup
119119
"rollbacks"
120120
[ test "simple rollback" Rollback.simpleRollback
121+
, test "drepDistr rollback" Rollback.drepDistrRollback
121122
, test "sync bigger chain" Rollback.bigChain
122123
, test "rollback while db-sync is off" Rollback.restartAndRollback
123124
, test "big rollback executed lazily" Rollback.lazyRollback

cardano-chain-gen/test/Test/Cardano/Db/Mock/Unit/Conway/Rollback.hs

Lines changed: 88 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{-# LANGUAGE NumericUnderscores #-}
2+
{-# OPTIONS_GHC -Wno-x-partial #-}
23

34
module Test.Cardano.Db.Mock.Unit.Conway.Rollback (
45
simpleRollback,
@@ -10,24 +11,28 @@ module Test.Cardano.Db.Mock.Unit.Conway.Rollback (
1011
stakeAddressRollback,
1112
rollbackChangeTxOrder,
1213
rollbackFullTx,
14+
drepDistrRollback,
1315
) where
1416

17+
import qualified Cardano.Db as DB
18+
import Cardano.DbSync.Era.Shelley.Generic.Util (unCredentialHash)
1519
import Cardano.Ledger.Coin (Coin (..))
1620
import Cardano.Ledger.Conway.TxCert (ConwayDelegCert (..), Delegatee (..))
1721
import Cardano.Mock.ChainSync.Server (IOManager (), addBlock, rollback)
1822
import Cardano.Mock.Forging.Interpreter (forgeNext)
1923
import qualified Cardano.Mock.Forging.Tx.Conway as Conway
2024
import Cardano.Mock.Forging.Tx.Generic (resolvePool)
25+
import qualified Cardano.Mock.Forging.Tx.Generic as Forging
2126
import Cardano.Mock.Forging.Types (PoolIndex (..), StakeIndex (..), UTxOIndex (..))
2227
import Cardano.Prelude
2328
import Data.Maybe.Strict (StrictMaybe (..))
2429
import Ouroboros.Network.Block (blockPoint)
2530
import Test.Cardano.Db.Mock.Config
2631
import Test.Cardano.Db.Mock.Examples (mockBlock0, mockBlock1, mockBlock2)
2732
import Test.Cardano.Db.Mock.UnifiedApi
28-
import Test.Cardano.Db.Mock.Validate (assertBlockNoBackoff, assertTxCount)
33+
import Test.Cardano.Db.Mock.Validate (assertBlockNoBackoff, assertEqQuery, assertTxCount)
2934
import Test.Tasty.HUnit (Assertion ())
30-
import Prelude (last)
35+
import Prelude (error, head, last)
3136

3237
simpleRollback :: IOManager -> [(Text, Text)] -> Assertion
3338
simpleRollback =
@@ -55,7 +60,7 @@ simpleRollback =
5560

5661
bigChain :: IOManager -> [(Text, Text)] -> Assertion
5762
bigChain =
58-
withFullConfig conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do
63+
withFullConfigDropDB conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do
5964
-- Forge some blocks
6065
forM_ (replicate 101 mockBlock0) (forgeNextAndSubmit interpreter mockServer)
6166

@@ -81,7 +86,7 @@ bigChain =
8186

8287
restartAndRollback :: IOManager -> [(Text, Text)] -> Assertion
8388
restartAndRollback =
84-
withFullConfig conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do
89+
withFullConfigDropDB conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do
8590
-- Forge some blocks
8691
forM_ (replicate 101 mockBlock0) (forgeNextAndSubmit interpreter mockServer)
8792

@@ -109,7 +114,7 @@ restartAndRollback =
109114

110115
lazyRollback :: IOManager -> [(Text, Text)] -> Assertion
111116
lazyRollback =
112-
withFullConfig conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do
117+
withFullConfigDropDB conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do
113118
startDBSync dbSync
114119

115120
-- Create a point to rollback to
@@ -134,7 +139,7 @@ lazyRollback =
134139

135140
lazyRollbackRestart :: IOManager -> [(Text, Text)] -> Assertion
136141
lazyRollbackRestart =
137-
withFullConfig conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do
142+
withFullConfigDropDB conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do
138143
startDBSync dbSync
139144

140145
-- Create a point to rollback to
@@ -162,7 +167,7 @@ lazyRollbackRestart =
162167

163168
doubleRollback :: IOManager -> [(Text, Text)] -> Assertion
164169
doubleRollback =
165-
withFullConfig conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do
170+
withFullConfigDropDB conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do
166171
startDBSync dbSync
167172

168173
-- Create points to rollback to
@@ -197,7 +202,7 @@ doubleRollback =
197202

198203
stakeAddressRollback :: IOManager -> [(Text, Text)] -> Assertion
199204
stakeAddressRollback =
200-
withFullConfig conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do
205+
withFullConfigDropDB conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do
201206
startDBSync dbSync
202207

203208
-- Create a point to rollbackTo
@@ -231,7 +236,7 @@ stakeAddressRollback =
231236

232237
rollbackChangeTxOrder :: IOManager -> [(Text, Text)] -> Assertion
233238
rollbackChangeTxOrder =
234-
withFullConfig conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do
239+
withFullConfigDropDB conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do
235240
startDBSync dbSync
236241

237242
-- Create a point to rollback to
@@ -262,7 +267,7 @@ rollbackChangeTxOrder =
262267

263268
rollbackFullTx :: IOManager -> [(Text, Text)] -> Assertion
264269
rollbackFullTx =
265-
withFullConfig conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do
270+
withFullConfigDropDB conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do
266271
startDBSync dbSync
267272

268273
-- Create a point to rollback to
@@ -291,3 +296,76 @@ rollbackFullTx =
291296
assertTxCount dbSync 14
292297
where
293298
testLabel = "conwayRollbackFullTx"
299+
300+
-- | Test for DrepDistr rollback edge case when rolling back to an epoch boundary.
301+
-- Verifies that DrepDistr records are properly deleted during rollback and replay succeeds
302+
-- without duplicate key constraint violations.
303+
drepDistrRollback :: IOManager -> [(Text, Text)] -> Assertion
304+
drepDistrRollback =
305+
withFullConfigDropDB conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do
306+
startDBSync dbSync
307+
308+
-- Register stake credentials and DReps
309+
void $ registerAllStakeCreds interpreter mockServer
310+
void $ registerDRepsAndDelegateVotes interpreter mockServer
311+
312+
-- Fill the rest of epoch 0 and cross into epoch 1
313+
-- This triggers insertDrepDistr for epoch 1 at the epoch boundary (first block of epoch 1)
314+
epoch0 <- fillUntilNextEpoch interpreter mockServer
315+
assertBlockNoBackoff dbSync (2 + length epoch0)
316+
317+
-- Verify DrepDistr for epoch 1 was inserted
318+
let drepId = Prelude.head Forging.unregisteredDRepIds
319+
assertEqQuery
320+
dbSync
321+
(DB.queryDRepDistrAmount (unCredentialHash drepId) 1)
322+
10_000
323+
"Expected DrepDistr for epoch 1 after crossing boundary"
324+
325+
-- Fill all of epoch 1 and cross into epoch 2
326+
epoch1 <- fillUntilNextEpoch interpreter mockServer
327+
assertBlockNoBackoff dbSync (2 + length epoch0 + length epoch1)
328+
329+
-- Verify DrepDistr for epoch 2 was inserted
330+
assertEqQuery
331+
dbSync
332+
(DB.queryDRepDistrAmount (unCredentialHash drepId) 2)
333+
10_000
334+
"Expected DrepDistr for epoch 2 after crossing boundary"
335+
336+
-- Identify the epoch 2 boundary block (last block of epoch1 list)
337+
rollbackPoint <- case reverse epoch1 of
338+
[] -> error "fillUntilNextEpoch returned empty list for epoch 1"
339+
(epoch2Boundary : _) -> pure $ blockPoint epoch2Boundary
340+
341+
-- Continue a bit into epoch 2 (after DrepDistr insertion at the boundary)
342+
blksAfter <- forgeAndSubmitBlocks interpreter mockServer 3
343+
assertBlockNoBackoff dbSync (2 + length epoch0 + length epoch1 + length blksAfter)
344+
345+
-- Rollback to the epoch 2 boundary (first block of epoch 2)
346+
rollbackTo interpreter mockServer rollbackPoint
347+
348+
-- Create fork - replay through the epoch 2 boundary
349+
-- This will re-insert DrepDistr for epoch 2
350+
-- SUCCESS: No duplicate key constraint violation because epoch 2 was properly deleted
351+
-- (If the fix didn't work, we'd get a unique constraint violation here)
352+
blksFork <- forgeAndSubmitBlocks interpreter mockServer 5
353+
354+
-- Verify DrepDistr for epoch 1 still exists (not affected by rollback)
355+
assertEqQuery
356+
dbSync
357+
(DB.queryDRepDistrAmount (unCredentialHash drepId) 1)
358+
10_000
359+
"DrepDistr for epoch 1 should still exist after rollback"
360+
361+
-- Verify final state
362+
assertBlockNoBackoff dbSync (2 + length epoch0 + length epoch1 + length blksFork)
363+
364+
-- Verify DrepDistr for both epochs exist after replay
365+
assertEqQuery
366+
dbSync
367+
(DB.queryDRepDistrAmount (unCredentialHash drepId) 2)
368+
10_000
369+
"DrepDistr for epoch 2 should be re-inserted after replay through boundary"
370+
where
371+
testLabel = "conwayDrepDistrRollback"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/tmp:5432:testing:*:*

cardano-db-sync/src/Cardano/DbSync/Era/Universal/Insert/LedgerEvent.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import Cardano.Prelude
1818
import Cardano.Slotting.Slot (EpochNo (..))
1919

2020
import Cardano.DbSync.Api
21-
import Cardano.DbSync.Api.Types (EpochStatistics (..), SyncEnv (..), InsertOptions (..), UnicodeNullSource, formatUnicodeNullSource)
21+
import Cardano.DbSync.Api.Types (EpochStatistics (..), InsertOptions (..), SyncEnv (..), UnicodeNullSource, formatUnicodeNullSource)
2222
import Cardano.DbSync.Cache.Types (textShowCacheStats)
2323
import Cardano.DbSync.Era.Cardano.Util (insertEpochSyncTime, resetEpochStatistics)
2424
import qualified Cardano.DbSync.Era.Shelley.Generic as Generic
@@ -54,7 +54,7 @@ insertNewEpochLedgerEvents syncEnv currentEpochNo@(EpochNo curEpoch) =
5454
tracer = getTrace syncEnv
5555
cache = envCache syncEnv
5656
ntw = getNetwork syncEnv
57-
    iopts = getInsertOptions syncEnv
57+
iopts = getInsertOptions syncEnv
5858

5959
subFromCurrentEpoch :: Word64 -> EpochNo
6060
subFromCurrentEpoch m =

cardano-db/src/Cardano/Db/Statement/Base.hs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -187,12 +187,18 @@ queryBlockNoAndEpochStmt =
187187
epochNo <- HsqlD.column (HsqlD.nonNullable $ fromIntegral <$> HsqlD.int8)
188188
pure (blockId, epochNo)
189189

190+
-- Get the block ID of the rollback point, but the epoch_no of the previous block.
191+
-- This handles the edge case where rollback is to the first block of a new epoch
192+
-- (where DrepDistr will be inserted). Using the previous block's epoch ensures
193+
-- DrepDistr for the current epoch gets deleted, preventing duplicates
194+
-- when replaying through the epoch boundary.
190195
sql =
191196
TextEnc.encodeUtf8 $
192197
Text.concat
193-
[ "SELECT id, epoch_no"
194-
, " FROM " <> tableName (Proxy @a)
195-
, " WHERE block_no = $1"
198+
[ "SELECT curr.id, prev.epoch_no"
199+
, " FROM " <> tableName (Proxy @a) <> " curr"
200+
, " JOIN " <> tableName (Proxy @a) <> " prev ON prev.block_no = $1 - 1"
201+
, " WHERE curr.block_no = $1"
196202
]
197203

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

695+
-- Log the epoch being used (comes from previous block's epoch for epoch boundary rollbacks)
696+
liftIO $ logInfo trce $ rb <> "Using epoch " <> textShow epochN <> " (from previous block) for epoch-related deletions"
697+
689698
withProgress (Just trce) 6 rb $ \progressRef -> do
690699
-- Step 0: Initialize
691700
liftIO $ updateProgress (Just trce) progressRef 0 (rb <> "Initializing rollback...")
@@ -781,13 +790,17 @@ deleteUsingEpochNo trce epochN = do
781790
let epochEncoder = fromIntegral >$< HsqlE.param (HsqlE.nonNullable HsqlE.int8)
782791
epochInt64 = fromIntegral epochN
783792

793+
-- Log which epoch is being used for deletion (this comes from previous block's epoch for boundary rollbacks)
794+
liftIO $ logInfo trce $ "Rollback - Using epoch " <> textShow epochN <> " for deletion (DrepDistr: epoch_no > " <> textShow epochN <> ")"
795+
784796
-- First, count what we're about to delete for progress tracking
785797
totalCounts <- withProgress (Just trce) 5 "Counting epoch records..." $ \progressRef -> do
786798
liftIO $ updateProgress (Just trce) progressRef 0 "Counting Epoch records..."
787799
ec <- runSession mkDbCallStack $ HsqlSes.statement epochN (parameterisedCountWhere @SC.Epoch "no" ">= $1" epochEncoder)
788800

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

792805
liftIO $ updateProgress (Just trce) progressRef 2 "Counting RewardRest records..."
793806
rrc <- runSession mkDbCallStack $ HsqlSes.statement epochN (parameterisedCountWhere @SC.RewardRest "spendable_epoch" "> $1" epochEncoder)

config/pgpass-mainnet-macos

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/tmp:5432:cexplorer:*:*

scripts/run-everything-tmux.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ tmux send-keys -t 0 "cardano-node run --config $TESTNET_DIR/config.json --databa
2727

2828
# Cardano DB-Sync
2929
tmux send-keys -t 1 "cd $CARDANO_DB_SYNC_DIR/" 'C-m'; sleep 3
30-
tmux send-keys -t 1 "export PGPASSFILE=$CARDANO_DB_SYNC_DIR/config/pgpass-mainnet" 'C-m'; sleep 2
31-
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'
30+
tmux send-keys -t 1 "export PGPASSFILE=$CARDANO_DB_SYNC_DIR/config/pgpass-mainnet-macos" 'C-m'; sleep 2
31+
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'
3232
# 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'
3333

3434
tmux -CC attach-session -t $session

0 commit comments

Comments
 (0)