Skip to content

Commit 308cf32

Browse files
authored
Enforce currency and token lengths in insertCoin and unValueData (#7372)
1 parent 7c50361 commit 308cf32

File tree

5 files changed

+96
-36
lines changed

5 files changed

+96
-36
lines changed

plutus-core/plutus-core/src/PlutusCore/Default/Builtins.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2047,7 +2047,7 @@ instance uni ~ DefaultUni => ToBuiltinMeaning uni DefaultFun where
20472047
(runCostingFunTwoArguments . paramBls12_381_G2_multiScalarMul)
20482048

20492049
toBuiltinMeaning _semvar InsertCoin =
2050-
let insertCoinDenotation :: ByteString -> ByteString -> Integer -> Value -> Value
2050+
let insertCoinDenotation :: ByteString -> ByteString -> Integer -> Value -> BuiltinResult Value
20512051
insertCoinDenotation = Value.insertCoin
20522052
{-# INLINE insertCoinDenotation #-}
20532053
in makeBuiltinMeaning

plutus-core/plutus-core/src/PlutusCore/Value.hs

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -188,28 +188,31 @@ instance Pretty Value where
188188
{-| \(O(\log \max(m, k))\), where \(m\) is the size of the outer map, and \(k\) is
189189
the size of the largest inner map.
190190
-}
191-
insertCoin :: ByteString -> ByteString -> Integer -> Value -> Value
191+
insertCoin :: ByteString -> ByteString -> Integer -> Value -> BuiltinResult Value
192192
insertCoin currency token amt v@(Value outer sizes size)
193-
| amt == 0 = deleteCoin currency token v
194-
| otherwise =
195-
let (mold, outer') = Map.alterF f (UnsafeK currency) outer
196-
(sizes', size') = case mold of
197-
Just old -> (updateSizes old (old + 1) sizes, size + 1)
198-
Nothing -> (sizes, size)
199-
in Value outer' sizes' size'
200-
where
201-
f
202-
:: Maybe (Map K Integer)
203-
-> ( -- Just (old size of inner map) if the total size grows by 1, otherwise Nothing
204-
Maybe Int
205-
, Maybe (Map K Integer)
206-
)
207-
f = \case
208-
Nothing -> (Just 0, Just (Map.singleton (UnsafeK token) amt))
209-
Just inner ->
210-
let (isJust -> exists, inner') =
211-
Map.insertLookupWithKey (\_ _ _ -> amt) (UnsafeK token) amt inner
212-
in (if exists then Nothing else Just (Map.size inner), Just inner')
193+
| amt == 0 = pure $ deleteCoin currency token v
194+
| otherwise = case (k currency, k token) of
195+
(Nothing, _) -> fail $ "insertCoin: invalid currency: " <> show (B.unpack currency)
196+
(_, Nothing) -> fail $ "insertCoin: invalid token: " <> show (B.unpack token)
197+
(Just ck, Just tk) ->
198+
let f
199+
:: Maybe (Map K Integer)
200+
-> ( -- Just (old size of inner map) if the total size grows by 1,
201+
-- otherwise Nothing
202+
Maybe Int
203+
, Maybe (Map K Integer)
204+
)
205+
f = \case
206+
Nothing -> (Just 0, Just (Map.singleton tk amt))
207+
Just inner ->
208+
let (isJust -> exists, inner') =
209+
Map.insertLookupWithKey (\_ _ _ -> amt) tk amt inner
210+
in (if exists then Nothing else Just (Map.size inner), Just inner')
211+
(mold, outer') = Map.alterF f ck outer
212+
(sizes', size') = case mold of
213+
Just old -> (updateSizes old (old + 1) sizes, size + 1)
214+
Nothing -> (sizes, size)
215+
in pure $ Value outer' sizes' size'
213216
{-# INLINEABLE insertCoin #-}
214217

215218
-- | \(O(\log \max(m, k))\)
@@ -312,7 +315,7 @@ unValueData =
312315
where
313316
unB :: Data -> BuiltinResult K
314317
unB = \case
315-
B b -> pure (UnsafeK b)
318+
B b -> maybe (fail $ "unValueData: invalid key: " <> show (B.unpack b)) pure (k b)
316319
_ -> fail "unValueData: non-B constructor"
317320

318321
unI :: Data -> BuiltinResult Integer
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
ByteString -> ByteString -> Integer -> Value -> Value
1+
ByteString -> ByteString -> Integer -> Value -> BuiltinResult Value

plutus-core/plutus-core/test/Value/Spec.hs

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{-# LANGUAGE OverloadedStrings #-}
22
{-# LANGUAGE TypeApplications #-}
33
{-# LANGUAGE ViewPatterns #-}
4+
{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}
45
{-# OPTIONS_GHC -Wno-orphans #-}
56

67
module Value.Spec (tests) where
@@ -11,15 +12,18 @@ import Data.Either
1112
import Data.Foldable qualified as F
1213
import Data.Map.Strict qualified as Map
1314
import Data.Maybe
14-
import PlutusCore.Flat qualified as Flat
15-
import PlutusCore.Generators.QuickCheck.Builtin (ValueAmount (..), genShortHex)
16-
import PlutusCore.Value (Value)
17-
import PlutusCore.Value qualified as V
1815
import Safe.Foldable (maximumMay)
1916
import Test.QuickCheck
2017
import Test.Tasty
2118
import Test.Tasty.QuickCheck
2219

20+
import PlutusCore.Builtin (BuiltinResult (..))
21+
import PlutusCore.Data (Data (..))
22+
import PlutusCore.Flat qualified as Flat
23+
import PlutusCore.Generators.QuickCheck.Builtin (ValueAmount (..), genShortHex)
24+
import PlutusCore.Value (Value)
25+
import PlutusCore.Value qualified as V
26+
2327
prop_packUnpackRoundtrip :: Value -> Property
2428
prop_packUnpackRoundtrip v = v === V.pack (V.unpack v)
2529

@@ -38,15 +42,15 @@ prop_insertCoinBookkeeping :: Value -> ValueAmount -> Property
3842
prop_insertCoinBookkeeping v (ValueAmount amt) =
3943
forAll (genShortHex (V.totalSize v)) $ \currency ->
4044
forAll (genShortHex (V.totalSize v)) $ \token ->
41-
let v' = V.insertCoin (V.unK currency) (V.unK token) amt v
45+
let BuiltinSuccess v' = V.insertCoin (V.unK currency) (V.unK token) amt v
4246
in checkSizes v'
4347

4448
-- | Verifies that @insertCoin@ preserves @Value@ invariants
4549
prop_insertCoinPreservesInvariants :: Value -> ValueAmount -> Property
4650
prop_insertCoinPreservesInvariants v (ValueAmount amt) =
4751
forAll (genShortHex (V.totalSize v)) $ \currency ->
4852
forAll (genShortHex (V.totalSize v)) $ \token ->
49-
let v' = V.insertCoin (V.unK currency) (V.unK token) amt v
53+
let BuiltinSuccess v' = V.insertCoin (V.unK currency) (V.unK token) amt v
5054
in checkInvariants v'
5155

5256
prop_unionCommutative :: Value -> Value -> Property
@@ -60,15 +64,33 @@ prop_insertCoinIdempotent :: Value -> Property
6064
prop_insertCoinIdempotent v =
6165
v
6266
=== F.foldl'
63-
(\acc (c, t, a) -> let v' = V.insertCoin (V.unK c) (V.unK t) a acc in v')
67+
(\acc (c, t, a) -> let BuiltinSuccess v' = V.insertCoin (V.unK c) (V.unK t) a acc in v')
6468
v
6569
(V.toFlatList v)
6670

71+
prop_insertCoinValidatesCurrency :: Value -> Property
72+
prop_insertCoinValidatesCurrency v =
73+
forAll gen33Bytes $ \c ->
74+
forAll gen32BytesOrFewer $ \t ->
75+
forAll (arbitrary `suchThat` (/= 0)) $ \amt ->
76+
case V.insertCoin c t amt v of
77+
BuiltinFailure{} -> property True
78+
_ -> property False
79+
80+
prop_insertCoinValidatesToken :: Value -> Property
81+
prop_insertCoinValidatesToken v =
82+
forAll gen32BytesOrFewer $ \c ->
83+
forAll gen33Bytes $ \t ->
84+
forAll (arbitrary `suchThat` (/= 0)) $ \amt ->
85+
case V.insertCoin c t amt v of
86+
BuiltinFailure{} -> property True
87+
_ -> property False
88+
6789
prop_lookupAfterInsertion :: Value -> ValueAmount -> Property
6890
prop_lookupAfterInsertion v (ValueAmount amt) =
6991
forAll (genShortHex (V.totalSize v)) $ \currency ->
7092
forAll (genShortHex (V.totalSize v)) $ \token ->
71-
let v' = V.insertCoin (V.unK currency) (V.unK token) amt v
93+
let BuiltinSuccess v' = V.insertCoin (V.unK currency) (V.unK token) amt v
7294
in V.lookupCoin (V.unK currency) (V.unK token) v' === amt
7395

7496
prop_lookupAfterDeletion :: Value -> Property
@@ -84,7 +106,7 @@ prop_deleteCoinIdempotent v0 =
84106
let v' = V.deleteCoin c t v
85107
in v' === V.deleteCoin c t v'
86108
where
87-
v = if V.totalSize v0 > 0 then v0 else V.insertCoin "c" "t" 1 v0
109+
BuiltinSuccess v = if V.totalSize v0 > 0 then pure v0 else V.insertCoin "c" "t" 1 v0
88110
fl = V.toFlatList v
89111

90112
prop_containsReflexive :: Value -> Property
@@ -113,7 +135,7 @@ prop_flatDecodeSuccess = forAll (arbitrary `suchThat` (/= 0)) $ \amt ->
113135
forAll gen32BytesOrFewer $ \c ->
114136
forAll gen32BytesOrFewer $ \t ->
115137
let flat = Flat.flat $ Map.singleton c (Map.singleton t amt)
116-
v = V.insertCoin c t amt V.empty
138+
BuiltinSuccess v = V.insertCoin c t amt V.empty
117139
in Flat.unflat flat === Right v
118140

119141
prop_flatDecodeInvalidCurrency :: Property
@@ -145,6 +167,24 @@ checkInvariants (V.unpack -> v) =
145167
property ((not . any Map.null) v)
146168
.&&. property ((not . any (elem 0)) v)
147169

170+
prop_unValueDataValidatesCurrency :: ValueAmount -> Property
171+
prop_unValueDataValidatesCurrency (ValueAmount amt) =
172+
forAll gen33Bytes $ \c ->
173+
forAll gen32BytesOrFewer $ \t ->
174+
let d = Map [(B c, Map [(B t, I amt)])]
175+
in case V.unValueData d of
176+
BuiltinFailure{} -> property True
177+
_ -> property False
178+
179+
prop_unValueDataValidatesToken :: ValueAmount -> Property
180+
prop_unValueDataValidatesToken (ValueAmount amt) =
181+
forAll gen32BytesOrFewer $ \c ->
182+
forAll gen33Bytes $ \t ->
183+
let d = Map [(B c, Map [(B t, I amt)])]
184+
in case V.unValueData d of
185+
BuiltinFailure{} -> property True
186+
_ -> property False
187+
148188
tests :: TestTree
149189
tests =
150190
testGroup
@@ -173,6 +213,12 @@ tests =
173213
, testProperty
174214
"insertCoinIdempotent"
175215
prop_insertCoinIdempotent
216+
, testProperty
217+
"insertCoinValidatesCurrency"
218+
prop_insertCoinValidatesCurrency
219+
, testProperty
220+
"insertCoinValidatesToken"
221+
prop_insertCoinValidatesToken
176222
, testProperty
177223
"lookupAfterInsertion"
178224
prop_lookupAfterInsertion
@@ -188,6 +234,12 @@ tests =
188234
, testProperty
189235
"containsAfterDeletion"
190236
prop_containsAfterDeletion
237+
, testProperty
238+
"unValueDataValidatesCurrency"
239+
prop_unValueDataValidatesCurrency
240+
, testProperty
241+
"unValueDataValidatesToken"
242+
prop_unValueDataValidatesToken
191243
, testProperty
192244
"flatRoundtrip"
193245
prop_flatRoundtrip

plutus-tx/src/PlutusTx/Builtins/Internal.hs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1084,8 +1084,13 @@ insertCoin
10841084
-> BuiltinInteger
10851085
-> BuiltinValue
10861086
-> BuiltinValue
1087-
insertCoin (BuiltinByteString c) (BuiltinByteString t) amt (BuiltinValue v) =
1088-
BuiltinValue $ Value.insertCoin c t amt v
1087+
insertCoin (BuiltinByteString c) (BuiltinByteString t) amt (BuiltinValue v0) =
1088+
case Value.insertCoin c t amt v0 of
1089+
BuiltinSuccess v -> BuiltinValue v
1090+
BuiltinSuccessWithLogs logs v -> traceAll logs (BuiltinValue v)
1091+
BuiltinFailure logs err ->
1092+
traceAll (logs <> pure (display err)) $
1093+
Haskell.error "insertCoin errored."
10891094
{-# OPAQUE insertCoin #-}
10901095

10911096
lookupCoin

0 commit comments

Comments
 (0)