Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Require PlutusV3 scripts to evaluate to BuiltinUnit #6159

Merged
merged 6 commits into from
Jun 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions doc/read-the-docs-site/howtos/Cip57Blueprint.hs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import GHC.Generics (Generic)
import PlutusLedgerApi.V3 (BuiltinData, ScriptContext, UnsafeFromData (..))
import PlutusTx.Blueprint.TH (makeIsDataSchemaIndexed)
import PlutusTx.Lift (makeLift)
import PlutusTx.Prelude (check)
import PlutusTx.Prelude (BuiltinUnit, check)

-- END imports
-- BEGIN MyParams annotations
Expand Down Expand Up @@ -88,7 +88,7 @@ typedValidator MkMyParams{..} datum redeemer _scriptContext =
R1 -> myBool
R2 -> myInteger == datum

untypedValidator :: MyParams -> BuiltinData -> BuiltinData -> BuiltinData -> ()
untypedValidator :: MyParams -> BuiltinData -> BuiltinData -> BuiltinData -> BuiltinUnit
untypedValidator params datum redeemer scriptContext =
check $ typedValidator params datum' redeemer' scriptContext'
where
Expand Down Expand Up @@ -162,4 +162,3 @@ writeBlueprintToFile :: FilePath -> IO ()
writeBlueprintToFile path = writeBlueprint path myContractBlueprint

-- END write blueprint to file

