Skip to content

Commit fc7d820

Browse files
committed
2118-epoch out_sum/fees corruption from numeric decoders
1 parent 1c9b35b commit fc7d820

14 files changed

Lines changed: 271 additions & 51 deletions

File tree

cardano-db/cardano-db.cabal

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ test-suite test
143143
, cardano-ledger-core
144144
, cardano-ledger-mary
145145
, hedgehog
146+
, scientific
146147
, wide-word
147148

148149
test-suite test-db
@@ -151,7 +152,8 @@ test-suite test-db
151152
main-is: test-db.hs
152153
hs-source-dirs: test
153154

154-
other-modules: Test.IO.Cardano.Db.Insert
155+
other-modules: Test.IO.Cardano.Db.EpochCalc
156+
Test.IO.Cardano.Db.Insert
155157
Test.IO.Cardano.Db.Migration
156158
Test.IO.Cardano.Db.Rollback
157159
Test.IO.Cardano.Db.TotalSupply
@@ -179,6 +181,7 @@ test-suite test-db
179181
, tasty-hunit
180182
, text
181183
, time
184+
, wide-word
182185

183186
-- test-suite schema-rollback
184187
-- default-language: Haskell2010

cardano-db/src/Cardano/Db/Schema/Core/Base.hs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,12 @@ instance DbInfo TxIn where
247247
, ("redeemer_id", "bigint[]")
248248
]
249249

250+
entityTxInDecoder :: D.Row (Entity TxIn)
251+
entityTxInDecoder =
252+
Entity
253+
<$> idDecoder TxInId
254+
<*> txInDecoder
255+
250256
txInDecoder :: D.Row TxIn
251257
txInDecoder =
252258
TxIn
@@ -621,6 +627,12 @@ data Withdrawal = Withdrawal
621627
type instance Key Withdrawal = WithdrawalId
622628
instance DbInfo Withdrawal
623629

630+
entityWithdrawalDecoder :: D.Row (Entity Withdrawal)
631+
entityWithdrawalDecoder =
632+
Entity
633+
<$> idDecoder WithdrawalId
634+
<*> withdrawalDecoder
635+
624636
withdrawalDecoder :: D.Row Withdrawal
625637
withdrawalDecoder =
626638
Withdrawal

cardano-db/src/Cardano/Db/Schema/Core/EpochAndProtocol.hs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,12 @@ type instance Key Epoch = EpochId
7070
instance DbInfo Epoch where
7171
uniqueFields _ = ["no"]
7272

73+
entityEpochDecoder :: D.Row (Entity Epoch)
74+
entityEpochDecoder =
75+
Entity
76+
<$> idDecoder EpochId
77+
<*> epochDecoder
78+
7379
epochDecoder :: D.Row Epoch
7480
epochDecoder =
7581
Epoch

cardano-db/src/Cardano/Db/Schema/Core/StakeDelegation.hs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import Hasql.Encoders as E
1616
import Cardano.Db.Schema.Ids
1717
import Cardano.Db.Schema.Types (textDecoder)
1818
import Cardano.Db.Statement.Function.Core (bulkEncoder)
19-
import Cardano.Db.Statement.Types (DbInfo (..), Key)
19+
import Cardano.Db.Statement.Types (DbInfo (..), Entity (..), Key)
2020
import Cardano.Db.Types (
2121
DbLovelace (..),
2222
RewardSource,
@@ -45,6 +45,12 @@ type instance Key StakeAddress = StakeAddressId
4545
instance DbInfo StakeAddress where
4646
uniqueFields _ = ["hash_raw"]
4747

48+
entityStakeAddressDecoder :: D.Row (Entity StakeAddress)
49+
entityStakeAddressDecoder =
50+
Entity
51+
<$> idDecoder StakeAddressId
52+
<*> stakeAddressDecoder
53+
4854
stakeAddressDecoder :: D.Row StakeAddress
4955
stakeAddressDecoder =
5056
StakeAddress
@@ -142,6 +148,12 @@ data Delegation = Delegation
142148
type instance Key Delegation = DelegationId
143149
instance DbInfo Delegation
144150

151+
entityDelegationDecoder :: D.Row (Entity Delegation)
152+
entityDelegationDecoder =
153+
Entity
154+
<$> idDecoder DelegationId
155+
<*> delegationDecoder
156+
145157
delegationDecoder :: D.Row Delegation
146158
delegationDecoder =
147159
Delegation

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1414,7 +1414,7 @@ queryTxInRedeemerStmt =
14141414
, " FROM " <> tableN
14151415
, " WHERE redeemer_id IS NOT NULL"
14161416
]
1417-
decoder = HsqlD.rowList SCB.txInDecoder
1417+
decoder = HsqlD.rowList (entityVal <$> SCB.entityTxInDecoder)
14181418

