Skip to content

Commit 221c28c

Browse files
committed
db: Fix for the outsum column of the epoch table
This column is the sum of transactions outputs for the epoch. On test networks (never been seen on mainnet) it is possible for this sum to end up larger than the maximum positive value that can be stored in a 64 bit signed integer ('Int64'). However, the output sum was being stored store in a 64 bit unsigned integer ('Word64') but the Persistent library does not correctly supported 'Word64'. Instead it "casts" it from 'Word64' to 'Int64'. For values great that the max positive value of an 'Int64' this results on the value wrapping to negative and hence being rejected by the SQL domain constraint which requres 'outsum >= 0'. This solution switches 'outsum' in the Haskell world from 'Word64' to 'Word128' which is incredibly unlikley to overflow given the limitation on number of transactions per block and per epoch. On the SQL side we use an PostgreSQL type of 'numeric (38, 0)'. Closes: #197
1 parent 0c01cde commit 221c28c

File tree

7 files changed

+53
-3
lines changed

7 files changed

+53
-3
lines changed

cardano-db/cardano-db.cabal

+4
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ library
4242
Cardano.Db.Run
4343
Cardano.Db.Schema
4444
Cardano.Db.Schema.Types
45+
Cardano.Db.Schema.Orphans
4546
Cardano.Db.Types
4647

4748

@@ -76,6 +77,7 @@ library
7677
, transformers
7778
-- This is never intended to run on non-POSIX systems.
7879
, unix
80+
, wide-word
7981

8082
executable cardano-db-tool
8183
default-language: Haskell2010
@@ -133,7 +135,9 @@ test-suite test
133135
, aeson
134136
, cardano-db
135137
, cardano-ledger
138+
, persistent
136139
, hedgehog
140+
, wide-word
137141

138142
test-suite test-db
139143
default-language: Haskell2010

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

+4-1
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@
1616

1717
module Cardano.Db.Schema where
1818

19+
import Cardano.Db.Schema.Orphans ()
20+
1921
import Data.ByteString.Char8 (ByteString)
2022
import Data.Int (Int64)
2123
import Data.Text (Text)
2224
import Data.Time.Clock (UTCTime)
2325
import Data.Word (Word16, Word64)
26+
import Data.WideWord.Word128 (Word128)
2427

2528
-- Do not use explicit imports from this module as the imports can change
2629
-- from version to version due to changes to the TH code in Persistent.
@@ -128,7 +131,7 @@ share
128131
-- hold 204 times the total Lovelace distribution. The chance of that much being transacted
129132
-- in a single epoch is relatively low.
130133
Epoch
131-
outSum Word64 sqltype=outsum
134+
outSum Word128 sqltype=word128
132135
txCount Word64 sqltype=uinteger
133136
blkCount Word64 sqltype=uinteger
134137
no Word64 sqltype=uinteger
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{-# LANGUAGE OverloadedStrings #-}
2+
3+
{-# OPTIONS_GHC -Wno-orphans #-}
4+
5+
module Cardano.Db.Schema.Orphans where
6+
7+
import Data.WideWord.Word128 (Word128)
8+
9+
import qualified Data.Text as Text
10+
11+
import Database.Persist.Class (PersistField (..))
12+
import Database.Persist.Types (PersistValue (..))
13+
14+
15+
instance PersistField Word128 where
16+
toPersistValue = PersistText . Text.pack . show
17+
fromPersistValue (PersistText bs) = Right $ read (Text.unpack bs)
18+
fromPersistValue x =
19+
Left $ mconcat [ "Failed to parse Haskell type Word128: ", Text.pack (show x) ]
20+

cardano-db/test/Test/Property/Cardano/Db/Types.hs

+15
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import Cardano.Chain.Common (maxLovelaceVal)
99

1010
import qualified Data.Aeson as Aeson
1111
import Data.Word (Word64)
12+
import Data.WideWord.Word128 (Word128 (..))
13+
14+
import Database.Persist.Class (PersistField (..))
1215

1316
import Cardano.Db
1417

@@ -24,6 +27,12 @@ prop_roundtrip_Ada_via_JSON =
2427
mv <- H.forAll genAda
2528
H.tripping mv Aeson.encode Aeson.eitherDecode
2629

30+
prop_roundtrip_Word128_PersistField :: Property
31+
prop_roundtrip_Word128_PersistField =
32+
H.withTests 5000 . H.property $ do
33+
w128 <- H.forAll genWord128
34+
H.tripping w128 toPersistValue fromPersistValue
35+
2736
-- -----------------------------------------------------------------------------
2837

2938
genAda :: Gen Ada
@@ -38,6 +47,12 @@ genAda =
3847
, Gen.word64 (Range.linear (maxLovelaceVal - 5000) maxLovelaceVal) -- Near max.
3948
]
4049

50+
genWord128 :: Gen Word128
51+
genWord128 = Word128 <$> genWord64 <*> genWord64
52+
53+
genWord64 :: Gen Word64
54+
genWord64 = Gen.word64 Range.constantBounded
55+
4156
-- -----------------------------------------------------------------------------
4257

4358
tests :: IO Bool

cardano-db/test/cardano-db-test.cabal

+1
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,4 @@ library
6161
, transformers
6262
-- This is never intended to run on non-POSIX systems.
6363
, unix
64+
, wide-word

schema/migration-1-0001-20190730.sql

+4
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ BEGIN
2121
-- Stake addresses are a 28 byte hash prepended with a byte describing the address.
2222
EXECUTE 'CREATE DOMAIN addr29type AS bytea CHECK (octet_length (VALUE) = 29);';
2323

24+
-- 'maxBound :: Word128' as a decimal has 39 digits, so we only need to check that it
25+
-- is positive.
26+
EXECUTE 'CREATE DOMAIN word128type AS numeric (38, 0) CHECK (VALUE >= 0);';
27+
2428
UPDATE "schema_version" SET stage_one = 1;
2529
RAISE NOTICE 'DB has been migrated to stage_one version %', next_version;
2630
END IF;

schema/migration-1-0003-20200211.sql

+5-2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ BEGIN
99
SELECT stage_one + 1 INTO next_version FROM "schema_version";
1010
IF next_version = 2 THEN
1111
-- Used as the sum of tx outputs for an epoch.
12-
-- Need this to catch possible overflow.
13-
EXECUTE 'CREATE DOMAIN outsum AS bigint CHECK (VALUE >= 0);';
12+
-- Persistent does not support more precision than 'Int64' (support for 'Word64'
13+
-- is done as a 'cast' to/from 'Int64' resulting in values greater than
14+
-- 'maxBound :: Int64' being represented in the database as negative values.
15+
-- Instead we we use 'Word128'.
16+
EXECUTE 'CREATE DOMAIN outsum AS word128type;';
1417

1518
UPDATE "schema_version" SET stage_one = next_version;
1619
RAISE NOTICE 'DB has been migrated to stage_one version %', next_version;

0 commit comments

Comments
 (0)