From 100554e07fb4b2bd304e5f238e5f443bbe468e43 Mon Sep 17 00:00:00 2001 From: Michael Karg Date: Fri, 3 Jul 2026 09:30:18 +0200 Subject: [PATCH 1/4] [WIP] new V3 and PV11 profiles --- .../Profile/Builtin/Plutuscall.hs | 20 ++++-- .../Benchmarking/Profile/Primitives.hs | 4 ++ .../Benchmarking/Profile/Vocabulary.hs | 46 ++++++++++++- .../plutus-scripts-bench.cabal | 3 +- .../src/Cardano/Benchmarking/PlutusScripts.hs | 2 + .../PlutusScripts/MultiScalarMulG1.hs | 69 +++++++++++++++++++ bench/tx-generator/app/calibrate-script.hs | 22 +++--- .../data/MultiScalarMulG1.redeemer.json | 31 +++++++++ .../scripts-fallback/MultiScalarMulG1.plutus | 5 ++ .../Cardano/TxGenerator/Calibrate/Utils.hs | 7 ++ bench/tx-generator/tx-generator.cabal | 2 +- wb_profiles.mk | 2 +- 12 files changed, 191 insertions(+), 22 deletions(-) create mode 100644 bench/plutus-scripts-bench/src/Cardano/Benchmarking/PlutusScripts/MultiScalarMulG1.hs create mode 100644 bench/tx-generator/data/MultiScalarMulG1.redeemer.json create mode 100644 bench/tx-generator/scripts-fallback/MultiScalarMulG1.plutus diff --git a/bench/cardano-profile/src/Cardano/Benchmarking/Profile/Builtin/Plutuscall.hs b/bench/cardano-profile/src/Cardano/Benchmarking/Profile/Builtin/Plutuscall.hs index ada6e81b6a1..311240122de 100644 --- a/bench/cardano-profile/src/Cardano/Benchmarking/Profile/Builtin/Plutuscall.hs +++ b/bench/cardano-profile/src/Cardano/Benchmarking/Profile/Builtin/Plutuscall.hs @@ -44,13 +44,16 @@ profilesPlutuscall = . P.analysisStandard . P.desc "Small dataset, honest 15 epochs duration" - loop = plutusCall & V.plutusTypeLoop . V.plutusDoubleSaturation . P.analysisSizeModerate . P.analysisEpoch3Plus - ecdsa = plutusCall & V.plutusTypeECDSA . V.plutusDoubleSaturation . P.analysisSizeModerate . P.analysisEpoch3Plus - schnorr = plutusCall & V.plutusTypeSchnorr . V.plutusDoubleSaturation . P.analysisSizeModerate . P.analysisEpoch3Plus + loop = plutusCall & V.plutusTypeLoop . V.plutusDoubleSaturation . P.analysisSizeModerate . P.analysisEpoch3Plus + ecdsa = plutusCall & V.plutusTypeECDSA . V.plutusDoubleSaturation . P.analysisSizeModerate . P.analysisEpoch3Plus + schnorr = plutusCall & V.plutusTypeSchnorr . V.plutusDoubleSaturation . P.analysisSizeModerate . P.analysisEpoch3Plus + schnorrV3 = plutusCall & V.plutusTypeSchnorrV3 . V.plutusDoubleSaturation . P.analysisSizeSmall . P.analysisEpoch3Plus + mscalmul = plutusCall & V.plutusTypeMultScalarMultG1 . V.plutusDoubleSaturation . P.analysisSizeSmall . P.analysisEpoch3Plus + ripemdVolt4tx = plutusCall & V.plutusTypeRIPEMD_4tx . V.plutusDoubleSaturation . P.analysisSizeSmall . P.analysisEpoch3Plus - loopVolt = plutusCall & V.plutusTypeLoop . V.plutusDoublePlusSaturation . P.analysisSizeSmall - blstVolt = plutusCall & V.plutusTypeBLST . V.plutusDoublePlusSaturation . P.analysisSizeModerate2 - ripemdVolt = plutusCall & V.plutusTypeRIPEMD . V.plutusDoublePlusSaturation . P.analysisSizeSmall + loopVolt = plutusCall & V.plutusTypeLoop . V.plutusDoublePlusSaturation . P.analysisSizeSmall + blstVolt = plutusCall & V.plutusTypeBLST . V.plutusDoublePlusSaturation . P.analysisSizeModerate2 + ripemdVolt = plutusCall & V.plutusTypeRIPEMD . V.plutusDoublePlusSaturation . P.analysisSizeSmall postPlomin = V.genesisVariantVoltaire in [ @@ -65,5 +68,8 @@ profilesPlutuscall = , blstVolt & P.name "plutuscall-volt-blst" . postPlomin , ripemdVolt & P.name "plutuscall-volt-ripemd" . postPlomin - , ripemdVolt & P.name "plutuscall-voltv11-ripemd" . postPlomin . P.v11Preview + -- PlutusV3 benchmarks, targeting PV11 + , ripemdVolt4tx & P.name "plutuscall-voltv11-ripemd" . postPlomin . P.v11Preview + , schnorrV3 & P.name "plutuscall-voltv11-schnorrv3" . postPlomin . P.v11Preview + , mscalmul & P.name "plutuscall-voltv11-mscalmul" . postPlomin . P.v11Preview ] diff --git a/bench/cardano-profile/src/Cardano/Benchmarking/Profile/Primitives.hs b/bench/cardano-profile/src/Cardano/Benchmarking/Profile/Primitives.hs index c1bb327f986..1f9d9b901b0 100644 --- a/bench/cardano-profile/src/Cardano/Benchmarking/Profile/Primitives.hs +++ b/bench/cardano-profile/src/Cardano/Benchmarking/Profile/Primitives.hs @@ -894,15 +894,19 @@ analysisFiltersAppend str = analysis else a {Types.filters = Types.filters a ++ [str]} ) +-- for 88k blocks: from 2.6k to 8.8k analysisSizeSmall :: HasCallStack => Types.Profile -> Types.Profile analysisSizeSmall = analysisFiltersAppend "size-small" +-- for 88k blocks: from 7k to 9.8k analysisSizeModerate :: HasCallStack => Types.Profile -> Types.Profile analysisSizeModerate = analysisFiltersAppend "size-moderate" +-- for 88k blocks: from 6.7k to 9.2k analysisSizeModerate2 :: HasCallStack => Types.Profile -> Types.Profile analysisSizeModerate2 = analysisFiltersAppend "size-moderate-2" +-- for 88k blocks: larger than 79.2k analysisSizeFull :: HasCallStack => Types.Profile -> Types.Profile analysisSizeFull = analysisFiltersAppend "size-full" diff --git a/bench/cardano-profile/src/Cardano/Benchmarking/Profile/Vocabulary.hs b/bench/cardano-profile/src/Cardano/Benchmarking/Profile/Vocabulary.hs index b8f11c598fb..3bfeb4f21fd 100644 --- a/bench/cardano-profile/src/Cardano/Benchmarking/Profile/Vocabulary.hs +++ b/bench/cardano-profile/src/Cardano/Benchmarking/Profile/Vocabulary.hs @@ -20,7 +20,8 @@ module Cardano.Benchmarking.Profile.Vocabulary ( , plutusSaturation, plutusDoubleSaturation, plutusDoublePlusSaturation , plutusTypeLoop, plutusTypeLoopV3, plutusTypeLoop2024, plutusTypeECDSA, plutusTypeSchnorr -, plutusTypeBLST, plutusTypeRIPEMD +, plutusTypeBLST, plutusTypeRIPEMD, plutusTypeRIPEMD_4tx, plutusTypeMultScalarMultG1 +, plutusTypeSchnorrV3 ) where -------------------------------------------------------------------------------- @@ -250,3 +251,46 @@ plutusTypeRIPEMD = , KeyMap.fromList [("bytes", Aeson.String "5a56da88e6fd8419181dec4d3dd6997bab953d2f")] ] . P.txFee 940000 + + -- the bytes content is arbitrary, but should be of the same size as RIPEMD-160 output, i.e. 160 bits +plutusTypeRIPEMD_4tx :: Types.Profile -> Types.Profile +plutusTypeRIPEMD_4tx = + P.plutusType "LimitTxPerBlock_4" . P.plutusScript "Ripemd160" + . P.redeemerFields [ + KeyMap.fromList [("int", Aeson.Number 1000000.0)] + , KeyMap.fromList [("bytes", Aeson.String "5a56da88e6fd8419181dec4d3dd6997bab953d2f")] + ] + . P.txFee 915500 + +-- there must always be 1 scalar value less than there are points. +-- for the rationale about the redeemer structure, see note in +-- the script's source: bench/plutus-scripts-bench/src/Cardano/Benchmarking/PlutusScripts/MultiScalarMulG1.hs +plutusTypeMultScalarMultG1 :: Types.Profile -> Types.Profile +plutusTypeMultScalarMultG1 = + P.plutusType "LimitTxPerBlock_4" . P.plutusScript "MultiScalarMulG1" + . P.redeemerFields [ + KeyMap.fromList [("int", Aeson.Number 1000000.0)] + , KeyMap.fromList [("list", Aeson.Array $ Vector.fromList [ + Aeson.Object $ KeyMap.fromList [("int", Aeson.Number 871345174532.0)] + , Aeson.Object $ KeyMap.fromList [("int", Aeson.Number 987654321.0)] + ]) + ] + , KeyMap.fromList [("list", Aeson.Array $ Vector.fromList [ + Aeson.Object $ KeyMap.fromList [("bytes", Aeson.String "ab5d1d67b495361c3297c721cf3c9dc510fc5055bdf92fefc1e67d91a00a765520150428eb4c2fb01902d41d5ca62d85")] + , Aeson.Object $ KeyMap.fromList [("bytes", Aeson.String "88c7e388ee58f1db9a24d7098b01d13634298bebf2d159254975bd450cb0d287fcc622eb71edde8b469a8513551baf1f")] + , Aeson.Object $ KeyMap.fromList [("bytes", Aeson.String "95e4262f370607b2765312297461a04a3fb1fbb83e998a8e7d0f237017df86c5fb01f85101177c3dd41586df05b038cb")] + ]) + ] + ] + . P.txFee 572000 + +plutusTypeSchnorrV3 :: Types.Profile -> Types.Profile +plutusTypeSchnorrV3 = + P.plutusType "LimitTxPerBlock_4" . P.plutusScript "SchnorrSecp256k1LoopV3" + . P.redeemerFields [ + KeyMap.fromList [("int", Aeson.Number 1000000.0)] + , KeyMap.fromList [("bytes", Aeson.String "599de3e582e2a3779208a210dfeae8f330b9af00a47a7fb22e9bb8ef596f301b")] + , KeyMap.fromList [("bytes", Aeson.String "30303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030")] + , KeyMap.fromList [("bytes", Aeson.String "5a56da88e6fd8419181dec4d3dd6997bab953d2fc71ab65e23cfc9e7e3d1a310613454a60f6703819a39fdac2a410a094442afd1fc083354443e8d8bb4461a9b")] + ] + . P.txFee 586000 \ No newline at end of file diff --git a/bench/plutus-scripts-bench/plutus-scripts-bench.cabal b/bench/plutus-scripts-bench/plutus-scripts-bench.cabal index 410aab20bcd..9d6570e32ea 100644 --- a/bench/plutus-scripts-bench/plutus-scripts-bench.cabal +++ b/bench/plutus-scripts-bench/plutus-scripts-bench.cabal @@ -1,6 +1,6 @@ cabal-version: 3.0 name: plutus-scripts-bench -version: 1.0.4.3 +version: 1.0.5.0 synopsis: Plutus scripts used for benchmarking description: Plutus scripts used for benchmarking. category: Cardano, @@ -75,6 +75,7 @@ library Cardano.Benchmarking.PlutusScripts.HashOntoG2AndAdd Cardano.Benchmarking.PlutusScripts.Loop2024 Cardano.Benchmarking.PlutusScripts.LoopV3 + Cardano.Benchmarking.PlutusScripts.MultiScalarMulG1 Cardano.Benchmarking.PlutusScripts.Ripemd160 Cardano.Benchmarking.PlutusScripts.SchnorrSecp256k1Loop Cardano.Benchmarking.PlutusScripts.SchnorrSecp256k1LoopV3 diff --git a/bench/plutus-scripts-bench/src/Cardano/Benchmarking/PlutusScripts.hs b/bench/plutus-scripts-bench/src/Cardano/Benchmarking/PlutusScripts.hs index 4a47da843fa..3c3b7340a4d 100644 --- a/bench/plutus-scripts-bench/src/Cardano/Benchmarking/PlutusScripts.hs +++ b/bench/plutus-scripts-bench/src/Cardano/Benchmarking/PlutusScripts.hs @@ -29,6 +29,7 @@ import qualified Cardano.Benchmarking.PlutusScripts.EcdsaSecp256k1LoopV3 as ECDS import qualified Cardano.Benchmarking.PlutusScripts.HashOntoG2AndAdd as HashG2Add import qualified Cardano.Benchmarking.PlutusScripts.Loop2024 as Loop2024 import qualified Cardano.Benchmarking.PlutusScripts.LoopV3 as LoopV3 +import qualified Cardano.Benchmarking.PlutusScripts.MultiScalarMulG1 as MultiScalarMulG1 import qualified Cardano.Benchmarking.PlutusScripts.Ripemd160 as Ripemd160 import qualified Cardano.Benchmarking.PlutusScripts.SchnorrSecp256k1Loop as Schnorr import qualified Cardano.Benchmarking.PlutusScripts.SchnorrSecp256k1LoopV3 as SchnorrV3 @@ -45,6 +46,7 @@ getAllScripts = , HashG2Add.script , Loop2024.script , LoopV3.script + , MultiScalarMulG1.script , Ripemd160.script , Schnorr.script , SchnorrV3.script diff --git a/bench/plutus-scripts-bench/src/Cardano/Benchmarking/PlutusScripts/MultiScalarMulG1.hs b/bench/plutus-scripts-bench/src/Cardano/Benchmarking/PlutusScripts/MultiScalarMulG1.hs new file mode 100644 index 00000000000..27f3d2d8c20 --- /dev/null +++ b/bench/plutus-scripts-bench/src/Cardano/Benchmarking/PlutusScripts/MultiScalarMulG1.hs @@ -0,0 +1,69 @@ +{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE TemplateHaskell #-} + +module Cardano.Benchmarking.PlutusScripts.MultiScalarMulG1 (script) where + +import Cardano.Api (PlutusScriptVersion (PlutusScriptV3)) +import Cardano.Benchmarking.ScriptAPI (PlutusBenchScript, mkPlutusBenchScript) +import Language.Haskell.TH.Syntax (Exp (LitE), Lit (StringL), Loc (loc_module), qLocation) +import PlutusLedgerApi.Common (serialiseCompiledCode) +import qualified PlutusLedgerApi.V3 as PlutusV3 +import qualified PlutusTx (compile) +import qualified PlutusTx.Builtins.Internal as BI (BuiltinList, head, snd, tail, unitval, + unsafeDataAsConstr) +import PlutusTx.Builtins as BI (bls12_381_G1_multiScalarMul) +import PlutusTx.Prelude as Tx hiding (Semigroup (..), (.), (<$>)) +import Prelude as Haskell ((.), (<$>)) + + +script :: PlutusBenchScript +script = mkPlutusBenchScript + $(LitE . StringL . loc_module <$> qLocation) + PlutusScriptV3 + (serialiseCompiledCode $$(PlutusTx.compile [|| mkValidator ||])) + +{-# INLINABLE mkValidator #-} +mkValidator :: BuiltinData -> BuiltinUnit +mkValidator arg = + if red_n < 1000000 -- large number ensures same bitsize for all counter values + then traceError "redeemer is < 1000000" + else loop (fmap Tx.bls12_381_G1_uncompress red_bss) red_is red_n + where + -- lazily decode script context up to redeemer, which is less expensive and results in much smaller tx size + constrArgs :: BuiltinData -> BI.BuiltinList BuiltinData + constrArgs = BI.snd . BI.unsafeDataAsConstr + + redeemerFollowedByScriptInfo :: BI.BuiltinList BuiltinData + redeemerFollowedByScriptInfo = BI.tail (constrArgs arg) + + redeemer :: BuiltinData + redeemer = BI.head redeemerFollowedByScriptInfo + + red_n :: Integer + red_is :: [Integer] + red_bss :: [BuiltinByteString] + (red_n, red_is, red_bss) = PlutusV3.unsafeFromBuiltinData redeemer + + -- see Note[1] + loop points scalars n + | n == 1000000 = BI.unitval + | otherwise = let !_ = BI.bls12_381_G1_multiScalarMul (n : scalars) points in loop points scalars (pred n) + +{- +Note[1]: + + The benchmarking loop's counter will always be used as a nonce, prepended to the list of scalars. + Hence, make sure that in the redeemer args, + >> THE LIST OF SCALARS IS ALWAYS 1 ELEMENT SHORTER THAN THE LIST OF POINTS << + + == Reason for Nonce-as-Head ('n : scalars'): + 1. Defeats Pippenger Bucket-Caching: Mutating a single scalar + head element breaks the windowed bit-partitioning configuration. This forces + the 'blst' library to perform full, un-cached linear combination logic from + scratch rather than reusing pre-computed bucket structures. + 2. Minmize execution units to achieve 1.: Prepending a head nonce element + guarantees a predictable O(1) overhead, focusing execution cost purely on the underlying + curve arithmetic. Also, this guarantees a stable memory footprint. +-} diff --git a/bench/tx-generator/app/calibrate-script.hs b/bench/tx-generator/app/calibrate-script.hs index 2ba9f26af07..d41799eeea7 100644 --- a/bench/tx-generator/app/calibrate-script.hs +++ b/bench/tx-generator/app/calibrate-script.hs @@ -206,10 +206,10 @@ runScaling :: -> IO [(PlutusBudgetSummary, ScriptRedeemer)] runScaling script budgetType basePParams baseline baseBudget (scaleSanitize -> scalesArg) | null scalesArg = go1 scaleBaseLine scaleAutoFactors - | otherwise = mapM go0 scalesArg + | otherwise = catMaybes <$> mapM go0 scalesArg where -- specific scaling requested - go0 :: Scale -> IO (PlutusBudgetSummary, ScriptRedeemer) + go0 :: Scale -> IO (Maybe (PlutusBudgetSummary, ScriptRedeemer)) go0 scale = let fields@[txm, txs, bm, bs] = scaleFields scale @@ -218,9 +218,8 @@ runScaling script budgetType basePParams baseline baseBudget (scaleSanitize -> s budget = withHint (maximum fields) baseBudget in do putStrLn $ "--> run: " ++ show scope - evaluate $ - summaryAndRedeermerOrDie scope $ - plutusAutoScaleBlockfit pparams (scriptNameExt, scope) script budget strategy 1 + summaryAndRedeermerSafe scope $ + plutusAutoScaleBlockfit pparams (scriptNameExt, scope) script budget strategy 1 -- auto-scale until conditions are met (last list entry), but include intermediate results go1 :: Scale -> [Double] -> IO [(PlutusBudgetSummary, ScriptRedeemer)] @@ -228,12 +227,13 @@ runScaling script budgetType basePParams baseline baseBudget (scaleSanitize -> s go1 scale_ factors@(factor:fs) = case bumpLimit strategy budgetType factor scale_ of Nothing -> go1 scaleBaseLine fs -- no further bump possible: give up and auto-scale for next factor - Just scale -> do - run@(summary, _) <- go0 scale - (run :) <$> - if all (\cond -> cond summary baseline) happilyCalibrated - then go1 scaleBaseLine fs -- auto-scale next factor - else go1 scale factors -- apply next bump to current scaling, re-run + Just scale -> go0 scale >>= \case + Nothing -> pure [] -- if there was an error, skip all remaining scaling runs + Just run@(summary, _) -> + (run :) <$> + if all (\cond -> cond summary baseline) happilyCalibrated + then go1 scaleBaseLine fs -- auto-scale next factor + else go1 scale factors -- apply next bump to current scaling, re-run -- conditions for a succesful calibration: same limiting factors, same txn count per block -- NEXT RELEASE: does that need to be strategy dependent with TargetBlockExpenditure? diff --git a/bench/tx-generator/data/MultiScalarMulG1.redeemer.json b/bench/tx-generator/data/MultiScalarMulG1.redeemer.json new file mode 100644 index 00000000000..185873e0501 --- /dev/null +++ b/bench/tx-generator/data/MultiScalarMulG1.redeemer.json @@ -0,0 +1,31 @@ +{ + "constructor": 0, + "fields": [ + { + "int": 1000000 + }, + { + "list": [ + { + "int": 871345174532 + }, + { + "int": 987654321 + } + ] + }, + { + "list": [ + { + "bytes": "ab5d1d67b495361c3297c721cf3c9dc510fc5055bdf92fefc1e67d91a00a765520150428eb4c2fb01902d41d5ca62d85" + }, + { + "bytes": "88c7e388ee58f1db9a24d7098b01d13634298bebf2d159254975bd450cb0d287fcc622eb71edde8b469a8513551baf1f" + }, + { + "bytes": "95e4262f370607b2765312297461a04a3fb1fbb83e998a8e7d0f237017df86c5fb01f85101177c3dd41586df05b038cb" + } + ] + } + ] +} \ No newline at end of file diff --git a/bench/tx-generator/scripts-fallback/MultiScalarMulG1.plutus b/bench/tx-generator/scripts-fallback/MultiScalarMulG1.plutus new file mode 100644 index 00000000000..d1f4d1abcda --- /dev/null +++ b/bench/tx-generator/scripts-fallback/MultiScalarMulG1.plutus @@ -0,0 +1,5 @@ +{ + "type": "PlutusScriptV3", + "description": "", + "cborHex": "59015e0101009800aab9daba0aba2ab9aaab9eaba1ab9caab9f488888888c8c8c8c88c8c96600266e200052080897a899319804a491572656465656d6572206973203c203130303030303000800466002600f20052220016400a444004800a008403120032220031919191acc004cdc3a400000512328009bad300d0029919198008009bac0022259800800c6001132328019bad002998020020008c050008c04000500e1806800cc8c8cc004004dd6001112cc004006300089919400cdd70014cc010010004602800460200028070c034c040004601e0031640340026016004601c0026ea8c020c02cc024dd500099180080091112cc004cdc3a410112f4003149a2653001005802400e66e040092002401466f70c0160060030021803001a018323001001229001a5eb02446601a00466008008002323001001229001a5eb92446601800466008008002323001001229001c0024500337760053300400400101" +} diff --git a/bench/tx-generator/src-calibrate/Cardano/TxGenerator/Calibrate/Utils.hs b/bench/tx-generator/src-calibrate/Cardano/TxGenerator/Calibrate/Utils.hs index 1a438ee17ba..f3f808dfc90 100644 --- a/bench/tx-generator/src-calibrate/Cardano/TxGenerator/Calibrate/Utils.hs +++ b/bench/tx-generator/src-calibrate/Cardano/TxGenerator/Calibrate/Utils.hs @@ -41,6 +41,13 @@ summaryAndRedeermerOrDie scope = (\(summary, budget, _) -> (summary, autoBudgetRedeemer budget)) where withScope s = concat [scope, ": ", s] +summaryAndRedeermerSafe :: String -> Either TxGenError (PlutusBudgetSummary, PlutusAutoBudget, b) -> IO (Maybe (PlutusBudgetSummary, ScriptRedeemer)) +summaryAndRedeermerSafe scope = + either + (\err -> putStrLn (withScope $ show err) >> pure Nothing) + (\(summary, budget, _) -> pure (Just (summary, autoBudgetRedeemer budget))) + where withScope s = concat [scope, ": ", s] + resolveRedeemer :: Either ScriptData TxGenPlutusParams -> IO (Either TxGenError HashableScriptData) resolveRedeemer = resolveRedeemerQuiet False diff --git a/bench/tx-generator/tx-generator.cabal b/bench/tx-generator/tx-generator.cabal index 5ebadb43f08..d7e604674c1 100644 --- a/bench/tx-generator/tx-generator.cabal +++ b/bench/tx-generator/tx-generator.cabal @@ -30,7 +30,7 @@ common with-library -- It makes sure, we only depend on that package if it is buildable. -- The tx-generator will fall back to pre-serialized Plutus scripts if this package is not present. if flag(withplutuslib) && !(impl(ghc <9.6) || impl(ghc >=9.7)) - build-depends: plutus-scripts-bench ^>= 1.0.4 + build-depends: plutus-scripts-bench ^>= 1.0.5 cpp-options: -DWITH_LIBRARY common maybe-unix diff --git a/wb_profiles.mk b/wb_profiles.mk index 20c9daba78b..b480fb6ec67 100644 --- a/wb_profiles.mk +++ b/wb_profiles.mk @@ -1,7 +1,7 @@ PROFILES_EMPTY := fast-solo fast fast-notracer fast-plutus ci-test ci-test-notracer ci-test-plutus ci-test-hydra trace-bench trace-bench-notracer trace-full default plutus plutus-secp-ecdsa plutus-secp-schnorr epoch-transition PROFILES_MINIATURE := ci-bench ci-bench-lmdb ci-bench-lsmt ci-bench-notracer ci-bench-drep ci-bench-plutus ci-bench-v11-plutus ci-bench-v11-plutusv3 ci-bench-plutus24 ci-bench-plutus-secp-ecdsa ci-bench-plutus-secp-schnorr ci-bench-plutusv3-blst ci-bench-plutusv3-ripemd ci-bench-plutusv3-ripemd-step2x 10 10-notracer 10-plutus 6-dense 6-dense-rtsprof 6-dense-1h 6-dense-1h-rtsprof 6-dense-1h-timeseries 6-dense-4h 6-dense-4h-rtsprof PROFILES_FORGE_STRESS := forge-stress-solo-xs forge-stress-solo forge-stress-plutus-solo forge-stress-pre-solo-xs forge-stress-pre-solo forge-stress-pre-solo-xl forge-stress forge-stress-notracer forge-stress-plutus forge-stress-large forge-stress-pre forge-stress-pre-rtsA4m forge-stress-pre-rtsA64m forge-stress-pre-rtsN3 forge-stress-pre-rtsA4mN3 forge-stress-pre-rtsA64mN3 forge-stress-pre-rtsxn forge-stress-pre-notracer forge-stress-pre-plutus forge-stress-pre-large forge-stress-pre-large-rtsqg1 forge-stress-pre-large-rtsN3 forge-stress-pre-large-rtsN4 forge-stress-pre-large-rtsqg1N4 -PROFILES_PLUTUSCALL := plutuscall-loop plutuscall-loop-memx2 plutuscall-secp-ecdsa plutuscall-secp-ecdsa-stepx2 plutuscall-secp-schnorr plutuscall-secp-schnorr-stepx2 plutuscall-volt-loop plutuscall-volt-blst plutuscall-volt-ripemd plutuscall-voltv11-ripemd +PROFILES_PLUTUSCALL := plutuscall-loop plutuscall-loop-memx2 plutuscall-secp-ecdsa plutuscall-secp-ecdsa-stepx2 plutuscall-secp-schnorr plutuscall-secp-schnorr-stepx2 plutuscall-volt-loop plutuscall-volt-blst plutuscall-volt-ripemd plutuscall-voltv11-ripemd plutuscall-voltv11-schnorrv3 plutuscall-voltv11-mscalmul PROFILES_MODEL := model-secp-ecdsa-stepx2 model-secp-ecdsa model-value model-value-test PROFILES_K3 := k3-3ep-5kTx-10000kU-1300kD-64kbs-fixed-loaded k3-3ep-9kTx-10000kU-1300kD-64kbs-5tps-fixed-loaded k3-3ep-18kTx-10000kU-1300kD-64kbs-10tps-fixed-loaded PROFILES_SCENARIOS := chainsync-early-byron chainsync-early-byron-notracer chainsync-early-alonzo chainsync-early-alonzo-notracer devops idle tracer-only From f6abd234a28ca4b007f861248df19c1056c96e81 Mon Sep 17 00:00:00 2001 From: Russoul Date: Fri, 10 Jul 2026 11:58:49 +0400 Subject: [PATCH 2/4] bench: add ExpModInteger benchmark script and plutuscall-voltv11-expmod profile Ports a fixed-iteration expModInteger loop benchmark into plutus-scripts-bench, wires it into tx-generator's script/redeemer resolution (data/ and scripts-fallback/), and adds a calibrated plutuscall-voltv11-expmod profile (LimitTxPerBlock_4) built off it. --- .../Profile/Builtin/Plutuscall.hs | 2 + .../Benchmarking/Profile/Vocabulary.hs | 25 +++- .../plutus-scripts-bench.cabal | 1 + .../src/Cardano/Benchmarking/PlutusScripts.hs | 2 + .../PlutusScripts/ExpModInteger.hs | 114 ++++++++++++++++++ .../data/ExpModInteger.redeemer.json | 19 +++ .../scripts-fallback/ExpModInteger.plutus | 5 + wb_profiles.mk | 2 +- 8 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 bench/plutus-scripts-bench/src/Cardano/Benchmarking/PlutusScripts/ExpModInteger.hs create mode 100644 bench/tx-generator/data/ExpModInteger.redeemer.json create mode 100644 bench/tx-generator/scripts-fallback/ExpModInteger.plutus diff --git a/bench/cardano-profile/src/Cardano/Benchmarking/Profile/Builtin/Plutuscall.hs b/bench/cardano-profile/src/Cardano/Benchmarking/Profile/Builtin/Plutuscall.hs index 311240122de..8f6645b9d8c 100644 --- a/bench/cardano-profile/src/Cardano/Benchmarking/Profile/Builtin/Plutuscall.hs +++ b/bench/cardano-profile/src/Cardano/Benchmarking/Profile/Builtin/Plutuscall.hs @@ -50,6 +50,7 @@ profilesPlutuscall = schnorrV3 = plutusCall & V.plutusTypeSchnorrV3 . V.plutusDoubleSaturation . P.analysisSizeSmall . P.analysisEpoch3Plus mscalmul = plutusCall & V.plutusTypeMultScalarMultG1 . V.plutusDoubleSaturation . P.analysisSizeSmall . P.analysisEpoch3Plus ripemdVolt4tx = plutusCall & V.plutusTypeRIPEMD_4tx . V.plutusDoubleSaturation . P.analysisSizeSmall . P.analysisEpoch3Plus + expmod = plutusCall & V.plutusTypeExpMod . V.plutusDoubleSaturation . P.analysisSizeSmall . P.analysisEpoch3Plus loopVolt = plutusCall & V.plutusTypeLoop . V.plutusDoublePlusSaturation . P.analysisSizeSmall blstVolt = plutusCall & V.plutusTypeBLST . V.plutusDoublePlusSaturation . P.analysisSizeModerate2 @@ -72,4 +73,5 @@ profilesPlutuscall = , ripemdVolt4tx & P.name "plutuscall-voltv11-ripemd" . postPlomin . P.v11Preview , schnorrV3 & P.name "plutuscall-voltv11-schnorrv3" . postPlomin . P.v11Preview , mscalmul & P.name "plutuscall-voltv11-mscalmul" . postPlomin . P.v11Preview + , expmod & P.name "plutuscall-voltv11-expmod" . postPlomin . P.v11Preview ] diff --git a/bench/cardano-profile/src/Cardano/Benchmarking/Profile/Vocabulary.hs b/bench/cardano-profile/src/Cardano/Benchmarking/Profile/Vocabulary.hs index 3bfeb4f21fd..0f3200067de 100644 --- a/bench/cardano-profile/src/Cardano/Benchmarking/Profile/Vocabulary.hs +++ b/bench/cardano-profile/src/Cardano/Benchmarking/Profile/Vocabulary.hs @@ -22,6 +22,7 @@ module Cardano.Benchmarking.Profile.Vocabulary ( , plutusTypeLoop, plutusTypeLoopV3, plutusTypeLoop2024, plutusTypeECDSA, plutusTypeSchnorr , plutusTypeBLST, plutusTypeRIPEMD, plutusTypeRIPEMD_4tx, plutusTypeMultScalarMultG1 , plutusTypeSchnorrV3 +, plutusTypeExpMod ) where -------------------------------------------------------------------------------- @@ -293,4 +294,26 @@ plutusTypeSchnorrV3 = , KeyMap.fromList [("bytes", Aeson.String "30303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030303030")] , KeyMap.fromList [("bytes", Aeson.String "5a56da88e6fd8419181dec4d3dd6997bab953d2fc71ab65e23cfc9e7e3d1a310613454a60f6703819a39fdac2a410a094442afd1fc083354443e8d8bb4461a9b")] ] - . P.txFee 586000 \ No newline at end of file + . P.txFee 586000 + +-- redeemer is `(n, (a, m))`: `n` (the loop counter) must stay the first +-- numeric value in the encoding, cf. bench/plutus-scripts-bench/src/Cardano/Benchmarking/PlutusScripts/ExpModInteger.hs +-- `n`'s placeholder here must NOT be 1000000: the mandatory `scriptDataModifyNumber +-- (const 1_000_000)` reset step (Cardano.TxGenerator.PlutusContext) detects "already +-- found a number to change" by comparing before/after values, so if this placeholder +-- already equals 1000000 the reset is a false-negative no-op and falls through to +-- corrupt `a` instead. Keep it at 0. +-- txFee is the unscaled "txperblock_4" baseline fee from `calibrate-script run ExpModInteger txperblock_4`, +-- cf. bench/tx-generator/summaries_ExpModInteger.json +plutusTypeExpMod :: Types.Profile -> Types.Profile +plutusTypeExpMod = + P.plutusType "LimitTxPerBlock_4" . P.plutusScript "ExpModInteger" + . P.redeemerFields [ + KeyMap.fromList [("int", Aeson.Number 0.0)] + , KeyMap.fromList [("constructor", Aeson.Number 0.0), ("fields", Aeson.Array $ Vector.fromList [ + Aeson.Object $ KeyMap.fromList [("int", Aeson.Number 3.0)] + , Aeson.Object $ KeyMap.fromList [("int", Aeson.Number 2305843009213693951.0)] + ]) + ] + ] + . P.txFee 1475455 \ No newline at end of file diff --git a/bench/plutus-scripts-bench/plutus-scripts-bench.cabal b/bench/plutus-scripts-bench/plutus-scripts-bench.cabal index 9d6570e32ea..99dc309ad3b 100644 --- a/bench/plutus-scripts-bench/plutus-scripts-bench.cabal +++ b/bench/plutus-scripts-bench/plutus-scripts-bench.cabal @@ -72,6 +72,7 @@ library Cardano.Benchmarking.PlutusScripts.CustomCallV3 Cardano.Benchmarking.PlutusScripts.EcdsaSecp256k1Loop Cardano.Benchmarking.PlutusScripts.EcdsaSecp256k1LoopV3 + Cardano.Benchmarking.PlutusScripts.ExpModInteger Cardano.Benchmarking.PlutusScripts.HashOntoG2AndAdd Cardano.Benchmarking.PlutusScripts.Loop2024 Cardano.Benchmarking.PlutusScripts.LoopV3 diff --git a/bench/plutus-scripts-bench/src/Cardano/Benchmarking/PlutusScripts.hs b/bench/plutus-scripts-bench/src/Cardano/Benchmarking/PlutusScripts.hs index 3c3b7340a4d..5f6c3b40f8e 100644 --- a/bench/plutus-scripts-bench/src/Cardano/Benchmarking/PlutusScripts.hs +++ b/bench/plutus-scripts-bench/src/Cardano/Benchmarking/PlutusScripts.hs @@ -26,6 +26,7 @@ import qualified Cardano.Benchmarking.PlutusScripts.CustomCall as CustomCall import qualified Cardano.Benchmarking.PlutusScripts.CustomCallV3 as CustomCallV3 import qualified Cardano.Benchmarking.PlutusScripts.EcdsaSecp256k1Loop as ECDSA import qualified Cardano.Benchmarking.PlutusScripts.EcdsaSecp256k1LoopV3 as ECDSAV3 +import qualified Cardano.Benchmarking.PlutusScripts.ExpModInteger as ExpModInteger import qualified Cardano.Benchmarking.PlutusScripts.HashOntoG2AndAdd as HashG2Add import qualified Cardano.Benchmarking.PlutusScripts.Loop2024 as Loop2024 import qualified Cardano.Benchmarking.PlutusScripts.LoopV3 as LoopV3 @@ -43,6 +44,7 @@ getAllScripts = , CustomCallV3.script , ECDSA.script , ECDSAV3.script + , ExpModInteger.script , HashG2Add.script , Loop2024.script , LoopV3.script diff --git a/bench/plutus-scripts-bench/src/Cardano/Benchmarking/PlutusScripts/ExpModInteger.hs b/bench/plutus-scripts-bench/src/Cardano/Benchmarking/PlutusScripts/ExpModInteger.hs new file mode 100644 index 00000000000..ab9f8925287 --- /dev/null +++ b/bench/plutus-scripts-bench/src/Cardano/Benchmarking/PlutusScripts/ExpModInteger.hs @@ -0,0 +1,114 @@ +{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE NoImplicitPrelude #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE ViewPatterns #-} + +-- | Fixed-iteration benchmark loop exercising the 'expModInteger' builtin, +-- ported from plutus-scripts-e2e's PlutusScripts.Batch6.ExpModInteger.Common. +module Cardano.Benchmarking.PlutusScripts.ExpModInteger + ( mkSimpleExpModIntegerPolicyBench + , compiledSimpleExpModIntegerPolicyBench + , script + ) where + +import Cardano.Api (PlutusScriptVersion (PlutusScriptV3)) +import Cardano.Benchmarking.ScriptAPI (PlutusBenchScript, mkPlutusBenchScript) +import Language.Haskell.TH.Syntax (Exp (LitE), Lit (StringL), Loc (loc_module), qLocation) +import PlutusLedgerApi.Common (serialiseCompiledCode) +import qualified PlutusLedgerApi.V3 as PlutusV3 +import qualified PlutusTx +import PlutusTx.Code (CompiledCode) +import qualified PlutusTx.Builtins as BI (expModInteger) +import qualified PlutusTx.Builtins.Internal as BI (BuiltinList, head, snd, tail, + unitval, unsafeDataAsConstr) +import PlutusTx.Prelude as P hiding (Semigroup (..), (.), (<$>)) +import Prelude as Haskell ((.), (<$>)) + + +data SimpleParams = SimpleParams + { a :: P.Integer -- Value to be exponentiated + , e :: P.Integer -- Exponent + , m :: P.Integer -- Modulus + , result :: P.Integer -- Expected result, -1 for failing tests + } + +-- | `a` and `m` +data P = P Integer Integer +PlutusTx.unstableMakeIsData ''P + +-- | `e` and `r` +data S = S Integer Integer + +-- | Correctness by induction: +-- base case: +-- a^{e₀} mod m = r₀, established via a single `expModInteger` call +-- (e₀ = m - 2, so a^{e₀} mod m is a's modular inverse by Fermat's little +-- theorem, since m = 2^61 - 1 is prime -- a realistic, non-arbitrary exponent size) +-- Inductive step: +-- a^{e_{i+1}} mod m = r_{i+1} +-- a^{e_i + 1} mod m = r_{i+1} +-- (a^{e_i} mod m) * a mod m = r_{i+1} +-- r_i * a mod m = r_{i+1} ✔ +-- +-- `e` only ever increments by 1 per iteration (never doubles), so its +-- bit-length -- and thus `expModInteger`'s per-call cost -- stays essentially +-- constant across the whole benchmark run, however many iterations it runs for. +-- This also keeps every iteration's arguments genuinely distinct (dependent on +-- the loop counter), so no compiler optimisation (e.g. full laziness) can +-- legally hoist/common up the `expModInteger` call across iterations. +interp :: P -> S -> SimpleParams +interp (P a m) (S e r) = SimpleParams a e m r + +seed :: P -> S +seed (P a m) = + let e0 = m - 2 + in S e0 (BI.expModInteger a e0 m) + +next :: P -> S -> S +next (P a m) (S e r) = S (e + 1) ((r * a) `modulo` m) + + +-- while n > 1000000, interp (param, st) and check expModInteger on that input. Fail if incorrect, otherwise loop +-- with n := n - 1, st := next param st +{-# INLINEABLE mkSimpleExpModIntegerPolicyBench #-} +mkSimpleExpModIntegerPolicyBench :: P.BuiltinData -> P.BuiltinUnit +mkSimpleExpModIntegerPolicyBench ctx = go (seed red_param) red_n + where + -- lazily decode script context up to redeemer, which is less expensive and results in much smaller tx size + constrArgs :: P.BuiltinData -> BI.BuiltinList P.BuiltinData + constrArgs = BI.snd . BI.unsafeDataAsConstr + + redeemerFollowedByScriptInfo :: BI.BuiltinList P.BuiltinData + redeemerFollowedByScriptInfo = BI.tail (constrArgs ctx) + + redeemer :: P.BuiltinData + redeemer = BI.head redeemerFollowedByScriptInfo + + -- `n` must come first in the redeemer's Data encoding: tx-generator's + -- auto-budget calibration (Cardano.TxGenerator.PlutusContext.scriptDataModifyNumber) + -- bumps whichever ScriptDataNumber it encounters first in a depth-first walk. + red_n :: Integer + red_param :: P + (red_n, red_param) = PlutusV3.unsafeFromBuiltinData redeemer + + go st n = + if n P.== 1000000 + then BI.unitval + else + let SimpleParams{..} = interp red_param st + in if BI.expModInteger a e m P.== result + then let !st' = next red_param st in go st' (P.pred n) + else P.traceError "mkSimpleExpModIntegerPolicyBench" + +compiledSimpleExpModIntegerPolicyBench + :: CompiledCode (P.BuiltinData -> P.BuiltinUnit) +compiledSimpleExpModIntegerPolicyBench = + $$(PlutusTx.compile [|| mkSimpleExpModIntegerPolicyBench ||]) + +script :: PlutusBenchScript +script = mkPlutusBenchScript + $(LitE . StringL . loc_module <$> qLocation) + PlutusScriptV3 + (serialiseCompiledCode compiledSimpleExpModIntegerPolicyBench) diff --git a/bench/tx-generator/data/ExpModInteger.redeemer.json b/bench/tx-generator/data/ExpModInteger.redeemer.json new file mode 100644 index 00000000000..ddf389e8518 --- /dev/null +++ b/bench/tx-generator/data/ExpModInteger.redeemer.json @@ -0,0 +1,19 @@ +{ + "constructor": 0, + "fields": [ + { + "int": 0 + }, + { + "constructor": 0, + "fields": [ + { + "int": 3 + }, + { + "int": 2305843009213693951 + } + ] + } + ] +} diff --git a/bench/tx-generator/scripts-fallback/ExpModInteger.plutus b/bench/tx-generator/scripts-fallback/ExpModInteger.plutus new file mode 100644 index 00000000000..dc274116537 --- /dev/null +++ b/bench/tx-generator/scripts-fallback/ExpModInteger.plutus @@ -0,0 +1,5 @@ +{ + "type": "PlutusScriptV3", + "description": "", + "cborHex": "5901300101009800aab9daba2ab9aaab9eaba1ab9c48888888c8c8ca60020039002911940040073001003800c0097ae19b810014801190039100110011112cc004cdc3a410112f4003149a264b30013370f3001900191110023200322220036400644440045eba400644440028994c004016003337020069001200a9005914802c8a00266e01200200299b86337040020080062264c660109201206d6b53696d706c654578704d6f64496e7465676572506f6c69637942656e6368008002016900491480248a002009002801c00420149001910008c8cc004dd51802180398029baa0022235980099b874800000a25001375a600e003330043754600e6014002446b30013370e9000001448c8ca0026eb400a6eb4c030004601c004601400316402c0028b20100012233001300900230060021" +} diff --git a/wb_profiles.mk b/wb_profiles.mk index b480fb6ec67..b425dba9f8f 100644 --- a/wb_profiles.mk +++ b/wb_profiles.mk @@ -1,7 +1,7 @@ PROFILES_EMPTY := fast-solo fast fast-notracer fast-plutus ci-test ci-test-notracer ci-test-plutus ci-test-hydra trace-bench trace-bench-notracer trace-full default plutus plutus-secp-ecdsa plutus-secp-schnorr epoch-transition PROFILES_MINIATURE := ci-bench ci-bench-lmdb ci-bench-lsmt ci-bench-notracer ci-bench-drep ci-bench-plutus ci-bench-v11-plutus ci-bench-v11-plutusv3 ci-bench-plutus24 ci-bench-plutus-secp-ecdsa ci-bench-plutus-secp-schnorr ci-bench-plutusv3-blst ci-bench-plutusv3-ripemd ci-bench-plutusv3-ripemd-step2x 10 10-notracer 10-plutus 6-dense 6-dense-rtsprof 6-dense-1h 6-dense-1h-rtsprof 6-dense-1h-timeseries 6-dense-4h 6-dense-4h-rtsprof PROFILES_FORGE_STRESS := forge-stress-solo-xs forge-stress-solo forge-stress-plutus-solo forge-stress-pre-solo-xs forge-stress-pre-solo forge-stress-pre-solo-xl forge-stress forge-stress-notracer forge-stress-plutus forge-stress-large forge-stress-pre forge-stress-pre-rtsA4m forge-stress-pre-rtsA64m forge-stress-pre-rtsN3 forge-stress-pre-rtsA4mN3 forge-stress-pre-rtsA64mN3 forge-stress-pre-rtsxn forge-stress-pre-notracer forge-stress-pre-plutus forge-stress-pre-large forge-stress-pre-large-rtsqg1 forge-stress-pre-large-rtsN3 forge-stress-pre-large-rtsN4 forge-stress-pre-large-rtsqg1N4 -PROFILES_PLUTUSCALL := plutuscall-loop plutuscall-loop-memx2 plutuscall-secp-ecdsa plutuscall-secp-ecdsa-stepx2 plutuscall-secp-schnorr plutuscall-secp-schnorr-stepx2 plutuscall-volt-loop plutuscall-volt-blst plutuscall-volt-ripemd plutuscall-voltv11-ripemd plutuscall-voltv11-schnorrv3 plutuscall-voltv11-mscalmul +PROFILES_PLUTUSCALL := plutuscall-loop plutuscall-loop-memx2 plutuscall-secp-ecdsa plutuscall-secp-ecdsa-stepx2 plutuscall-secp-schnorr plutuscall-secp-schnorr-stepx2 plutuscall-volt-loop plutuscall-volt-blst plutuscall-volt-ripemd plutuscall-voltv11-ripemd plutuscall-voltv11-schnorrv3 plutuscall-voltv11-mscalmul plutuscall-voltv11-expmod PROFILES_MODEL := model-secp-ecdsa-stepx2 model-secp-ecdsa model-value model-value-test PROFILES_K3 := k3-3ep-5kTx-10000kU-1300kD-64kbs-fixed-loaded k3-3ep-9kTx-10000kU-1300kD-64kbs-5tps-fixed-loaded k3-3ep-18kTx-10000kU-1300kD-64kbs-10tps-fixed-loaded PROFILES_SCENARIOS := chainsync-early-byron chainsync-early-byron-notracer chainsync-early-alonzo chainsync-early-alonzo-notracer devops idle tracer-only From 7c1e70d3ef82ce299b68d153d0b4f4e361a1c7f5 Mon Sep 17 00:00:00 2001 From: Russoul Date: Fri, 10 Jul 2026 13:48:42 +0400 Subject: [PATCH 3/4] bench: simplify ExpModInteger, flatten redeemer, drop e2e correctness check Inline the P/S wrapper types into loose Integers and flatten the redeemer from a nested (n, (a, m)) to a flat (n, a, m), removing custom IsData instances. Move interp/seed/next into mkSimpleExpModIntegerPolicyBench's where clause so the plugin can inline them, and inline the compiled-code binding into `script`, exporting only `script`. Drop the expModInteger-vs-expected-result equality check carried over from plutus-scripts-e2e's correctness tests: this is a benchmark script, not a correctness test, so it now just force-evaluates expModInteger each iteration (`let !_ = ...`) instead of threading a running result forward. Add the same "redeemer counter must be >= 1000000" sanity check the other benchmark scripts have. Re-calibrate plutusTypeExpMod's txFee against the simplified script. --- .../Benchmarking/Profile/Vocabulary.hs | 14 ++- .../PlutusScripts/ExpModInteger.hs | 98 ++++++------------- .../data/ExpModInteger.redeemer.json | 13 +-- .../scripts-fallback/ExpModInteger.plutus | 2 +- 4 files changed, 40 insertions(+), 87 deletions(-) diff --git a/bench/cardano-profile/src/Cardano/Benchmarking/Profile/Vocabulary.hs b/bench/cardano-profile/src/Cardano/Benchmarking/Profile/Vocabulary.hs index 0f3200067de..29efe971c79 100644 --- a/bench/cardano-profile/src/Cardano/Benchmarking/Profile/Vocabulary.hs +++ b/bench/cardano-profile/src/Cardano/Benchmarking/Profile/Vocabulary.hs @@ -296,24 +296,22 @@ plutusTypeSchnorrV3 = ] . P.txFee 586000 --- redeemer is `(n, (a, m))`: `n` (the loop counter) must stay the first +-- redeemer is a flat `(n, a, m)`: `n` (the loop counter) must stay the first -- numeric value in the encoding, cf. bench/plutus-scripts-bench/src/Cardano/Benchmarking/PlutusScripts/ExpModInteger.hs -- `n`'s placeholder here must NOT be 1000000: the mandatory `scriptDataModifyNumber -- (const 1_000_000)` reset step (Cardano.TxGenerator.PlutusContext) detects "already -- found a number to change" by comparing before/after values, so if this placeholder -- already equals 1000000 the reset is a false-negative no-op and falls through to -- corrupt `a` instead. Keep it at 0. --- txFee is the unscaled "txperblock_4" baseline fee from `calibrate-script run ExpModInteger txperblock_4`, +-- txFee is rounded up from the unscaled "txperblock_4" baseline fee (1262499) +-- reported by `calibrate-script run ExpModInteger txperblock_4`, -- cf. bench/tx-generator/summaries_ExpModInteger.json plutusTypeExpMod :: Types.Profile -> Types.Profile plutusTypeExpMod = P.plutusType "LimitTxPerBlock_4" . P.plutusScript "ExpModInteger" . P.redeemerFields [ KeyMap.fromList [("int", Aeson.Number 0.0)] - , KeyMap.fromList [("constructor", Aeson.Number 0.0), ("fields", Aeson.Array $ Vector.fromList [ - Aeson.Object $ KeyMap.fromList [("int", Aeson.Number 3.0)] - , Aeson.Object $ KeyMap.fromList [("int", Aeson.Number 2305843009213693951.0)] - ]) - ] + , KeyMap.fromList [("int", Aeson.Number 3.0)] + , KeyMap.fromList [("int", Aeson.Number 2305843009213693951.0)] ] - . P.txFee 1475455 \ No newline at end of file + . P.txFee 1275000 \ No newline at end of file diff --git a/bench/plutus-scripts-bench/src/Cardano/Benchmarking/PlutusScripts/ExpModInteger.hs b/bench/plutus-scripts-bench/src/Cardano/Benchmarking/PlutusScripts/ExpModInteger.hs index ab9f8925287..4a160d8f1a3 100644 --- a/bench/plutus-scripts-bench/src/Cardano/Benchmarking/PlutusScripts/ExpModInteger.hs +++ b/bench/plutus-scripts-bench/src/Cardano/Benchmarking/PlutusScripts/ExpModInteger.hs @@ -1,16 +1,12 @@ {-# LANGUAGE BangPatterns #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE NoImplicitPrelude #-} -{-# LANGUAGE RecordWildCards #-} {-# LANGUAGE TemplateHaskell #-} -{-# LANGUAGE ViewPatterns #-} -- | Fixed-iteration benchmark loop exercising the 'expModInteger' builtin, -- ported from plutus-scripts-e2e's PlutusScripts.Batch6.ExpModInteger.Common. module Cardano.Benchmarking.PlutusScripts.ExpModInteger - ( mkSimpleExpModIntegerPolicyBench - , compiledSimpleExpModIntegerPolicyBench - , script + ( script ) where import Cardano.Api (PlutusScriptVersion (PlutusScriptV3)) @@ -19,7 +15,6 @@ import Language.Haskell.TH.Syntax (Exp (LitE), Lit (StringL), Loc (loc import PlutusLedgerApi.Common (serialiseCompiledCode) import qualified PlutusLedgerApi.V3 as PlutusV3 import qualified PlutusTx -import PlutusTx.Code (CompiledCode) import qualified PlutusTx.Builtins as BI (expModInteger) import qualified PlutusTx.Builtins.Internal as BI (BuiltinList, head, snd, tail, unitval, unsafeDataAsConstr) @@ -27,54 +22,14 @@ import PlutusTx.Prelude as P hiding (Semigroup (..), (.), (<$>)) import Prelude as Haskell ((.), (<$>)) -data SimpleParams = SimpleParams - { a :: P.Integer -- Value to be exponentiated - , e :: P.Integer -- Exponent - , m :: P.Integer -- Modulus - , result :: P.Integer -- Expected result, -1 for failing tests - } - --- | `a` and `m` -data P = P Integer Integer -PlutusTx.unstableMakeIsData ''P - --- | `e` and `r` -data S = S Integer Integer - --- | Correctness by induction: --- base case: --- a^{e₀} mod m = r₀, established via a single `expModInteger` call --- (e₀ = m - 2, so a^{e₀} mod m is a's modular inverse by Fermat's little --- theorem, since m = 2^61 - 1 is prime -- a realistic, non-arbitrary exponent size) --- Inductive step: --- a^{e_{i+1}} mod m = r_{i+1} --- a^{e_i + 1} mod m = r_{i+1} --- (a^{e_i} mod m) * a mod m = r_{i+1} --- r_i * a mod m = r_{i+1} ✔ --- --- `e` only ever increments by 1 per iteration (never doubles), so its --- bit-length -- and thus `expModInteger`'s per-call cost -- stays essentially --- constant across the whole benchmark run, however many iterations it runs for. --- This also keeps every iteration's arguments genuinely distinct (dependent on --- the loop counter), so no compiler optimisation (e.g. full laziness) can --- legally hoist/common up the `expModInteger` call across iterations. -interp :: P -> S -> SimpleParams -interp (P a m) (S e r) = SimpleParams a e m r - -seed :: P -> S -seed (P a m) = - let e0 = m - 2 - in S e0 (BI.expModInteger a e0 m) - -next :: P -> S -> S -next (P a m) (S e r) = S (e + 1) ((r * a) `modulo` m) - - --- while n > 1000000, interp (param, st) and check expModInteger on that input. Fail if incorrect, otherwise loop --- with n := n - 1, st := next param st +-- while n > 1000000, force-evaluate expModInteger a e m (discarding the +-- result) and loop with n := n - 1, e := e + 1 {-# INLINEABLE mkSimpleExpModIntegerPolicyBench #-} mkSimpleExpModIntegerPolicyBench :: P.BuiltinData -> P.BuiltinUnit -mkSimpleExpModIntegerPolicyBench ctx = go (seed red_param) red_n +mkSimpleExpModIntegerPolicyBench ctx = + if red_n P.< 1000000 -- large number ensures same bitsize for all counter values + then P.traceError "redeemer is < 1000000" + else go e0 red_n where -- lazily decode script context up to redeemer, which is less expensive and results in much smaller tx size constrArgs :: P.BuiltinData -> BI.BuiltinList P.BuiltinData @@ -86,29 +41,34 @@ mkSimpleExpModIntegerPolicyBench ctx = go (seed red_param) red_n redeemer :: P.BuiltinData redeemer = BI.head redeemerFollowedByScriptInfo - -- `n` must come first in the redeemer's Data encoding: tx-generator's + -- `n` must come first in the redeemer's (flat) Data encoding: tx-generator's -- auto-budget calibration (Cardano.TxGenerator.PlutusContext.scriptDataModifyNumber) -- bumps whichever ScriptDataNumber it encounters first in a depth-first walk. - red_n :: Integer - red_param :: P - (red_n, red_param) = PlutusV3.unsafeFromBuiltinData redeemer - - go st n = + red_n :: Integer + red_a :: Integer + red_m :: Integer + (red_n, red_a, red_m) = PlutusV3.unsafeFromBuiltinData redeemer + + -- e₀ = m - 2, a realistic, non-arbitrary exponent size (a's modular inverse + -- exponent by Fermat's little theorem, since m = 2^61 - 1 is prime). + -- + -- `e` only ever increments by 1 per iteration (never doubles), so its + -- bit-length -- and thus `expModInteger`'s per-call cost -- stays essentially + -- constant across the whole benchmark run, however many iterations it runs for. + -- This also keeps every iteration's arguments genuinely distinct (dependent on + -- the loop counter), so no compiler optimisation (e.g. full laziness) can + -- legally hoist/common up the `expModInteger` call across iterations. + e0 :: Integer + e0 = red_m - 2 + + go :: Integer -> Integer -> P.BuiltinUnit + go e n = if n P.== 1000000 then BI.unitval - else - let SimpleParams{..} = interp red_param st - in if BI.expModInteger a e m P.== result - then let !st' = next red_param st in go st' (P.pred n) - else P.traceError "mkSimpleExpModIntegerPolicyBench" - -compiledSimpleExpModIntegerPolicyBench - :: CompiledCode (P.BuiltinData -> P.BuiltinUnit) -compiledSimpleExpModIntegerPolicyBench = - $$(PlutusTx.compile [|| mkSimpleExpModIntegerPolicyBench ||]) + else let !_ = BI.expModInteger red_a e red_m in go (e P.+ 1) (P.pred n) script :: PlutusBenchScript script = mkPlutusBenchScript $(LitE . StringL . loc_module <$> qLocation) PlutusScriptV3 - (serialiseCompiledCode compiledSimpleExpModIntegerPolicyBench) + (serialiseCompiledCode $$(PlutusTx.compile [|| mkSimpleExpModIntegerPolicyBench ||])) diff --git a/bench/tx-generator/data/ExpModInteger.redeemer.json b/bench/tx-generator/data/ExpModInteger.redeemer.json index ddf389e8518..300847f4b37 100644 --- a/bench/tx-generator/data/ExpModInteger.redeemer.json +++ b/bench/tx-generator/data/ExpModInteger.redeemer.json @@ -5,15 +5,10 @@ "int": 0 }, { - "constructor": 0, - "fields": [ - { - "int": 3 - }, - { - "int": 2305843009213693951 - } - ] + "int": 3 + }, + { + "int": 2305843009213693951 } ] } diff --git a/bench/tx-generator/scripts-fallback/ExpModInteger.plutus b/bench/tx-generator/scripts-fallback/ExpModInteger.plutus index dc274116537..6e4a89ac61b 100644 --- a/bench/tx-generator/scripts-fallback/ExpModInteger.plutus +++ b/bench/tx-generator/scripts-fallback/ExpModInteger.plutus @@ -1,5 +1,5 @@ { "type": "PlutusScriptV3", "description": "", - "cborHex": "5901300101009800aab9daba2ab9aaab9eaba1ab9c48888888c8c8ca60020039002911940040073001003800c0097ae19b810014801190039100110011112cc004cdc3a410112f4003149a264b30013370f3001900191110023200322220036400644440045eba400644440028994c004016003337020069001200a9005914802c8a00266e01200200299b86337040020080062264c660109201206d6b53696d706c654578704d6f64496e7465676572506f6c69637942656e6368008002016900491480248a002009002801c00420149001910008c8cc004dd51802180398029baa0022235980099b874800000a25001375a600e003330043754600e6014002446b30013370e9000001448c8ca0026eb400a6eb4c030004601c004601400316402c0028b20100012233001300900230060021" + "cborHex": "58d10101009800aab9daba2ab9aaab9eaba1ab9c48888888c8c8c8c96600266e200052080897a89931980324811572656465656d6572206973203c20313030303030300080044cc008cdc0a801a4008002804a400e4440063230010012225980099b874820225e800629344c8ca600200b00199b810034800900519b804800800e6003200b222002400aa008bd72014190019110008c8c8c8d6600266e1d200000289194004dd698040014dd69804000cdd6980418058008c0280062c8040004c018008c020004dd51801980318021baa00101" } From 549d792c0367f68bb7225b03d606b33cc0214980 Mon Sep 17 00:00:00 2001 From: Russoul Date: Fri, 10 Jul 2026 15:39:50 +0400 Subject: [PATCH 4/4] bench: fix stale primality comment on ExpModInteger's e0 The Fermat's-little-theorem/primality justification for e0 = m - 2 stopped applying once the correctness-check-against-expected-result was dropped; nothing in the code relies on m being prime anymore. --- .../src/Cardano/Benchmarking/PlutusScripts/ExpModInteger.hs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bench/plutus-scripts-bench/src/Cardano/Benchmarking/PlutusScripts/ExpModInteger.hs b/bench/plutus-scripts-bench/src/Cardano/Benchmarking/PlutusScripts/ExpModInteger.hs index 4a160d8f1a3..979420d3901 100644 --- a/bench/plutus-scripts-bench/src/Cardano/Benchmarking/PlutusScripts/ExpModInteger.hs +++ b/bench/plutus-scripts-bench/src/Cardano/Benchmarking/PlutusScripts/ExpModInteger.hs @@ -49,8 +49,10 @@ mkSimpleExpModIntegerPolicyBench ctx = red_m :: Integer (red_n, red_a, red_m) = PlutusV3.unsafeFromBuiltinData redeemer - -- e₀ = m - 2, a realistic, non-arbitrary exponent size (a's modular inverse - -- exponent by Fermat's little theorem, since m = 2^61 - 1 is prime). + -- e₀ = m - 2: a realistic, non-arbitrary exponent size, tracking `m`'s + -- own bit-length (not tied to `m` being prime -- there's no correctness + -- check against an expected result here, just a representative-sized + -- exponent to feed `expModInteger`). -- -- `e` only ever increments by 1 per iteration (never doubles), so its -- bit-length -- and thus `expModInteger`'s per-call cost -- stays essentially