9 changes: 7 additions & 2 deletions doc/read-the-docs-site/tutorials/AuctionValidator.hs
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,12 @@ auctionTypedValidator params (AuctionDatum highestBid) redeemer ctx@(ScriptConte
Nothing -> PlutusTx.traceError ("Not found: Output paid to highest bidder")
-- BLOCK8
{-# INLINEABLE auctionUntypedValidator #-}
auctionUntypedValidator :: AuctionParams -> BuiltinData -> BuiltinData -> BuiltinData -> ()
auctionUntypedValidator ::
AuctionParams ->
BuiltinData ->
BuiltinData ->
BuiltinData ->
PlutusTx.BuiltinUnit
auctionUntypedValidator params datum redeemer ctx =
PlutusTx.check
( auctionTypedValidator
Expand All @@ -204,7 +209,7 @@ auctionUntypedValidator params datum redeemer ctx =

auctionValidatorScript ::
AuctionParams ->
CompiledCode (BuiltinData -> BuiltinData -> BuiltinData -> ())
CompiledCode (BuiltinData -> BuiltinData -> BuiltinData -> PlutusTx.BuiltinUnit)
auctionValidatorScript params =
$$(PlutusTx.compile [||auctionUntypedValidator||])
`PlutusTx.unsafeApplyCode` PlutusTx.liftCode plcVersion100 params
Expand Down
4 changes: 2 additions & 2 deletions doc/read-the-docs-site/tutorials/BasicPolicies.hs
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,14 @@ currencyValueOf (Value m) c = case Map.lookup c m of
-- BLOCK2
-- The 'plutus-ledger' package from 'plutus-apps' provides helper functions to automate
-- some of this boilerplate.
oneAtATimePolicyUntyped :: BuiltinData -> BuiltinData -> ()
oneAtATimePolicyUntyped :: BuiltinData -> BuiltinData -> BuiltinUnit
-- 'check' fails with 'error' if the argument is not 'True'.
oneAtATimePolicyUntyped r c =
check $ oneAtATimePolicy (unsafeFromBuiltinData r) (unsafeFromBuiltinData c)

-- We can use 'compile' to turn a minting policy into a compiled Plutus Core program,
-- just as for validator scripts.
oneAtATimeCompiled :: CompiledCode (BuiltinData -> BuiltinData -> ())
oneAtATimeCompiled :: CompiledCode (BuiltinData -> BuiltinData -> BuiltinUnit)
oneAtATimeCompiled = $$(compile [|| oneAtATimePolicyUntyped ||])
-- BLOCK3
singleSignerPolicy :: () -> ScriptContext -> Bool
Expand Down
6 changes: 3 additions & 3 deletions doc/read-the-docs-site/tutorials/BasicValidators.hs
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,16 @@ beforeEnd (Date d) (Fixed e) = d <= e
beforeEnd (Date _) Never = True

-- | Check that the date in the redeemer is before the limit in the datum.
validateDate :: BuiltinData -> BuiltinData -> BuiltinData -> ()
validateDate :: BuiltinData -> BuiltinData -> BuiltinData -> BuiltinUnit
-- The 'check' function takes a 'Bool' and fails if it is false.
-- This is handy since it's more natural to talk about booleans.
validateDate datum redeemer _ =
check $ beforeEnd (unsafeFromBuiltinData datum) (unsafeFromBuiltinData redeemer)

dateValidator :: CompiledCode (BuiltinData -> BuiltinData -> BuiltinData -> ())
dateValidator :: CompiledCode (BuiltinData -> BuiltinData -> BuiltinData -> BuiltinUnit)
dateValidator = $$(compile [|| validateDate ||])
-- BLOCK4
validatePayment :: BuiltinData -> BuiltinData -> BuiltinData -> ()
validatePayment :: BuiltinData -> BuiltinData -> BuiltinData -> BuiltinUnit
validatePayment _ _ ctx =
let valCtx = unsafeFromBuiltinData ctx
-- The 'TxInfo' in the validation context is the representation of the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ import PlutusLedgerApi.V2 (CurrencySymbol, ScriptContext (scriptContextTxInfo),
import PlutusLedgerApi.V2.Contexts (valueSpent)
import PlutusTx (CompiledCode, unsafeFromBuiltinData)
import PlutusTx.Plugin ()
import PlutusTx.Prelude as PlutusTxPrelude (Bool (..), BuiltinData, check, toBuiltin, ($), (.))
import PlutusTx.Prelude as PlutusTxPrelude (Bool (..), BuiltinData, BuiltinUnit, check, toBuiltin,
($), (.))

import Cardano.Crypto.Hash qualified as Hash
import Data.ByteString qualified as BS
Expand All @@ -65,7 +66,7 @@ mkRolePayoutValidator (currency, role) _ ctx =


-- | Compute the hash of a script.
hashScript :: CompiledCode (BuiltinData -> BuiltinData -> BuiltinData -> ()) -> ScriptHash
hashScript :: CompiledCode (BuiltinData -> BuiltinData -> BuiltinData -> BuiltinUnit) -> ScriptHash
hashScript =
-- FIXME: Apparently this is the wrong recipe, since its hash disagrees with `cardano-cli`.
ScriptHash
Expand All @@ -78,11 +79,11 @@ hashScript =
{-# INLINABLE rolePayoutValidator #-}

-- | The Marlowe payout validator.
rolePayoutValidator :: CompiledCode (BuiltinData -> BuiltinData -> BuiltinData -> ())
rolePayoutValidator :: CompiledCode (BuiltinData -> BuiltinData -> BuiltinData -> BuiltinUnit)
rolePayoutValidator =
$$(PlutusTx.compile [|| rolePayoutValidator' ||])
where
rolePayoutValidator' :: BuiltinData -> BuiltinData -> BuiltinData -> ()
rolePayoutValidator' :: BuiltinData -> BuiltinData -> BuiltinData -> BuiltinUnit
rolePayoutValidator' d r p =
check
$ mkRolePayoutValidator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@ import PlutusTx (CompiledCode, makeIsDataIndexed, makeLift, unsafeFromBuiltinDat
import PlutusTx.Plugin ()
import PlutusTx.Prelude as PlutusTxPrelude (AdditiveGroup ((-)), AdditiveMonoid (zero),
AdditiveSemigroup ((+)), Bool (..), BuiltinByteString,
BuiltinData, BuiltinString, Enum (fromEnum), Eq (..),
Functor (fmap), Integer, Maybe (..), Ord ((>)),
Semigroup ((<>)), all, any, check, elem, filter, find,
foldMap, null, otherwise, snd, toBuiltin, ($), (&&),
(.), (/=), (||))
BuiltinData, BuiltinString, BuiltinUnit,
Enum (fromEnum), Eq (..), Functor (fmap), Integer,
Maybe (..), Ord ((>)), Semigroup ((<>)), all, any,
check, elem, filter, find, foldMap, null, otherwise,
snd, toBuiltin, ($), (&&), (.), (/=), (||))

import Cardano.Crypto.Hash qualified as Hash
import Data.ByteString qualified as BS
Expand Down Expand Up @@ -406,7 +406,7 @@ makeIsDataIndexed ''MarloweTxInput [('Input,0),('MerkleizedTxInput,1)]


-- | Compute the hash of a script.
hashScript :: CompiledCode (BuiltinData -> BuiltinData -> BuiltinData -> ()) -> ScriptHash
hashScript :: CompiledCode (BuiltinData -> BuiltinData -> BuiltinData -> BuiltinUnit) -> ScriptHash
hashScript =
-- FIXME: Apparently this is the wrong recipe, since its hash disagrees with `cardano-cli`.
ScriptHash
Expand All @@ -417,10 +417,10 @@ hashScript =


-- | The validator for Marlowe semantics.
marloweValidator :: CompiledCode (BuiltinData -> BuiltinData -> BuiltinData -> ())
marloweValidator :: CompiledCode (BuiltinData -> BuiltinData -> BuiltinData -> BuiltinUnit)
marloweValidator =
let
marloweValidator' :: ScriptHash -> BuiltinData -> BuiltinData -> BuiltinData -> ()
marloweValidator' :: ScriptHash -> BuiltinData -> BuiltinData -> BuiltinData -> BuiltinUnit
marloweValidator' rpvh d r p =
check
$ mkMarloweValidator rpvh
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
### Changed

- `evaluateScriptRestricting` and `evaluateScriptCounting` now require Plutus V3
scripts to return `BuiltinUnit`, otherwise the evaluation is considered to
have failed, and a `InvalidReturnValue` error is thrown. There is no such
requirement on Plutus V1 and V2 scripts.
15 changes: 10 additions & 5 deletions plutus-ledger-api/plutus-ledger-api.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -185,17 +185,22 @@ test-suite plutus-ledger-api-plugin-test
ghc-options: -threaded -rtsopts -with-rtsopts=-N
other-modules:
Spec.Budget
Spec.ReturnUnit.V1
Spec.ReturnUnit.V2
Spec.ReturnUnit.V3
Spec.Value

build-depends:
, base >=4.9 && <5
, base >=4.9 && <5
, containers
, plutus-core:{plutus-core, plutus-core-testlib} ^>=1.28
, plutus-ledger-api ^>=1.28
, plutus-tx-plugin ^>=1.28
, plutus-tx:{plutus-tx, plutus-tx-testlib} ^>=1.28
, mtl
, plutus-core:{plutus-core, plutus-core-testlib} ^>=1.28
, plutus-ledger-api:{plutus-ledger-api, plutus-ledger-api-testlib} ^>=1.28
, plutus-tx-plugin ^>=1.28
, plutus-tx:{plutus-tx, plutus-tx-testlib} ^>=1.28
, prettyprinter
, tasty
, tasty-hunit

-- This is a nightly test, so it is an executable instead of test-suite to avoid
-- running this in CI.
Expand Down
39 changes: 35 additions & 4 deletions plutus-ledger-api/src/PlutusLedgerApi/Common/Eval.hs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ module PlutusLedgerApi.Common.Eval
) where

import PlutusCore
import PlutusCore.Builtin (readKnown)
import PlutusCore.Data as Plutus
import PlutusCore.Default
import PlutusCore.Evaluation.Machine.CostModelInterface as Plutus
Expand Down Expand Up @@ -53,6 +54,7 @@ data EvaluationError =
| CodecError !ScriptDecodeError -- ^ A deserialisation error
-- TODO: make this error more informative when we have more information about what went wrong
| CostModelParameterMismatch -- ^ An error indicating that the cost model parameters didn't match what we expected
| InvalidReturnValue -- ^ The script evaluated to a value that is not a valid return value.
deriving stock (Show, Eq)
makeClassyPrisms ''EvaluationError

Expand All @@ -64,6 +66,10 @@ instance Pretty EvaluationError where
pretty (DeBruijnError e) = pretty e
pretty (CodecError e) = pretty e
pretty CostModelParameterMismatch = "Cost model parameters were not as we expected"
pretty InvalidReturnValue =
"The evaluation finished but the result value is not valid. "
<> "Plutus V3 scripts must return BuiltinUnit. "
<> "Returning any other value is considered a failure."

-- | A simple toggle indicating whether or not we should accumulate logs during script execution.
data VerboseMode =
Expand Down Expand Up @@ -226,8 +232,7 @@ evaluateScriptRestricting ll pv verbose ectx budget p args = swap $ runWriter @L
appliedTerm <- mkTermToEvaluate ll pv p args
let (res, UPLC.RestrictingSt (ExRestrictingBudget final), logs) =
evaluateTerm (UPLC.restricting $ ExRestrictingBudget budget) pv verbose ectx appliedTerm
tell logs
liftEither $ first CekError $ void res
processLogsAndErrors ll logs res
pure (budget `minusExBudget` final)

{-| Evaluates a script, returning the minimum budget that the script would need
Expand All @@ -249,10 +254,36 @@ evaluateScriptCounting ll pv verbose ectx p args = swap $ runWriter @LogOutput $
appliedTerm <- mkTermToEvaluate ll pv p args
let (res, UPLC.CountingSt final, logs) =
evaluateTerm UPLC.counting pv verbose ectx appliedTerm
tell logs
liftEither $ first CekError $ void res
processLogsAndErrors ll logs res
pure final

processLogsAndErrors ::
forall m.
(MonadError EvaluationError m, MonadWriter LogOutput m) =>
PlutusLedgerLanguage ->
LogOutput ->
Either
(UPLC.CekEvaluationException NamedDeBruijn DefaultUni DefaultFun)
(UPLC.Term UPLC.NamedDeBruijn DefaultUni DefaultFun ()) ->
m ()
processLogsAndErrors ll logs res = do
tell logs
case res of
Left e -> throwError (CekError e)
Right v -> unless (isResultValid ll v) (throwError InvalidReturnValue)
{-# INLINE processLogsAndErrors #-}

isResultValid ::
PlutusLedgerLanguage ->
UPLC.Term UPLC.NamedDeBruijn DefaultUni DefaultFun () ->
Bool
isResultValid ll res = ll == PlutusV1 || ll == PlutusV2 || isBuiltinUnit res
where
isBuiltinUnit t = case readKnown t of
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we over-constrain ourselves to BuiltinUnit ? Theoretically we would be fine with any constant? I was tempted and used asConstant to see what happens and it works, but I am not sure if it is as safe as readKnown

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what the CIP proposed. To quote @michaelpj 's words:

I don't think we should have multiple different kinds of accepted return value. unit would be okay, so long as then it has to be unit. The whole point of this CIP is to narrow down what scripts return as much as possible, so its very clear if you're doing it correctly or not. I don't really see the benefit to having a larger space of possibilities.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough.

Right () -> True
_ -> False
{-# INLINE isResultValid #-}

{- Note [Checking the Plutus Core language version]
Since long ago this check has been in `mkTermToEvaluate`, which makes it a phase 2 failure.
But this is really far too strict: we can check when deserializing, so it can be a phase 1
Expand Down
6 changes: 6 additions & 0 deletions plutus-ledger-api/test-plugin/Spec.hs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
module Main where

import Spec.Budget qualified
import Spec.ReturnUnit.V1 qualified
import Spec.ReturnUnit.V2 qualified
import Spec.ReturnUnit.V3 qualified
import Spec.Value qualified

import Test.Tasty
Expand All @@ -12,4 +15,7 @@ tests :: TestTree
tests = testGroup "plutus-ledger-api-plugin-test"
[ Spec.Budget.tests
, Spec.Value.test_EqValue
, Spec.ReturnUnit.V1.tests
, Spec.ReturnUnit.V2.tests
, Spec.ReturnUnit.V3.tests
]
87 changes: 87 additions & 0 deletions plutus-ledger-api/test-plugin/Spec/ReturnUnit/V1.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NegativeLiterals #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE ViewPatterns #-}
{-# OPTIONS_GHC -fplugin-opt PlutusTx.Plugin:target-version=1.0.0 #-}

module Spec.ReturnUnit.V1 where

import PlutusLedgerApi.Common.Versions
import PlutusLedgerApi.Test.V1.EvaluationContext qualified as V1
import PlutusLedgerApi.V1 as V1
import PlutusPrelude
import PlutusTx.Builtins qualified as PlutusTx
import PlutusTx.Code
import PlutusTx.IsData qualified as PlutusTx
import PlutusTx.Prelude (BuiltinUnit, check)
import PlutusTx.TH (compile)

import Test.Tasty (TestName, TestTree, testGroup)
import Test.Tasty.HUnit

import Control.Monad.Writer

tests :: TestTree
tests =
testGroup
"Plutus V1 validators may evaluate to any value"
[ expectSuccess "should succeed" good (I 1)
, expectSuccess "returns Bool" returnsBool (I 1)
, expectSuccess "too many parameters" tooManyParameters (I 1)
]

evalCtx :: V1.EvaluationContext
evalCtx =
fst . unsafeFromRight . runWriterT . V1.mkEvaluationContext $
fmap snd V1.costModelParamsForTesting

expectSuccess ::
forall a.
TestName ->
CompiledCode (BuiltinData -> a) ->
-- | Script argument
Data ->
TestTree
expectSuccess name code arg = testCase name $ case res of
Left _ -> assertFailure "fails"
Right _ -> pure ()
where
sScript = serialiseCompiledCode code
script = either (error . show) id $ V1.deserialiseScript conwayPV sScript
effectfully marked this conversation as resolved.
Show resolved Hide resolved
(_, res) = V1.evaluateScriptCounting conwayPV V1.Quiet evalCtx script [arg]

good :: CompiledCode (BuiltinData -> BuiltinUnit)
good =
$$( compile
[||
\d ->
let n = PlutusTx.unsafeFromBuiltinData d
in check (PlutusTx.greaterThanInteger n 0)
||]
)

returnsBool :: CompiledCode (BuiltinData -> Bool)
returnsBool =
$$( compile
[||
\d ->
let n = PlutusTx.unsafeFromBuiltinData d
in PlutusTx.greaterThanInteger n 0
||]
)

tooManyParameters :: CompiledCode (BuiltinData -> BuiltinData -> BuiltinUnit)
tooManyParameters =
effectfully marked this conversation as resolved.
Show resolved Hide resolved
$$( compile
[||
\d _ ->
let n = PlutusTx.unsafeFromBuiltinData d
in check (PlutusTx.greaterThanInteger n 0)
||]
)
Loading
Loading