Skip to content

Commit 7c50361

Browse files
authored
Value decoding/parsing: reject currencies/tokens longer than 32 bytes (#7371)
1 parent bcd2b30 commit 7c50361

File tree

10 files changed

+227
-54
lines changed

10 files changed

+227
-54
lines changed

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import PlutusCore.Core
2020
import PlutusCore.Data (Data)
2121
import PlutusCore.DeBruijn
2222
import PlutusCore.Name.Unique
23-
import PlutusCore.Value (Value)
2423

2524
import Data.Proxy
2625
import PlutusCore.Flat
@@ -122,8 +121,6 @@ decodeConstant = dBEBits8 constantWidth
122121

123122
deriving via FlatViaSerialise Data instance Flat Data
124123

125-
deriving via FlatViaSerialise Value instance Flat Value
126-
127124
decodeKindedUniFlat :: Closed uni => Get (SomeTypeIn (Kinded uni))
128125
decodeKindedUniFlat =
129126
go . decodeKindedUni . map (fromIntegral :: Word8 -> Int)

plutus-core/plutus-core/src/PlutusCore/Parser/Builtin.hs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import PlutusCore.Value qualified as PLC (Value)
1818
import PlutusCore.Value qualified as Value
1919

2020
import Control.Monad.Combinators
21-
import Data.ByteString (ByteString, pack)
21+
import Data.ByteString (ByteString, pack, unpack)
2222
import Data.Map.Strict qualified as Map
2323
import Data.Text qualified as T
2424
import Data.Text.Internal.Read (hexDigitToInt)
@@ -90,7 +90,16 @@ conArray uniA = Vector.fromList <$> conList uniA
9090

9191
-- | Parser for values.
9292
conValue :: Parser PLC.Value
93-
conValue = Value.fromList <$> conList knownUni
93+
conValue = do
94+
Value.fromList <$> (traverse validateKeys =<< conList knownUni)
95+
where
96+
validateToken (token, amt) = do
97+
tk <- maybe (fail $ "Invalid token: " <> show (unpack token)) pure (Value.k token)
98+
pure (tk, amt)
99+
validateKeys (currency, tokens) = do
100+
ck <- maybe (fail $ "Invalid currency: " <> show (unpack currency)) pure (Value.k currency)
101+
tks <- traverse validateToken tokens
102+
pure (ck, tks)
94103

95104
-- | Parser for pairs.
96105
conPair :: DefaultUni (Esc a) -> DefaultUni (Esc b) -> Parser (a, b)

plutus-core/plutus-core/src/PlutusCore/Pretty/PrettyConst.hs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,9 @@ instance PrettyBy ConstConfig Data where
159159
I i -> ("I" <+> prettyArg i) :| []
160160
B b -> ("B" <+> prettyArg b) :| []
161161

162+
instance PrettyBy ConstConfig Value.K where
163+
prettyBy config = prettyBy config . Value.unK
164+
162165
instance PrettyBy ConstConfig Value where
163166
prettyBy config = prettyBy config . Value.toList
164167

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

Lines changed: 81 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55

66
module PlutusCore.Value (
77
Value, -- Do not expose data constructor
8+
K, -- Do not expose data constructor
9+
k,
10+
unK,
11+
maxKeyLen,
812
NestedMap,
913
unpack,
1014
pack,
@@ -23,11 +27,12 @@ module PlutusCore.Value (
2327
unValueData,
2428
) where
2529

26-
import Codec.Serialise (Serialise)
30+
import Codec.Serialise qualified as CBOR
2731
import Control.DeepSeq (NFData)
2832
import Data.Bifunctor
2933
import Data.Bitraversable
3034
import Data.ByteString (ByteString)
35+
import Data.ByteString qualified as B
3136
import Data.ByteString.Base64 qualified as Base64
3237
import Data.Functor
3338
import Data.Hashable (Hashable)
@@ -39,11 +44,45 @@ import Data.Map.Strict qualified as Map
3944
import Data.Maybe
4045
import Data.Text.Encoding qualified as Text
4146
import GHC.Generics
47+
4248
import PlutusCore.Builtin.Result
4349
import PlutusCore.Data (Data (..))
50+
import PlutusCore.Flat qualified as Flat
4451
import PlutusPrelude (Pretty (..))
4552

46-
type NestedMap = Map ByteString (Map ByteString Integer)
53+
-- Max length (in bytes) for currency symbols and token names in `Value`,
54+
-- both of which cannot exceed 32 bytes. Currency symbols are in fact either
55+
-- empty or 28 bytes, but for simplicity we allow anything between 0 and 32 bytes.
56+
maxKeyLen :: Int
57+
maxKeyLen = 32
58+
{-# INLINE maxKeyLen #-}
59+
60+
-- | A `ByteString` with maximum length of `maxKeyLen` bytes.
61+
newtype K = UnsafeK {unK :: ByteString}
62+
deriving newtype (Eq, Ord, Show, Hashable, NFData)
63+
deriving stock (Generic)
64+
65+
k :: ByteString -> Maybe K
66+
k b = if B.length b <= maxKeyLen then Just (UnsafeK b) else Nothing
67+
{-# INLINEABLE k #-}
68+
69+
instance Flat.Flat K where
70+
encode (UnsafeK b) = Flat.encode b
71+
{-# INLINE encode #-}
72+
decode = do
73+
b <- Flat.decode
74+
maybe (fail $ "Invalid Value key: " <> show (B.unpack b)) pure (k b)
75+
{-# INLINEABLE decode #-}
76+
77+
instance CBOR.Serialise K where
78+
encode (UnsafeK b) = CBOR.encode b
79+
{-# INLINE encode #-}
80+
decode = do
81+
b <- CBOR.decode
82+
maybe (fail $ "Invalid Value key: " <> show (B.unpack b)) pure (k b)
83+
{-# INLINEABLE decode #-}
84+
85+
type NestedMap = Map K (Map K Integer)
4786

4887
-- | The underlying type of the UPLC built-in type @Value@.
4988
data Value
@@ -61,11 +100,23 @@ data Value
61100
Invariant: all values are positive.
62101
-}
63102
{-# UNPACK #-} !Int
64-
{-^ Total size, i.e., sum total of inner map sizes. This avoids recomputing
103+
{- ^ Total size, i.e., sum total of inner map sizes. This avoids recomputing
65104
the total size during the costing of operations like `unionValue`.
66105
-}
67106
deriving stock (Eq, Show, Generic)
68-
deriving anyclass (Hashable, Serialise, NFData)
107+
deriving anyclass (Hashable, NFData)
108+
109+
instance CBOR.Serialise Value where
110+
encode (Value v _ _) = CBOR.encode v
111+
{-# INLINE encode #-}
112+
decode = pack <$> CBOR.decode
113+
{-# INLINE decode #-}
114+
115+
instance Flat.Flat Value where
116+
encode (Value v _ _) = Flat.encode v
117+
{-# INLINE encode #-}
118+
decode = pack <$> Flat.decode
119+
{-# INLINE decode #-}
69120

70121
{-| Unpack a `Value` into a map from (currency symbol, token name) to amount.
71122
@@ -110,19 +161,20 @@ empty :: Value
110161
empty = Value mempty mempty 0
111162
{-# INLINE empty #-}
112163

113-
toList :: Value -> [(ByteString, [(ByteString, Integer)])]
164+
toList :: Value -> [(K, [(K, Integer)])]
114165
toList = Map.toList . Map.map Map.toList . unpack
115166
{-# INLINEABLE toList #-}
116167

117-
toFlatList :: Value -> [(ByteString, ByteString, Integer)]
168+
toFlatList :: Value -> [(K, K, Integer)]
118169
toFlatList (toList -> xs) = [(c, t, a) | (c, ys) <- xs, (t, a) <- ys]
119170
{-# INLINEABLE toFlatList #-}
120171

121-
fromList :: [(ByteString, [(ByteString, Integer)])] -> Value
172+
fromList :: [(K, [(K, Integer)])] -> Value
122173
fromList =
123174
pack
124175
. Map.fromListWith (Map.unionWith (+))
125176
. fmap (second (Map.fromListWith (+)))
177+
{-# INLINEABLE fromList #-}
126178

127179
normalize :: NestedMap -> NestedMap
128180
normalize = Map.filter (not . Map.null) . Map.map (Map.filter (/= 0))
@@ -131,7 +183,7 @@ normalize = Map.filter (not . Map.null) . Map.map (Map.filter (/= 0))
131183
instance Pretty Value where
132184
pretty = pretty . fmap (bimap toText (fmap (first toText))) . toList
133185
where
134-
toText = Text.decodeLatin1 . Base64.encode
186+
toText = Text.decodeLatin1 . Base64.encode . unK
135187

136188
{-| \(O(\log \max(m, k))\), where \(m\) is the size of the outer map, and \(k\) is
137189
the size of the largest inner map.
@@ -140,38 +192,40 @@ insertCoin :: ByteString -> ByteString -> Integer -> Value -> Value
140192
insertCoin currency token amt v@(Value outer sizes size)
141193
| amt == 0 = deleteCoin currency token v
142194
| otherwise =
143-
let (mold, outer') = Map.alterF f currency outer
195+
let (mold, outer') = Map.alterF f (UnsafeK currency) outer
144196
(sizes', size') = case mold of
145197
Just old -> (updateSizes old (old + 1) sizes, size + 1)
146198
Nothing -> (sizes, size)
147199
in Value outer' sizes' size'
148200
where
149201
f
150-
:: Maybe (Map ByteString Integer)
202+
:: Maybe (Map K Integer)
151203
-> ( -- Just (old size of inner map) if the total size grows by 1, otherwise Nothing
152204
Maybe Int
153-
, Maybe (Map ByteString Integer)
205+
, Maybe (Map K Integer)
154206
)
155207
f = \case
156-
Nothing -> (Just 0, Just (Map.singleton token amt))
208+
Nothing -> (Just 0, Just (Map.singleton (UnsafeK token) amt))
157209
Just inner ->
158-
let (isJust -> exists, inner') = Map.insertLookupWithKey (\_ _ _ -> amt) token amt inner
210+
let (isJust -> exists, inner') =
211+
Map.insertLookupWithKey (\_ _ _ -> amt) (UnsafeK token) amt inner
159212
in (if exists then Nothing else Just (Map.size inner), Just inner')
160213
{-# INLINEABLE insertCoin #-}
161214

162215
-- | \(O(\log \max(m, k))\)
163216
deleteCoin :: ByteString -> ByteString -> Value -> Value
164-
deleteCoin currency token (Value outer sizes size) = Value outer' sizes' size'
217+
deleteCoin (UnsafeK -> currency) (UnsafeK -> token) (Value outer sizes size) =
218+
Value outer' sizes' size'
165219
where
166220
(mold, outer') = Map.alterF f currency outer
167221
(sizes', size') = case mold of
168222
Just old -> (updateSizes old (old - 1) sizes, size - 1)
169223
Nothing -> (sizes, size)
170224
f
171-
:: Maybe (Map ByteString Integer)
225+
:: Maybe (Map K Integer)
172226
-> ( -- Just (old size of inner map) if the total size shrinks by 1, otherwise Nothing
173227
Maybe Int
174-
, Maybe (Map ByteString Integer)
228+
, Maybe (Map K Integer)
175229
)
176230
f = \case
177231
Nothing -> (Nothing, Nothing)
@@ -181,9 +235,10 @@ deleteCoin currency token (Value outer sizes size) = Value outer' sizes' size'
181235

182236
-- | \(O(\log \max(m, k))\)
183237
lookupCoin :: ByteString -> ByteString -> Value -> Integer
184-
lookupCoin currency token (unpack -> outer) = case Map.lookup currency outer of
185-
Nothing -> 0
186-
Just inner -> Map.findWithDefault 0 token inner
238+
lookupCoin (UnsafeK -> currency) (UnsafeK -> token) (unpack -> outer) =
239+
case Map.lookup currency outer of
240+
Nothing -> 0
241+
Just inner -> Map.findWithDefault 0 token inner
187242

188243
{-| \(O(n_{2}\log \max(m_{1}, k_{1}))\), where \(n_{2}\) is the total size of the second
189244
`Value`, \(m_{1}\) is the size of the outer map in the first `Value` and \(k_{1}\) is
@@ -200,7 +255,7 @@ valueContains v = Map.foldrWithKey' go True . unpack
200255
where
201256
goInner t a2 =
202257
(&&)
203-
( let a1 = lookupCoin c t v
258+
( let a1 = lookupCoin (unK c) (unK t) v
204259
in if a2 > 0
205260
then a1 >= a2
206261
else a1 == a2
@@ -240,10 +295,10 @@ unionValue (unpack -> vA) (unpack -> vB) =
240295
This is the denotation of @ValueData@ in Plutus V1, V2 and V3.
241296
-}
242297
valueData :: Value -> Data
243-
valueData = Map . fmap (bimap B tokensData) . Map.toList . unpack
298+
valueData = Map . fmap (bimap (B . unK) tokensData) . Map.toList . unpack
244299
where
245-
tokensData :: Map ByteString Integer -> Data
246-
tokensData = Map . fmap (bimap B I) . Map.toList
300+
tokensData :: Map K Integer -> Data
301+
tokensData = Map . fmap (bimap (B . unK) I) . Map.toList
247302
{-# INLINEABLE valueData #-}
248303

249304
{-| \(O(n \log n)\). Decodes `Data` into `Value`, in the same way as non-builtin @Value@.
@@ -255,17 +310,17 @@ unValueData =
255310
Map cs -> fmap (Map.fromListWith (Map.unionWith (+))) (traverse (bitraverse unB unTokens) cs)
256311
_ -> fail "unValueData: non-Map constructor"
257312
where
258-
unB :: Data -> BuiltinResult ByteString
313+
unB :: Data -> BuiltinResult K
259314
unB = \case
260-
B b -> pure b
315+
B b -> pure (UnsafeK b)
261316
_ -> fail "unValueData: non-B constructor"
262317

263318
unI :: Data -> BuiltinResult Integer
264319
unI = \case
265320
I i -> pure i
266321
_ -> fail "unValueData: non-I constructor"
267322

268-
unTokens :: Data -> BuiltinResult (Map ByteString Integer)
323+
unTokens :: Data -> BuiltinResult (Map K Integer)
269324
unTokens = \case
270325
Map ts -> fmap (Map.fromListWith (+)) (traverse (bitraverse unB unI) ts)
271326
_ -> fail "unValueData: non-Map constructor"

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import Hedgehog.Gen qualified as Gen
1515
import Hedgehog.Range qualified as Range
1616
import Test.Tasty
1717
import Test.Tasty.Hedgehog
18+
import Test.Tasty.HUnit
1819

1920
-- | The `SrcSpan` of a parsed `Term` should not including trailing whitespaces.
2021
propTermSrcSpan :: Property
@@ -30,6 +31,46 @@ propTermSrcSpan = property $ do
3031
in (srcSpanELine sp, srcSpanECol sp) === (endingLine, endingCol + 1)
3132
Left err -> annotate (display err) >> failure
3233

34+
expectParserSuccess :: T.Text -> Assertion
35+
expectParserSuccess code = case runQuoteT (parseTerm code) of
36+
Right _ -> pure ()
37+
Left _ -> assertFailure $ "Unexpected failure when parsing term: " <> T.unpack code
38+
39+
expectParserFailure :: T.Text -> Assertion
40+
expectParserFailure code = case runQuoteT (parseTerm code) of
41+
Right _ -> assertFailure $ "Unexpected success when parsing term: " <> T.unpack code
42+
Left _ -> pure ()
43+
44+
parseValueInvalidCurrency :: Assertion
45+
parseValueInvalidCurrency = do
46+
expectParserFailure code
47+
where
48+
-- Currency is 33 bytes
49+
code = "(con value \
50+
\[ ( #616161616161616161616161616161616161616161616161616161616161616161\
51+
\, [ ( #6161616161616161616161616161616161616161616161616161616161616161\
52+
\ , -100 ) ] ) ])"
53+
54+
parseValueInvalidToken :: Assertion
55+
parseValueInvalidToken = do
56+
expectParserFailure code
57+
where
58+
-- Token is 33 bytes
59+
code = "(con value \
60+
\[ ( #6161616161616161616161616161616161616161616161616161616161616161\
61+
\, [ ( #616161616161616161616161616161616161616161616161616161616161616161\
62+
\ , -100 ) ] ) ])"
63+
64+
parseValueValid :: Assertion
65+
parseValueValid = do
66+
expectParserSuccess code
67+
where
68+
-- Both currency and token are 32 bytes
69+
code = "(con value \
70+
\[ ( #6161616161616161616161616161616161616161616161616161616161616161\
71+
\, [ ( #6161616161616161616161616161616161616161616161616161616161616161\
72+
\ , -100 ) ] ) ])"
73+
3374
tests :: TestTree
3475
tests =
3576
testGroup
@@ -38,4 +79,13 @@ tests =
3879
"parser captures ending positions correctly"
3980
"propTermSrcSpan"
4081
propTermSrcSpan
82+
, testCase
83+
"parser of Value should fail upon invalid currency"
84+
parseValueInvalidCurrency
85+
, testCase
86+
"parser of Value should fail upon invalid token"
87+
parseValueInvalidToken
88+
, testCase
89+
"parser of Value should succeed"
90+
parseValueValid
4191
]

0 commit comments

Comments
 (0)