Skip to content

Commit a5413ed

Browse files
committed
cardano-rpc: Reject zero denominator in RationalNumber conversion
1 parent c8f8cde commit a5413ed

7 files changed

Lines changed: 75 additions & 25 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
project: cardano-rpc
2+
pr: 1253
3+
kind:
4+
- bugfix
5+
description: |
6+
Fix server crash when a client-supplied RationalNumber has a zero denominator (the protobuf field default): the value is now rejected as invalid instead of throwing.

cardano-rpc/cardano-rpc.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ library
6969
Cardano.Rpc.Server.Internal.UtxoRpc.Type.ChainPoint
7070
Cardano.Rpc.Server.Internal.UtxoRpc.Type.PlutusData
7171
Cardano.Rpc.Server.Internal.UtxoRpc.Type.ProtocolParameters
72+
Cardano.Rpc.Server.Internal.UtxoRpc.Type.Rational
7273
Cardano.Rpc.Server.Internal.UtxoRpc.Type.Script
7374
Cardano.Rpc.Server.Internal.UtxoRpc.Type.TxEval
7475
Cardano.Rpc.Server.Internal.UtxoRpc.Type.TxOutput

cardano-rpc/src/Cardano/Rpc/Server/Internal/Orphans.hs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,6 @@ import Network.GRPC.Spec
3131

3232
-- It's easier to use 'Proto a' wrappers for RPC types, because it makes lens automatically available.
3333

34-
instance Inject (Proto U5c.RationalNumber) Rational where
35-
inject r = r ^. U5c.numerator . to fromIntegral % r ^. U5c.denominator . to fromIntegral
36-
3734
-- | Convert a 'Rational' into a protobuf 'U5c.RationalNumber'.
3835
--
3936
-- The UTxO RPC spec fixes the field widths to an @int32@ numerator and a

cardano-rpc/src/Cardano/Rpc/Server/Internal/UtxoRpc/Type.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ module Cardano.Rpc.Server.Internal.UtxoRpc.Type
1313
, protocolParamsToUtxoRpcPParams
1414
, simpleScriptToUtxoRpcNativeScript
1515
, utxoRpcBigIntToInteger
16+
, utxoRpcRationalNumberToRational
1617
, mkChainPointMsg
1718
, utxoRpcChainPointMsgToChainPoint
1819
, scriptDataToUtxoRpcPlutusData
@@ -28,6 +29,7 @@ import Cardano.Rpc.Server.Internal.UtxoRpc.Type.BigInt
2829
import Cardano.Rpc.Server.Internal.UtxoRpc.Type.ChainPoint
2930
import Cardano.Rpc.Server.Internal.UtxoRpc.Type.PlutusData
3031
import Cardano.Rpc.Server.Internal.UtxoRpc.Type.ProtocolParameters
32+
import Cardano.Rpc.Server.Internal.UtxoRpc.Type.Rational
3133
import Cardano.Rpc.Server.Internal.UtxoRpc.Type.Script
3234
import Cardano.Rpc.Server.Internal.UtxoRpc.Type.TxEval
3335
import Cardano.Rpc.Server.Internal.UtxoRpc.Type.TxOutput

cardano-rpc/src/Cardano/Rpc/Server/Internal/UtxoRpc/Type/ProtocolParameters.hs

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import Cardano.Rpc.Proto.Api.UtxoRpc.Query qualified as U5c
1717
import Cardano.Rpc.Proto.Api.UtxoRpc.Query qualified as UtxoRpc
1818
import Cardano.Rpc.Server.Internal.Orphans ()
1919
import Cardano.Rpc.Server.Internal.UtxoRpc.Type.BigInt
20+
import Cardano.Rpc.Server.Internal.UtxoRpc.Type.Rational
2021

