{-# LANGUAGE OverloadedStrings #-}

{- | Threat model for detecting Datum Bloat Attack vulnerabilities.

A Datum Bloat Attack exploits validators that don't limit the size of data
fields within their datums. Unlike the Large Data Attack (which adds extra
constructor fields), this attack inflates /existing/ fields - specifically
lists and byte strings within the datum structure.

== Consequences ==

1. __Increased execution costs__: Processing bloated datums wastes CPU/memory
   execution units, making transactions more expensive.

2. __Permanent fund locking__: If a list or bytestring field is bloated sufficiently:

   - Deserializing the datum may exceed execution unit limits
   - The transaction required to spend the UTxO may exceed protocol size limits

   In these cases, the UTxO becomes __permanently unspendable__ and funds
   are locked forever with no possibility of recovery.

== Vulnerable Patterns ==

=== Pattern 1: Unbounded list fields ===

@
type Datum {
  owner: VerificationKeyHash,
  messages: List<ByteArray>  -- No list length limit!
}
@

An attacker can append arbitrarily many items to the messages list,
bloating the datum beyond transaction limits. Caught by 'datumListBloatAttack'.

=== Pattern 2: Unbounded ByteString fields ===

@
type Datum {
  owner: VerificationKeyHash,
  messages: List<ByteArray>  -- No ByteArray SIZE limit!
}
@

An attacker can replace small ByteArrays with huge ones (e.g., "Hello" -> 100KB).
Caught by 'datumByteBloatAttack'.

== Mitigation ==

A secure validator should either:

- Enforce maximum field sizes in the validator logic
- Check list lengths explicitly (e.g., @length messages <= maxMessages@)
- Limit ByteArray sizes (e.g., @lengthOfByteString msg <= maxMsgSize@)
- Hash large data instead of storing it inline

This threat model tests if a script output with an inline datum still validates
when list fields are bloated with additional large items, or when byte string
fields are replaced with much larger ones.
-}
module Convex.ThreatModel.DatumBloat (
  -- * List bloating attacks
  datumListBloatAttack,
  datumListBloatAttackWith,
  datumListBloatAttackWithGen,
  bloatLists,

  -- * ByteString inflation attacks
  datumByteBloatAttack,
  datumByteBloatAttackWith,
  datumByteBloatAttackWithGen,
  inflateBytes,
  inflateFirstListItem,
) where

import Convex.ThreatModel
import Data.ByteString qualified as BS
import Test.QuickCheck (Gen, choose)

{- | Default datum-list-bloat attack. The number of items and item size are
drawn per transaction from curated ranges, so QuickCheck explores the
parameter space and shrinks counterexamples toward the smallest triggering
values.
-}
datumListBloatAttack :: ThreatModel ()
datumListBloatAttack :: ThreatModel ()
datumListBloatAttack = Gen (Int, Int) -> ThreatModel ()
datumListBloatAttackWithGen ((,) (Int -> Int -> (Int, Int)) -> Gen Int -> Gen (Int -> (Int, Int))
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> (Int, Int) -> Gen Int
forall a. Random a => (a, a) -> Gen a
choose (Int
1, Int
20) Gen (Int -> (Int, Int)) -> Gen Int -> Gen (Int, Int)
forall a b. Gen (a -> b) -> Gen a -> Gen b
forall (f :: * -> *) a b. Applicative f => f (a -> b) -> f a -> f b
<*> (Int, Int) -> Gen Int
forall a. Random a => (a, a) -> Gen a
choose (Int
1, Int
500))

{- | Datum-list-bloat attack with fixed parameters. Keep using this for
deterministic regression tests and golden seeds.
-}
datumListBloatAttackWith :: Int -> Int -> ThreatModel ()
datumListBloatAttackWith :: Int -> Int -> ThreatModel ()
datumListBloatAttackWith Int
numItems Int
itemSize = Gen (Int, Int) -> ThreatModel ()
datumListBloatAttackWithGen ((Int, Int) -> Gen (Int, Int)
forall a. a -> Gen a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Int
numItems, Int
itemSize))

{- | Datum-list-bloat attack parameterised by a generator for the number of
items and the per-item byte size. This is the primitive the other two forms
delegate to.
-}
datumListBloatAttackWithGen :: Gen (Int, Int) -> ThreatModel ()
datumListBloatAttackWithGen :: Gen (Int, Int) -> ThreatModel ()
datumListBloatAttackWithGen Gen (Int, Int)
gen =
  String -> ThreatModel () -> ThreatModel ()
