diff --git a/cardano-chain-gen/test/Test/Cardano/Db/Mock/Unit/Conway.hs b/cardano-chain-gen/test/Test/Cardano/Db/Mock/Unit/Conway.hs index 7ae03a9f7..b7a753d3e 100644 --- a/cardano-chain-gen/test/Test/Cardano/Db/Mock/Unit/Conway.hs +++ b/cardano-chain-gen/test/Test/Cardano/Db/Mock/Unit/Conway.hs @@ -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 diff --git a/cardano-chain-gen/test/Test/Cardano/Db/Mock/Unit/Conway/Rollback.hs b/cardano-chain-gen/test/Test/Cardano/Db/Mock/Unit/Conway/Rollback.hs index 0a524e99b..27176412b 100644 --- a/cardano-chain-gen/test/Test/Cardano/Db/Mock/Unit/Conway/Rollback.hs +++ b/cardano-chain-gen/test/Test/Cardano/Db/Mock/Unit/Conway/Rollback.hs @@ -1,4 +1,5 @@ {-# LANGUAGE NumericUnderscores #-} +{-# OPTIONS_GHC -Wno-x-partial #-} module Test.Cardano.Db.Mock.Unit.Conway.Rollback ( simpleRollback, @@ -10,14 +11,18 @@ 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 (..)) @@ -25,9 +30,9 @@ 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 = @@ -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) @@ -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) @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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" diff --git a/cardano-chain-gen/test/testfiles/fingerprint/conwayDrepDistrRollback b/cardano-chain-gen/test/testfiles/fingerprint/conwayDrepDistrRollback new file mode 100644 index 000000000..b1a9e7690 --- /dev/null +++ b/cardano-chain-gen/test/testfiles/fingerprint/conwayDrepDistrRollback @@ -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] \ No newline at end of file diff --git a/cardano-chain-gen/test/testfiles/pgpass-testing-macos b/cardano-chain-gen/test/testfiles/pgpass-testing-macos new file mode 100644 index 000000000..c3e830cab --- /dev/null +++ b/cardano-chain-gen/test/testfiles/pgpass-testing-macos @@ -0,0 +1 @@ +/tmp:5432:testing:*:* diff --git a/cardano-db-sync/src/Cardano/DbSync/Era/Universal/Insert/LedgerEvent.hs b/cardano-db-sync/src/Cardano/DbSync/Era/Universal/Insert/LedgerEvent.hs index 9d873a5ea..e0e5c0cf8 100644 --- a/cardano-db-sync/src/Cardano/DbSync/Era/Universal/Insert/LedgerEvent.hs +++ b/cardano-db-sync/src/Cardano/DbSync/Era/Universal/Insert/LedgerEvent.hs @@ -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 @@ -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 = diff --git a/cardano-db/src/Cardano/Db/Statement/Base.hs b/cardano-db/src/Cardano/Db/Statement/Base.hs index 5417fd7bd..ae0d6fa79 100644 --- a/cardano-db/src/Cardano/Db/Statement/Base.hs +++ b/cardano-db/src/Cardano/Db/Statement/Base.hs @@ -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" + , " WHERE curr.block_no = $1" ] queryBlockNoAndEpoch :: Word64 -> DbM (Maybe (Id.BlockId, Word64)) @@ -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...") @@ -781,6 +790,9 @@ 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..." @@ -788,6 +800,7 @@ deleteUsingEpochNo trce epochN = do 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) diff --git a/config/pgpass-mainnet-macos b/config/pgpass-mainnet-macos new file mode 100644 index 000000000..bb4733a29 --- /dev/null +++ b/config/pgpass-mainnet-macos @@ -0,0 +1 @@ +/tmp:5432:cexplorer:*:* diff --git a/scripts/run-everything-tmux.sh b/scripts/run-everything-tmux.sh index d85138770..c6d845c24 100755 --- a/scripts/run-everything-tmux.sh +++ b/scripts/run-everything-tmux.sh @@ -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