2122
import Cardano.Ledger.Api qualified as L
2223
import Cardano.Ledger.BaseTypes qualified as L
@@ -149,15 +150,25 @@ utxoRpcPParamsToProtocolParams era pp = conwayEraOnwardsConstraints (convert era
149150
, pure . (L.ppEMaxL .~ pp ^. U5c.poolRetirementEpochBound . to fromIntegral . to L.EpochInterval)
150151
, pure . (L.ppNOptL .~ pp ^. U5c.desiredNumberOfPools . to fromIntegral)
151152
, \r -> do
152-
poolInfluence <- pp ^. U5c.poolInfluence . to inject . to L.boundRational ?! "Invalid poolInfluence"
153+
poolInfluence <-
154+
pp
155+
^. U5c.poolInfluence
156+
. to (L.boundRational <=< utxoRpcRationalNumberToRational)
157+
?! "Invalid poolInfluence"
153158
pure $ set L.ppA0L poolInfluence r
154159
, \r -> do
155160
monetaryExpansion <-
156-
pp ^. U5c.monetaryExpansion . to inject . to L.boundRational ?! "Invalid monetaryExpansion"
161+
pp
162+
^. U5c.monetaryExpansion
163+
. to (L.boundRational <=< utxoRpcRationalNumberToRational)
164+
?! "Invalid monetaryExpansion"
157165
pure $ set L.ppRhoL monetaryExpansion r
158166
, \r -> do
159167
treasuryExpansion <-
160-
pp ^. U5c.treasuryExpansion . to inject . to L.boundRational ?! "Invalid treasuryExpansion"
168+
pp
169+
^. U5c.treasuryExpansion
170+
. to (L.boundRational <=< utxoRpcRationalNumberToRational)
171+
?! "Invalid treasuryExpansion"
161172
pure $ set L.ppTauL treasuryExpansion r
162173
, \r -> do
163174
minPoolCost <- pp ^. U5c.minPoolCost . to utxoRpcBigIntToInteger ?! "Invalid minPoolCost"
@@ -183,8 +194,18 @@ utxoRpcPParamsToProtocolParams era pp = conwayEraOnwardsConstraints (convert era
183194
pure $
184195
r & L.ppCostModelsL .~ L.mkCostModels nonEmptyCostModels
185196
, \r -> do
186-
steps <- pp ^. U5c.prices . U5c.steps . to inject . to L.boundRational ?! "Invalid prices.steps"
187-
mem <- pp ^. U5c.prices . U5c.memory . to inject . to L.boundRational ?! "Invalid prices.mem"
197+
steps <-
198+
pp
199+
^. U5c.prices
200+
. U5c.steps
201+
. to (L.boundRational <=< utxoRpcRationalNumberToRational)
202+
?! "Invalid prices.steps"
203+
mem <-
204+
pp
205+
^. U5c.prices
206+
. U5c.memory
207+
. to (L.boundRational <=< utxoRpcRationalNumberToRational)
208+
?! "Invalid prices.mem"
188209
pure $
189210
r
190211
& L.ppPricesL . prStepsL .~ steps
@@ -195,8 +216,7 @@ utxoRpcPParamsToProtocolParams era pp = conwayEraOnwardsConstraints (convert era
195216
minFeeScriptRefCostPerByte <-
196217
pp
197218
^. U5c.minFeeScriptRefCostPerByte
198-
. to inject
199-
. to L.boundRational
219+
. to (L.boundRational <=< utxoRpcRationalNumberToRational)
200220
?! "Invalid minFeeScriptRefCostPerByte"
201221
pure $ set L.ppMinFeeRefScriptCostPerByteL minFeeScriptRefCostPerByte r
202222
, \r -> do
@@ -218,7 +238,7 @@ utxoRpcPParamsToProtocolParams era pp = conwayEraOnwardsConstraints (convert era
218238
, (?! "Invalid value in poolVotingThresholds: hardForkInitiation")
219239
, (?! "Invalid value in poolVotingThresholds: ppSecurityGroup")
220240
]
221-
$ map (L.boundRational . inject) thresholds
241+
$ map (L.boundRational <=< utxoRpcRationalNumberToRational) thresholds
222242
pure $
223243
r
224244
& L.ppPoolVotingThresholdsL . L.pvtMotionNoConfidenceL .~ motionNoConfidence
@@ -255,7 +275,7 @@ utxoRpcPParamsToProtocolParams era pp = conwayEraOnwardsConstraints (convert era
255275
, (?! "Invalid value in drepVotingThresholds: ppGovGroup")
256276
, (?! "Invalid value in drepVotingThresholds: treasuryWithdrawal")
257277
]
258-
$ map (L.boundRational . inject) thresholds
278+
$ map (L.boundRational <=< utxoRpcRationalNumberToRational) thresholds
259279
pure $
260280
r
261281
& L.ppDRepVotingThresholdsL . L.dvtMotionNoConfidenceL .~ motionNoConfidence
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
module Cardano.Rpc.Server.Internal.UtxoRpc.Type.Rational
2+
( utxoRpcRationalNumberToRational
3+
)
4+
where
5+
6+
import Cardano.Rpc.Proto.Api.UtxoRpc.Query qualified as U5c
7+
8+
import RIO
9+
10+
import Data.Ratio ((%))
11+
import Network.GRPC.Spec
12+
13+
-- | Convert a protobuf 'U5c.RationalNumber' into a 'Rational'.
14+
--
15+
-- Fails with 'Nothing' when the denominator is zero, which is also the
16+
-- default value of the protobuf field when it is omitted.
17+
utxoRpcRationalNumberToRational :: Proto U5c.RationalNumber -> Maybe Rational
18+
utxoRpcRationalNumberToRational r =
19+
case r ^. U5c.denominator of
20+
0 -> Nothing
21+
den -> Just $ r ^. U5c.numerator . to fromIntegral % fromIntegral den

cardano-rpc/test/cardano-rpc-test/Test/Cardano/Rpc/Type.hs

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -48,42 +48,44 @@ hprop_roundtrip_rational_in_range = H.property $ do
4848
den <- forAll . H.integral $ H.linear 1 (fromIntegral $ maxBound @Word32)
4949
let r = num % den :: Rational
5050
msg = inject r :: Proto U5c.RationalNumber
51-
inject msg === r
51+
utxoRpcRationalNumberToRational msg === Just r
5252

5353
-- | Rationals with components exceeding the fixed proto field widths convert
5454
-- to the best representable approximation.
5555
hprop_rational_conversion_approximates_out_of_range :: Property
5656
hprop_rational_conversion_approximates_out_of_range = H.propertyOnce $ do
5757
H.note_ "In-range values convert exactly"
58-
throughProto (3 % 4) === 3 % 4
59-
throughProto (-(3 % 7)) === (-3) % 7
58+
throughProto (3 % 4) === Just (3 % 4)
59+
throughProto (-(3 % 7)) === Just ((-3) % 7)
6060
throughProto (fromIntegral (maxBound @Int32) % fromIntegral (maxBound @Word32))
61-
=== fromIntegral (maxBound @Int32) % fromIntegral (maxBound @Word32)
61+
=== Just (fromIntegral (maxBound @Int32) % fromIntegral (maxBound @Word32))
6262

6363
H.note_ "The approximation of 1 % 2^33 carries a valid, non-zero denominator"
6464
let smallRational = 1 % 2 ^ (33 :: Int) :: Rational
6565
smallMsg = inject smallRational :: Proto U5c.RationalNumber
6666
H.assertWith (smallMsg ^. U5c.denominator) (/= 0)
67+
smallValue <- H.nothingFail $ utxoRpcRationalNumberToRational smallMsg
6768
H.note_ "The approximation stays within 1 % 2^33 of the original value"
68-
H.assertWith (abs (inject smallMsg - smallRational)) (<= 1 % 2 ^ (33 :: Int))
69+
H.assertWith (abs (smallValue - smallRational)) (<= 1 % 2 ^ (33 :: Int))
6970
H.note_ "0 is the best approximation: it is closer to 1 % 2^33 than 1 % maxBound @Word32 is"
70-
inject smallMsg === (0 :: Rational)
71+
smallValue === (0 :: Rational)
7172

7273
H.note_ "1 % 2^32 approximates to the largest representable denominator"
73-
throughProto (1 % 2 ^ (32 :: Int)) === 1 % (2 ^ (32 :: Int) - 1)
74+
throughProto (1 % 2 ^ (32 :: Int)) === Just (1 % (2 ^ (32 :: Int) - 1))
7475

7576
H.note_ "Values beyond the numerator bounds are clamped to them"
76-
throughProto (2 ^ (40 :: Int) % 3) === fromIntegral (maxBound @Int32) % 1
77-
throughProto (-((2 ^ (40 :: Int)) % 3)) === (-(fromIntegral $ maxBound @Int32)) % 1
77+
throughProto (2 ^ (40 :: Int) % 3) === Just (fromIntegral (maxBound @Int32) % 1)
78+
throughProto (-((2 ^ (40 :: Int)) % 3)) === Just ((-(fromIntegral $ maxBound @Int32)) % 1)
7879

7980
H.note_ "Unit-interval-scale values keep sub-2^-32 precision"
8081
let unitScale = 1234567890123456789 % 9876543210987654321 :: Rational
8182
unitScaleMsg = inject unitScale :: Proto U5c.RationalNumber
8283
H.assertWith (unitScaleMsg ^. U5c.denominator) (/= 0)
83-
H.assertWith (abs (inject unitScaleMsg - unitScale)) (<= 1 % 2 ^ (32 :: Int))
84+
unitScaleValue <- H.nothingFail $ utxoRpcRationalNumberToRational unitScaleMsg
85+
H.assertWith (abs (unitScaleValue - unitScale)) (<= 1 % 2 ^ (32 :: Int))
8486
where
85-
throughProto :: Rational -> Rational
86-
throughProto r = inject (inject r :: Proto U5c.RationalNumber)
87+
throughProto :: Rational -> Maybe Rational
88+
throughProto r = utxoRpcRationalNumberToRational (inject r :: Proto U5c.RationalNumber)
8789

8890
-- | For arbitrary rationals in the unit interval with components up to
8991
-- 'Word64' scale (like ledger @UnitInterval@ values), the proto approximation
@@ -95,7 +97,8 @@ hprop_rational_approximation_error_bound = H.property $ do
9597
let r = num % den :: Rational
9698
msg = inject r :: Proto U5c.RationalNumber
9799
H.assertWith (msg ^. U5c.denominator) (/= 0)
98-
H.assertWith (abs (inject msg - r)) (<= 1 % 2 ^ (32 :: Int))
100+
converted <- H.nothingFail $ utxoRpcRationalNumberToRational msg
101+
H.assertWith (abs (converted - r)) (<= 1 % 2 ^ (32 :: Int))
99102

100103
-- | Test that ChainPoint protobuf message roundtrips, including the timestamp field.
101104
-- Note: @At (BlockNo 0)@ is excluded because it encodes identically to @Origin@.

0 commit comments

Comments
 (0)