diff --git a/ytxp-plutarch/examples/script-manager/doc/README.md b/ytxp-plutarch/examples/script-manager/doc/README.md new file mode 100644 index 00000000..594ed56f --- /dev/null +++ b/ytxp-plutarch/examples/script-manager/doc/README.md @@ -0,0 +1,16 @@ +# Script manager + +This is a script manager example. + +## Components + +- [Script](components/script.md) + +## Transaction Families + +- [Script deploying](transaction-families/deploying.md) +- [Script removing](transaction-families/removing.md) + +## Transaction Flows + +None. diff --git a/ytxp-plutarch/examples/script-manager/doc/components/script.md b/ytxp-plutarch/examples/script-manager/doc/components/script.md new file mode 100644 index 00000000..1e8ee235 --- /dev/null +++ b/ytxp-plutarch/examples/script-manager/doc/components/script.md @@ -0,0 +1,41 @@ +# Script component + +This component holds the deployed script. + +## UTxO Specification + +The UTxO reflecting this component has the following attributes: + +### Address + +The component sits on the address of the YTxP yielding-validator of the protocol. + +### Value + +Undefined. + +### Datum + +The component must have a unit datum. + +### Reference Script + +The content is undefined, although it is expected to contain the deployed script. + +## States + +The Script component does not have meaningful states. + +## Participation in Transaction Families + +### Introduction + +- [Script deploying](../transaction-families/deploying.md) + +### Modification + +None. + +### Termination + +- [Script removing](../transaction-families/removing.md) diff --git a/ytxp-plutarch/examples/script-manager/doc/transaction-families/deploying.md b/ytxp-plutarch/examples/script-manager/doc/transaction-families/deploying.md new file mode 100644 index 00000000..2f5b5df3 --- /dev/null +++ b/ytxp-plutarch/examples/script-manager/doc/transaction-families/deploying.md @@ -0,0 +1,72 @@ +# Script Deploying Transaction Family + +This transaction family describes the deployment of a script. + +## Script Execution Specification + +### Transaction Family Parameters + +1. `yieldingValidatorAddress :: ScriptAddress` + The address locking yielding UTxOs + +2. `yieldingMPSymbol :: CurrencySymbol` + The currency symbol of the script component token + +3. `signatory :: PubKeyHash` + The signatory required to authorize the deployment + +### Script Purpose + +This script must fail on any script purpose except `Rewarding`. + +### Redeemer + +None. + +### Reference Inputs + +This transaction family does not check the reference inputs to determine whether the script is successful or not. + +However, the following reference inputs should be present to support the YTxP. + +1. Authorized Reference Script UTxO (1) +This input must contain the transaction hash for the `AuthorisedScript` script representing this transaction family. + +### Inputs + +Undefined. + +### Redeemer Map + +Undefined. + +### Mints + +1. Script component token minting policy (1) + +Other mints may happen. + +### Outputs + +The outputs of this transaction family are + +1. Script (1) + - The Script component is created at the yielding validator script address with no staking address + - The Script component value has + - 1 Script component token + - The datum must: + - Must have a unit datum + +Other outputs are permitted. + +### Staking Events + +The withdrawal from this staking validator. + +### Validity Range + +Undefined. + +### Signatories + +The transaction must be signed by the signatory specified in the transaction family parameter. diff --git a/ytxp-plutarch/examples/script-manager/doc/transaction-families/removing.md b/ytxp-plutarch/examples/script-manager/doc/transaction-families/removing.md new file mode 100644 index 00000000..0d77ccc3 --- /dev/null +++ b/ytxp-plutarch/examples/script-manager/doc/transaction-families/removing.md @@ -0,0 +1,63 @@ +# Script Removing Transaction Family + +This transaction family describes the removal of a script. + +## Script Execution Specification + +### Transaction Family Parameters + +1. `yieldingMPSymbol :: CurrencySymbol` + The currency symbol of the script component token +2. `signatory :: PubKeyHash` + The signatory required to authorize the deployment + +### Script Purpose + +This script must fail on any script purpose except `Rewarding`. + +### Redeemer + +None. + +### Reference Inputs + +This transaction family does not check the reference inputs to determine whether the script is successful or not. + +However, the follow reference inputs should be present to support the YTxP. + +1. Authorized Reference Script UTxO (1) +This input must contain the transaction hash for the `AuthorisedScript` script representing this transaction family. + +### Inputs + +1. Script (1) + - The Script component value has + - 1 Script component token + +Other inputs may exist. + +### Redeemer Map + +Undefined. + +### Mints + +1. Script component token minting policy (-1) + +Other mints may happen. + +### Outputs + +Undefined. + +### Staking Events + +The withdrawal of this staking validator. + +### Validity Range + +Undefined. + +### Signatories + +The transaction must be signed by the signatory specified in the transaction family parameter. diff --git a/ytxp-plutarch/examples/script-manager/src/Cardano/YTxP/Example/PUtils.hs b/ytxp-plutarch/examples/script-manager/src/Cardano/YTxP/Example/PUtils.hs new file mode 100644 index 00000000..cb62cd12 --- /dev/null +++ b/ytxp-plutarch/examples/script-manager/src/Cardano/YTxP/Example/PUtils.hs @@ -0,0 +1,94 @@ +{-# LANGUAGE LambdaCase #-} + +module Cardano.YTxP.Example.PUtils ( + pisRewarding, + ptraceDebugC, + tryGetComponentOutput, + tryGetComponentInput, +) where + +import Plutarch.LedgerApi.V3 ( + PAddress, + PCurrencySymbol, + PScriptInfo (PRewardingScript), + PTxInInfo, + PTxOut, + ptxInInfo'resolved, + ptxOut'address, + ptxOut'value, + ) +import Plutarch.LedgerApi.Value (pvalueOf) +import PlutusLedgerApi.V3 (TokenName (TokenName)) + +-- | Return False if script purpose is not rewarding +pisRewarding :: Term s (PScriptInfo :--> PBool) +pisRewarding = phoistAcyclic $ plam $ \purpose -> + pmatch purpose $ \case + PRewardingScript _ -> pcon PTrue + _other -> pcon PFalse + +-- | Like `ptraceDebug`, but works in a `TermCont` monad +ptraceDebugC :: Term s PString -> TermCont s () +ptraceDebugC s = tcont $ \f -> ptraceInfo s (f ()) + +-- | Find the first valid component output +tryGetComponentOutput :: + Term s PCurrencySymbol -> + Term s PAddress -> + Term s (PBuiltinList (PAsData PTxOut)) -> + Term s (PAsData PTxOut) +tryGetComponentOutput yieldingMPSymbol yieldingValidatorAddress outputs = + pmatch outputs $ \case + PCons componentOutput' rest -> unTermCont $ do + componentOutput <- pmatchC $ pfromData componentOutput' + return $ + pif + ( ( pvalueOf + # pfromData (ptxOut'value componentOutput) + # yieldingMPSymbol + # pconstant (TokenName "") + ) + #== pconstant 1 + #&& yieldingValidatorAddress + #== ptxOut'address componentOutput + ) + componentOutput' + ( pif + (rest #== pcon PNil) + ( ptraceInfoError + "tryGetComponentOutput: Component not found" + ) + (tryGetComponentOutput yieldingMPSymbol yieldingValidatorAddress rest) + ) + _other -> ptraceInfoError "tryGetComponentOutput: Empty output list" + +-- | Find the first valid component input +tryGetComponentInput :: + Term s PCurrencySymbol -> + Term s (PBuiltinList (PAsData PTxInInfo)) -> + Term s PTxOut +tryGetComponentInput yieldingMPSymbol inputs = + pmatch inputs $ \case + PCons componentInInfo' rest -> unTermCont $ do + componentInInfo <- pmatchC $ pfromData componentInInfo' + resolved' <- pletC $ ptxInInfo'resolved componentInInfo + resolved <- pmatchC resolved' + + return $ + pif + ( ( pvalueOf + # pfromData (ptxOut'value resolved) + # yieldingMPSymbol + # pconstant (TokenName "") + ) + #== pconstant 1 + ) + resolved' + ( pif + (rest #== pcon PNil) + ( ptraceInfoError + "tryGetComponentInput: Component not found" + ) + (tryGetComponentInput yieldingMPSymbol rest) + ) + _other -> ptraceInfoError "tryGetComponentInput: Empty output list" diff --git a/ytxp-plutarch/examples/script-manager/src/Cardano/YTxP/Example/Script/Deploying.hs b/ytxp-plutarch/examples/script-manager/src/Cardano/YTxP/Example/Script/Deploying.hs new file mode 100644 index 00000000..629ff3c0 --- /dev/null +++ b/ytxp-plutarch/examples/script-manager/src/Cardano/YTxP/Example/Script/Deploying.hs @@ -0,0 +1,101 @@ +{-# LANGUAGE OverloadedRecordDot #-} + +module Cardano.YTxP.Example.Script.Deploying ( + -- * TxF Params + Params, + + -- * Script + deployingTxF, +) where + +import Cardano.YTxP.Example.PUtils ( + pisRewarding, + ptraceDebugC, + tryGetComponentOutput, + ) +import Control.Monad (void) +import Plutarch.Builtin.Unit (punit) +import Plutarch.LedgerApi.V3 ( + PDatum (PDatum), + POutputDatum (POutputDatum), + PScriptContext (pscriptContext'scriptInfo), + pscriptContext'txInfo, + ptxInfo'mint, + ptxInfo'outputs, + ptxInfo'signatories, + ptxOut'datum, + ) +import Plutarch.LedgerApi.Value (pvalueOf) +import PlutusLedgerApi.V3 ( + Address (Address), + Credential (ScriptCredential), + CurrencySymbol, + PubKeyHash, + ScriptHash, + TokenName (TokenName), + ) + +-- * Parameters + +-- | Parameters for the Deploying Transaction Family +data Params = Params + { yieldingValidatorScriptHash :: !ScriptHash + , yieldingMPSymbol :: !CurrencySymbol + , signatory :: !PubKeyHash + } + deriving stock (Show) + +-- * Script + +{- | Deploying Transaction Family implemented as a YTxP YieldedTo Staking Validator. +Refer to the transaction family specification (examples/script-manager/doc/transaction-families/deploying.md) +for a complete description. +-} +deployingTxF :: Params -> Term s (PScriptContext :--> PUnit) +deployingTxF params = phoistAcyclic $ plam $ \context' -> unTermCont $ do + -- ScriptContext extraction + + ptraceDebugC "ScriptContext extraction" + + context <- pmatchC context' + + -- Ensures that this script is activated by the rewarding event + void $ + pguardC "The current script is expected to be activated by a rewarding event" $ + pisRewarding # pscriptContext'scriptInfo context + + txInfo <- pmatchC $ pscriptContext'txInfo context + + -- Deploying must be authorised by the signatory + pguardC "The transaction is expected to be signed by the signatory" $ + pelem + # pdata (pconstant params.signatory) + # pfromData (ptxInfo'signatories txInfo) + + -- The component token must be minted + pguardC "The component token is expected to be minted" $ + pvalueOf + # pfromData (ptxInfo'mint txInfo) + # pconstant params.yieldingMPSymbol + # pconstant (TokenName "") + #== 1 + + -- Script output extraction + + ptraceDebugC "Script output extraction" + + scriptOutput <- + pmatchC $ + pfromData $ + tryGetComponentOutput + (pconstant params.yieldingMPSymbol) + ( pconstant . flip Address Nothing . ScriptCredential $ + params.yieldingValidatorScriptHash + ) + (pfromData $ ptxInfo'outputs txInfo) + + pguardC "The component must have a unit datum" $ + ptxOut'datum scriptOutput + #== pcon (POutputDatum $ pcon $ PDatum (pforgetData (pdata punit))) + + pure punit diff --git a/ytxp-plutarch/examples/script-manager/src/Cardano/YTxP/Example/Script/Removing.hs b/ytxp-plutarch/examples/script-manager/src/Cardano/YTxP/Example/Script/Removing.hs new file mode 100644 index 00000000..a0e04265 --- /dev/null +++ b/ytxp-plutarch/examples/script-manager/src/Cardano/YTxP/Example/Script/Removing.hs @@ -0,0 +1,80 @@ +{-# LANGUAGE OverloadedRecordDot #-} + +module Cardano.YTxP.Example.Script.Removing ( + -- * TxF Params + Params, + + -- * Script + removingTxF, +) where + +import Cardano.YTxP.Example.PUtils ( + pisRewarding, + ptraceDebugC, + tryGetComponentInput, + ) +import Control.Monad (void) +import Plutarch.Builtin.Unit (punit) +import Plutarch.LedgerApi.V3 ( + PScriptContext (pscriptContext'scriptInfo), + pscriptContext'txInfo, + ptxInfo'inputs, + ptxInfo'mint, + ptxInfo'signatories, + ) +import Plutarch.LedgerApi.Value (pvalueOf) +import PlutusLedgerApi.V3 (CurrencySymbol, PubKeyHash, TokenName (TokenName)) + +-- * Parameters + +-- | Parameters for the Removing Transaction Family +data Params = Params + { yieldingMPSymbol :: CurrencySymbol + , signatory :: PubKeyHash + } + deriving stock (Show) + +-- * Script + +{- | Removing Transaction Family implemented as a YTxP YieldedTo Staking Validator. +Refer to the transaction family specification (examples/script-manager/doc/transaction-families/removing.md) +for a complete description. +-} +removingTxF :: Params -> Term s (PScriptContext :--> PUnit) +removingTxF params = phoistAcyclic $ plam $ \context' -> unTermCont $ do + -- ScriptContext extraction + + ptraceDebugC "ScriptContext extraction" + + context <- pmatchC context' + txInfo' <- pletC $ pscriptContext'txInfo context + scriptInfo <- pletC $ pscriptContext'scriptInfo context + txInfo <- pmatchC txInfo' + + -- Deploying must be authorised by the signatory + pguardC "The transaction is expected to be signed by the signatory" $ + pelem + # pdata (pconstant params.signatory) + # pfromData (ptxInfo'signatories txInfo) + + -- Ensures that this script is activated by the rewarding event + void $ + pguardC "The current script is expected to be activated by a rewarding event" $ + pisRewarding # scriptInfo + + -- The component token must be burned + pguardC "The component token is expected to be burned" $ + pvalueOf + # pfromData (ptxInfo'mint txInfo) + # pconstant params.yieldingMPSymbol + # pconstant (TokenName "") + #== -1 + + -- Ensures there is a valid component input + _ <- + pmatchC $ + tryGetComponentInput + (pconstant params.yieldingMPSymbol) + (pfromData $ ptxInfo'inputs txInfo) + + pure punit diff --git a/ytxp-plutarch/src/Cardano/YTxP/Control/Yielding/Helper.hs b/ytxp-plutarch/src/Cardano/YTxP/Control/Yielding/Helper.hs index bf6eaeda..d4d2dad1 100644 --- a/ytxp-plutarch/src/Cardano/YTxP/Control/Yielding/Helper.hs +++ b/ytxp-plutarch/src/Cardano/YTxP/Control/Yielding/Helper.hs @@ -1,7 +1,11 @@ {- | This module export a helper function that produces a two argument yielding script that we use to implement the logic for yielding validator, minting policy and staking validator -} -module Cardano.YTxP.Control.Yielding.Helper (yieldingHelper) where +module Cardano.YTxP.Control.Yielding.Helper ( + yieldingHelper, + oneshotHelper, +) +where import Cardano.YTxP.Control.Yielding ( PAuthorisedScriptPurpose (PMinting, PRewarding, PSpending), @@ -14,7 +18,9 @@ import Plutarch.LedgerApi.V3 ( PCurrencySymbol, PRedeemer (PRedeemer), PScriptContext, + PTxOutRef, paddress'credential, + pfindOwnInput, pscriptContext'redeemer, pscriptContext'txInfo, ptxInInfo'resolved, @@ -24,6 +30,7 @@ import Plutarch.LedgerApi.V3 ( ptxInfo'wdrl, ptxOut'address, ) +import Plutarch.Maybe (pisJust) import Utils (pcheck, pscriptHashToCurrencySymbol) -- - Look at the UTxO at the `n` th entry in the `txInfoReferenceInputs`, where `n` is equal to `authorisedScriptIndex`. @@ -90,3 +97,17 @@ yieldingHelper = plam $ \pylstcs ctx' -> unTermCont $ do PPubKeyCredential _ -> ptraceInfoError "Staking credential at specified index is not a script credential" + +-- | One-Shot check +oneshotHelper :: + forall (s :: S). + Term + s + ( PTxOutRef :--> PScriptContext :--> PBool + ) +oneshotHelper = plam $ \oref ctx -> pmatch ctx $ + \ctx' -> pmatch (pscriptContext'txInfo ctx') $ + \txInfo -> + -- FIXME: There is no list fusion atm. + let inputs = pmap # plam pfromData # pfromData (ptxInfo'inputs txInfo) + in pisJust #$ pfindOwnInput # inputs # oref diff --git a/ytxp-plutarch/src/Cardano/YTxP/Control/Yielding/Scripts.hs b/ytxp-plutarch/src/Cardano/YTxP/Control/Yielding/Scripts.hs index 1f144dcc..1203ac24 100644 --- a/ytxp-plutarch/src/Cardano/YTxP/Control/Yielding/Scripts.hs +++ b/ytxp-plutarch/src/Cardano/YTxP/Control/Yielding/Scripts.hs @@ -1,9 +1,18 @@ module Cardano.YTxP.Control.Yielding.Scripts ( yielding, + yielding', ) where -import Cardano.YTxP.Control.Yielding.Helper (yieldingHelper) -import Plutarch.LedgerApi.V3 (PCurrencySymbol, PScriptContext) +import Cardano.YTxP.Control.Yielding.Helper ( + oneshotHelper, + yieldingHelper, + ) +import Plutarch.Builtin.Unit (punit) +import Plutarch.LedgerApi.V3 ( + PCurrencySymbol, + PScriptContext, + PTxOutRef, + ) -------------------------------------------------------------------------------- -- Plutarch level terms @@ -17,3 +26,20 @@ yielding :: ) yielding = plam $ \psymbol _nonce ctx -> yieldingHelper # psymbol # ctx + +-- | Yielding Validator with one shot backdoor +yielding' :: + forall (s :: S). + Term + s + ( PTxOutRef + :--> PCurrencySymbol + :--> PAsData PInteger + :--> PScriptContext + :--> PUnit + ) +yielding' = plam $ \oref psymbol _nonce ctx -> + pif + (oneshotHelper # oref # ctx) + punit + (yieldingHelper # psymbol # ctx) diff --git a/ytxp-plutarch/test/Cardano/YTxP/Test/Control/Yielding/Scripts.hs b/ytxp-plutarch/test/Cardano/YTxP/Test/Control/Yielding/Scripts.hs index a5d90068..dab3bf67 100644 --- a/ytxp-plutarch/test/Cardano/YTxP/Test/Control/Yielding/Scripts.hs +++ b/ytxp-plutarch/test/Cardano/YTxP/Test/Control/Yielding/Scripts.hs @@ -1,6 +1,3 @@ -{-# OPTIONS_GHC -Wno-deferred-out-of-scope-variables #-} -{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-} - module Cardano.YTxP.Test.Control.Yielding.Scripts (tests) where import Cardano.YTxP.SDK.SdkParameters ( @@ -15,11 +12,12 @@ import Cardano.YTxP.Test.Control.Yielding.Scripts.Utils ( ScriptsTestsParams, authorisedScriptHash, authorisedScriptsManagerHash, - authorisedScriptsSTCS + authorisedScriptsSTCS, + oneshotUtxo ), ) import Control.Monad.Reader (Reader, runReader) -import PlutusLedgerApi.V3 (CurrencySymbol (CurrencySymbol)) +import PlutusLedgerApi.V3 (CurrencySymbol (CurrencySymbol), TxOutRef (TxOutRef)) import Test.Tasty (TestTree, testGroup) dummyParams :: ScriptsTestsParams @@ -32,11 +30,17 @@ dummyParams = CurrencySymbol "33333333333333333333333333333333333333333333333333333333" , authorisedScriptsManagerHash = "11111111111111111111111111111111111111111111111111111111" + , oneshotUtxo = + TxOutRef "d44c22ef78ab49fd975ef4f07e0c8440ede296efca48eeed425096ab783c41d1" 0 } tests :: TestTree tests = runReader testsR dummyParams + testsR :: Reader ScriptsTestsParams TestTree testsR = - let tests' = [testNominalCasesR, testAttacksR] + let tests' = + [ testNominalCasesR + , testAttacksR + ] in testGroup "YieldingScripts" <$> sequence tests' diff --git a/ytxp-plutarch/test/Cardano/YTxP/Test/Control/Yielding/Scripts/Attacks.hs b/ytxp-plutarch/test/Cardano/YTxP/Test/Control/Yielding/Scripts/Attacks.hs index 06e8496f..4fc2675d 100644 --- a/ytxp-plutarch/test/Cardano/YTxP/Test/Control/Yielding/Scripts/Attacks.hs +++ b/ytxp-plutarch/test/Cardano/YTxP/Test/Control/Yielding/Scripts/Attacks.hs @@ -20,23 +20,27 @@ import Cardano.YTxP.SDK.SdkParameters ( ) import Cardano.YTxP.Test.Control.Yielding.Scripts.NominalCases ( mintNominalCaseBuilderR, + oneshotNominalCaseBuilderR, rewardNominalCaseBuilderR, spendNominalCaseBuilderR, ) import Cardano.YTxP.Test.Control.Yielding.Scripts.ScriptsBuilders ( yieldingScriptR, + yieldingScriptR', ) import Cardano.YTxP.Test.Control.Yielding.Scripts.Utils ( ScriptsTestsParams ( authorisedScriptHash, authorisedScriptsSTCS ), + oneshotUtxo, toLedgerRedeemer, ) import Control.Lens (over, set, traversed, view, (&), _1, _2, _Wrapped) import Control.Monad.Reader (Reader, asks) import Data.Monoid (Endo (Endo, appEndo)) import Optics qualified as PlutusLedgerApiOptics +import Plutarch.Script (Script) import PlutusLedgerApi.V3 ( Credential (ScriptCredential), CurrencySymbol (CurrencySymbol), @@ -45,6 +49,7 @@ import PlutusLedgerApi.V3 ( ScriptHash, ToData (toBuiltinData), TxInInfo, + TxOutRef (TxOutRef), Value (Value, getValue), getScriptHash, unsafeFromBuiltinData, @@ -60,12 +65,21 @@ import Text.RE.TDFA.Text (re) testAttacksR :: Reader ScriptsTestsParams TestTree testAttacksR = do - -- Yielding Script - yScript <- yieldingScriptR + yAttacks <- yieldingScriptR >>= attackTestTrees + yOneshotAttacks <- yieldingScriptR' >>= attackTestTrees + pure $ + testGroup + "Attacks" + [ testGroup "Yielding" yAttacks + , testGroup "Yielding with oneshot backdoor" yOneshotAttacks + ] +attackTestTrees :: Script -> Reader ScriptsTestsParams [TestTree] +attackTestTrees script = do -- Redeemers and contexts mintNominalContext <- mintNominalCaseBuilderR spendNominalContext <- spendNominalCaseBuilderR + oneshotNominalContext <- oneshotNominalCaseBuilderR rewardNominalContext <- rewardNominalCaseBuilderR -- Attacks @@ -80,80 +94,89 @@ testAttacksR = do ppAttackAuthorisedVProofIndexInvalid <- mkAttack attackAuthorisedProofIndexInvalidIndex - ppAttackAuthorisedVProofMismatch <- mkAttack attackAuthorisedVProofIndexMismatch + ppAttackAuthorisedVProofMismatch <- + mkAttack attackAuthorisedVProofIndexMismatch ppAttackAuthorisedSVProofIndexInvalid <- mkAttack attackAuthorisedProofIndexInvalidIndex ppAttackAuthorisedSVProofMismatch <- mkAttack attackAuthorisedSVProofIndexMismatch - pure $ - testGroup - "Attacks" - [ txfCEKUnitCase $ - attackCaseBasicRegex - "ref input not present" - [re|^(.*)$|] - mintNominalContext - yScript - ppNoRefInput - , txfCEKUnitCase $ - attackCaseBasicRegex - "ref input present but not authorised" - [re|Reference input does not contain AuthorisedScriptsSTCS|] - mintNominalContext - yScript - ppRefInputNoAuth - , txfCEKUnitCase $ - attackCaseBasicRegex - "attackAuthorisedScriptIndex does not points to valid reference input" - [re|^(.*)$|] - mintNominalContext - yScript - ppAuthorisedScriptIndexInvalid - , txfCEKUnitCase $ - attackCaseBasicRegex - "(MP) AuthorisedScriptProofIndex does not index to valid proof" - [re|^(.*)$|] - mintNominalContext - yScript - ppAttackAuthorisedMPProofIndexInvalid - , txfCEKUnitCase $ - attackCaseBasicRegex - "(MP) AuthorisedScriptProofIndex point to a wrong script" - [re|Minting policy does not match expected authorised minting policy|] - mintNominalContext - yScript - ppAttackAuthorisedMPProofMismatch - , txfCEKUnitCase $ - attackCaseBasicRegex - "(V) AuthorisedScriptProofIndex does not index to valid proof" - [re|^(.*)$|] - spendNominalContext - yScript - ppAttackAuthorisedVProofIndexInvalid - , txfCEKUnitCase $ - attackCaseBasicRegex - "(V) AuthorisedScriptProofIndex point to a wrong script" - [re|Input does not match expected authorised validator|] - spendNominalContext - yScript - ppAttackAuthorisedVProofMismatch - , txfCEKUnitCase $ - attackCaseBasicRegex - "(SV) AuthorisedScriptProofIndex does not index to valid proof" - [re|^(.*)$|] - rewardNominalContext - yScript - ppAttackAuthorisedSVProofIndexInvalid - , txfCEKUnitCase $ - attackCaseBasicRegex - "(SV) AuthorisedScriptProofIndex point to a wrong script" - [re|Withdrawal does not match expected authorised staking validator|] - rewardNominalContext - yScript - ppAttackAuthorisedSVProofMismatch - ] + ppAttackOneshotTxOutRefMismatch <- + mkAttack attackOneshotTxOutRefMismatch + + pure + [ txfCEKUnitCase $ + attackCaseBasicRegex + "ref input not present" + [re|^(.*)$|] + mintNominalContext + script + ppNoRefInput + , txfCEKUnitCase $ + attackCaseBasicRegex + "ref input present but not authorised" + [re|Reference input does not contain AuthorisedScriptsSTCS|] + mintNominalContext + script + ppRefInputNoAuth + , txfCEKUnitCase $ + attackCaseBasicRegex + "attackAuthorisedScriptIndex does not points to valid reference input" + [re|^(.*)$|] + mintNominalContext + script + ppAuthorisedScriptIndexInvalid + , txfCEKUnitCase $ + attackCaseBasicRegex + "(MP) AuthorisedScriptProofIndex does not index to valid proof" + [re|^(.*)$|] + mintNominalContext + script + ppAttackAuthorisedMPProofIndexInvalid + , txfCEKUnitCase $ + attackCaseBasicRegex + "(MP) AuthorisedScriptProofIndex point to a wrong script" + [re|Minting policy does not match expected authorised minting policy|] + mintNominalContext + script + ppAttackAuthorisedMPProofMismatch + , txfCEKUnitCase $ + attackCaseBasicRegex + "(V) AuthorisedScriptProofIndex does not index to valid proof" + [re|^(.*)$|] + spendNominalContext + script + ppAttackAuthorisedVProofIndexInvalid + , txfCEKUnitCase $ + attackCaseBasicRegex + "(V) AuthorisedScriptProofIndex point to a wrong script" + [re|Input does not match expected authorised validator|] + spendNominalContext + script + ppAttackAuthorisedVProofMismatch + , txfCEKUnitCase $ + attackCaseBasicRegex + "(SV) AuthorisedScriptProofIndex does not index to valid proof" + [re|^(.*)$|] + rewardNominalContext + script + ppAttackAuthorisedSVProofIndexInvalid + , txfCEKUnitCase $ + attackCaseBasicRegex + "(SV) AuthorisedScriptProofIndex point to a wrong script" + [re|Withdrawal does not match expected authorised staking validator|] + rewardNominalContext + script + ppAttackAuthorisedSVProofMismatch + , txfCEKUnitCase $ + attackCaseBasicRegex + "Spend the wrong oneshot UTXO" + [re|^(.*)$|] + oneshotNominalContext + script + ppAttackOneshotTxOutRefMismatch + ] ----------------------------------------------------------------------- -- Attacks helpers @@ -199,6 +222,12 @@ updateScriptCredential oldScriptHash newScriptHash cred | cred == ScriptCredential oldScriptHash = ScriptCredential newScriptHash | otherwise = cred +-- | Replace an @TxOutRef@ with a new one if a match is found +updateUtxo :: TxOutRef -> TxOutRef -> TxOutRef -> TxOutRef +updateUtxo oldUtxo newUtxo currentUtxo + | currentUtxo == oldUtxo = newUtxo + | otherwise = oldUtxo + -- | Replace an @ScriptHash@ credentials for input with a provided one (if present) replaceInput :: ScriptHash -> ScriptHash -> TxInInfo -> TxInInfo replaceInput oldScriptHash newScriptHash = @@ -222,6 +251,13 @@ replaceWdrl oldScript newScript = in replaceIfPresent oldCredential newCredential +-- | Replace an @TxOutRef@ for input with a provided one (if present) +replaceUtxo :: TxOutRef -> TxOutRef -> TxInInfo -> TxInInfo +replaceUtxo oldUtxo newUtxo = + over + PlutusLedgerApiOptics.txOutRef + (updateUtxo oldUtxo newUtxo) + ----------------------------------------------------------------------- -- Attacks @@ -344,3 +380,16 @@ attackAuthorisedSVProofIndexMismatch = do over (PlutusLedgerApiOptics.txInfo . PlutusLedgerApiOptics.wdrl) (replaceWdrl authorisedValidator differentValidator) + +attackOneshotTxOutRefMismatch :: + Reader ScriptsTestsParams (Endo ScriptContext) +attackOneshotTxOutRefMismatch = do + outref <- asks oneshotUtxo + let + differentOneshotUtxo = + TxOutRef "d44c22ef78ab49fd975ef4f07e0c8440ede296efca48eeed425096ab783c41d1" 1 + pure $ + Endo $ + over + (PlutusLedgerApiOptics.txInfo . PlutusLedgerApiOptics.inputs . traversed) + (replaceUtxo outref differentOneshotUtxo) diff --git a/ytxp-plutarch/test/Cardano/YTxP/Test/Control/Yielding/Scripts/NominalCases.hs b/ytxp-plutarch/test/Cardano/YTxP/Test/Control/Yielding/Scripts/NominalCases.hs index bdc168b0..198752ca 100644 --- a/ytxp-plutarch/test/Cardano/YTxP/Test/Control/Yielding/Scripts/NominalCases.hs +++ b/ytxp-plutarch/test/Cardano/YTxP/Test/Control/Yielding/Scripts/NominalCases.hs @@ -3,6 +3,7 @@ module Cardano.YTxP.Test.Control.Yielding.Scripts.NominalCases ( mintNominalCaseBuilderR, spendNominalCaseBuilderR, rewardNominalCaseBuilderR, + oneshotNominalCaseBuilderR, ) where import Cardano.TestUtils (nominalCaseBasic, txfCEKUnitCase) @@ -14,11 +15,13 @@ import Cardano.YTxP.SDK.Redeemers ( ) import Cardano.YTxP.Test.Control.Yielding.Scripts.ScriptsBuilders ( yieldingScriptR, + yieldingScriptR', ) import Cardano.YTxP.Test.Control.Yielding.Scripts.Utils ( ScriptsTestsParams, authorisedScriptRefInputContext, mintContext, + oneshotSpendContext, rewardContext, spendContext, ) @@ -37,15 +40,27 @@ import Test.Tasty (TestTree, testGroup) testNominalCasesR :: Reader ScriptsTestsParams TestTree testNominalCasesR = do yieldingScript <- yieldingScriptR - context' <- mintNominalCaseBuilderR + oneshotYieldingScript <- yieldingScriptR' + context <- mintNominalCaseBuilderR + oneshotContext <- oneshotNominalCaseBuilderR pure $ testGroup "Nominal Case" [ txfCEKUnitCase $ nominalCaseBasic "Yielding Case" - context' + context yieldingScript + , txfCEKUnitCase $ + nominalCaseBasic + "Yielding Case (with oneshot backdoor script)" + context + oneshotYieldingScript + , txfCEKUnitCase $ + nominalCaseBasic + "Backdoor Case" + oneshotContext + oneshotYieldingScript ] -- | Helper that produces a @Reader@ that yields a compiled a redeemerScript, throws an error is compilation fails @@ -59,6 +74,11 @@ mkNominalCaseBuilderR redeemer builder contextBuilder = do context <- (<>) <$> authorisedScriptRefInputContext <*> builder pure $ contextBuilder $ scriptRedeemer redeemer <> mkOutRefIndices context +oneshotNominalCaseBuilderR :: + Reader ScriptsTestsParams ScriptContext +oneshotNominalCaseBuilderR = + buildSpending' . mkOutRefIndices <$> oneshotSpendContext + mintNominalCaseBuilderR :: Reader ScriptsTestsParams ScriptContext mintNominalCaseBuilderR = diff --git a/ytxp-plutarch/test/Cardano/YTxP/Test/Control/Yielding/Scripts/ScriptsBuilders.hs b/ytxp-plutarch/test/Cardano/YTxP/Test/Control/Yielding/Scripts/ScriptsBuilders.hs index 5651f157..0b389108 100644 --- a/ytxp-plutarch/test/Cardano/YTxP/Test/Control/Yielding/Scripts/ScriptsBuilders.hs +++ b/ytxp-plutarch/test/Cardano/YTxP/Test/Control/Yielding/Scripts/ScriptsBuilders.hs @@ -1,14 +1,16 @@ module Cardano.YTxP.Test.Control.Yielding.Scripts.ScriptsBuilders ( yieldingScriptR, + yieldingScriptR', ) where -import Cardano.YTxP.Control.Yielding.Scripts (yielding) +import Cardano.YTxP.Control.Yielding.Scripts (yielding, yielding') import Cardano.YTxP.SDK.SdkParameters ( AuthorisedScriptsSTCS (AuthorisedScriptsSTCS), ) import Cardano.YTxP.Test.Control.Yielding.Scripts.Utils ( ScriptsTestsParams, authorisedScriptsSTCS, + oneshotUtxo, ) import Control.Monad.Reader (Reader, asks) import Data.Text qualified as T @@ -33,3 +35,19 @@ yieldingScriptR = do case compile (Tracing LogInfo DetTracing) closedTerm of Left err -> error $ unwords ["Plutarch compilation error:", T.unpack err] Right script' -> pure script' + +{- | Helper that produces a @Reader@ that yields a compiled Script, throws an error is compilation fails +This yielding script comes with a oneshot backdoor +-} +yieldingScriptR' :: Reader ScriptsTestsParams Script +yieldingScriptR' = do + (AuthorisedScriptsSTCS authorisedScriptsSTCS') <- asks authorisedScriptsSTCS + utxo <- asks oneshotUtxo + let + closedTerm :: + forall (s :: S). + Term s (PScriptContext :--> PUnit) + closedTerm = yielding' # pconstant utxo # pconstant authorisedScriptsSTCS' # pconstant 42 + case compile (Tracing LogInfo DetTracing) closedTerm of + Left err -> error $ unwords ["Plutarch compilation error:", T.unpack err] + Right script' -> pure script' diff --git a/ytxp-plutarch/test/Cardano/YTxP/Test/Control/Yielding/Scripts/Utils.hs b/ytxp-plutarch/test/Cardano/YTxP/Test/Control/Yielding/Scripts/Utils.hs index 42128c80..321a1e72 100644 --- a/ytxp-plutarch/test/Cardano/YTxP/Test/Control/Yielding/Scripts/Utils.hs +++ b/ytxp-plutarch/test/Cardano/YTxP/Test/Control/Yielding/Scripts/Utils.hs @@ -6,13 +6,15 @@ module Cardano.YTxP.Test.Control.Yielding.Scripts.Utils ( ScriptsTestsParams, authorisedScriptsSTCS, authorisedScriptHash, - authorisedScriptsManagerHash + authorisedScriptsManagerHash, + oneshotUtxo ), -- * Script context builders authorisedScriptRefInputContext, mintContext, spendContext, + oneshotSpendContext, rewardContext, -- * Misc @@ -48,6 +50,7 @@ import Plutus.ContextBuilder ( referenceInput, script, withMinting, + withRef, withReferenceScript, withRewarding, withSpendingUTXO, @@ -61,6 +64,7 @@ import PlutusLedgerApi.V3 ( ScriptHash (getScriptHash), StakingCredential (StakingHash), TokenName (TokenName), + TxOutRef, singleton, toBuiltinData, ) @@ -70,6 +74,7 @@ data ScriptsTestsParams = ScriptsTestsParams { authorisedScriptsSTCS :: AuthorisedScriptsSTCS , authorisedScriptHash :: ScriptHash , authorisedScriptsManagerHash :: ScriptHash + , oneshotUtxo :: TxOutRef } -- | Produces a @Reader@ that yields a context builder with an _authorised_ reference using @ScriptsTestsParams@ @@ -110,6 +115,15 @@ spendContext = do return $ input consumedUTxO <> withSpendingUTXO consumedUTxO +-- | Produces a @Reader@ that yields a context builder for one shot spending use case +oneshotSpendContext :: Reader ScriptsTestsParams SpendingBuilder +oneshotSpendContext = do + oref <- asks oneshotUtxo + let + oneshot = withRef oref + return $ + input oneshot <> withSpendingUTXO oneshot + -- | Produces a @Reader@ that yields a context builder for a rewarding use case rewardContext :: Reader ScriptsTestsParams RewardingBuilder rewardContext = do diff --git a/ytxp-plutarch/test/Optics.hs b/ytxp-plutarch/test/Optics.hs index abd5321c..df11830c 100644 --- a/ytxp-plutarch/test/Optics.hs +++ b/ytxp-plutarch/test/Optics.hs @@ -40,7 +40,7 @@ import PlutusLedgerApi.V3 ( StakingCredential, TxCert, TxId, - TxInInfo (txInInfoResolved), + TxInInfo (txInInfoOutRef, txInInfoResolved), TxInfo (txInfoId), TxOut (txOutAddress), TxOutRef (txOutRefId), @@ -141,6 +141,12 @@ instance HasTxId TxOutRef where ---------------------------------------- +instance HasTxOutRef TxInInfo where + txOutRef = lens g s + where + g = txInInfoOutRef + s txInInfo' txOutRef' = txInInfo' {txInInfoOutRef = txOutRef'} + instance HasTxOut TxInInfo where txOut = lens g s where diff --git a/ytxp-plutarch/ytxp-plutarch.cabal b/ytxp-plutarch/ytxp-plutarch.cabal index 19e7283e..2c8697bc 100644 --- a/ytxp-plutarch/ytxp-plutarch.cabal +++ b/ytxp-plutarch/ytxp-plutarch.cabal @@ -19,8 +19,8 @@ flag dev common common-language ghc-options: -Wall -Wcompat -fprint-explicit-foralls -fprint-explicit-kinds - -fwarn-missing-import-lists -Weverything -Wno-unsafe - -Wno-missing-safe-haskell-mode -Wno-implicit-prelude + -fwrite-ide-info -fwarn-missing-import-lists -Weverything + -Wno-unsafe -Wno-missing-safe-haskell-mode -Wno-implicit-prelude -Wno-missing-kind-signatures -Wno-all-missed-specializations -Wno-unused-packages -Wno-operator-whitespace @@ -167,6 +167,21 @@ library direct-offer-example Cardano.YTxP.Example.Offer.PUtils Cardano.YTxP.Example.Offer.Reclaiming +library script-manager-example + import: common-language + default-language: Haskell2010 + hs-source-dirs: examples/script-manager/src + build-depends: + , base ^>=4.18.1.0 + , generics-sop + , plutus-ledger-api + , pprelude + + exposed-modules: + Cardano.YTxP.Example.PUtils + Cardano.YTxP.Example.Script.Deploying + Cardano.YTxP.Example.Script.Removing + -- Executables executable ytxp