14191419
queryTxInRedeemer :: HasCallStack => DbM [SCB.TxIn]
14201420
queryTxInRedeemer =
@@ -1438,7 +1438,7 @@ queryTxInFailedTxStmt =
14381438
, " ON tx_in.tx_in_id = tx.id"
14391439
, " WHERE tx.valid_contract = FALSE"
14401440
]
1441-
decoder = HsqlD.rowList SCB.txInDecoder
1441+
decoder = HsqlD.rowList (entityVal <$> SCB.entityTxInDecoder)
14421442

14431443
queryTxInFailedTx :: HasCallStack => DbM [SCB.TxIn]
14441444
queryTxInFailedTx = runSession mkDbCallStack $ HsqlSes.statement () queryTxInFailedTxStmt
@@ -1469,7 +1469,7 @@ queryWithdrawalScriptStmt =
14691469
, " FROM " <> tableN
14701470
, " WHERE redeemer_id IS NOT NULL"
14711471
]
1472-
decoder = HsqlD.rowList SCB.withdrawalDecoder
1472+
decoder = HsqlD.rowList (entityVal <$> SCB.entityWithdrawalDecoder)
14731473

14741474
queryWithdrawalScript :: HasCallStack => DbM [SCB.Withdrawal]
14751475
queryWithdrawalScript = runSession mkDbCallStack $ HsqlSes.statement () queryWithdrawalScriptStmt

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import qualified Cardano.Db.Schema.Variants.TxOutCore as SVC
3232
import Cardano.Db.Statement.Function.Core (runSession, runSessionEntity)
3333
import Cardano.Db.Statement.Function.Query (countAll, countWhere, parameterisedCountWhere)
3434
import Cardano.Db.Statement.Types (DbInfo (..), Entity (..), tableName)
35-
import Cardano.Db.Types (Ada, DbM, RewardSource, rewardSourceDecoder, word64ToAda)
35+
import Cardano.Db.Types (Ada, DbM, RewardSource, rewardSourceDecoder, scientificToWord64, word64ToAda)
3636

3737
queryEpochParamWithEpochNoStmt :: HsqlStmt.Statement Word64 (Maybe (Entity SCE.EpochParam))
3838
queryEpochParamWithEpochNoStmt =
@@ -311,7 +311,7 @@ queryTreasuryDonationsStmt =
311311
, " FROM " <> txTableN
312312
]
313313

314-
decoder = HsqlD.singleRow (HsqlD.column $ HsqlD.nonNullable $ fromIntegral <$> HsqlD.int8)
314+
decoder = HsqlD.singleRow (HsqlD.column $ HsqlD.nonNullable $ scientificToWord64 <$> HsqlD.numeric)
315315

316316
queryTreasuryDonations :: DbM Word64
317317
queryTreasuryDonations =
@@ -477,7 +477,7 @@ queryTxFeeDepositStmt =
477477
]
478478
encoder = fromIntegral >$< HsqlE.param (HsqlE.nonNullable HsqlE.int8)
479479
decoder = HsqlD.rowMaybe $ do
480-
fee <- HsqlD.column (HsqlD.nonNullable $ fromIntegral <$> HsqlD.int8)
480+
fee <- HsqlD.column (HsqlD.nonNullable $ scientificToWord64 <$> HsqlD.numeric)
481481
deposit <- HsqlD.column (HsqlD.nullable HsqlD.int8)
482482
pure (word64ToAda fee, fromMaybe 0 deposit)
483483

@@ -618,7 +618,7 @@ queryTxWithdrawalStmt =
618618

619619
encoder = fromIntegral >$< HsqlE.param (HsqlE.nonNullable HsqlE.int8)
620620
decoder = HsqlD.singleRow $ do
621-
amount <- HsqlD.column (HsqlD.nonNullable $ fromIntegral <$> HsqlD.int8)
621+
amount <- HsqlD.column (HsqlD.nonNullable $ scientificToWord64 <$> HsqlD.numeric)
622622
pure $ word64ToAda amount
623623

