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 0f7ce78ee..7ecc65617 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 @@ -156,6 +156,7 @@ unitTests iom knownMigrations = , test "stake address pointers" Stake.stakeAddressPtr , test "stake address pointers deregistration" Stake.stakeAddressPtrDereg , test "stake address pointers. Use before registering." Stake.stakeAddressPtrUseBefore + , test "stake address pointers have NULL stake_address_id in Conway" Stake.stakeAddressPtrNullInConway , test "register stake creds" Stake.registerStakeCreds , test "register stake creds with shelley disabled" Stake.registerStakeCredsNoShelley ] diff --git a/cardano-chain-gen/test/Test/Cardano/Db/Mock/Unit/Conway/Stake.hs b/cardano-chain-gen/test/Test/Cardano/Db/Mock/Unit/Conway/Stake.hs index 798eca6ee..59fc41048 100644 --- a/cardano-chain-gen/test/Test/Cardano/Db/Mock/Unit/Conway/Stake.hs +++ b/cardano-chain-gen/test/Test/Cardano/Db/Mock/Unit/Conway/Stake.hs @@ -10,6 +10,7 @@ module Test.Cardano.Db.Mock.Unit.Conway.Stake ( stakeAddressPtr, stakeAddressPtrDereg, stakeAddressPtrUseBefore, + stakeAddressPtrNullInConway, registerStakeCreds, registerStakeCredsNoShelley, @@ -37,7 +38,7 @@ import Ouroboros.Network.Block (blockSlot) import Test.Cardano.Db.Mock.Config import qualified Test.Cardano.Db.Mock.UnifiedApi as Api import Test.Cardano.Db.Mock.Validate -import Test.Tasty.HUnit (Assertion ()) +import Test.Tasty.HUnit (Assertion (), assertBool) import Prelude () registrationTx :: IOManager -> [(Text, Text)] -> Assertion @@ -229,6 +230,43 @@ stakeAddressPtrUseBefore = where testLabel = "conwayStakeAddressPtrUseBefore" +-- | Test that Pointer addresses in Conway era have NULL stake_address_id +-- This verifies the fix for issue #2051: Pointer addresses in Conway era +-- should be treated as Enterprise addresses with no stake key association +stakeAddressPtrNullInConway :: IOManager -> [(Text, Text)] -> Assertion +stakeAddressPtrNullInConway = + withFullConfig conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do + startDBSync dbSync + + -- Forge a block with a stake registration cert + blk <- + Api.withConwayFindLeaderAndSubmitTx interpreter mockServer $ + Conway.mkSimpleDCertTx [(StakeIndexNew 1, Conway.mkRegTxCert SNothing)] + + -- Forge a block with a payment to a Pointer address + -- The pointer references the stake registration certificate we just created + let ptr = Ptr (unsafeToSlotNo32 $ blockSlot blk) (TxIx 0) (CertIx 0) + void $ + Api.withConwayFindLeaderAndSubmitTx interpreter mockServer $ + Conway.mkPaymentTx (UTxOIndex 0) (UTxOAddressNewWithPtr 0 ptr) 20_000 20_000 0 + + -- Wait for it to sync + assertBlockNoBackoff dbSync 2 + + -- Get the tx_out variant type for queries + let txOutVariantType = txOutVariantTypeFromConfig dbSync + + -- Query for Pointer address tx_outs with NULL stake_address_id + -- In Conway era, Pointer addresses should NOT have stake_address_id populated + ptrCount <- runQuery dbSync $ DB.queryPtrTxOutNullStake txOutVariantType + + -- Assert that we have at least one Pointer address with NULL stake_address_id + assertBool + "Pointer address in Conway should have NULL stake_address_id (treated as Enterprise address)" + (ptrCount > 0) + where + testLabel = "conwayStakeAddressPtrNullInConway" + stakeDistGenesis :: IOManager -> [(Text, Text)] -> Assertion stakeDistGenesis = withFullConfigDropDB conwayConfigDir testLabel $ \interpreter mockServer dbSync -> do diff --git a/cardano-db-sync/src/Cardano/DbSync/Api/Ledger.hs b/cardano-db-sync/src/Cardano/DbSync/Api/Ledger.hs index 26658ded9..244ea3593 100644 --- a/cardano-db-sync/src/Cardano/DbSync/Api/Ledger.hs +++ b/cardano-db-sync/src/Cardano/DbSync/Api/Ledger.hs @@ -78,9 +78,9 @@ storeUTxOFromLedger :: ExtLedgerState CardanoBlock mk -> ExceptT SyncNodeError DB.DbM () storeUTxOFromLedger env st = case ledgerState st of - LedgerStateBabbage bts -> storeUTxO env (getUTxO bts) - LedgerStateConway stc -> storeUTxO env (getUTxO stc) - LedgerStateDijkstra stc -> storeUTxO env (getUTxO stc) + LedgerStateBabbage bts -> storeUTxO env Babbage (getUTxO bts) + LedgerStateConway stc -> storeUTxO env Conway (getUTxO stc) + LedgerStateDijkstra stc -> storeUTxO env Dijkstra (getUTxO stc) _otherwise -> liftIO $ logError trce "storeUTxOFromLedger is only supported after Babbage" where trce = getTrace env @@ -96,9 +96,10 @@ storeUTxO :: , NativeScript era ~ Timelock era ) => SyncEnv -> + BlockEra -> Map TxIn (BabbageTxOut era) -> ExceptT SyncNodeError DB.DbM () -storeUTxO env mp = do +storeUTxO env blkEra mp = do liftIO $ logInfo trce $ mconcat @@ -107,7 +108,7 @@ storeUTxO env mp = do , " tx_out as pages of " , textShow bulkSize ] - mapM_ (storePage env pagePerc) . zip [0 ..] . chunksOf bulkSize . Map.toList $ mp + mapM_ (storePage env blkEra pagePerc) . zip [0 ..] . chunksOf bulkSize . Map.toList $ mp where trce = getTrace env bulkSize = DB.getTxOutBulkSize (getTxOutVariantType env) @@ -124,12 +125,13 @@ storePage :: , NativeScript era ~ Timelock era ) => SyncEnv -> + BlockEra -> Float -> (Int, [(TxIn, BabbageTxOut era)]) -> ExceptT SyncNodeError DB.DbM () -storePage syncEnv percQuantum (n, ls) = do +storePage syncEnv blkEra percQuantum (n, ls) = do when (n `mod` 10 == 0) $ liftIO $ logInfo trce $ "Bootstrap in progress " <> prc <> "%" - txOuts <- mapM (prepareTxOut syncEnv) ls + txOuts <- mapM (prepareTxOut syncEnv blkEra) ls txOutIds <- lift $ DB.insertBulkTxOut False $ etoTxOut . fst <$> txOuts let maTxOuts = concatMap (mkmaTxOuts txOutVariantType) $ zip txOutIds (snd <$> txOuts) void . lift $ DB.insertBulkMaTxOutPiped [maTxOuts] @@ -147,12 +149,13 @@ prepareTxOut :: , NativeScript era ~ Timelock era ) => SyncEnv -> + BlockEra -> (TxIn, BabbageTxOut era) -> ExceptT SyncNodeError DB.DbM (ExtendedTxOut, [MissingMaTxOut]) -prepareTxOut syncEnv (TxIn txIntxId (TxIx index), txOut) = do +prepareTxOut syncEnv blkEra (TxIn txIntxId (TxIx index), txOut) = do let txHashByteString = Generic.safeHashToByteString $ unTxId txIntxId let genTxOut = fromTxOut (fromIntegral index) txOut txId <- liftDbLookupEither mkSyncNodeCallStack $ queryTxIdWithCache syncEnv txIntxId - insertTxOut syncEnv iopts (txId, txHashByteString) genTxOut + insertTxOut syncEnv iopts (txId, txHashByteString) genTxOut blkEra where iopts = soptInsertOptions $ envOptions syncEnv diff --git a/cardano-db-sync/src/Cardano/DbSync/Era/Shelley/Genesis.hs b/cardano-db-sync/src/Cardano/DbSync/Era/Shelley/Genesis.hs index 5be9b3295..ebdf8c0fb 100644 --- a/cardano-db-sync/src/Cardano/DbSync/Era/Shelley/Genesis.hs +++ b/cardano-db-sync/src/Cardano/DbSync/Era/Shelley/Genesis.hs @@ -26,6 +26,7 @@ import Cardano.DbSync.Era.Universal.Insert.Certificate (insertDelegation, insert import Cardano.DbSync.Era.Universal.Insert.Other (insertStakeAddressRefIfMissing) import Cardano.DbSync.Era.Universal.Insert.Pool (insertPoolRegister) import Cardano.DbSync.Error +import Cardano.DbSync.Types (BlockEra (..)) import Cardano.DbSync.Util import Cardano.Ledger.Address (serialiseAddr) import qualified Cardano.Ledger.Coin as Ledger @@ -269,7 +270,7 @@ insertTxOuts syncEnv blkId (TxIn txInId _, txOut) = do } tryUpdateCacheTx (envCache syncEnv) txInId txId - _ <- insertStakeAddressRefIfMissing (withNoCache syncEnv) (txOut ^. Core.addrTxOutL) + _ <- insertStakeAddressRefIfMissing (withNoCache syncEnv) (txOut ^. Core.addrTxOutL) Shelley case ioTxOutVariantType . soptInsertOptions $ envOptions syncEnv of DB.TxOutVariantCore -> void diff --git a/cardano-db-sync/src/Cardano/DbSync/Era/Universal/Block.hs b/cardano-db-sync/src/Cardano/DbSync/Era/Universal/Block.hs index cfda9a4b1..b44f3a5c5 100644 --- a/cardano-db-sync/src/Cardano/DbSync/Era/Universal/Block.hs +++ b/cardano-db-sync/src/Cardano/DbSync/Era/Universal/Block.hs @@ -97,7 +97,7 @@ insertBlockUniversal syncEnv shouldLog withinTwoMins withinHalfHour blk details let zippedTx = zip [0 ..] (Generic.blkTxs blk) let txInserter = insertTx syncEnv isMember blkId (sdEpochNo details) (Generic.blkSlotNo blk) applyResult - blockGroupedData <- foldM (\gp (idx, tx) -> txInserter idx tx gp) mempty zippedTx + blockGroupedData <- foldM (\gp (idx, tx) -> txInserter idx tx gp (Generic.blkEra blk)) mempty zippedTx minIds <- insertBlockGroupedData syncEnv blockGroupedData diff --git a/cardano-db-sync/src/Cardano/DbSync/Era/Universal/Insert/Other.hs b/cardano-db-sync/src/Cardano/DbSync/Era/Universal/Insert/Other.hs index a11d91823..687c4f9b0 100644 --- a/cardano-db-sync/src/Cardano/DbSync/Era/Universal/Insert/Other.hs +++ b/cardano-db-sync/src/Cardano/DbSync/Era/Universal/Insert/Other.hs @@ -1,5 +1,6 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeFamilies #-} @@ -26,6 +27,7 @@ import qualified Cardano.DbSync.Era.Shelley.Generic as Generic import Cardano.DbSync.Era.Universal.Insert.Grouped import Cardano.DbSync.Era.Util (safeDecodeToJson) import Cardano.DbSync.Error (SyncNodeError) +import Cardano.DbSync.Types (BlockEra (..)) import Cardano.DbSync.Util import qualified Cardano.Ledger.Address as Ledger import qualified Cardano.Ledger.BaseTypes as Ledger @@ -136,16 +138,20 @@ insertWithdrawals syncEnv txId redeemers txWdrl = do insertStakeAddressRefIfMissing :: SyncEnv -> Ledger.Addr -> + BlockEra -> ExceptT SyncNodeError DB.DbM (Maybe DB.StakeAddressId) -insertStakeAddressRefIfMissing syncEnv addr = +insertStakeAddressRefIfMissing syncEnv addr era = case addr of Ledger.AddrBootstrap {} -> pure Nothing Ledger.Addr nw _pcred sref -> case sref of Ledger.StakeRefBase cred -> do Just <$> queryOrInsertStakeAddress syncEnv UpdateCache nw cred - Ledger.StakeRefPtr ptr -> do - lift $ DB.queryStakeRefPtr ptr + Ledger.StakeRefPtr ptr -> + -- In Conway era onwards, Pointer addresses are treated as Enterprise addresses + case era of + Conway -> pure Nothing + _ -> lift $ DB.queryStakeRefPtr ptr Ledger.StakeRefNull -> pure Nothing insertMultiAsset :: diff --git a/cardano-db-sync/src/Cardano/DbSync/Era/Universal/Insert/Tx.hs b/cardano-db-sync/src/Cardano/DbSync/Era/Universal/Insert/Tx.hs index e3fdda4ee..81f975aa5 100644 --- a/cardano-db-sync/src/Cardano/DbSync/Era/Universal/Insert/Tx.hs +++ b/cardano-db-sync/src/Cardano/DbSync/Era/Universal/Insert/Tx.hs @@ -56,6 +56,7 @@ import Cardano.DbSync.Era.Universal.Insert.Pool (IsPoolMember) import Cardano.DbSync.Era.Util (safeDecodeToJson) import Cardano.DbSync.Error (SyncNodeError) import Cardano.DbSync.Ledger.Types (ApplyResult (..), getGovExpiresAt, lookupDepositsMap) +import Cardano.DbSync.Types (BlockEra) import Cardano.DbSync.Util import Cardano.DbSync.Util.Cbor (serialiseTxMetadataToCbor) @@ -72,8 +73,9 @@ insertTx :: Word64 -> Generic.Tx -> BlockGroupedData -> + BlockEra -> ExceptT SyncNodeError DB.DbM BlockGroupedData -insertTx syncEnv isMember blkId epochNo slotNo applyResult blockIndex tx grouped = do +insertTx syncEnv isMember blkId epochNo slotNo applyResult blockIndex tx grouped era = do let !txHash = Generic.txHash tx let !mdeposits = if not (Generic.txValidContract tx) then Just (Coin 0) else lookupDepositsMap txHash (apDepositsMap applyResult) let !outSum = fromIntegral $ unCoin $ Generic.txOutSum tx @@ -140,7 +142,7 @@ insertTx syncEnv isMember blkId epochNo slotNo applyResult blockIndex tx grouped if not (Generic.txValidContract tx) then do - !txOutsGrouped <- mapM (insertTxOut syncEnv iopts (txId, txHash)) (Generic.txOutputs tx) + !txOutsGrouped <- mapM (\txOut -> insertTxOut syncEnv iopts (txId, txHash) txOut era) (Generic.txOutputs tx) let !txIns = map (prepareTxIn txId Map.empty) resolvedInputs -- There is a custom semigroup instance for BlockGroupedData which uses addition for the values `fees` and `outSum`. @@ -149,7 +151,7 @@ insertTx syncEnv isMember blkId epochNo slotNo applyResult blockIndex tx grouped else do -- The following operations only happen if the script passes stage 2 validation (or the tx has -- no script). - !txOutsGrouped <- mapM (insertTxOut syncEnv iopts (txId, txHash)) (Generic.txOutputs tx) + !txOutsGrouped <- mapM (\txOut -> insertTxOut syncEnv iopts (txId, txHash) txOut era) (Generic.txOutputs tx) !redeemers <- Map.fromList @@ -161,7 +163,7 @@ insertTx syncEnv isMember blkId epochNo slotNo applyResult blockIndex tx grouped mapM_ (insertDatum syncEnv txId) (Generic.txData tx) mapM_ (insertCollateralTxIn syncEnv tracer txId) (Generic.txCollateralInputs tx) mapM_ (insertReferenceTxIn syncEnv tracer txId) (Generic.txReferenceInputs tx) - mapM_ (insertCollateralTxOut syncEnv iopts (txId, txHash)) (Generic.txCollateralOutputs tx) + mapM_ (\txOut -> insertCollateralTxOut syncEnv iopts (txId, txHash) txOut era) (Generic.txCollateralOutputs tx) txMetadata <- whenFalseMempty (ioMetadata iopts) $ @@ -213,9 +215,10 @@ insertTxOut :: InsertOptions -> (DB.TxId, ByteString) -> Generic.TxOut -> + BlockEra -> ExceptT SyncNodeError DB.DbM (ExtendedTxOut, [MissingMaTxOut]) -insertTxOut syncEnv iopts (txId, txHash) (Generic.TxOut index addr value maMap mScript dt) = do - mSaId <- insertStakeAddressRefIfMissing syncEnv addr +insertTxOut syncEnv iopts (txId, txHash) (Generic.TxOut index addr value maMap mScript dt) era = do + mSaId <- insertStakeAddressRefIfMissing syncEnv addr era mDatumId <- whenFalseEmpty (ioPlutusExtra iopts) Nothing $ Generic.whenInlineDatum dt $ @@ -387,9 +390,10 @@ insertCollateralTxOut :: InsertOptions -> (DB.TxId, ByteString) -> Generic.TxOut -> + BlockEra -> ExceptT SyncNodeError DB.DbM () -insertCollateralTxOut syncEnv iopts (txId, _txHash) (Generic.TxOut index addr value maMap mScript dt) = do - mSaId <- insertStakeAddressRefIfMissing syncEnv addr +insertCollateralTxOut syncEnv iopts (txId, _txHash) (Generic.TxOut index addr value maMap mScript dt) era = do + mSaId <- insertStakeAddressRefIfMissing syncEnv addr era mDatumId <- whenFalseEmpty (ioPlutusExtra iopts) Nothing $ Generic.whenInlineDatum dt $ diff --git a/cardano-db/src/Cardano/Db/Statement/Variants/TxOut.hs b/cardano-db/src/Cardano/Db/Statement/Variants/TxOut.hs index 7d9c0dd48..11dedeff7 100644 --- a/cardano-db/src/Cardano/Db/Statement/Variants/TxOut.hs +++ b/cardano-db/src/Cardano/Db/Statement/Variants/TxOut.hs @@ -800,6 +800,53 @@ setNullTxOutConsumedBatchStmt = encoder = Id.idEncoder Id.getTxId decoder = HsqlD.singleRow (HsqlD.column (HsqlD.nonNullable HsqlD.int8)) +-------------------------------------------------------------------------------- +-- Query to count tx_outs with NULL stake_address_id for Pointer addresses +-- Used to verify that Pointer addresses in Conway era don't have stake keys +-------------------------------------------------------------------------------- +queryPtrTxOutNullStakeCoreStmt :: HsqlStmt.Statement () Word64 +queryPtrTxOutNullStakeCoreStmt = + HsqlStmt.Statement sql HsqlE.noParams decoder True + where + sql = + TextEnc.encodeUtf8 $ + Text.concat + [ "SELECT COUNT(*) FROM tx_out" + , " WHERE stake_address_id IS NULL" + , " AND address LIKE 'addr_test1g%'" -- Pointer address prefix for testnet + ] + decoder = + HsqlD.singleRow $ + fromIntegral <$> HsqlD.column (HsqlD.nonNullable HsqlD.int8) + +queryPtrTxOutNullStakeAddressStmt :: HsqlStmt.Statement () Word64 +queryPtrTxOutNullStakeAddressStmt = + HsqlStmt.Statement sql HsqlE.noParams decoder True + where + sql = + TextEnc.encodeUtf8 $ + Text.concat + [ "SELECT COUNT(*) FROM tx_out" + , " INNER JOIN address ON tx_out.address_id = address.id" + , " WHERE tx_out.stake_address_id IS NULL" + , " AND address.address LIKE 'addr_test1g%'" -- Pointer address prefix for testnet + ] + decoder = + HsqlD.singleRow $ + fromIntegral <$> HsqlD.column (HsqlD.nonNullable HsqlD.int8) + +-- | Count tx_outs with NULL stake_address_id for Pointer addresses (Conway era check) +queryPtrTxOutNullStake :: TxOutVariantType -> DbM Word64 +queryPtrTxOutNullStake txOutVariantType = + case txOutVariantType of + TxOutVariantCore -> + runSession mkDbCallStack $ + HsqlSes.statement () queryPtrTxOutNullStakeCoreStmt + TxOutVariantAddress -> + runSession mkDbCallStack $ + HsqlSes.statement () queryPtrTxOutNullStakeAddressStmt + +-------------------------------------------------------------------------------- -- Main function to set NULL for tx_out consumed_by_tx_id querySetNullTxOut :: TxOutVariantType ->