forall a. String -> ThreatModel a -> ThreatModel a
Named String
"Datum List Bloat Attack" (ThreatModel () -> ThreatModel ())
-> ThreatModel () -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$ do
    (Int
numItems, Int
itemSize) <- Gen (Int, Int)
-> ((Int, Int) -> [(Int, Int)]) -> ThreatModel (Int, Int)
forall a. Show a => Gen a -> (a -> [a]) -> ThreatModel a
forAllTM Gen (Int, Int)
gen (Int, Int) -> [(Int, Int)]
shrinkPair

    -- Skip iterations where the draw is too small to be a meaningful attack.
    Bool -> ThreatModel ()
ensure (Int
numItems Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
>= Int
1 Bool -> Bool -> Bool
&& Int
itemSize Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
>= Int
1)

    (Output
target, ScriptData
originalDatum) <- ThreatModel (Output, ScriptData)
anyGuardedOutputWithInlineDatum

    -- Check if the datum contains any lists to bloat
    Bool -> ThreatModel () -> ThreatModel ()
forall {f :: * -> *}. Applicative f => Bool -> f () -> f ()
unless (ScriptData -> Bool
containsList ScriptData
originalDatum) (ThreatModel () -> ThreatModel ())
-> ThreatModel () -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$
      String -> ThreatModel ()
forall a. String -> ThreatModel a
failPrecondition String
"Datum contains no list fields to bloat"

    let bloatedDatum :: ScriptData
bloatedDatum = Int -> Int -> ScriptData -> ScriptData
bloatLists Int
numItems Int
itemSize ScriptData
originalDatum

    String -> ThreatModel ()
counterexampleTM (String -> ThreatModel ()) -> String -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$
      [String] -> String
paragraph
        [ String
"The transaction contains a script output at index"
        , TxIx -> String
forall a. Show a => a -> String
show (Output -> TxIx
outputIx Output
target)
        , String
"with an inline datum containing list fields."
        ]

    String -> ThreatModel ()
counterexampleTM (String -> ThreatModel ()) -> String -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$
      [String] -> String
paragraph
        [ String
"Testing if the lists can be bloated with"
        , Int -> String
forall a. Show a => a -> String
show Int
numItems
        , String
"items of"
        , Int -> String
forall a. Show a => a -> String
show Int
itemSize
        , String
"bytes each while still passing validation."
        ]

    String -> ThreatModel ()
counterexampleTM (String -> ThreatModel ()) -> String -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$
      [String] -> String
paragraph
        [ String
"If this validates, the script doesn't enforce datum field size limits."
        , String
"An attacker could exploit this to:"
        , String
"1) Inflate the datum beyond transaction size limits"
        , String
"2) Increase execution costs for processing the datum"
        , String
"3) Potentially lock funds permanently if limits are exceeded"
        ]

    String -> [String] -> ThreatModel ()
tabulateTM String
"items" [Int -> String
bucketItems Int
numItems]
    String -> [String] -> ThreatModel ()
tabulateTM String
"item bytes" [Int -> String
bucketSize Int
itemSize]

    -- Try to validate with the bloated datum
    TxModifier -> ThreatModel ()
shouldNotValidate (TxModifier -> ThreatModel ()) -> TxModifier -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$ Output -> Datum -> TxModifier
forall t. IsInputOrOutput t => t -> Datum -> TxModifier
changeDatumOf Output
target (ScriptData -> Datum
toInlineDatum ScriptData
bloatedDatum)
 where
  unless :: Bool -> f () -> f ()
unless Bool
False f ()
action = f ()
action
  unless Bool
True f ()
_ = () -> f ()
forall a. a -> f a
forall (f :: * -> *) a. Applicative f => a -> f a
pure ()

-- | Shrink a pair of positive integers toward (1, 1).
shrinkPair :: (Int, Int) -> [(Int, Int)]
shrinkPair :: (Int, Int) -> [(Int, Int)]
shrinkPair (Int
a, Int
b) =
  [(Int
a', Int
b) | Int
a' <- Int -> [Int]
shrinkPositive Int
a]
    [(Int, Int)] -> [(Int, Int)] -> [(Int, Int)]
