Skip to content

Commit f6d33d7

Browse files
committed
Add budget consistency test across CEK evaluation paths (VariantC)
Tests that three CEK evaluation paths produce identical ExBudget for the same term, using a kitchen-sink UPLC term that exercises 55 builtins available at changPV (PV9, batch 1-4). The three paths compared: A) defaultCekParametersForVariant (noinline, no JSON round-trip) B) mkDynEvaluationContext (inline, JSON round-trip) C) V3.mkEvaluationContext with [Int64] (production/node path)
1 parent b6d66c6 commit f6d33d7

File tree

3 files changed

+292
-0
lines changed

3 files changed

+292
-0
lines changed

plutus-ledger-api/plutus-ledger-api.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,7 @@ test-suite plutus-ledger-api-test
206206
hs-source-dirs: test
207207
ghc-options: -threaded -rtsopts -with-rtsopts=-N
208208
other-modules:
209+
Spec.BudgetConsistency
209210
Spec.CBOR.DeserialiseFailureInfo
210211
Spec.ContextDecoding
211212
Spec.CostModelParams

plutus-ledger-api/test/Spec.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import PlutusLedgerApi.Test.V3.EvaluationContext qualified as V3
99
import PlutusLedgerApi.V1 as V1
1010
import PlutusLedgerApi.V3 as V3
1111
import PlutusPrelude
12+
import Spec.BudgetConsistency qualified
1213
import Spec.CBOR.DeserialiseFailureInfo qualified
1314
import Spec.ContextDecoding qualified
1415
import Spec.CostModelParams qualified
@@ -227,4 +228,5 @@ tests =
227228
]
228229
, Spec.Eq.Golden.eqGoldenTests
229230
, Spec.Ord.Golden.ordGoldenTests
231+
, Spec.BudgetConsistency.tests
230232
]
Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
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

Comments
 (0)