624624
-- | It is probably not possible to have two withdrawals in a single Tx.

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

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import Data.Functor.Contravariant ((>$<))
99
import qualified Data.Text as Text
1010
import qualified Data.Text.Encoding as TextEnc
1111
import Data.Time (UTCTime)
12-
import Data.WideWord (Word128 (..))
1312
import qualified Hasql.Decoders as HsqlD
1413
import qualified Hasql.Encoders as HsqlE
1514
import qualified Hasql.Session as HsqlSes
@@ -23,7 +22,7 @@ import Cardano.Db.Statement.Function.Core (ResultType (..), runSession, runSessi
2322
import Cardano.Db.Statement.Function.Insert (insert, insertCheckUnique, insertReplace)
2423
import Cardano.Db.Statement.Function.Query (countAll, replace, selectByFieldFirst)
2524
import Cardano.Db.Statement.Types (Entity (..), tableName)
26-
import Cardano.Db.Types (DbLovelace (..), DbM)
25+
import Cardano.Db.Types (DbLovelace (..), DbM, scientificToWord128, scientificToWord64)
2726

2827
--------------------------------------------------------------------------------
2928
-- CostModel
@@ -129,7 +128,7 @@ queryEpochEntryStmt =
129128
HsqlStmt.Statement sql encoder decoder True
130129
where
131130
encoder = HsqlE.param (HsqlE.nonNullable $ fromIntegral >$< HsqlE.int8)
132-
decoder = HsqlD.rowMaybe SEnP.epochDecoder
131+
decoder = HsqlD.rowMaybe (entityVal <$> SEnP.entityEpochDecoder)
133132
sql =
134133
TextEnc.encodeUtf8 $
135134
Text.concat
@@ -185,8 +184,8 @@ queryCalcEpochEntryStmt =
185184
blockCount <- HsqlD.column (HsqlD.nonNullable $ fromIntegral <$> HsqlD.int8)
186185
minTime <- HsqlD.column (HsqlD.nullable utcTimeAsTimestampDecoder)
187186
maxTime <- HsqlD.column (HsqlD.nullable utcTimeAsTimestampDecoder)
188-
outSum <- HsqlD.column (HsqlD.nonNullable HsqlD.int8) -- Decode as single int8
189-
feeSum <- HsqlD.column (HsqlD.nonNullable HsqlD.int8)
187+
outSum <- HsqlD.column (HsqlD.nonNullable (scientificToWord128 <$> HsqlD.numeric))
188+
feeSum <- HsqlD.column (HsqlD.nonNullable (scientificToWord64 <$> HsqlD.numeric))
190189
txCount <- HsqlD.column (HsqlD.nonNullable $ fromIntegral <$> HsqlD.int8)
191190

192191
pure $ case (blockCount, minTime, maxTime) of
@@ -196,8 +195,8 @@ queryCalcEpochEntryStmt =
196195
then convertBlk epochNo (blockCount, Just start, Just end)
197196
else
198197
SEnP.Epoch
199-
{ SEnP.epochOutSum = Word128 0 (fromIntegral outSum) -- Construct Word128 from single value
200-
, SEnP.epochFees = DbLovelace $ fromIntegral feeSum
198+
{ SEnP.epochOutSum = outSum
199+
, SEnP.epochFees = DbLovelace feeSum
201200
, SEnP.epochTxCount = txCount
202201
, SEnP.epochBlkCount = blockCount
203202
, SEnP.epochNo = epochNo
@@ -269,7 +268,7 @@ queryLatestEpochStmt =
269268
, " WHERE no = (SELECT MAX(no) FROM epoch)"
270269
]
271270

272-
decoder = HsqlD.rowMaybe SEnP.epochDecoder
271+
decoder = HsqlD.rowMaybe (entityVal <$> SEnP.entityEpochDecoder)
273272

274273
-- | Get the most recent epoch in the Epoch DB table.
275274
queryLatestEpoch :: DbM (Maybe SEnP.Epoch)

cardano-db/src/Cardano/Db/Statement/Function/Query.hs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -253,16 +253,14 @@ queryStatementCacheSize :: HasCallStack => DbM Int
253253
queryStatementCacheSize =
254254
runSession mkDbCallStack $ HsqlSes.statement () queryStatementCacheStmt
255255

256-
-- Decoder for Ada amounts from database int8 values
257256
adaDecoder :: HsqlD.Row Ada
258257
adaDecoder = do
259258
amount <- HsqlD.column (HsqlD.nonNullable HsqlD.int8)
260259
pure $ lovelaceToAda (fromIntegral amount)
261260

262-
-- Decoder for summed Ada amounts with null handling
263261
adaSumDecoder :: HsqlD.Row Ada
264262
adaSumDecoder = do
265-
amount <- HsqlD.column (HsqlD.nullable HsqlD.int8)
263+
amount <- HsqlD.column (HsqlD.nullable HsqlD.numeric)
266264
case amount of
267-
Just value -> pure $ lovelaceToAda (fromIntegral value)
265+
Just value -> pure $ lovelaceToAda (floor value)
268266
Nothing -> pure $ Ada 0

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import Cardano.Db.Statement.Function.Core (ResultType (..), ResultTypeBulk (..),
2828
import Cardano.Db.Statement.Function.Insert (insert, insertCheckUnique)
2929
import Cardano.Db.Statement.Function.InsertBulk (insertBulk, insertBulkMaybeIgnore, insertBulkMaybeIgnoreWithConstraint)
3030
import Cardano.Db.Statement.Function.Query (adaSumDecoder, countAll)
31-
import Cardano.Db.Statement.Types (DbInfo (..))
31+
import Cardano.Db.Statement.Types (DbInfo (..), Entity (..))
3232
import Cardano.Db.Types (Ada, DbLovelace, DbM, RewardSource, dbLovelaceDecoder, rewardSourceDecoder, rewardSourceEncoder)
3333
import Cardano.Ledger.BaseTypes
3434
import Cardano.Ledger.Credential (Ptr (..), SlotNo32 (..))
@@ -61,7 +61,7 @@ queryDelegationScriptStmt =
6161
, " FROM " <> tableN
6262
, " WHERE redeemer_id IS NOT NULL"
6363
]
64-
decoder = HsqlD.rowList SS.delegationDecoder
64+
decoder = HsqlD.rowList (entityVal <$> SS.entityDelegationDecoder)
6565

6666
queryDelegationScript :: DbM [SS.Delegation]
6767
queryDelegationScript =
@@ -486,7 +486,7 @@ queryStakeAddressScriptStmt =
486486
, " FROM " <> tableN
487487
, " WHERE script_hash IS NOT NULL"
488488
]
489-
decoder = HsqlD.rowList SS.stakeAddressDecoder
489+
decoder = HsqlD.rowList (entityVal <$> SS.entityStakeAddressDecoder)
490490

491491
queryStakeAddressScript :: DbM [SS.StakeAddress]
492492
queryStakeAddressScript =

cardano-db/src/Cardano/Db/Types.hs

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import Data.Fixed (Micro, showFixed)
2828
import Data.Functor.Contravariant ((>$<))
2929
import Data.Int (Int64)
3030
import Data.Pool (Pool)
31-
import Data.Scientific (Scientific (..), coefficient, scientific, toBoundedInteger)
31+
import Data.Scientific (Scientific (..), scientific, toBoundedInteger)
3232
import Data.Text (Text)
3333
import qualified Data.Text as Text
3434
import Data.WideWord (Word128 (..))
@@ -176,12 +176,10 @@ newtype DbWord64 = DbWord64 {unDbWord64 :: Word64}
176176
deriving (Eq, Generic, Num)
177177
deriving (Read, Show) via (Quiet DbWord64)
178178

179-
-- Helper to replicate the original Persistent fromPersistValue behavior for DbWord64
180-
-- This matches the PersistRational case: fromIntegral $ numerator r
181179
scientificToWord64 :: Scientific -> Word64
182180
scientificToWord64 s = case toBoundedInteger @Word64 s of
183181
Just w64 -> w64
184-
Nothing -> fromIntegral $ coefficient s -- Fallback to coefficient for out-of-bounds values
182+
Nothing -> fromInteger (floor s)
185183

186184
-- Value encoder for DbWord64 using numeric (matches word64type domain)
187185
dbWord64ValueEncoder :: HsqlE.Value DbWord64
@@ -530,10 +528,28 @@ integerToDbInt65 i
530528
| otherwise = toDbInt65 (fromIntegral i)
531529

532530
word128Encoder :: HsqlE.Value Word128
533-
word128Encoder = fromInteger . toInteger >$< HsqlE.numeric
531+
word128Encoder = word128ToScientific >$< HsqlE.numeric
534532

535533
word128Decoder :: HsqlD.Value Word128
536-
word128Decoder = fromInteger . fromIntegral . coefficient <$> HsqlD.numeric
534+
word128Decoder = scientificToWord128 <$> HsqlD.numeric
535+
536+
-- | Convert a 'Word128' to a 'Scientific' for storage in a PostgreSQL @numeric@ column.
537+
-- This is exposed for testing the encoder/decoder pair without a database connection.
538+
word128ToScientific :: Word128 -> Scientific
539+
word128ToScientific = fromInteger . toInteger
540+
541+
-- | Convert a 'Scientific' read from a PostgreSQL @numeric@ column back to a 'Word128'.
542+
--
543+
-- PostgreSQL strips trailing zeros in its binary @numeric@ representation, so a
544+
-- value like @380_000_000_000_000_000@ may come back as @Scientific 38 16@
545+
-- (coefficient @38@, @base10Exponent = 16@). 'coefficient' alone would drop
546+
-- the exponent, so we use 'floor' on the full 'Scientific' which is exact for
547+
-- non-negative integral values - the only shape we ever encode here, since
548+
-- 'Word128' is non-negative and stored in @numeric(39,0)@.
549+
--
550+
-- This is exposed for testing the encoder/decoder pair without a database connection.
551+
scientificToWord128 :: Scientific -> Word128
552+
scientificToWord128 = fromInteger . floor
537553

538554
lovelaceToAda :: Integer -> Ada
539555
lovelaceToAda ll =

0 commit comments

Comments
 (0)