forall a. [a] -> [a] -> [a]
++ [(Int
a, Int
b') | Int
b' <- Int -> [Int]
shrinkPositive Int
b]

-- | Coarse bucket for the item-count distribution report.
bucketItems :: Int -> String
bucketItems :: Int -> String
bucketItems Int
n
  | Int
n Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
<= Int
5 = String
"001-005"
  | Int
n Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
<= Int
10 = String
"006-010"
  | Int
n Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
<= Int
15 = String
"011-015"
  | Bool
otherwise = String
"016-020"

-- | Coarse bucket for the per-item byte-size distribution report.
bucketSize :: Int -> String
bucketSize :: Int -> String
bucketSize Int
n
  | Int
n Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
<= Int
50 = String
"001-050"
  | Int
n Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
<= Int
100 = String
"051-100"
  | Int
n Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
<= Int
250 = String
"101-250"
  | Bool
otherwise = String
"251-500"

{- | Recursively bloat all list fields in a @ScriptData@ value.

For @ScriptDataList items@, appends @numItems@ copies of
@ScriptDataBytes (BS.replicate itemSize 0x42)@ to the list.

Recursively processes @ScriptDataConstructor@ fields and nested lists.

For other @ScriptData@ variants (Map, Number, Bytes), returns
the value unchanged.
-}
bloatLists :: Int -> Int -> ScriptData -> ScriptData
bloatLists :: Int -> Int -> ScriptData -> ScriptData
bloatLists Int
numItems Int
itemSize = ScriptData -> ScriptData
go
 where
  largeItem :: ScriptData
largeItem = ByteString -> ScriptData
ScriptDataBytes (Int -> Word8 -> ByteString
BS.replicate Int
itemSize Word8
0x42)

  go :: ScriptData -> ScriptData
go (ScriptDataConstructor Integer
idx [ScriptData]
fields) =
    Integer -> [ScriptData] -> ScriptData
ScriptDataConstructor Integer
idx ((ScriptData -> ScriptData) -> [ScriptData] -> [ScriptData]
forall a b. (a -> b) -> [a] -> [b]
map ScriptData -> ScriptData
go [ScriptData]
fields)
  go (ScriptDataList [ScriptData]
items) =
    [ScriptData] -> ScriptData
ScriptDataList ((ScriptData -> ScriptData) -> [ScriptData] -> [ScriptData]
forall a b. (a -> b) -> [a] -> [b]
map ScriptData -> ScriptData
go [ScriptData]
items [ScriptData] -> [ScriptData] -> [ScriptData]
forall a. [a] -> [a] -> [a]
++ Int -> ScriptData -> [ScriptData]
forall a. Int -> a -> [a]
replicate Int
numItems ScriptData
largeItem)
  go (ScriptDataMap [(ScriptData, ScriptData)]
entries) =
    [(ScriptData, ScriptData)] -> ScriptData
ScriptDataMap [(ScriptData -> ScriptData
go ScriptData
k, ScriptData -> ScriptData
go ScriptData
v) | (ScriptData
k, ScriptData
v) <- [(ScriptData, ScriptData)]
entries]
  go ScriptData
other = ScriptData
other

-- | Check if a @ScriptData@ value contains any list fields.
containsList :: ScriptData -> Bool
containsList :: ScriptData -> Bool
containsList (ScriptDataConstructor Integer
_ [ScriptData]
fields) = (ScriptData -> Bool) -> [ScriptData] -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any ScriptData -> Bool
containsList [ScriptData]
fields
containsList (ScriptDataList [ScriptData]
_) = Bool
True
containsList (ScriptDataMap [(ScriptData, ScriptData)]
entries) = ((ScriptData, ScriptData) -> Bool)
-> [(ScriptData, ScriptData)] -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
any (\(ScriptData
k, ScriptData
v) -> ScriptData -> Bool
containsList ScriptData
k Bool -> Bool -> Bool
|| ScriptData -> Bool
containsList ScriptData
v) [(ScriptData, ScriptData)]
entries
containsList ScriptData
_ = Bool
False

-- ----------------------------------------------------------------------------
-- ByteString Inflation Attack
-- ----------------------------------------------------------------------------

{- | Default datum-byte-bloat attack. The inflation size is drawn per
transaction from a curated range, so QuickCheck explores the parameter
space and shrinks counterexamples toward the smallest triggering value.
-}
datumByteBloatAttack :: ThreatModel ()
datumByteBloatAttack :: ThreatModel ()
datumByteBloatAttack = Gen Int -> ThreatModel ()
datumByteBloatAttackWithGen ((Int, Int) -> Gen Int
forall a. Random a => (a, a) -> Gen a
choose (Int
1, Int
10000))

{- | Datum-byte-bloat attack with a fixed inflation size. Keep using this
for deterministic regression tests and golden seeds.
-}
datumByteBloatAttackWith :: Int -> ThreatModel ()
datumByteBloatAttackWith :: Int -> ThreatModel ()
datumByteBloatAttackWith = Gen Int -> ThreatModel ()
datumByteBloatAttackWithGen (Gen Int -> ThreatModel ())
-> (Int -> Gen Int) -> Int -> ThreatModel ()
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Int -> Gen Int
forall a. a -> Gen a
forall (f :: * -> *) a. Applicative f => a -> f a
pure

{- | Datum-byte-bloat attack parameterised by a generator for the inflation
size. This is the primitive the other two forms delegate to.
-}
datumByteBloatAttackWithGen :: Gen Int -> ThreatModel ()
datumByteBloatAttackWithGen :: Gen Int -> ThreatModel ()
datumByteBloatAttackWithGen Gen Int
gen =
  String -> ThreatModel () -> ThreatModel ()
forall a. String -> ThreatModel a -> ThreatModel a
Named String
"Datum Byte Bloat Attack" (ThreatModel () -> ThreatModel ())
-> ThreatModel () -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$ do
    Int
inflatedSize <- Gen Int -> (Int -> [Int]) -> ThreatModel Int
forall a. Show a => Gen a -> (a -> [a]) -> ThreatModel a
forAllTM Gen Int
gen Int -> [Int]
shrinkPositive

    -- Skip iterations where the draw is too small to be a meaningful attack.
    Bool -> ThreatModel ()
ensure (Int
inflatedSize Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
>= Int
1)

    (Output
target, ScriptData
originalDatum) <- ThreatModel (Output, ScriptData)
anyGuardedOutputWithInlineDatum

    let bloatedDatum :: ScriptData
bloatedDatum = Int -> ScriptData -> ScriptData
inflateFirstListItem Int
inflatedSize ScriptData
originalDatum

    -- Only proceed if something actually changed (datum has list with items to inflate)
    ThreatModel () -> ThreatModel ()
forall a. ThreatModel a -> ThreatModel a
threatPrecondition (ThreatModel () -> ThreatModel ())
-> ThreatModel () -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$ Bool -> ThreatModel ()
ensure (ScriptData
bloatedDatum ScriptData -> ScriptData -> Bool
forall a. Eq a => a -> a -> Bool
/= ScriptData
originalDatum)

    String -> ThreatModel ()
counterexampleTM (String -> ThreatModel ()) -> String -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$
      [String] -> String
paragraph
        [ String
"The transaction contains a script output with an inline datum."
        , String
"Testing if the first item in list fields can be inflated to"
        , Int -> String
forall a. Show a => a -> String
show Int
inflatedSize
        , String
"bytes while still passing validation."
        ]

    String -> ThreatModel ()
counterexampleTM (String -> ThreatModel ()) -> String -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$
      [String] -> String
paragraph
        [ String
"If this validates, the script doesn't limit ByteString field sizes,"
        , String
"enabling a datum bloat DoS attack where an attacker can add"
        , String
"a huge message/data item to bloat the datum beyond spendable limits."
        ]

    String -> [String] -> ThreatModel ()
tabulateTM String
"inflated bytes" [Int -> String
bucket Int
inflatedSize]

    TxModifier -> ThreatModel ()
shouldNotValidate (TxModifier -> ThreatModel ()) -> TxModifier -> ThreatModel ()
forall a b. (a -> b) -> a -> b
$ Output -> Datum -> TxModifier
forall t. IsInputOrOutput t => t -> Datum -> TxModifier
changeDatumOf Output
target (ScriptData -> Datum
toInlineDatum ScriptData
bloatedDatum)

{- | Shrink a positive integer toward 1 (the smallest meaningful value),
never reaching 0.
-}

-- | Coarse bucket for the inflation-size distribution report.
bucket :: Int -> String
bucket :: Int -> String
bucket Int
n
  | Int
n Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
<= Int
1000 = String
"0001-1000"
  | Int
n Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
<= Int
5000 = String
"1001-5000"
  | Int
n Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
<= Int
10000 = String
"5001-10000"
  | Bool
otherwise = String
"10000+"

{- | Replace all @ScriptDataBytes@ with inflated versions.

Preserves the first field of the top-level constructor (typically an
owner/address hash that must match exactly for validation).

Inflates all other @ScriptDataBytes@ found at any depth with a ByteString
of the given size filled with @0x42@ ('B').

For the tipjar use case, this inflates EVERY message in the list, which
changes the structure too much. For validators that do structural checks
like @list.push(old_msgs, new_msg) == new_msgs@, this will fail.

Use 'inflateFirstListItem' for a more targeted attack that only inflates
the first (newest) message in a list.
-}
inflateBytes :: Int -> ScriptData -> ScriptData
inflateBytes :: Int -> ScriptData -> ScriptData
inflateBytes Int
size = ScriptData -> ScriptData
goTop
 where
  largeBytes :: ByteString
largeBytes = Int -> Word8 -> ByteString
BS.replicate Int
size Word8
0x42

  -- At top level, preserve first field of constructor
  goTop :: ScriptData -> ScriptData
goTop (ScriptDataConstructor Integer
idx [ScriptData]
fields) =
    case [ScriptData]
fields of
      (ScriptData
first : [ScriptData]
rest) -> Integer -> [ScriptData] -> ScriptData
ScriptDataConstructor Integer
idx (ScriptData
first ScriptData -> [ScriptData] -> [ScriptData]
forall a. a -> [a] -> [a]
: (ScriptData -> ScriptData) -> [ScriptData] -> [ScriptData]
forall a b. (a -> b) -> [a] -> [b]
map ScriptData -> ScriptData
go [ScriptData]
rest)
      [] -> Integer -> [ScriptData] -> ScriptData
ScriptDataConstructor Integer
idx []
  goTop ScriptData
other = ScriptData -> ScriptData
go ScriptData
other

  -- Recursive case: inflate all ByteStrings
  go :: ScriptData -> ScriptData
go (ScriptDataConstructor Integer
idx [ScriptData]
fields) = Integer -> [ScriptData] -> ScriptData
ScriptDataConstructor Integer
idx ((ScriptData -> ScriptData) -> [ScriptData] -> [ScriptData]
forall a b. (a -> b) -> [a] -> [b]
map ScriptData -> ScriptData
go [ScriptData]
fields)
  go (ScriptDataList [ScriptData]
items) = [ScriptData] -> ScriptData
ScriptDataList ((ScriptData -> ScriptData) -> [ScriptData] -> [ScriptData]
forall a b. (a -> b) -> [a] -> [b]
map ScriptData -> ScriptData
go [ScriptData]
items)
  go (ScriptDataMap [(ScriptData, ScriptData)]
entries) = [(ScriptData, ScriptData)] -> ScriptData
ScriptDataMap [(ScriptData -> ScriptData
go ScriptData
k, ScriptData -> ScriptData
go ScriptData
v) | (ScriptData
k, ScriptData
v) <- [(ScriptData, ScriptData)]
entries]
  go (ScriptDataBytes ByteString
_) = ByteString -> ScriptData
ScriptDataBytes ByteString
largeBytes
  go ScriptData
other = ScriptData
other

{- | Inflate only the FIRST @ScriptDataBytes@ found in lists.

This is a more targeted attack for validators like tipjar that check:
@list.push(input_messages, new_msg) == output_messages@

The validator only cares that the NEW message (head of the list) was
correctly prepended. It doesn't check the SIZE of that message.

For a tipjar datum @Con0(owner_hash, [\"New\", \"Old1\", \"Old2\"])@:

* @owner_hash@ is preserved
* @\"New\"@ (first/newest message) gets inflated to 10KB
* @\"Old1\"@, @\"Old2\"@ are left unchanged (must match input)
* Result: @Con0(owner_hash, [<10KB>, \"Old1\", \"Old2\"])@

The validator check:
* Input: @[\"Old1\", \"Old2\"]@
* @list.push([\"Old1\", \"Old2\"], <10KB>) = [<10KB>, \"Old1\", \"Old2\"]@
* This equals the output! Vulnerability exploited.
-}
inflateFirstListItem :: Int -> ScriptData -> ScriptData
inflateFirstListItem :: Int -> ScriptData -> ScriptData
inflateFirstListItem Int
size = ScriptData -> ScriptData
goTop
 where
  largeBytes :: ByteString
largeBytes = Int -> Word8 -> ByteString
BS.replicate Int
size Word8
0x42

  -- At top level, preserve first field of constructor (owner hash)
  goTop :: ScriptData -> ScriptData
goTop (ScriptDataConstructor Integer
idx [ScriptData]
fields) =
    case [ScriptData]
fields of
      (ScriptData
first : [ScriptData]
rest) -> Integer -> [ScriptData] -> ScriptData
ScriptDataConstructor Integer
idx (ScriptData
first ScriptData -> [ScriptData] -> [ScriptData]
forall a. a -> [a] -> [a]
: (ScriptData -> ScriptData) -> [ScriptData] -> [ScriptData]
forall a b. (a -> b) -> [a] -> [b]
map ScriptData -> ScriptData
goList [ScriptData]
rest)
      [] -> Integer -> [ScriptData] -> ScriptData
ScriptDataConstructor Integer
idx []
  goTop ScriptData
other = ScriptData -> ScriptData
goList ScriptData
other

  -- Find lists and inflate only the first item
  goList :: ScriptData -> ScriptData
goList (ScriptDataConstructor Integer
idx [ScriptData]
fields) = Integer -> [ScriptData] -> ScriptData
ScriptDataConstructor Integer
idx ((ScriptData -> ScriptData) -> [ScriptData] -> [ScriptData]
forall a b. (a -> b) -> [a] -> [b]
map ScriptData -> ScriptData
goList [ScriptData]
fields)
  goList (ScriptDataList (ScriptData
firstItem : [ScriptData]
restItems)) =
    -- Inflate only the first item in the list, leave rest unchanged
    [ScriptData] -> ScriptData
ScriptDataList (ScriptData -> ScriptData
inflateItem ScriptData
firstItem ScriptData -> [ScriptData] -> [ScriptData]
forall a. a -> [a] -> [a]
: [ScriptData]
restItems)
  goList (ScriptDataList []) = [ScriptData] -> ScriptData
ScriptDataList []
  goList (ScriptDataMap [(ScriptData, ScriptData)]
entries) = [(ScriptData, ScriptData)] -> ScriptData
ScriptDataMap [(ScriptData -> ScriptData
goList ScriptData
k, ScriptData -> ScriptData
goList ScriptData
v) | (ScriptData
k, ScriptData
v) <- [(ScriptData, ScriptData)]
entries]
  goList ScriptData
other = ScriptData
other

  -- Inflate a single item (recursively inflate all ByteStrings in it)
  inflateItem :: ScriptData -> ScriptData
inflateItem (ScriptDataBytes ByteString
_) = ByteString -> ScriptData
ScriptDataBytes ByteString
largeBytes
  inflateItem (ScriptDataConstructor Integer
idx [ScriptData]
fields) =
    Integer -> [ScriptData] -> ScriptData
ScriptDataConstructor Integer
idx ((ScriptData -> ScriptData) -> [ScriptData] -> [ScriptData]
forall a b. (a -> b) -> [a] -> [b]
map ScriptData -> ScriptData
inflateItem [ScriptData]
fields)
  inflateItem (ScriptDataList [ScriptData]
items) = [ScriptData] -> ScriptData
ScriptDataList ((ScriptData -> ScriptData) -> [ScriptData] -> [ScriptData]
forall a b. (a -> b) -> [a] -> [b]
map ScriptData -> ScriptData
inflateItem [ScriptData]
items)
  inflateItem (ScriptDataMap [(ScriptData, ScriptData)]
entries) =
    [(ScriptData, ScriptData)] -> ScriptData
ScriptDataMap [(ScriptData -> ScriptData
inflateItem ScriptData
k, ScriptData -> ScriptData
inflateItem ScriptData
v) | (ScriptData
k, ScriptData
v) <- [(ScriptData, ScriptData)]
entries]
  inflateItem ScriptData
other = ScriptData
other