Skip to content

Commit 4235e19

Browse files
authored
Merge pull request #5127 from IntersectMBO/lehins/plutus-debug-made-pure
Pure version of `plutusDebug`
2 parents b7f6382 + e3ab092 commit 4235e19

File tree

4 files changed

+143
-34
lines changed

4 files changed

+143
-34
lines changed

libs/cardano-ledger-core/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## 1.18.0.0
44

5+
* Add a timeout argument to `plutusDebug`
6+
* Add `pdoExUnitsEnforced` to `PlutusDebugOverrides` and add `defaultPlutusDebugOverrides`
7+
* Add `NFData` instance for `PlutusDebugInfo`
8+
* Add `DebugTimedOut` constructor to `PlutusDebugInfo`
9+
* Add `debugPlutusUnbounded`
510
* Added `binaryUpgradeTx`, `binaryUpgradeTxBody`, `binaryUpgradeTxWits`, `binaryUpgradeTxAuxData`
611
* Remove `upgradeTx` and `TxUpgradeError` from `EraTx`
712
* Remove `upgradeTxBody` and `TxBodyUpgradeError` from `EraTxBody`

libs/cardano-ledger-core/app/CLI.hs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
{-# LANGUAGE NumericUnderscores #-}
2+
13
module CLI (
24
Opts (..),
35
optsParser,
@@ -6,9 +8,11 @@ module CLI (
68
import Cardano.Ledger.Binary (mkVersion64)
79
import Cardano.Ledger.Plutus.Evaluate
810
import Options.Applicative
11+
import Text.Read (readMaybe)
912

1013
data Opts = Opts
1114
{ optsScriptWithContext :: !String
15+
, optsTimeout :: !Int
1216
, optsOverrides :: !PlutusDebugOverrides
1317
}
1418
deriving (Show)
@@ -19,6 +23,7 @@ overridesParser =
1923
<$> option
2024
(Just <$> str)
2125
( long "script"
26+
<> short 's'
2227
<> value Nothing
2328
<> help "Plutus script hex without context"
2429
)
@@ -32,12 +37,14 @@ overridesParser =
3237
<*> option
3338
(Just <$> auto)
3439
( long "language"
40+
<> short 'l'
3541
<> value Nothing
3642
<> help "Plutus language version"
3743
)
3844
<*> option
39-
(str >>= pure . Just . map read . words)
45+
(mapM readMaybe . words <$> str)
4046
( long "cost-model-values"
47+
<> short 'c'
4148
<> value Nothing
4249
<> help ""
4350
)
@@ -53,10 +60,28 @@ overridesParser =
5360
<> value Nothing
5461
<> help ""
5562
)
63+
<*> switch
64+
( long "enforce-execution-units"
65+
<> help
66+
( "By default plutus-debug upon a failure will re-evaluate supplied script one more time "
67+
<> "without bounding execution in order to report expected execution units. "
68+
<> "In case when this unbounded computation is a problem, this flag allows for "
69+
<> "disabling this reporting of expected execution units."
70+
)
71+
)
5672

5773
optsParser :: Parser Opts
5874
optsParser =
5975
Opts
60-
<$> strArgument
61-
(metavar "SCRIPT_WITH_CONTEXT(BASE64)")
76+
<$> strArgument (metavar "SCRIPT_WITH_CONTEXT(BASE64)")
77+
<*> option
78+
auto
79+
( long "timeout"
80+
<> short 't'
81+
<> value 5_000_000
82+
<> help
83+
( "Timeout in number of milliseconds. Default is 5000000 ms (or 5 seconds). "
84+
<> "Specifying a negative number will effectively remove the timeout and unbound execution."
85+
)
86+
)
6287
<*> overridesParser

libs/cardano-ledger-core/app/PlutusDebug.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ main = do
2020
<> "and the script itself with the available command line options."
2121
)
2222
<> footer
23-
( "EXAMPLE: plutus-debug \"hgmCAVksj...\" --script \"5906ab010...\" "
23+
( "EXAMPLE: plutus-debug \"hgmCAVksj...\" --script \"WQrOAQEAMjI...\" "
2424
<> "Note when rewriting the script with the `--script` option "
2525
<> "you will have to provide the hex of the Plutus script as seen in "
2626
<> "`Test.Cardano.Ledger.Plutus.Examples`."
2727
)
2828
)
29-
debugPlutus optsScriptWithContext optsOverrides >>= print
29+
debugPlutus optsScriptWithContext optsTimeout optsOverrides >>= print

libs/cardano-ledger-core/src/Cardano/Ledger/Plutus/Evaluate.hs

Lines changed: 108 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
{-# LANGUAGE LambdaCase #-}
66
{-# LANGUAGE MultiParamTypeClasses #-}
77
{-# LANGUAGE NamedFieldPuns #-}
8-
{-# LANGUAGE NumericUnderscores #-}
98
{-# LANGUAGE OverloadedStrings #-}
109
{-# LANGUAGE PolyKinds #-}
1110
{-# LANGUAGE RankNTypes #-}
@@ -18,14 +17,16 @@
1817
{-# LANGUAGE UndecidableSuperClasses #-}
1918

2019
module Cardano.Ledger.Plutus.Evaluate (
21-
PlutusDebugOverrides (..),
2220
PlutusWithContext (..),
2321
ScriptFailure (..),
2422
ScriptResult (..),
2523
scriptPass,
2624
scriptFail,
2725
PlutusDebugInfo (..),
26+
PlutusDebugOverrides (..),
27+
defaultPlutusDebugOverrides,
2828
debugPlutus,
29+
debugPlutusUnbounded,
2930
runPlutusScript,
3031
runPlutusScriptWithLogs,
3132
evaluatePlutusWithContext,
@@ -66,25 +67,28 @@ import Cardano.Ledger.Plutus.Language (
6667
withSamePlutusLanguage,
6768
)
6869
import Cardano.Ledger.Plutus.TxInfo
69-
import Control.DeepSeq (NFData (..), force)
70-
import Control.Exception (evaluate)
71-
import Control.Monad (join, unless)
70+
import Codec.Extras.SerialiseViaFlat (DeserialiseFailureInfo (..), DeserialiseFailureReason (..))
71+
import Control.DeepSeq (NFData (..), deepseq, ($!!))
72+
import Control.Monad (unless)
7273
import Data.ByteString (ByteString)
7374
import qualified Data.ByteString.Base16 as B16
7475
import qualified Data.ByteString.Base64 as B64
7576
import qualified Data.ByteString.Char8 as BSC
7677
import qualified Data.ByteString.Short as SBS
7778
import qualified Data.ByteString.UTF8 as BSU
7879
import Data.Either (fromRight)
80+
import Data.Functor ((<&>))
7981
import Data.Int (Int64)
8082
import Data.List.NonEmpty (NonEmpty (..))
8183
import Data.Maybe (fromMaybe)
8284
import Data.Text (Text, pack)
8385
import GHC.Generics (Generic)
86+
import GHC.Stack (HasCallStack)
8487
import Numeric.Natural (Natural)
8588
import qualified PlutusLedgerApi.Common as P (
86-
EvaluationError (CodecError),
89+
EvaluationError (..),
8790
ExBudget,
91+
ScriptDecodeError (..),
8892
VerboseMode (..),
8993
)
9094
import Prettyprinter (Pretty (..))
@@ -231,20 +235,80 @@ data PlutusDebugInfo
231235
-- be executed within 5 second limit or there is a problem with decoding plutus script
232236
-- itself.
233237
(Maybe P.ExBudget)
238+
| -- | Script did not terminate within the imposed limit
239+
DebugTimedOut
240+
-- | Wall clock limit in microseconds that was imposed on the script execution.
241+
Int
234242
deriving (Show)
235243

244+
instance NFData PlutusDebugInfo where
245+
rnf = \case
246+
DebugBadHex str -> rnf str
247+
DebugCannotDecode str -> rnf str
248+
DebugSuccess logs exBudget -> logs `deepseq` rnf exBudget
249+
DebugFailure logs evalError pwc mExBudget ->
250+
let
251+
-- TODO: Upstream `NFData` instance for `EvaluationError`
252+
seqEvalError = \case
253+
P.CekError exc -> deepseq exc
254+
P.DeBruijnError err -> deepseq err
255+
P.CodecError err -> deepseqCodecError err
256+
P.CostModelParameterMismatch -> id
257+
P.InvalidReturnValue -> id
258+
-- TODO: Upstream `NFData` instance for `CodecError` and `MajorProtocolVersion`
259+
deepseqCodecError = \case
260+
P.CBORDeserialiseError failureInfo -> deepseqDeserialiseFailureInfo failureInfo
261+
P.RemainderError bsl -> deepseq bsl
262+
P.LedgerLanguageNotAvailableError pll ipv tpv -> pll `deepseq` ipv `seq` (tpv `seq`)
263+
P.PlutusCoreLanguageNotAvailableError pll ipv tpv -> pll `deepseq` ipv `seq` (tpv `seq`)
264+
-- TODO: Upstream `NFData` instance for `DeserialiseFailureInfo`
265+
deepseqDeserialiseFailureInfo = \case
266+
DeserialiseFailureInfo bo reason ->
267+
bo `deepseq`
268+
( -- TODO: Upstream `NFData` instance for `DeserialiseFailureReason`
269+
case reason of
270+
EndOfInput -> id
271+
ExpectedBytes -> id
272+
OtherReason str -> deepseq str
273+
)
274+
in
275+
logs `deepseq` evalError `seqEvalError` pwc `deepseq` rnf mExBudget
276+
DebugTimedOut t -> rnf t
277+
278+
-- | Various overrides that can be supplied to `plutusDebug` and `plutusDebugUnbouded`
236279
data PlutusDebugOverrides = PlutusDebugOverrides
237280
{ pdoScript :: !(Maybe ByteString)
281+
-- ^ Hex encoded version of the script
238282
, pdoProtocolVersion :: !(Maybe Version)
283+
-- ^ Protocol version to be used for decoding and exection
239284
, pdoLanguage :: !(Maybe Language)
285+
-- ^ Plutus ledger language version
240286
, pdoCostModelValues :: !(Maybe [Int64])
287+
-- ^ Cost model to be used for deciding execution units
241288
, pdoExUnitsMem :: !(Maybe Natural)
289+
-- ^ Memory execution units to be used for execution
242290
, pdoExUnitsSteps :: !(Maybe Natural)
291+
-- ^ CPU execution units to be used for execution
292+
, pdoExUnitsEnforced :: !Bool
293+
-- ^ Setting this flag to True will disable reporting expected execution units upon a failure,
294+
-- which would protect against a potentially unbounded script execution.
243295
}
244296
deriving (Show)
245297

298+
defaultPlutusDebugOverrides :: PlutusDebugOverrides
299+
defaultPlutusDebugOverrides =
300+
PlutusDebugOverrides
301+
{ pdoScript = Nothing
302+
, pdoProtocolVersion = Nothing
303+
, pdoLanguage = Nothing
304+
, pdoCostModelValues = Nothing
305+
, pdoExUnitsMem = Nothing
306+
, pdoExUnitsSteps = Nothing
307+
, pdoExUnitsEnforced = False
308+
}
309+
246310
-- TODO: Add support for overriding arguments.
247-
overrideContext :: PlutusWithContext -> PlutusDebugOverrides -> PlutusWithContext
311+
overrideContext :: HasCallStack => PlutusWithContext -> PlutusDebugOverrides -> PlutusWithContext
248312
overrideContext PlutusWithContext {..} PlutusDebugOverrides {..} =
249313
-- NOTE: due to GADTs, we can't do a record update here and need to
250314
-- copy all the fields. Otherwise GHC will greet us with
@@ -254,6 +318,7 @@ overrideContext PlutusWithContext {..} PlutusDebugOverrides {..} =
254318
, pwcScript = overrideScript
255319
, pwcExUnits = overrideExUnits
256320
, pwcCostModel = overrideCostModel
321+
, pwcScriptHash = overrideSriptHash
257322
, ..
258323
}
259324
where
@@ -266,38 +331,52 @@ overrideContext PlutusWithContext {..} PlutusDebugOverrides {..} =
266331
mkCostModel
267332
(fromMaybe (getCostModelLanguage pwcCostModel) pdoLanguage)
268333
(fromMaybe (getCostModelParams pwcCostModel) pdoCostModelValues)
269-
overrideScript =
334+
(overrideSriptHash, overrideScript) =
270335
case pdoScript of
271-
Nothing -> pwcScript
272-
Just script ->
273-
either error (Left . Plutus . PlutusBinary . SBS.toShort) . B16.decode $ BSC.filter (/= '\n') script
336+
Nothing -> (pwcScriptHash, pwcScript)
337+
Just hexScript ->
338+
case Plutus . PlutusBinary . SBS.toShort <$> B16.decode (BSC.filter (/= '\n') hexScript) of
339+
Left err -> error $ "Failed hex decoding of the custom script: " <> err
340+
Right script -> (hashPlutusScript script, Left script)
341+
342+
-- | Execute a hex encoded script with the context that was produced within the ledger predicate
343+
-- failure. Using `PlutusDebugOverrides` it is possible to override any part of the execution.
344+
debugPlutus :: HasCallStack => String -> Int -> PlutusDebugOverrides -> IO PlutusDebugInfo
345+
debugPlutus scriptsWithContext limit opts =
346+
timeout limit (pure $!! debugPlutusUnbounded scriptsWithContext opts)
347+
<&> \case
348+
Nothing -> DebugTimedOut limit
349+
Just res -> res
274350

275-
debugPlutus :: String -> PlutusDebugOverrides -> IO PlutusDebugInfo
276-
debugPlutus scriptsWithContext opts =
351+
-- | This is just like `debugPlutus`, except it is pure and if a supplied script contains an
352+
-- infinite loop or a very expensive computation, it might not terminate within a reasonable
353+
-- timeframe.
354+
debugPlutusUnbounded :: HasCallStack => String -> PlutusDebugOverrides -> PlutusDebugInfo
355+
debugPlutusUnbounded scriptsWithContext opts =
277356
case B64.decode (BSU.fromString scriptsWithContext) of
278-
Left e -> pure $ DebugBadHex (show e)
357+
Left e -> DebugBadHex e
279358
Right bs ->
280359
case Plain.decodeFull' bs of
281-
Left e -> pure $ DebugCannotDecode $ show e
360+
Left e -> DebugCannotDecode $ show e
282361
Right pwcOriginal ->
283362
let pwc = overrideContext pwcOriginal opts
284363
cm = getEvaluationContext $ pwcCostModel pwc
285364
eu = transExUnits $ pwcExUnits pwc
286-
onDecoderError err = pure $ DebugFailure [] err pwc Nothing
365+
onDecoderError err = DebugFailure [] err pwc Nothing
287366
in withRunnablePlutusWithContext pwc onDecoderError $ \plutusRunnable args ->
288-
let toDebugInfo = \case
289-
(logs, Left err@(P.CodecError {})) -> pure $ DebugFailure logs err pwc Nothing
290-
(logs, Left err) -> do
291-
mExpectedExUnits <-
292-
timeout 5_000_000 $ do
293-
let res =
294-
evaluatePlutusRunnableBudget (pwcProtocolVersion pwc) P.Verbose cm plutusRunnable args
295-
case snd res of
296-
Left {} -> pure Nothing
297-
Right exUnits -> Just <$> evaluate (force exUnits)
298-
pure $ DebugFailure logs err pwc (join mExpectedExUnits)
299-
(logs, Right ex) -> pure $ DebugSuccess logs ex
300-
in toDebugInfo $
367+
let toDebugInfo logs = \case
368+
Left err@(P.CodecError {}) -> DebugFailure logs err pwc Nothing
369+
Left err | pdoExUnitsEnforced opts -> DebugFailure logs err pwc Nothing
370+
Left err ->
371+
let res =
372+
evaluatePlutusRunnableBudget (pwcProtocolVersion pwc) P.Verbose cm plutusRunnable args
373+
mExpectedExUnits =
374+
case snd res of
375+
Left {} -> Nothing
376+
Right exUnits -> Just exUnits
377+
in DebugFailure logs err pwc mExpectedExUnits
378+
Right ex -> DebugSuccess logs ex
379+
in uncurry toDebugInfo $
301380
evaluatePlutusRunnable (pwcProtocolVersion pwc) P.Verbose cm eu plutusRunnable args
302381

303382
runPlutusScript :: PlutusWithContext -> ScriptResult

0 commit comments

Comments
 (0)