|
| 1 | +-- editorconfig-checker-disable-file |
| 2 | +{-# LANGUAGE OverloadedStrings #-} |
| 3 | + |
| 4 | +{-| Tests that different CEK machine evaluation paths produce identical ExBudget |
| 5 | +for the same term. This addresses the concern from |
| 6 | +https://github.com/IntersectMBO/plutus-private/issues/2084 that using |
| 7 | +'runCekNoEmit' with 'defaultCekParametersForVariant' might produce different |
| 8 | +budget numbers than using 'evaluateTerm' with a properly constructed |
| 9 | +'EvaluationContext'. |
| 10 | +
|
| 11 | +We compare three paths: |
| 12 | +
|
| 13 | + Path A ("direct"): 'defaultCekParametersForVariant' — uses noinline, reads the |
| 14 | + cost model from JSON directly, no applyCostModelParams round-trip. |
| 15 | +
|
| 16 | + Path B ("benchmark"): 'defaultCostModelParamsForVariant' fed to |
| 17 | + 'mkDynEvaluationContext' — uses inline, goes through |
| 18 | + applyCostModelParams (JSON round-trip). |
| 19 | +
|
| 20 | + Path C ("production"): V3.'mkEvaluationContext' with @[Int64]@ in ledger |
| 21 | + order — the exact code path the node uses. |
| 22 | +
|
| 23 | +Note: Path A uses @def :: CaserBuiltin@ while Paths B/C use a proper CaserBuiltin. |
| 24 | +The test terms avoid 'Case' on built-in types, so this difference does not |
| 25 | +affect the budget. -} |
| 26 | +module Spec.BudgetConsistency (tests) where |
| 27 | + |
| 28 | +import PlutusCore.Builtin (CaserBuiltin (..), caseBuiltin) |
| 29 | +import PlutusCore.Data (Data (..)) |
| 30 | +import PlutusCore.Default |
| 31 | +import PlutusCore.Evaluation.Machine.ExBudget (ExBudget (..)) |
| 32 | +import PlutusCore.Evaluation.Machine.ExBudgetingDefaults qualified as PLC |
| 33 | +import PlutusCore.MkPlc (mkConstant) |
| 34 | +import PlutusLedgerApi.Common |
| 35 | + ( EvaluationContext |
| 36 | + , MajorProtocolVersion |
| 37 | + , PlutusLedgerLanguage (..) |
| 38 | + , mkDynEvaluationContext |
| 39 | + , toMachineParameters |
| 40 | + ) |
| 41 | +import PlutusLedgerApi.Common.Versions (changPV) |
| 42 | +import PlutusLedgerApi.Test.V3.EvaluationContext qualified as V3Test |
| 43 | +import PlutusLedgerApi.V3.EvaluationContext qualified as V3 |
| 44 | +import PlutusPrelude (unsafeFromRight) |
| 45 | +import UntypedPlutusCore qualified as UPLC |
| 46 | +import UntypedPlutusCore.Evaluation.Machine.Cek qualified as Cek |
| 47 | + |
| 48 | +import Control.Monad.Writer (runWriterT) |
| 49 | +import Data.ByteString (ByteString) |
| 50 | +import Data.List (foldl') |
| 51 | +import Data.Text (Text) |
| 52 | +import Test.Tasty |
| 53 | +import Test.Tasty.HUnit |
| 54 | + |
| 55 | +-- | Semantics variant under test: VariantC corresponds to Chang era (PV9). |
| 56 | +semVar :: BuiltinSemanticsVariant DefaultFun |
| 57 | +semVar = DefaultFunSemanticsVariantC |
| 58 | + |
| 59 | +-- | Protocol version that maps to VariantC for PlutusV3. |
| 60 | +pv :: MajorProtocolVersion |
| 61 | +pv = changPV |
| 62 | + |
| 63 | +-- | Type alias for the terms we test with. |
| 64 | +type NTerm = UPLC.Term UPLC.NamedDeBruijn DefaultUni DefaultFun () |
| 65 | + |
| 66 | +-- ------------------------------------------------------------------- |
| 67 | +-- Kitchen sink term |
| 68 | +-- ------------------------------------------------------------------- |
| 69 | + |
| 70 | +{-| A single term exercising 55 builtins available at changPV (PV9, |
| 71 | +batch 1–4). Each builtin is called with minimal valid arguments; results |
| 72 | +are chained via 'seq_' so every call is fully evaluated by the CEK machine. |
| 73 | +
|
| 74 | +Builtins NOT covered (need complex test data or are in later batches): |
| 75 | + - VerifyEd25519Signature, VerifyEcdsaSecp256k1Signature, |
| 76 | + VerifySchnorrSecp256k1Signature (need valid signatures) |
| 77 | + - BLS12_381_* (17 builtins, need group elements) |
| 78 | + - Batch 5 bitwise ops (plominPV), Batch 6 (vanRossemPV) -} |
| 79 | +termKitchenSink :: NTerm |
| 80 | +termKitchenSink = foldr seq_ unit_ expressions |
| 81 | + where |
| 82 | + expressions = |
| 83 | + -- Integer arithmetic (10) |
| 84 | + [ ap (blt AddInteger) [int_ 99, int_ 2] |
| 85 | + , ap (blt SubtractInteger) [int_ 88, int_ 3] |
| 86 | + , ap (blt MultiplyInteger) [int_ 7, int_ 8] |
| 87 | + , ap (blt DivideInteger) [int_ 100, int_ 3] |
| 88 | + , ap (blt QuotientInteger) [int_ 100, int_ 3] |
| 89 | + , ap (blt RemainderInteger) [int_ 100, int_ 3] |
| 90 | + , ap (blt ModInteger) [int_ 100, int_ 3] |
| 91 | + , ap (blt EqualsInteger) [int_ 42, int_ 42] |
| 92 | + , ap (blt LessThanInteger) [int_ 1, int_ 2] |
| 93 | + , ap (blt LessThanEqualsInteger) [int_ 1, int_ 2] |
| 94 | + , -- ByteString operations (8) |
| 95 | + ap (blt AppendByteString) [bs_ "abc", bs_ "def"] |
| 96 | + , ap (blt ConsByteString) [int_ 65, bs_ "bc"] |
| 97 | + , ap (blt SliceByteString) [int_ 0, int_ 2, bs_ "hello"] |
| 98 | + , ap (blt LengthOfByteString) [bs_ "hello"] |
| 99 | + , ap (blt IndexByteString) [bs_ "hello", int_ 0] |
| 100 | + , ap (blt EqualsByteString) [bs_ "abc", bs_ "abc"] |
| 101 | + , ap (blt LessThanByteString) [bs_ "abc", bs_ "abd"] |
| 102 | + , ap (blt LessThanEqualsByteString) [bs_ "abc", bs_ "abd"] |
| 103 | + , -- Crypto hashes (5) |
| 104 | + ap (blt Sha2_256) [bs_ "test"] |
| 105 | + , ap (blt Sha3_256) [bs_ "test"] |
| 106 | + , ap (blt Blake2b_256) [bs_ "test"] |
| 107 | + , ap (blt Keccak_256) [bs_ "test"] |
| 108 | + , ap (blt Blake2b_224) [bs_ "test"] |
| 109 | + , -- String operations (4) |
| 110 | + ap (blt AppendString) [str_ "hello", str_ " world"] |
| 111 | + , ap (blt EqualsString) [str_ "abc", str_ "abc"] |
| 112 | + , ap (blt EncodeUtf8) [str_ "hello"] |
| 113 | + , ap (blt DecodeUtf8) [bs_ "hello"] |
| 114 | + , -- Data constructors and destructors (15) |
| 115 | + ap (blt IData) [int_ 42] |
| 116 | + , ap (blt BData) [bs_ "test"] |
| 117 | + , ap (blt UnIData) [dat_ (I 42)] |
| 118 | + , ap (blt UnBData) [dat_ (B "test")] |
| 119 | + , ap (blt ConstrData) [int_ 0, datList_ [I 1, I 2]] |
| 120 | + , ap (blt ListData) [datList_ [I 1, I 2]] |
| 121 | + , ap (blt MapData) [pairList_ [(I 1, B "x")]] |
| 122 | + , ap (blt UnConstrData) [dat_ (Constr 0 [I 1])] |
| 123 | + , ap (blt UnMapData) [dat_ (Map [(I 1, B "x")])] |
| 124 | + , ap (blt UnListData) [dat_ (List [I 1])] |
| 125 | + , ap (blt EqualsData) [dat_ (I 1), dat_ (I 1)] |
| 126 | + , ap (blt SerialiseData) [dat_ (I 42)] |
| 127 | + , ap (blt MkPairData) [dat_ (I 1), dat_ (B "x")] |
| 128 | + , ap (blt MkNilData) [unit_] |
| 129 | + , ap (blt MkNilPairData) [unit_] |
| 130 | + , -- Conversion (2) |
| 131 | + ap (blt IntegerToByteString) [bool_ True, int_ 4, int_ 42] |
| 132 | + , ap (blt ByteStringToInteger) [bool_ True, bs_ "\x00\x2a"] |
| 133 | + , -- 1 Force: polymorphic builtins (8) |
| 134 | + ap (force (blt IfThenElse)) [bool_ True, int_ 1, int_ 2] |
| 135 | + , ap (force (blt HeadList)) [datList_ [I 1, I 2]] |
| 136 | + , ap (force (blt TailList)) [datList_ [I 1, I 2]] |
| 137 | + , ap (force (blt NullList)) [datList_ []] |
| 138 | + , ap (force (blt MkCons)) [dat_ (I 1), datList_ []] |
| 139 | + , ap (force (blt Trace)) [str_ "x", int_ 1] |
| 140 | + , ap (force (blt ChooseUnit)) [unit_, int_ 1] |
| 141 | + , ap |
| 142 | + (force (blt ChooseData)) |
| 143 | + [dat_ (I 42), int_ 1, int_ 2, int_ 3, int_ 4, int_ 5] |
| 144 | + , -- 2 Forces: polymorphic builtins (3) |
| 145 | + ap (forceN 2 (blt FstPair)) [datPair_ (I 1, B "x")] |
| 146 | + , ap (forceN 2 (blt SndPair)) [datPair_ (I 1, B "x")] |
| 147 | + , ap (forceN 2 (blt ChooseList)) [datList_ [], int_ 1, int_ 2] |
| 148 | + ] |
| 149 | + |
| 150 | +-- ------------------------------------------------------------------- |
| 151 | +-- Evaluation paths |
| 152 | +-- ------------------------------------------------------------------- |
| 153 | + |
| 154 | +-- | Extract ExBudget from a counting-mode CekReport. |
| 155 | +extractBudget |
| 156 | + :: Cek.CekReport Cek.CountingSt UPLC.NamedDeBruijn DefaultUni DefaultFun -> ExBudget |
| 157 | +extractBudget (Cek.CekReport _result (Cek.CountingSt budget) _logs) = budget |
| 158 | + |
| 159 | +{-| Path A: Direct CEK via 'defaultCekParametersForVariant'. |
| 160 | +This is how the bug report (issue 2084) and some tests run scripts. |
| 161 | +Uses noinline, no JSON round-trip of cost model params. -} |
| 162 | +budgetDirect :: NTerm -> ExBudget |
| 163 | +budgetDirect = |
| 164 | + extractBudget |
| 165 | + . Cek.runCekDeBruijn |
| 166 | + (PLC.defaultCekParametersForVariant semVar) |
| 167 | + Cek.counting |
| 168 | + Cek.noEmitter |
| 169 | + |
| 170 | +{-| Path B: Via 'mkDynEvaluationContext' + 'defaultCostModelParamsForVariant'. |
| 171 | +This is how validation benchmarks build EvaluationContext (mkEvalCtx). |
| 172 | +Uses inline, JSON round-trip via applyCostModelParams. -} |
| 173 | +benchmarkEvalCtx :: EvaluationContext |
| 174 | +benchmarkEvalCtx = |
| 175 | + case PLC.defaultCostModelParamsForVariant semVar of |
| 176 | + Just p -> |
| 177 | + either (error . show) id $ |
| 178 | + mkDynEvaluationContext |
| 179 | + PlutusV3 |
| 180 | + (\_ -> CaserBuiltin caseBuiltin) |
| 181 | + [semVar] |
| 182 | + (const semVar) |
| 183 | + p |
| 184 | + Nothing -> error "defaultCostModelParamsForVariant: Nothing" |
| 185 | + |
| 186 | +budgetBenchmark :: NTerm -> ExBudget |
| 187 | +budgetBenchmark = |
| 188 | + extractBudget |
| 189 | + . Cek.runCekDeBruijn |
| 190 | + (toMachineParameters pv benchmarkEvalCtx) |
| 191 | + Cek.counting |
| 192 | + Cek.noEmitter |
| 193 | + |
| 194 | +{-| Path C: Via V3.'mkEvaluationContext' + @[Int64]@ in ledger order. |
| 195 | +This is how the Cardano node builds EvaluationContext. -} |
| 196 | +productionEvalCtx :: EvaluationContext |
| 197 | +productionEvalCtx = |
| 198 | + fst . unsafeFromRight . runWriterT $ |
| 199 | + V3.mkEvaluationContext (map snd V3Test.costModelParamsForTesting) |
| 200 | + |
| 201 | +budgetProduction :: NTerm -> ExBudget |
| 202 | +budgetProduction = |
| 203 | + extractBudget |
| 204 | + . Cek.runCekDeBruijn |
| 205 | + (toMachineParameters pv productionEvalCtx) |
| 206 | + Cek.counting |
| 207 | + Cek.noEmitter |
| 208 | + |
| 209 | +-- ------------------------------------------------------------------- |
| 210 | +-- Tests |
| 211 | +-- ------------------------------------------------------------------- |
| 212 | + |
| 213 | +tests :: TestTree |
| 214 | +tests = |
| 215 | + testGroup |
| 216 | + "Budget consistency across evaluation paths (VariantC)" |
| 217 | + [ testCase "direct (A) == benchmark (B)" $ |
| 218 | + budgetDirect termKitchenSink @?= budgetBenchmark termKitchenSink |
| 219 | + , testCase "benchmark (B) == production (C)" $ |
| 220 | + budgetBenchmark termKitchenSink @?= budgetProduction termKitchenSink |
| 221 | + , testCase "direct (A) == production (C)" $ |
| 222 | + budgetDirect termKitchenSink @?= budgetProduction termKitchenSink |
| 223 | + , testCase "budgets are non-zero" $ do |
| 224 | + let b = budgetDirect termKitchenSink |
| 225 | + assertBool "CPU should be positive" (exBudgetCPU b > 0) |
| 226 | + assertBool "Memory should be positive" (exBudgetMemory b > 0) |
| 227 | + , testCase "print all budgets for inspection" $ do |
| 228 | + putStrLn "" |
| 229 | + putStrLn $ " Path A (direct): " ++ show (budgetDirect termKitchenSink) |
| 230 | + putStrLn $ " Path B (benchmark): " ++ show (budgetBenchmark termKitchenSink) |
| 231 | + putStrLn $ " Path C (production): " ++ show (budgetProduction termKitchenSink) |
| 232 | + ] |
| 233 | + |
| 234 | +-- ------------------------------------------------------------------- |
| 235 | +-- UPLC smart constructors |
| 236 | +-- ------------------------------------------------------------------- |
| 237 | + |
| 238 | +app :: NTerm -> NTerm -> NTerm |
| 239 | +app = UPLC.Apply () |
| 240 | + |
| 241 | +-- | Iterated application: @ap f [a, b, c] = f a b c@. |
| 242 | +ap :: NTerm -> [NTerm] -> NTerm |
| 243 | +ap = foldl' app |
| 244 | + |
| 245 | +force :: NTerm -> NTerm |
| 246 | +force = UPLC.Force () |
| 247 | + |
| 248 | +-- | Force @n@ times (for polymorphic builtins with multiple type variables). |
| 249 | +forceN :: Int -> NTerm -> NTerm |
| 250 | +forceN n t = iterate force t !! n |
| 251 | + |
| 252 | +blt :: DefaultFun -> NTerm |
| 253 | +blt = UPLC.Builtin () |
| 254 | + |
| 255 | +-- | Lambda abstraction (unused binder for sequencing). |
| 256 | +lam :: NTerm -> NTerm |
| 257 | +lam = UPLC.LamAbs () (UPLC.NamedDeBruijn "_" 0) |
| 258 | + |
| 259 | +-- | Evaluate @a@, discard its result, return @b@. |
| 260 | +seq_ :: NTerm -> NTerm -> NTerm |
| 261 | +seq_ a b = app (lam b) a |
| 262 | + |
| 263 | +-- Constants |
| 264 | +int_ :: Integer -> NTerm |
| 265 | +int_ = mkConstant () |
| 266 | + |
| 267 | +bs_ :: ByteString -> NTerm |
| 268 | +bs_ = mkConstant () |
| 269 | + |
| 270 | +str_ :: Text -> NTerm |
| 271 | +str_ = mkConstant () |
| 272 | + |
| 273 | +bool_ :: Bool -> NTerm |
| 274 | +bool_ = mkConstant () |
| 275 | + |
| 276 | +unit_ :: NTerm |
| 277 | +unit_ = mkConstant () () |
| 278 | + |
| 279 | +dat_ :: Data -> NTerm |
| 280 | +dat_ = mkConstant () |
| 281 | + |
| 282 | +datList_ :: [Data] -> NTerm |
| 283 | +datList_ = mkConstant () |
| 284 | + |
| 285 | +datPair_ :: (Data, Data) -> NTerm |
| 286 | +datPair_ = mkConstant () |
| 287 | + |
| 288 | +pairList_ :: [(Data, Data)] -> NTerm |
| 289 | +pairList_ = mkConstant () |
0 commit comments