Skip to content

Commit 909298e

Browse files
committed
feat: worst-case key generation for benchmarks
1 parent 65eeca2 commit 909298e

File tree

1 file changed

+72
-15
lines changed
  • plutus-core/cost-model/budgeting-bench/Benchmarks

1 file changed

+72
-15
lines changed

plutus-core/cost-model/budgeting-bench/Benchmarks/Values.hs

Lines changed: 72 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{-# LANGUAGE BlockArguments #-}
22
{-# LANGUAGE ImportQualifiedPost #-}
3+
{-# LANGUAGE LambdaCase #-}
34
{-# LANGUAGE NumericUnderscores #-}
45
{-# LANGUAGE TupleSections #-}
56

@@ -10,14 +11,19 @@ import Prelude
1011
import Common
1112
import Control.Monad (replicateM)
1213
import Criterion.Main (Benchmark)
14+
import Data.Bits (shiftR, (.&.))
1315
import Data.ByteString (ByteString)
16+
import Data.ByteString qualified as BS
1417
import Data.Int (Int64)
18+
import Data.Word (Word8)
19+
import GHC.Stack (HasCallStack)
1520
import PlutusCore (DefaultFun (LookupCoin, UnValueData, ValueContains, ValueData))
21+
import PlutusCore.Builtin (BuiltinResult (BuiltinFailure, BuiltinSuccess, BuiltinSuccessWithLogs))
1622
import PlutusCore.Evaluation.Machine.ExMemoryUsage (ValueLogOuterOrMaxInner (..),
1723
ValueTotalSize (..))
1824
import PlutusCore.Value (K, Value)
1925
import PlutusCore.Value qualified as Value
20-
import System.Random.Stateful (StatefulGen, StdGen, runStateGen_, uniformByteStringM, uniformRM)
26+
import System.Random.Stateful (StatefulGen, StdGen, runStateGen_, uniformRM)
2127

2228
----------------------------------------------------------------------------------------------------
2329
-- Benchmarks --------------------------------------------------------------------------------------
@@ -90,10 +96,11 @@ valueContainsArgs gen = runStateGen_ gen \g -> replicateM 100 do
9096

9197
-- Group selected entries back by policy
9298
let contained =
93-
Value.fromList
94-
[ (policyId, [(tokenName, quantity)])
95-
| (policyId, tokenName, quantity) <- selectedEntries
96-
]
99+
unsafeFromBuiltinResult $
100+
Value.fromList
101+
[ (policyId, [(tokenName, quantity)])
102+
| (policyId, tokenName, quantity) <- selectedEntries
103+
]
97104

98105
pure (container, contained)
99106

@@ -162,25 +169,75 @@ generateConstrainedValue numPolicies tokensPerPolicy g = do
162169
policyIds <- replicateM numPolicies (generateKey g)
163170
tokenNames <- replicateM tokensPerPolicy (generateKey g)
164171

165-
let quantity :: Integer
166-
quantity = fromIntegral (maxBound :: Int64)
172+
let
173+
qty :: Value.Quantity
174+
qty = case Value.quantity (fromIntegral (maxBound :: Int64)) of
175+
Just q -> q
176+
Nothing -> error "generateConstrainedValue: Int64 maxBound should be valid Quantity"
167177

168-
nestedMap :: [(K, [(K, Integer)])]
169-
nestedMap = (,(,quantity) <$> tokenNames) <$> policyIds
178+
nestedMap :: [(K, [(K, Value.Quantity)])]
179+
nestedMap = (,(,qty) <$> tokenNames) <$> policyIds
170180

171-
pure $ Value.fromList nestedMap
181+
pure $ unsafeFromBuiltinResult $ Value.fromList nestedMap
172182

173183
----------------------------------------------------------------------------------------------------
174184
-- Other Generators --------------------------------------------------------------------------------
175185

176-
-- | Generate random key (always maxKeyLen bytes for Cardano compliance)
186+
{-| Generate a worst-case key for benchmarking ByteString comparisons.
187+
188+
ByteString comparison is lexicographic and short-circuits on the first differing byte.
189+
Random keys typically differ in the first 1-2 bytes, making comparisons artificially cheap.
190+
191+
For accurate worst-case costing, we generate keys with a common prefix (0xFF bytes) that
192+
differ only in the last bytes. This forces full-length comparisons during Map lookups,
193+
providing conservative cost estimates for adversarial on-chain scenarios.
194+
195+
We use a random integer to ensure uniqueness while maintaining the worst-case prefix pattern.
196+
-}
177197
generateKey :: (StatefulGen g m) => g -> m K
178198
generateKey g = do
179-
bs <- uniformByteStringM Value.maxKeyLen g
180-
case Value.k bs of
199+
-- Generate a random integer for uniqueness
200+
n <- uniformRM (0, maxBound :: Int) g
201+
case Value.k (mkWorstCaseKey n) of
181202
Just key -> pure key
182203
Nothing -> error "Internal error: maxKeyLen key should always be valid"
183204

184-
-- | Generate random key as ByteString (for lookup arguments)
205+
{-| Generate worst-case key as ByteString (for lookup arguments).
206+
207+
Like generateKey, but returns ByteString directly for use in lookup operations.
208+
Uses BS.copy to ensure physical distinctness for worst-case equality testing.
209+
-}
185210
generateKeyBS :: (StatefulGen g m) => g -> m ByteString
186-
generateKeyBS = uniformByteStringM Value.maxKeyLen
211+
generateKeyBS g = BS.copy . mkWorstCaseKey <$> uniformRM (0, maxBound :: Int) g
212+
213+
{-| Helper: Create a worst-case ByteString key from an integer
214+
The key has maxKeyLen-4 bytes of 0xFF prefix, followed by 4 bytes encoding the integer
215+
-}
216+
mkWorstCaseKey :: Int -> ByteString
217+
mkWorstCaseKey n =
218+
let prefixLen = Value.maxKeyLen - 4
219+
prefix = BS.replicate prefixLen (0xFF :: Word8)
220+
-- Encode the integer in big-endian format (last 4 bytes)
221+
b0 = fromIntegral $ (n `shiftR` 24) .&. 0xFF
222+
b1 = fromIntegral $ (n `shiftR` 16) .&. 0xFF
223+
b2 = fromIntegral $ (n `shiftR` 8) .&. 0xFF
224+
b3 = fromIntegral $ n .&. 0xFF
225+
suffix = BS.pack [b0, b1, b2, b3]
226+
in prefix <> suffix
227+
228+
----------------------------------------------------------------------------------------------------
229+
-- Helper Functions --------------------------------------------------------------------------------
230+
231+
{-| Extract value from BuiltinResult for benchmark data generation.
232+
233+
This function is intended for use in test and benchmark code where BuiltinResult
234+
failures indicate bugs in the generator code, not runtime errors.
235+
236+
Errors if BuiltinResult indicates failure (should never happen with valid inputs).
237+
The call stack will provide context about where the error occurred.
238+
-}
239+
unsafeFromBuiltinResult :: (HasCallStack) => BuiltinResult a -> a
240+
unsafeFromBuiltinResult = \case
241+
BuiltinSuccess x -> x
242+
BuiltinSuccessWithLogs _ x -> x
243+
BuiltinFailure _ err -> error $ "BuiltinResult failed: " <> show err

0 commit comments

Comments
 (0)