diff --git a/.gitignore b/.gitignore index 92ceffe9a..6940e17d8 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ dist-newstyle *.swp docs/ .pre-commit-config.yaml +.github/copilot-instructions.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dd415fa7..46f7e88b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,44 @@ ### Added +- New `txSkelOptProceedAfterValidationFailures` boolean option in `TxSkelOpts` + (with its `txSkelOptProceedAfterValidationFailuresL` optic). When set to + `True`, transaction validation failures no longer abort the mockchain run. + Default is `False`. +- New `UserScriptHash` constructor for `User`, representing an allocation-mode + script owner known only by its `Api.ScriptHash` (no script body). It can be + used to pay to a bare script hash through `receives` (a new + `IsTxSkelOutAllowedOwner Api.ScriptHash` instance). Spending an output owned by + such a user requires providing the full script through a matching reference + input; otherwise a new `MCESpendingHashOnlyScript` error is raised. The + `userVScriptL` optic is now restricted to `User IsScript Redemption`, since an + allocation-mode script owner may no longer carry a script body. +- New `SomeTxSkelOutDatumHash` constructor for `TxSkelOutDatum`, representing an + output datum known only by its hash (no datum content). It is mirrored by a + new `UtxoPayloadDatumHash` constructor in the resulting `UtxoState`, and a new + `MCESpendingHashOnlyDatum` error is raised when attempting to build the + spending witness of a script output whose datum is only a hash. + ### Changed +- Time-related primitives have been regrouped into a new dedicated + `Cooked.MockChain.Effect.Time.MockChainTime` effect. The time queries + (`currentSlot`, `currentMSRange`, `getEnclosingSlot`, `slotToMSRange`, + `slotRangeBefore`, `slotRangeAfter`) that used to live in `MockChainReadChain` + and the waiting primitives (`waitNSlots`, `awaitSlot`, `awaitEnclosingSlot`, + `waitNMSFromSlotLowerBound`, `waitNMSFromSlotUpperBound`) that used to live in + `MockChainWrite` are now all provided by `MockChainTime`, with `waitNSlots` as + its sole state-modifying primitive. +- The former `MockChainState` has been split into two independent records, each + backed by its own state monad: `EmulatorState` (the emulator `Params` and + `EmulatedLedgerState`, only relevant when running against the emulated ledger) + and `ChainIndex` (the map of known outputs and the constitution script, which + is backend-agnostic and also meaningful for the node backend). Accordingly, + `mcstToUtxoState` is now `chainIndexToUtxoState`, the `mcst*L` optics are + replaced by `emulatorState*L`/`chainIndex*L`, `MockChainConf` now carries + `mccInitialEmulatorState` and `mccInitialChainIndex`, and + `RunnableMockChain.runMockChain` takes an `EmulatorState` and a `ChainIndex`. + ### Removed ### Fixed diff --git a/README.md b/README.md index 975478802..6795e0318 100644 --- a/README.md +++ b/README.md @@ -146,7 +146,7 @@ handling fees or balancing. 6. Submit the transaction: ``` haskell validateTxSkel_ - txSkelTemplate + txSkelEmulatorTemplate { txSkelOuts = [bob `receives` Value (Script.ada 10)], txSkelSignatories = txSkelSignatoriesFromList [wallet 1] } diff --git a/cooked-validators.cabal b/cooked-validators.cabal index 1a929ac8b..00f6b9649 100644 --- a/cooked-validators.cabal +++ b/cooked-validators.cabal @@ -24,6 +24,7 @@ library Cooked.Families Cooked.Ltl Cooked.MockChain + Cooked.MockChain.Automation Cooked.MockChain.Automation.AutoFilling.Constitution Cooked.MockChain.Automation.AutoFilling.MinAda Cooked.MockChain.Automation.AutoFilling.ReferenceScripts @@ -44,7 +45,11 @@ library Cooked.MockChain.Common Cooked.MockChain.Effect.Log Cooked.MockChain.Effect.Misc - Cooked.MockChain.Effect.Read + Cooked.MockChain.Effect.Read.Chain + Cooked.MockChain.Effect.Read.Conf + Cooked.MockChain.Effect.Submission + Cooked.MockChain.Effect.Time + Cooked.MockChain.Effect.Validation Cooked.MockChain.Effect.Write Cooked.MockChain.Run.Instances Cooked.MockChain.Run.Runnable @@ -127,15 +132,18 @@ library , bytestring , cardano-api , cardano-crypto + , cardano-ledger-alonzo , cardano-ledger-conway , cardano-ledger-core , cardano-ledger-shelley , cardano-node-emulator + , cardano-slotting , cardano-strict-containers , containers , data-default , either , exceptions + , extra , http-conduit , lens , microlens @@ -154,6 +162,8 @@ library , tasty-hunit , tasty-quickcheck , text + , time + , witherable default-language: Haskell2010 test-suite spec diff --git a/doc/CHEATSHEET.md b/doc/CHEATSHEET.md index b1a6265cc..a58e86ec3 100644 --- a/doc/CHEATSHEET.md +++ b/doc/CHEATSHEET.md @@ -466,7 +466,7 @@ options. It is built upon a transaction skeleton template. Each field can then be overridden. ```haskell -myTxSkel = txSkelTemplate +myTxSkel = txSkelEmulatorTemplate { txSkelInputs = ..., txSkelOutputs = ..., txSkelOpts = ..., @@ -496,7 +496,7 @@ Transaction can be signed with one of more wallets. They will both be part of the required and actual signers of the transaction. ```haskell -txSkelTemplate +txSkelEmulatorTemplate { ... txSkelSignatories = txSkelSignatoriesFromList [wallet 1, ...] ... @@ -513,7 +513,7 @@ myUser1 myUser2 :: MyType myUser1 = ... myUser2 = ... -txSkelTemplate +txSkelEmulatorTemplate { ... txSkelSignatories = signatoryPubKey <$> [myUser1, myUser2, ...] ... @@ -558,7 +558,7 @@ Payments can automatically be adjusted in terms of minimal ADA requirements: Payments are given in the transaction using the `txSkelOutputs` field: ```haskell -txSkelTemplate +txSkelEmulatorTemplate { ... txSkelOutputs = [party1 `receives` payment1, party2 `receives` payment2, ...] ... @@ -590,7 +590,7 @@ myRedeemer = ## Inputs ```haskell -txSkelTemplate +txSkelEmulatorTemplate { ... txSkelInputs = Map.fromList [ (txOutRef1, someTxSkelRedeemer red), @@ -610,7 +610,7 @@ txSkelTemplate * Burn a single kind of token for a given minting policy: `burn barPolicy myTxSkelRedeemer "barName" 6` ```haskell -txSkelTemplate +txSkelEmulatorTemplate { ... txSkelMints = txSkelMintsFromList [ Mint ..., @@ -628,7 +628,7 @@ txSkelTemplate * Within redeemers manually ``withReferenceInput myTxSkelRedeemer myRefInput`` * Additional reference inputs not bound to redeemers: ```haskell -txSkelTemplate +txSkelEmulatorTemplate { ... txSkelReferenceInputs = Set.fromList [txOutRef1, txOutRef2, ...] ... @@ -642,7 +642,7 @@ also be provided manually. * From first signer (default): ``` -txSkelTemplate +txSkelEmulatorTemplate { ... txSkelSignatories = [signatory1, signatory2], ... @@ -651,7 +651,7 @@ txSkelTemplate * From another wallet: ``` -txSkelTemplate +txSkelEmulatorTemplate { ... txSkelSignatories = [TxSkelSignatory user1 ... , TxSkelSignatory user2 ...], txSkelOpts = def {txSkelOptCollateralUtxos = CollateralUtxosFromUser user2} @@ -661,7 +661,7 @@ txSkelTemplate * From a direct UTxO list (make sure the owner of these utxo sign the transaction): ``` -txSkelTemplate +txSkelEmulatorTemplate { ... txSkelOpts = def {txSkelOptCollateralUtxos = CollateralUtxosFromSet (Set.fromList [txOutRef1, txOutRef2]) user2} ... @@ -695,7 +695,7 @@ do * Using the builtin constructor for proposals. ```haskell -txSkelTemplate +txSkelEmulatorTemplate { ... txSkelProposals = [ TxSkelProposal @@ -721,7 +721,7 @@ txSkelTemplate * Using smart constructors and (optional) helpers. ```haskell -txSkelTemplate +txSkelEmulatorTemplate { ... txSkelProposals = [ simpleProposal @@ -746,7 +746,7 @@ redeemer, logging an `MCLogAutoFilledConstitution` event. * Automatic withdrawal of the available rewards ```haskell -txSkelTemplate +txSkelEmulatorTemplate { txSkelWithdrawals = txSkelWithdrawalsFromList [ scriptWithdrawal myWithdrawingScript myTxSkelRedeemer, pubKeyWithdrawal myWithdrawingPubKey, @@ -759,7 +759,7 @@ txSkelTemplate * Manual withdrawal of a certain amount (for testing purposes only) ```haskell - txSkelTemplate + txSkelEmulatorTemplate { txSkelWithdrawals = txSkelWithdrawalsFromList [ Withdrawal (UserPubKey myWithdrawingPeer) (Just $ Api.Lovelace 2_000_000), ... @@ -781,7 +781,7 @@ myCertificateAction2 = DRepUpdate ... corresponds to the kind of allowed user. ```haskell -txSkelTemplate +txSkelEmulatorTemplate { txSkelCertificates = [ TxSkelCertificate myUser myCertificateAction, pubKeyCertificate myPubKey myCertificateAction1, @@ -797,7 +797,7 @@ txSkelTemplate * First signatory (default): ```haskell -txSkelTemplate +txSkelEmulatorTemplate { ... txSkelSignatories = [signatory1, signatory2] ... @@ -806,7 +806,7 @@ txSkelTemplate * Another signatory: ```haskell -txSkelTemplate +txSkelEmulatorTemplate { ... txSkelSignatories = [signatory1, signatory2], txSkelOpts = def {txSkelOptBalancingPolicy = BalanceWith (wallet 2)} @@ -817,7 +817,7 @@ txSkelTemplate ### Do not automatically balance ```haskell -txSkelTemplate +txSkelEmulatorTemplate { ... txSkelOpts = def {txSkelOptBalancingPolicy = DoNotBalance} ... diff --git a/package.yaml b/package.yaml index c55466a87..28763733d 100644 --- a/package.yaml +++ b/package.yaml @@ -12,15 +12,18 @@ library: - bytestring - cardano-api - cardano-crypto + - cardano-ledger-alonzo - cardano-ledger-core - cardano-ledger-shelley - cardano-ledger-conway - cardano-node-emulator + - cardano-slotting - cardano-strict-containers - containers - data-default - either - exceptions + - extra - http-conduit - lens - microlens @@ -39,6 +42,8 @@ library: - tasty-hunit - tasty-quickcheck - text + - time + - witherable ghc-options: -Wall -Wcompat diff --git a/src/Cooked/Families.hs b/src/Cooked/Families.hs index 4cb56f853..c8b562946 100644 --- a/src/Cooked/Families.hs +++ b/src/Cooked/Families.hs @@ -23,6 +23,7 @@ module Cooked.Families HList (..), hHead, hTail, + hSingleton, ) where @@ -89,6 +90,10 @@ hHead (HCons a _) = a hTail :: HList (a ': l) -> HList l hTail (HCons _ l) = l +-- | A singleton wrapped in an 'HList' +hSingleton :: a -> HList '[a] +hSingleton = (`HCons` HEmpty) + instance Eq (HList '[]) where _ == _ = True diff --git a/src/Cooked/MockChain.hs b/src/Cooked/MockChain.hs index 7dff3ba5e..c413e3540 100644 --- a/src/Cooked/MockChain.hs +++ b/src/Cooked/MockChain.hs @@ -2,10 +2,13 @@ -- elements related to logs and inner state. module Cooked.MockChain (module X) where -import Cooked.MockChain.Automation.Balancing as X +import Cooked.MockChain.Automation as X import Cooked.MockChain.Common as X import Cooked.MockChain.Effect.Misc as X -import Cooked.MockChain.Effect.Read as X +import Cooked.MockChain.Effect.Read.Chain as X +import Cooked.MockChain.Effect.Read.Conf as X +import Cooked.MockChain.Effect.Time as X +import Cooked.MockChain.Effect.Validation as X import Cooked.MockChain.Effect.Write as X import Cooked.MockChain.Run.Instances as X import Cooked.MockChain.Run.Runnable as X diff --git a/src/Cooked/MockChain/Automation.hs b/src/Cooked/MockChain/Automation.hs new file mode 100644 index 000000000..c43c78a7b --- /dev/null +++ b/src/Cooked/MockChain/Automation.hs @@ -0,0 +1,71 @@ +-- | This module runs the full automation pipeline that completes a +-- `Cooked.Skeleton.TxSkel` into an actual transaction. It also serves as an +-- umbrella re-exporting all the automation submodules (auto-filling, balancing +-- and transaction generation). +module Cooked.MockChain.Automation + ( runAutomationPipeline, + module X, + ) +where + +import Control.Monad +import Cooked.MockChain.Automation.AutoFilling.Constitution as X +import Cooked.MockChain.Automation.AutoFilling.MinAda as X +import Cooked.MockChain.Automation.AutoFilling.ReferenceScripts as X +import Cooked.MockChain.Automation.AutoFilling.Withdrawals as X +import Cooked.MockChain.Automation.Balancing as X +import Cooked.MockChain.Automation.GenerateTx.Anchor as X +import Cooked.MockChain.Automation.GenerateTx.Body as X +import Cooked.MockChain.Automation.GenerateTx.Certificate as X +import Cooked.MockChain.Automation.GenerateTx.Collateral as X +import Cooked.MockChain.Automation.GenerateTx.Credential as X +import Cooked.MockChain.Automation.GenerateTx.Input as X +import Cooked.MockChain.Automation.GenerateTx.Mint as X +import Cooked.MockChain.Automation.GenerateTx.Output as X +import Cooked.MockChain.Automation.GenerateTx.Proposal as X +import Cooked.MockChain.Automation.GenerateTx.ReferenceInputs as X +import Cooked.MockChain.Automation.GenerateTx.Withdrawals as X +import Cooked.MockChain.Automation.GenerateTx.Witness as X +import Cooked.MockChain.Effect.Log +import Cooked.MockChain.Effect.Read.Chain +import Cooked.MockChain.Effect.Read.Conf +import Cooked.MockChain.Runtime.Error +import Cooked.Skeleton +import Cooked.Tweak.Common +import Ledger.Orphans () +import Ledger.Tx qualified as P.Ledger +import Polysemy +import Polysemy.Error +import Polysemy.Fail + +-- | This runs the full automation pipeline: +-- 1. autofill min ada on eligible outputs +-- 2. autofill constution on eligible proposals +-- 3. autofill reference inputs on eligible redeemers +-- 4. autofill amount on eligible withdrawals +-- 5. balance the skeleton +-- 6. compute fees and collaterals +-- 7. generate a cardano transaction body +-- 8. fetch phase 2 failures +runAutomationPipeline :: + ( Members + '[ Error P.Ledger.ToCardanoError, + Error MockChainError, + MockChainLog, + MockChainReadChain, + MockChainReadConf, + Fail + ] + effs + ) => + TxSkel -> + Sem effs ExtendedTxSkel +runAutomationPipeline = + ( `execTweak` + do + autoFillMinAda + autoFillConstitution + autoFillReferenceScripts + autoFillWithdrawalAmounts + ) + >=> balanceTxSkel diff --git a/src/Cooked/MockChain/Automation/AutoFilling/Constitution.hs b/src/Cooked/MockChain/Automation/AutoFilling/Constitution.hs index 3d0e437c5..e26e33b62 100644 --- a/src/Cooked/MockChain/Automation/AutoFilling/Constitution.hs +++ b/src/Cooked/MockChain/Automation/AutoFilling/Constitution.hs @@ -7,8 +7,9 @@ module Cooked.MockChain.Automation.AutoFilling.Constitution where import Control.Monad +import Control.Monad.Extra import Cooked.MockChain.Effect.Log -import Cooked.MockChain.Effect.Read +import Cooked.MockChain.Effect.Read.Chain import Cooked.Skeleton import Cooked.Tweak.Common import Cooked.Tweak.Update @@ -23,16 +24,22 @@ import Polysemy -- existing specified script in such proposals. Logs an event when the -- constitution script has been successfully auto-filled. autoFillConstitution :: - (Members '[MockChainRead, Tweak, MockChainLog] effs) => + ( Members + '[ MockChainReadChain, + Tweak, + MockChainLog + ] + effs + ) => Sem effs () autoFillConstitution = do - currentConstitution <- getConstitutionScript - case currentConstitution of - Nothing -> return () - Just constitutionScript -> do - traverseTweak (txSkelProposalsL % traversed) $ \prop -> do + maybeM + (return ()) + ( \constitutionScript -> traverseTweak (txSkelProposalsL % traversed) $ \prop -> do when (isn't txSkelProposalConstitutionAT prop) $ logEvent $ MCLogAutoFilledConstitution $ Script.toScriptHash constitutionScript - return (fillConstitution constitutionScript prop) + return (fillConstitutionWhenEmpty constitutionScript prop) + ) + getConstitutionScript diff --git a/src/Cooked/MockChain/Automation/AutoFilling/MinAda.hs b/src/Cooked/MockChain/Automation/AutoFilling/MinAda.hs index 2d5e2112c..f4e712654 100644 --- a/src/Cooked/MockChain/Automation/AutoFilling/MinAda.hs +++ b/src/Cooked/MockChain/Automation/AutoFilling/MinAda.hs @@ -10,11 +10,11 @@ where import Cardano.Api qualified as Cardano import Cardano.Ledger.Shelley.Core qualified as Shelley -import Cardano.Node.Emulator.Internal.Node.Params qualified as Emulator import Control.Monad import Cooked.MockChain.Automation.GenerateTx.Output import Cooked.MockChain.Effect.Log -import Cooked.MockChain.Effect.Read +import Cooked.MockChain.Effect.Read.Chain +import Cooked.MockChain.Effect.Read.Conf import Cooked.Skeleton import Cooked.Tweak.Common import Cooked.Tweak.Update @@ -28,11 +28,11 @@ import Polysemy.Error -- | Compute the required minimal ADA for a given output getTxSkelOutMinAda :: - (Members '[MockChainRead, Error P.Ledger.ToCardanoError] effs) => + (Members '[MockChainReadChain, MockChainReadConf, Error P.Ledger.ToCardanoError] effs) => TxSkelOut -> Sem effs Integer getTxSkelOutMinAda txSkelOut = do - params <- Emulator.pEmulatorPParams <$> getParams + params <- getParams Cardano.unCoin . Shelley.getMinCoinTxOut params . Cardano.toShelleyTxOut Cardano.ShelleyBasedEraConway @@ -45,7 +45,7 @@ getTxSkelOutMinAda txSkelOut = do -- will increase the size of the UTXO which in turn might need more ADA. toTxSkelOutWithMinAda :: forall effs. - (Members '[MockChainRead, MockChainLog, Error P.Ledger.ToCardanoError] effs) => + (Members '[MockChainReadChain, MockChainReadConf, MockChainLog, Error P.Ledger.ToCardanoError] effs) => TxSkelOut -> Sem effs TxSkelOut -- The auto adjustment is disabled so nothing is done here @@ -72,6 +72,6 @@ toTxSkelOutWithMinAda txSkelOut = do -- their ada value when requested by the user and required by the protocol -- parameters. Logs an event whenever such a change occurs. autoFillMinAda :: - (Members '[Tweak, MockChainRead, MockChainLog, Error P.Ledger.ToCardanoError] effs) => + (Members '[Tweak, MockChainReadChain, MockChainReadConf, MockChainLog, Error P.Ledger.ToCardanoError] effs) => Sem effs () autoFillMinAda = traverseTweak (txSkelOutputsL % traversed) toTxSkelOutWithMinAda diff --git a/src/Cooked/MockChain/Automation/AutoFilling/ReferenceScripts.hs b/src/Cooked/MockChain/Automation/AutoFilling/ReferenceScripts.hs index 066792b4e..fb7d48ad1 100644 --- a/src/Cooked/MockChain/Automation/AutoFilling/ReferenceScripts.hs +++ b/src/Cooked/MockChain/Automation/AutoFilling/ReferenceScripts.hs @@ -9,7 +9,7 @@ where import Control.Monad import Cooked.MockChain.Effect.Log -import Cooked.MockChain.Effect.Read +import Cooked.MockChain.Effect.Read.Chain import Cooked.MockChain.UtxoSearch import Cooked.Skeleton import Cooked.Tweak.Common @@ -17,6 +17,7 @@ import Cooked.Tweak.Query import Cooked.Tweak.Update import Data.List (find) import Data.Map qualified as Map +import Data.Set qualified as Set import Optics.Core import Plutus.Script.Utils.Scripts qualified as Script import PlutusLedgerApi.V3 qualified as Api @@ -28,7 +29,7 @@ import Polysemy -- given script hash, and attaches it to a redeemer when it does not yet have a -- reference input and when it is allowed, in which case an event is logged. updateRedeemedScript :: - (Members '[MockChainLog, MockChainRead] effs) => + (Members '[MockChainLog, MockChainReadChain] effs) => [Api.TxOutRef] -> User IsScript Redemption -> Sem effs (User IsScript Redemption) @@ -48,11 +49,11 @@ updateRedeemedScript return $ over userRedeemerAT (fillReferenceInput oRef) rs ) $ case oRefsInInputs of - [] -> Nothing + s | null s -> Nothing -- If possible, we use a reference input appearing in regular inputs - l | Just oRefM' <- find (`elem` inputs) l -> Just oRefM' + s | Just oRefM' <- find (`elem` inputs) s -> Just oRefM' -- If none exist, we use the first one we find elsewhere - (oRefM' : _) -> Just oRefM' + s -> Just $ Set.elemAt 0 s updateRedeemedScript _ rs = return rs -- | Goes through the various parts of the skeleton where a redeemer can appear, @@ -60,7 +61,7 @@ updateRedeemedScript _ rs = return rs -- allowed and one has not already been set. Logs an event whenever such an -- addition occurs. autoFillReferenceScripts :: - (Members '[Tweak, MockChainRead, MockChainLog] effs) => + (Members '[Tweak, MockChainReadChain, MockChainLog] effs) => Sem effs () autoFillReferenceScripts = do inputsKeys <- viewTweak $ txSkelInputsL % to Map.keys diff --git a/src/Cooked/MockChain/Automation/AutoFilling/Withdrawals.hs b/src/Cooked/MockChain/Automation/AutoFilling/Withdrawals.hs index 12c2271fe..8e73398d1 100644 --- a/src/Cooked/MockChain/Automation/AutoFilling/Withdrawals.hs +++ b/src/Cooked/MockChain/Automation/AutoFilling/Withdrawals.hs @@ -6,7 +6,7 @@ module Cooked.MockChain.Automation.AutoFilling.Withdrawals where import Cooked.MockChain.Effect.Log -import Cooked.MockChain.Effect.Read +import Cooked.MockChain.Effect.Read.Chain import Cooked.Skeleton import Cooked.Tweak.Common import Cooked.Tweak.Update @@ -21,7 +21,7 @@ import Polysemy -- tamper with an existing specified amount in such withdrawals. Logs an event -- when an amount has been successfully auto-filled. autoFillWithdrawalAmounts :: - (Members '[MockChainRead, Tweak, MockChainLog] effs) => + (Members '[MockChainReadChain, Tweak, MockChainLog] effs) => Sem effs () autoFillWithdrawalAmounts = do traverseTweak (txSkelWithdrawalsL % txSkelWithdrawalsListI % traversed) $ \withdrawal -> do diff --git a/src/Cooked/MockChain/Automation/Balancing.hs b/src/Cooked/MockChain/Automation/Balancing.hs index 8f6502da3..c041f9680 100644 --- a/src/Cooked/MockChain/Automation/Balancing.hs +++ b/src/Cooked/MockChain/Automation/Balancing.hs @@ -2,8 +2,7 @@ -- computation of fees and collaterals because their computation cannot be -- separated from the balancing. module Cooked.MockChain.Automation.Balancing - ( Body, - ExtendedTxSkel (..), + ( ExtendedTxSkel (..), balanceTxSkel, getMinAndMaxFee, estimateTxSkelFee, @@ -14,19 +13,19 @@ import Cardano.Api qualified as Cardano import Cardano.Api.Ledger qualified as Cardano import Cardano.Ledger.Conway.Core qualified as Conway import Cardano.Ledger.Conway.PParams qualified as Conway -import Cardano.Node.Emulator.Internal.Node.Params qualified as Emulator import Control.Monad import Cooked.MockChain.Automation.AutoFilling.MinAda import Cooked.MockChain.Automation.GenerateTx.Body import Cooked.MockChain.Automation.GenerateTx.Output import Cooked.MockChain.Common import Cooked.MockChain.Effect.Log -import Cooked.MockChain.Effect.Read +import Cooked.MockChain.Effect.Read.Chain +import Cooked.MockChain.Effect.Read.Conf import Cooked.MockChain.Runtime.Error import Cooked.MockChain.UtxoSearch import Cooked.Skeleton import Data.ByteString qualified as BS -import Data.List (find, partition) +import Data.Foldable.Extra import Data.Map qualified as Map import Data.Maybe (fromMaybe) import Data.Ratio qualified as Rat @@ -45,10 +44,7 @@ import Polysemy import Polysemy.Error import Polysemy.Fail --- | A transaction body -type Body = Cardano.TxBody Cardano.ConwayEra - --- | A `TxSkel` with extra pieces of information produced during balancing +-- | A 'TxSkel' with extra pieces of information produced during balancing data ExtendedTxSkel = ExtendedTxSkel { -- | The skeleton itself eSkel :: TxSkel, @@ -57,7 +53,9 @@ data ExtendedTxSkel = ExtendedTxSkel -- | The optional collaterals associated with this skeleton eMCollaterals :: Maybe Collaterals, -- | The Cardano body generated from this skeleton - eBody :: Body + eBody :: Body, + -- | The script errors uncovered during body generation + eExUnitsFailures :: ExUnitsFailures } -- | This is the main entry point of our balancing mechanism. This function @@ -67,7 +65,16 @@ data ExtendedTxSkel = ExtendedTxSkel -- skeleton control whether it should be balanced, and how to compute its -- associated elements. balanceTxSkel :: - (Members '[MockChainRead, MockChainLog, Error MockChainError, Error P.Ledger.ToCardanoError, Fail] effs) => + ( Members + '[ MockChainReadChain, + MockChainReadConf, + MockChainLog, + Error MockChainError, + Error P.Ledger.ToCardanoError, + Fail + ] + effs + ) => TxSkel -> Sem effs ExtendedTxSkel balanceTxSkel skelUnbal@TxSkel {..} = do @@ -107,14 +114,13 @@ balanceTxSkel skelUnbal@TxSkel {..} = do -- Some scripts involved, and a specific collateral user provided. -- We fetch vanilla UTxOs from this user and return them. (False, CollateralUtxosFromUser (Script.toPubKeyHash -> cUser)) -> - Just . (,UserPubKey cUser) . Set.fromList - <$> getTxOutRefs (utxosAtSearch cUser ensureOnlyValueOutputs) + Just . (,UserPubKey cUser) <$> getTxOutRefs (utxosAtSearch cUser ensureOnlyValueOutputs) -- Some scripts involved, and no specific collateral options provided. (False, CollateralUtxosFromBalancingUser) -> case balancingUser of -- If no balancing wallet exists, we throw an error Nothing -> throw $ MCEBalancingError MissingBalancingUser -- If a balancing wallet exists, we use it as collateral user - Just bUser -> Just . (,bUser) . Set.fromList <$> getTxOutRefs (utxosAtSearch bUser ensureOnlyValueOutputs) + Just bUser -> Just . (,bUser) <$> getTxOutRefs (utxosAtSearch bUser ensureOnlyValueOutputs) -- At this point, the presence (or absence) of balancing user dictates -- whether the transaction should be automatically balanced or not. @@ -126,23 +132,23 @@ balanceTxSkel skelUnbal@TxSkel {..} = do AutoFeeComputation -> maxFee ManualFee fee' -> fee' mCols <- collateralsFromFee fee mCollaterals - cBody <- txSkelToTxBody skelUnbal fee mCols - return $ ExtendedTxSkel skelUnbal fee mCols cBody + (cBody, cExUnitsFailures) <- txSkelToTxBody skelUnbal fee mCols + return $ ExtendedTxSkel skelUnbal fee mCols cBody cExUnitsFailures Just bUser -> do -- The balancing should be performed. We collect the candidates balancing -- utxos based on the associated policy balancingUtxos <- case txSkelOptBalancingUtxos txSkelOpts of - BalancingUtxosFromBalancingUser -> getTxOutRefsAndOutputs $ utxosAtSearch bUser ensureOnlyValueOutputs + BalancingUtxosFromBalancingUser -> getUtxos $ utxosAtSearch bUser ensureOnlyValueOutputs BalancingUtxosFromSet utxos -> -- We resolve the given set of utxos - getTxOutRefsAndOutputs (txSkelOutByRefSearch' (Set.toList utxos)) + getUtxos (txSkelOutByRefSearch' utxos) -- We filter out those belonging to scripts, while throwing a -- warning if any was actually discarded. - >>= filterAndWarn (is (txSkelOutOwnerL % userPubKeyHashAT) . snd) "They belong to scripts." + >>= filterAndWarn (const $ is (txSkelOutOwnerL % userPubKeyHashAT)) "They belong to scripts." -- We filter the candidate utxos by removing those already present in the -- skeleton, throwing a warning if any was actually discarded - >>= filterAndWarn ((`notElem` txSkelKnownTxOutRefs skelUnbal) . fst) "They are already used in the skeleton." + >>= filterAndWarn (flip $ const (`notElem` txSkelKnownTxOutRefs skelUnbal)) "They are already used in the skeleton." case txSkelOptFeePolicy txSkelOpts of -- If fees are left for us to compute, we run a dichotomic search. This @@ -154,17 +160,25 @@ balanceTxSkel skelUnbal@TxSkel {..} = do ManualFee fee -> do mCols <- collateralsFromFee fee mCollaterals balancedSkel <- computeBalancedTxSkel bUser balancingUtxos skelUnbal fee - cBody <- txSkelToTxBody balancedSkel fee mCols - return $ ExtendedTxSkel balancedSkel fee mCols cBody + (cBody, cExUnitsFailures) <- txSkelToTxBody balancedSkel fee mCols + return $ ExtendedTxSkel balancedSkel fee mCols cBody cExUnitsFailures where filterAndWarn f s l - | (ok, toInteger . length -> koLength) <- partition f l = + | (ok, toInteger . length -> koLength) <- Map.partitionWithKey f l = unless (koLength == 0) (logEvent $ MCLogDiscardedUtxos koLength s) >> return ok -- | Computes optimal fee for a given skeleton and balances it around those fees. -- This uses a dichotomic search for an optimal "balanceable around" fee. computeFeeAndBalance :: - (Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError, Fail] effs) => + ( Members + '[ MockChainReadChain, + MockChainReadConf, + Error MockChainError, + Error P.Ledger.ToCardanoError, + Fail + ] + effs + ) => Peer -> Fee -> Fee -> @@ -183,11 +197,11 @@ computeFeeAndBalance balancingUser minFee maxFee balancingUtxos mCollaterals ske ( do newSkel <- computeBalancedTxSkel balancingUser balancingUtxos skel fee mCols <- collateralsFromFee fee mCollaterals - (newFee, body) <- estimateTxSkelFee newSkel fee mCols + (newFee, body, sErrors) <- estimateTxSkelFee newSkel fee mCols if -- The skeleton was balanceable, we cannot try smaller fee, but -- the used fee is sufficient for the generated body - | minFee == maxFee && newFee <= fee -> return $ ExtendedTxSkel newSkel newFee mCols body + | minFee == maxFee && newFee <= fee -> return $ ExtendedTxSkel newSkel newFee mCols body sErrors -- The skeleton was balanceable, we cannot try smaller fee, but -- the used fee is insufficient for the generated body | minFee == maxFee -> throw $ MCEBalancingError $ NotEnoughFundForProperFee balancingUser @@ -221,7 +235,14 @@ computeFeeAndBalance balancingUser minFee maxFee balancingUtxos mCollaterals ske -- min ada requirements in the associated return collateral and the maximum -- number of collateral inputs authorized by protocol parameters. collateralsFromFee :: - (Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError] effs) => + ( Members + '[ MockChainReadChain, + MockChainReadConf, + Error MockChainError, + Error P.Ledger.ToCardanoError + ] + effs + ) => -- | The fee from which these collaterals should be computed Fee -> -- | The optional candidate UTxOs to be used as collaterals, alongside the @@ -233,7 +254,7 @@ collateralsFromFee :: collateralsFromFee _ Nothing = return Nothing collateralsFromFee fee (Just (collateralIns, returnCollateralUser)) = do -- We retrieve the protocol parameters - params <- Emulator.pEmulatorPParams <$> getParams + params <- getParams -- We retrieve the max number of collateral inputs, with a default of 10. In -- practice this will be around 3. let nbMax = toInteger $ Microlens.view Conway.ppMaxCollateralInputsL params @@ -244,7 +265,7 @@ collateralsFromFee fee (Just (collateralIns, returnCollateralUser)) = do -- add one because of ledger requirement which seem to round up this value. let totalCollateral = Script.lovelace . (+ 1) . (`div` 100) . (* percentage) $ fee -- Collateral tx outputs sorted by decreasing ada amount - collateralTxOuts <- getTxOutRefsAndOutputs $ txSkelOutByRefSearch' $ Set.toList collateralIns + collateralTxOuts <- getUtxos $ txSkelOutByRefSearch' collateralIns -- Candidate subsets of utxos to be used as collaterals reachedValue <- reachValue collateralTxOuts totalCollateral nbMax $ Right returnCollateralUser -- A value might, or might not have been reached @@ -257,7 +278,13 @@ collateralsFromFee fee (Just (collateralIns, returnCollateralUser)) = do reachValue :: forall effs. - (Members '[MockChainRead, Error P.Ledger.ToCardanoError] effs) => + ( Members + '[ MockChainReadChain, + MockChainReadConf, + Error P.Ledger.ToCardanoError + ] + effs + ) => -- | The Utxos available to reach the value Utxos -> -- | The target value to reach @@ -273,10 +300,10 @@ reachValue :: -- the surplus output, which is either built from scratch or from the provided -- surplus output, if any. Sem effs (Maybe ([Api.TxOutRef], Maybe TxSkelOut)) -reachValue utxos target fuel outputOrUser = do +reachValue (Map.toList -> utxos) target fuel outputOrUser = do -- We retrieve the current protocol version, which is going to be used to -- compute the size of the inputs and outputs added by this function - Cardano.ProtVer majorVersion _ <- Microlens.view Conway.ppProtocolVersionL . Emulator.emulatorPParams <$> getParams + Cardano.ProtVer majorVersion _ <- Microlens.view Conway.ppProtocolVersionL <$> getParams -- We annotate @outputOrUser@ with the size of the existing output, if any outputOrUser' <- case outputOrUser of Left output -> Left . (output,) <$> outputSize majorVersion output @@ -391,30 +418,45 @@ reachValue utxos target fuel outputOrUser = do -- | Estimates the required fee for a given skeleton with a given initial fee -- and collaterals estimateTxSkelFee :: - (Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError, Fail] effs) => + ( Members + '[ MockChainReadChain, + MockChainReadConf, + Error MockChainError, + Error P.Ledger.ToCardanoError, + Fail + ] + effs + ) => TxSkel -> Fee -> Maybe Collaterals -> - Sem effs (Fee, Body) + Sem effs (Fee, Body, ExUnitsFailures) estimateTxSkelFee skel fee mCollaterals = do -- We retrieve the necessary data to generate the transaction body - params <- Emulator.pEmulatorPParams <$> getParams + params <- getParams -- We build the index known to the skeleton index <- txSkelToIndex skel mCollaterals -- We build the transaction body - txBody <- txSkelToTxBody skel fee mCollaterals + (txBody, exUnitsFailures) <- txSkelToTxBody skel fee mCollaterals -- We retrieve the amount of signatories let nbOfSignatories = fromIntegral $ length $ txSkelSignatories skel -- We compute the estimated fee let Cardano.Coin newFee = Cardano.calculateMinTxFee Cardano.ShelleyBasedEraConway params index txBody nbOfSignatories -- We return both the new fee and generated body - return (newFee, txBody) + return (newFee, txBody, exUnitsFailures) -- | This creates a balanced skeleton from a given skeleton and fee. In other -- words, this ensures that the following equation holds: input value + minted -- value + withdrawn value = output value + burned value + fee + deposits computeBalancedTxSkel :: - (Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError] effs) => + ( Members + '[ MockChainReadChain, + MockChainReadConf, + Error MockChainError, + Error P.Ledger.ToCardanoError + ] + effs + ) => Peer -> Utxos -> TxSkel -> @@ -468,7 +510,7 @@ computeBalancedTxSkel balancingUser balancingUtxos txSkel@TxSkel {..} (Script.lo (additionalInsTxOutRefs, newTxSkelOuts) <- case solution of -- There is no solution with the provided parameters Nothing -> do - let totalValue = mconcat $ view txSkelOutValueL . snd <$> balancingUtxos + let totalValue = foldOf (traversed % txSkelOutValueL) balancingUtxos difference = snd $ Api.split $ missingLeft <> PlutusTx.negate totalValue throw $ MCEBalancingError $ @@ -498,13 +540,18 @@ computeBalancedTxSkel balancingUser balancingUtxos txSkel@TxSkel {..} (Script.lo -- See https://github.com/IntersectMBO/cardano-ledger/blob/master/docs/adr/2024-08-14_009-refscripts-fee-change.md -- for more information getMinAndMaxFee :: - (Members '[MockChainRead] effs) => + ( Members + '[ MockChainReadChain, + MockChainReadConf + ] + effs + ) => Integer -> Sem effs (Fee, Fee) getMinAndMaxFee nbOfScripts = do -- We retrieve the necessary parameters to compute the maximum possible fee -- for a transaction. There are quite a few of them. - params <- Emulator.pEmulatorPParams <$> getParams + params <- getParams let maxTxSize = toInteger $ Microlens.view Conway.ppMaxTxSizeL params Cardano.Coin txFeePerByte = Microlens.view Conway.ppMinFeeAL params Cardano.Coin txFeeFixed = Microlens.view Conway.ppMinFeeBL params diff --git a/src/Cooked/MockChain/Automation/GenerateTx/Body.hs b/src/Cooked/MockChain/Automation/GenerateTx/Body.hs index 1019f7c8b..663ab7f2d 100644 --- a/src/Cooked/MockChain/Automation/GenerateTx/Body.hs +++ b/src/Cooked/MockChain/Automation/GenerateTx/Body.hs @@ -6,12 +6,11 @@ module Cooked.MockChain.Automation.GenerateTx.Body txSkelToTxBodyContent, txSkelToIndex, txSignatoriesAndBodyToCardanoTx, - txSkelToCardanoTx, ) where import Cardano.Api qualified as Cardano -import Cardano.Node.Emulator.Internal.Node qualified as Emulator +import Cardano.Ledger.Alonzo.Plutus.Evaluate qualified as Alonzo import Control.Monad import Cooked.MockChain.Automation.GenerateTx.Certificate import Cooked.MockChain.Automation.GenerateTx.Collateral @@ -23,26 +22,37 @@ import Cooked.MockChain.Automation.GenerateTx.ReferenceInputs import Cooked.MockChain.Automation.GenerateTx.Withdrawals import Cooked.MockChain.Automation.GenerateTx.Witness import Cooked.MockChain.Common -import Cooked.MockChain.Effect.Read +import Cooked.MockChain.Effect.Read.Chain +import Cooked.MockChain.Effect.Read.Conf import Cooked.MockChain.Runtime.Error import Cooked.Skeleton +import Data.Bifunctor (first) import Data.Map qualified as Map -import Data.Maybe import Data.Set qualified as Set import Ledger.Address qualified as P.Ledger import Ledger.Tx.CardanoAPI qualified as P.Ledger +import Optics.Core import Plutus.Script.Utils.Address qualified as Script import Polysemy import Polysemy.Error import Polysemy.Fail +import Witherable -- | Generates a body content from a skeleton txSkelToTxBodyContent :: - (Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError, Fail] effs) => + ( Members + '[ MockChainReadChain, + MockChainReadConf, + Error MockChainError, + Error P.Ledger.ToCardanoError, + Fail + ] + effs + ) => TxSkel -> Fee -> Maybe Collaterals -> - Sem effs (Cardano.TxBodyContent Cardano.BuildTx Cardano.ConwayEra) + Sem effs BodyContent txSkelToTxBodyContent skel@TxSkel {..} fee mCollaterals = do txIns <- mapM toTxInAndWitness $ Map.toList txSkelInputs txInsReference <- toInsReference skel @@ -57,15 +67,16 @@ txSkelToTxBodyContent skel@TxSkel {..} fee mCollaterals = do Cardano.TxExtraKeyWitnesses Cardano.AlonzoEraOnwardsConway <$> fromEither (mapM (P.Ledger.toCardanoPaymentKeyHash . P.Ledger.PaymentPubKeyHash . Script.toPubKeyHash) txSkelSignatories) - txProtocolParams <- Cardano.BuildTxWith . Just . Emulator.ledgerProtocolParameters <$> getParams + txProtocolParams <- Cardano.BuildTxWith . Just . Cardano.LedgerProtocolParameters <$> getParams txProposalProcedures <- Just . Cardano.Featured Cardano.ConwayEraOnwardsConway <$> toProposalProcedures txSkelProposals txWithdrawals <- toWithdrawals txSkelWithdrawals txCertificates <- toCertificates txSkelCertificates let txFee = Cardano.TxFeeExplicit Cardano.ShelleyBasedEraConway $ Cardano.Coin fee + -- This is filled later on, after computing the execution units + txScriptValidity = Cardano.TxScriptValidityNone txMetadata = Cardano.TxMetadataNone txAuxScripts = Cardano.TxAuxScriptsNone txUpdateProposal = Cardano.TxUpdateProposalNone - txScriptValidity = Cardano.TxScriptValidityNone txVotingProcedures = Nothing txCurrentTreasuryValue = Nothing txTreasuryDonation = Nothing @@ -73,17 +84,23 @@ txSkelToTxBodyContent skel@TxSkel {..} fee mCollaterals = do -- | Generates a transaction body from a body content txBodyContentToTxBody :: - (Members '[MockChainRead, Error P.Ledger.ToCardanoError] effs) => - Cardano.TxBodyContent Cardano.BuildTx Cardano.ConwayEra -> - Sem effs (Cardano.TxBody Cardano.ConwayEra) -txBodyContentToTxBody txBodyContent = do - params <- getParams - -- We create the associated Shelley TxBody - fromEither $ Emulator.createTransactionBody params $ P.Ledger.CardanoBuildTx txBodyContent + (Member (Error P.Ledger.ToCardanoError) effs) => + BodyContent -> + Sem effs Body +txBodyContentToTxBody = + fromEither + . first (P.Ledger.TxBodyError . Cardano.displayError) + . Cardano.createTransactionBody Cardano.shelleyBasedEra -- | Generates an index with utxos known to a 'TxSkel' txSkelToIndex :: - (Members '[MockChainRead, Error P.Ledger.ToCardanoError] effs) => + ( Members + '[ MockChainReadChain, + MockChainReadConf, + Error P.Ledger.ToCardanoError + ] + effs + ) => TxSkel -> Maybe Collaterals -> Sem effs (Cardano.UTxO Cardano.ConwayEra) @@ -97,61 +114,70 @@ txSkelToIndex txSkel mCollaterals = do txOutL <- forM knownTxOuts toCardanoTxOut -- We build the index and handle the possible error txInL <- fromEither $ forM knownTxORefs P.Ledger.toCardanoTxIn + -- We reshape the built index to the right format and return it return $ Cardano.UTxO $ Map.fromList $ zip txInL $ Cardano.toCtxUTxOTxOut <$> txOutL -- | Generates a transaction body from a 'TxSkel' and associated fee and -- collateral information. This transaction body accounts for the actual --- execution units of each of the scripts involved in the skeleton. +-- execution units of each of the scripts involved in the skeleton. During the +-- computation of these execution units, some validation errors can occur, in +-- which case the body will not account for them, but the error maps will be +-- returned. txSkelToTxBody :: - (Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError, Fail] effs) => + ( Members + '[ MockChainReadChain, + MockChainReadConf, + Error P.Ledger.ToCardanoError, + Error MockChainError, + Fail + ] + effs + ) => TxSkel -> Fee -> Maybe Collaterals -> - Sem effs (Cardano.TxBody Cardano.ConwayEra) + Sem effs (Body, ExUnitsFailures) txSkelToTxBody txSkel fee mCollaterals = do -- We create a first body content and body, without execution units txBodyContent' <- txSkelToTxBodyContent txSkel fee mCollaterals txBody' <- txBodyContentToTxBody txBodyContent' -- We create a full transaction from the body - let tx' = txSignatoriesAndBodyToCardanoTx (txSkelSignatories txSkel) txBody' - -- We retrieve the index and parameters to feed to @getTxExUnitsWithLogs@ + let (Cardano.ShelleyTx _ tx) = txSignatoriesAndBodyToCardanoTx (txSkelSignatories txSkel) txBody' + -- We build the index of known utxos index <- txSkelToIndex txSkel mCollaterals + -- We retrieve the parameters params <- getParams - -- We retrieve the execution units associated with the transaction - case Emulator.getTxExUnitsWithLogs params (P.Ledger.fromPlutusIndex index) tx' of - -- Computing the execution units can result in all kinds of phase 2 - -- validation failures, except for the ones related to the execution units - -- themselves. Unless required in the options, we throw the validation - -- failure right away when applicable. - Left err | not $ txSkelOptDeferPhase2FailuresDuringBalancing $ txSkelOpts txSkel -> throw $ uncurry MCEValidationError err - -- The other option is to ignore those and return the unchanged body with - -- the existing execution units, postponing the handling of the failures. - Left _ -> return txBody' - -- When no error arises, we get an execution unit for each script usage. We - -- first have to transform this Ledger map to a cardano API map. - Right (Map.mapKeysMonotonic (Cardano.toScriptIndex Cardano.AlonzoEraOnwardsConway) . fmap (Cardano.fromAlonzoExUnits . snd) -> exUnits) -> - -- We can then assign the right execution units to the body content - case Cardano.substituteExecutionUnits exUnits txBodyContent' of - -- This can only be a @TxBodyErrorScriptWitnessIndexMissingFromExecUnitsMap@ - Left err -> throw $ MCEFailure $ "Error while assigning execution units: " <> show err - -- We now have a body content with proper execution units and can create - -- the final body from it - Right txBodyContent -> txBodyContentToTxBody txBodyContent + -- We retrieve the @epochInfo@ from the era history + epochInfo <- Cardano.unLedgerEpochInfo . Cardano.toLedgerEpochInfo <$> getEraHistory + -- We retrieve the system start + systemStart <- getSystemStart + -- We compute the execution units associated with the transaction + let exUnitsReport = Alonzo.evalTxExUnits params tx (P.Ledger.fromPlutusIndex index) epochInfo systemStart + -- We transform the keys to Cardano script index + let cExUnitsReport = Map.mapKeysMonotonic (Cardano.toScriptIndex Cardano.AlonzoEraOnwardsConway) exUnitsReport + -- We extract the succesful cases from the map + let executionUnitsMap = mapMaybe (preview (_Right % to Cardano.fromAlonzoExUnits)) cExUnitsReport + -- We also extract the failures + let failuresMap = mapMaybe (preview _Left) cExUnitsReport + -- We attempt to insert the execution units in the body + let (txBodyContent, scriptValid) = + Cardano.substituteExecutionUnits executionUnitsMap txBodyContent' + & either + -- If this fails, this can only be a + -- @TxBodyErrorScriptWitnessIndexMissingFromExecUnitsMap@ which means that + -- some scripts failed (@failureMap@ is not empty) in which case we return + -- the original body, and mark the scripts as invalid. + (const (txBodyContent', Cardano.ScriptInvalid)) + -- We now have a body content with proper execution units and can create + -- the final body from it, while marking the scripts as valid. + (,Cardano.ScriptValid) + -- We generate the final tx body from the body content and the script validity + finalTxBody <- txBodyContentToTxBody txBodyContent {Cardano.txScriptValidity = Cardano.TxScriptValidity Cardano.AlonzoEraOnwardsConway scriptValid} + return (finalTxBody, failuresMap) -- | Generates a Cardano transaction and signs it txSignatoriesAndBodyToCardanoTx :: [TxSkelSignatory] -> - Cardano.TxBody Cardano.ConwayEra -> - Cardano.Tx Cardano.ConwayEra + Body -> + Transaction txSignatoriesAndBodyToCardanoTx signatories txBody = Cardano.Tx txBody $ mapMaybe (toKeyWitness txBody) signatories - --- | Generates a full Cardano transaction from a skeleton, fees and collaterals -txSkelToCardanoTx :: - (Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError, Fail] effs) => - TxSkel -> - Fee -> - Maybe Collaterals -> - Sem effs (Cardano.Tx Cardano.ConwayEra) -txSkelToCardanoTx txSkel fee = - fmap (txSignatoriesAndBodyToCardanoTx (txSkelSignatories txSkel)) - . txSkelToTxBody txSkel fee diff --git a/src/Cooked/MockChain/Automation/GenerateTx/Certificate.hs b/src/Cooked/MockChain/Automation/GenerateTx/Certificate.hs index 7cfb0707c..603d122a4 100644 --- a/src/Cooked/MockChain/Automation/GenerateTx/Certificate.hs +++ b/src/Cooked/MockChain/Automation/GenerateTx/Certificate.hs @@ -7,10 +7,10 @@ import Cardano.Ledger.Conway.TxCert qualified as Conway import Cardano.Ledger.DRep qualified as C.Ledger import Cardano.Ledger.PoolParams qualified as C.Ledger import Cardano.Ledger.Shelley.TxCert qualified as Shelley -import Cardano.Node.Emulator.Internal.Node qualified as Emulator import Cooked.MockChain.Automation.GenerateTx.Credential import Cooked.MockChain.Automation.GenerateTx.Witness -import Cooked.MockChain.Effect.Read +import Cooked.MockChain.Effect.Read.Chain +import Cooked.MockChain.Effect.Read.Conf import Cooked.MockChain.Runtime.Error import Cooked.Skeleton.Certificate import Cooked.Skeleton.User @@ -25,7 +25,7 @@ import Polysemy.Error import Polysemy.Fail toDRep :: - (Members '[MockChainRead, Error P.Ledger.ToCardanoError] effs) => + (Members '[MockChainReadChain, MockChainReadConf, Error P.Ledger.ToCardanoError] effs) => Api.DRep -> Sem effs C.Ledger.DRep toDRep Api.DRepAlwaysAbstain = return C.Ledger.DRepAlwaysAbstain @@ -33,7 +33,7 @@ toDRep Api.DRepAlwaysNoConfidence = return C.Ledger.DRepAlwaysNoConfidence toDRep (Api.DRep (Api.DRepCredential cred)) = C.Ledger.DRepCredential <$> toDRepCredential cred toDelegatee :: - (Members '[MockChainRead, Error P.Ledger.ToCardanoError] effs) => + (Members '[MockChainReadChain, MockChainReadConf, Error P.Ledger.ToCardanoError] effs) => Api.Delegatee -> Sem effs Conway.Delegatee toDelegatee (Api.DelegStake pkh) = Conway.DelegStake <$> toStakePoolKeyHash pkh @@ -41,7 +41,7 @@ toDelegatee (Api.DelegVote dRep) = Conway.DelegVote <$> toDRep dRep toDelegatee (Api.DelegStakeVote pkh dRep) = liftA2 Conway.DelegStakeVote (toStakePoolKeyHash pkh) (toDRep dRep) toCertificate :: - (Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError] effs) => + (Members '[MockChainReadChain, MockChainReadConf, Error MockChainError, Error P.Ledger.ToCardanoError] effs) => TxSkelCertificate -> Sem effs (Cardano.Certificate Cardano.ConwayEra) toCertificate txSkelCert = @@ -77,7 +77,7 @@ toCertificate txSkelCert = Shelley.RetirePool (toStakePoolKeyHash poolHash) ( do - eeh <- Emulator.emulatorEraHistory <$> getParams + eeh <- getEraHistory case Cardano.slotToEpoch (fromIntegral slot) eeh of -- TODO: we could have a dedicated error for this case if the -- can occur at several places in the codebase @@ -90,7 +90,7 @@ toCertificate txSkelCert = Conway.ConwayTxCertGov . (`Conway.ConwayResignCommitteeColdKey` SNothing) <$> toColdCredential cred toCertificateWitness :: - (Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError] effs) => + (Members '[MockChainReadChain, MockChainReadConf, Error MockChainError, Error P.Ledger.ToCardanoError] effs) => TxSkelCertificate -> Sem effs (Maybe (Cardano.ScriptWitness Cardano.WitCtxStake Cardano.ConwayEra)) toCertificateWitness = @@ -104,7 +104,7 @@ toCertificateWitness = -- | Builds a 'Cardano.TxCertificates' from a list of 'TxSkelCertificate' toCertificates :: - (Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError, Fail] effs) => + (Members '[MockChainReadChain, MockChainReadConf, Error MockChainError, Error P.Ledger.ToCardanoError, Fail] effs) => [TxSkelCertificate] -> Sem effs (Cardano.TxCertificates Cardano.BuildTx Cardano.ConwayEra) toCertificates = diff --git a/src/Cooked/MockChain/Automation/GenerateTx/Collateral.hs b/src/Cooked/MockChain/Automation/GenerateTx/Collateral.hs index 98ef617e9..918f03934 100644 --- a/src/Cooked/MockChain/Automation/GenerateTx/Collateral.hs +++ b/src/Cooked/MockChain/Automation/GenerateTx/Collateral.hs @@ -8,7 +8,8 @@ where import Cardano.Api qualified as Cardano import Cooked.MockChain.Automation.GenerateTx.Output import Cooked.MockChain.Common -import Cooked.MockChain.Effect.Read +import Cooked.MockChain.Effect.Read.Chain +import Cooked.MockChain.Effect.Read.Conf import Cooked.Skeleton.Output import Cooked.Skeleton.Value import Data.Map qualified as Map @@ -31,7 +32,7 @@ import Polysemy.Error -- These quantity should satisfy the equation (in terms of their values): -- collateral inputs = total collateral + return collateral toCollateralTriplet :: - (Members '[MockChainRead, Error P.Ledger.ToCardanoError] effs) => + (Members '[MockChainReadChain, MockChainReadConf, Error P.Ledger.ToCardanoError] effs) => Maybe Collaterals -> Sem effs diff --git a/src/Cooked/MockChain/Automation/GenerateTx/Input.hs b/src/Cooked/MockChain/Automation/GenerateTx/Input.hs index 7d2c6091e..637d6ac65 100644 --- a/src/Cooked/MockChain/Automation/GenerateTx/Input.hs +++ b/src/Cooked/MockChain/Automation/GenerateTx/Input.hs @@ -3,10 +3,12 @@ module Cooked.MockChain.Automation.GenerateTx.Input (toTxInAndWitness) where import Cardano.Api qualified as Cardano import Cooked.MockChain.Automation.GenerateTx.Witness -import Cooked.MockChain.Effect.Read +import Cooked.MockChain.Effect.Read.Chain import Cooked.MockChain.Runtime.Error import Cooked.Skeleton import Ledger.Tx.CardanoAPI qualified as P.Ledger +import Optics.Core +import Plutus.Script.Utils.Scripts qualified as Script import PlutusLedgerApi.V3 qualified as Api import Polysemy import Polysemy.Error @@ -14,7 +16,7 @@ import Polysemy.Error -- | Converts a 'TxSkel' input, which consists of a 'Api.TxOutRef' and a -- 'TxSkelRedeemer', into a 'Cardano.TxIn', together with the appropriate witness. toTxInAndWitness :: - (Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError] effs) => + (Members '[MockChainReadChain, Error MockChainError, Error P.Ledger.ToCardanoError] effs) => (Api.TxOutRef, TxSkelRedeemer) -> Sem effs @@ -23,13 +25,26 @@ toTxInAndWitness :: ) toTxInAndWitness (txOutRef, txSkelRedeemer) = do TxSkelOut {txSkelOutOwner, txSkelOutDatum} <- txSkelOutByRef txOutRef + let toScriptDatum = case txSkelOutDatum of + NoTxSkelOutDatum -> return $ Cardano.ScriptDatumForTxIn Nothing + SomeTxSkelOutDatum _ Inline -> return Cardano.InlineScriptDatum + SomeTxSkelOutDatum dat _ -> return $ Cardano.ScriptDatumForTxIn $ Just $ P.Ledger.toCardanoScriptData $ Api.toBuiltinData dat + SomeTxSkelOutDatumHash hash -> throw $ MCESpendingHashOnlyDatum txOutRef hash witness <- case txSkelOutOwner of UserPubKey _ -> return $ Cardano.KeyWitness Cardano.KeyWitnessForSpending - UserScript script -> - fmap (Cardano.ScriptWitness Cardano.ScriptWitnessForSpending) $ - toScriptWitness script txSkelRedeemer $ - case txSkelOutDatum of - NoTxSkelOutDatum -> Cardano.ScriptDatumForTxIn Nothing - SomeTxSkelOutDatum _ Inline -> Cardano.InlineScriptDatum - SomeTxSkelOutDatum dat _ -> Cardano.ScriptDatumForTxIn $ Just $ P.Ledger.toCardanoScriptData $ Api.toBuiltinData dat + UserScript script -> do + scriptDatum <- toScriptDatum + Cardano.ScriptWitness Cardano.ScriptWitnessForSpending <$> toScriptWitness script txSkelRedeemer scriptDatum + UserScriptHash sHash -> do + scriptDatum <- toScriptDatum + -- The full script is not available in the owner, so it must be recovered + -- from the reference script of the redeemer's reference input. + mVScript <- case txSkelRedeemerReferenceInput txSkelRedeemer of + Nothing -> return Nothing + Just refOutRef -> preview txSkelOutReferenceScriptAT <$> txSkelOutByRef refOutRef + case mVScript of + Just vScript + | Script.toScriptHash vScript == sHash -> + Cardano.ScriptWitness Cardano.ScriptWitnessForSpending <$> toScriptWitness vScript txSkelRedeemer scriptDatum + _ -> throw $ MCESpendingHashOnlyScript txOutRef sHash (,Cardano.BuildTxWith witness) <$> fromEither (P.Ledger.toCardanoTxIn txOutRef) diff --git a/src/Cooked/MockChain/Automation/GenerateTx/Mint.hs b/src/Cooked/MockChain/Automation/GenerateTx/Mint.hs index 16ef83498..0ec5beca4 100644 --- a/src/Cooked/MockChain/Automation/GenerateTx/Mint.hs +++ b/src/Cooked/MockChain/Automation/GenerateTx/Mint.hs @@ -4,7 +4,7 @@ module Cooked.MockChain.Automation.GenerateTx.Mint (toMintValue) where import Cardano.Api qualified as Cardano import Control.Monad import Cooked.MockChain.Automation.GenerateTx.Witness -import Cooked.MockChain.Effect.Read +import Cooked.MockChain.Effect.Read.Chain import Cooked.MockChain.Runtime.Error import Cooked.Skeleton.Mint import Cooked.Skeleton.User @@ -21,7 +21,7 @@ import Polysemy.Error -- | Converts a 'TxSkelMints' into a 'Cardano.TxMintValue' toMintValue :: - (Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError] effs) => + (Members '[MockChainReadChain, Error MockChainError, Error P.Ledger.ToCardanoError] effs) => TxSkelMints -> Sem effs (Cardano.TxMintValue Cardano.BuildTx Cardano.ConwayEra) toMintValue txSkelMints | txSkelMints == mempty = return Cardano.TxMintNone diff --git a/src/Cooked/MockChain/Automation/GenerateTx/Output.hs b/src/Cooked/MockChain/Automation/GenerateTx/Output.hs index 0b833e3d8..f5584b6bb 100644 --- a/src/Cooked/MockChain/Automation/GenerateTx/Output.hs +++ b/src/Cooked/MockChain/Automation/GenerateTx/Output.hs @@ -2,8 +2,8 @@ module Cooked.MockChain.Automation.GenerateTx.Output (toCardanoTxOut) where import Cardano.Api qualified as Cardano -import Cardano.Node.Emulator.Internal.Node.Params qualified as Emulator -import Cooked.MockChain.Effect.Read +import Cooked.MockChain.Effect.Read.Chain +import Cooked.MockChain.Effect.Read.Conf import Cooked.Skeleton.Datum import Cooked.Skeleton.Output import Ledger.Tx.CardanoAPI qualified as P.Ledger @@ -15,7 +15,7 @@ import Polysemy.Error -- | Converts a 'TxSkelOut' to the corresponding 'Cardano.TxOut' toCardanoTxOut :: - (Members '[MockChainRead, Error P.Ledger.ToCardanoError] effs) => + (Members '[MockChainReadChain, MockChainReadConf, Error P.Ledger.ToCardanoError] effs) => TxSkelOut -> Sem effs (Cardano.TxOut Cardano.CtxTx Cardano.ConwayEra) toCardanoTxOut output = do @@ -23,7 +23,7 @@ toCardanoTxOut output = do oValue = view txSkelOutValueL output oDatum = view txSkelOutDatumL output oRefScript = view txSkelOutMReferenceScriptL output - networkId <- Emulator.pNetworkId <$> getParams + networkId <- getNetworkId address <- fromEither $ P.Ledger.toCardanoAddressInEra networkId oAddress (P.Ledger.toCardanoTxOutValue -> value) <- fromEither $ P.Ledger.toCardanoValue oValue datum <- case oDatum of @@ -41,4 +41,7 @@ toCardanoTxOut output = do Cardano.TxOutDatumInline Cardano.BabbageEraOnwardsConway $ P.Ledger.toCardanoScriptData $ Api.toBuiltinData datum + SomeTxSkelOutDatumHash hash -> + Cardano.TxOutDatumHash Cardano.AlonzoEraOnwardsConway + <$> fromEither (P.Ledger.toCardanoScriptDataHash hash) return $ Cardano.TxOut address value datum $ P.Ledger.toCardanoReferenceScript oRefScript diff --git a/src/Cooked/MockChain/Automation/GenerateTx/Proposal.hs b/src/Cooked/MockChain/Automation/GenerateTx/Proposal.hs index 8a92b45df..a9268ec72 100644 --- a/src/Cooked/MockChain/Automation/GenerateTx/Proposal.hs +++ b/src/Cooked/MockChain/Automation/GenerateTx/Proposal.hs @@ -12,7 +12,8 @@ import Control.Monad import Cooked.MockChain.Automation.GenerateTx.Anchor import Cooked.MockChain.Automation.GenerateTx.Credential import Cooked.MockChain.Automation.GenerateTx.Witness -import Cooked.MockChain.Effect.Read +import Cooked.MockChain.Effect.Read.Chain +import Cooked.MockChain.Effect.Read.Conf import Cooked.MockChain.Runtime.Error import Cooked.Skeleton.Proposal import Cooked.Skeleton.User @@ -84,7 +85,7 @@ toPParamsUpdate pChange ppu = -- | Translates a given skeleton proposal into a governance action toGovAction :: - (Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError] effs) => + (Members '[MockChainReadChain, MockChainReadConf, Error MockChainError, Error P.Ledger.ToCardanoError] effs) => GovernanceAction a -> StrictMaybe Conway.ScriptHash -> Sem effs (Conway.GovAction Emulator.EmulatorEra) @@ -100,7 +101,7 @@ toGovAction (TreasuryWithdrawals (Map.toList -> withdrawals)) sHash = -- | Translates a list of skeleton proposals into a proposal procedures toProposalProcedures :: - (Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError] effs) => + (Members '[MockChainReadChain, MockChainReadConf, Error MockChainError, Error P.Ledger.ToCardanoError] effs) => [TxSkelProposal] -> Sem effs (Cardano.TxProposalProcedures Cardano.BuildTx Cardano.ConwayEra) toProposalProcedures props | null props = return Cardano.TxProposalProceduresNone diff --git a/src/Cooked/MockChain/Automation/GenerateTx/ReferenceInputs.hs b/src/Cooked/MockChain/Automation/GenerateTx/ReferenceInputs.hs index 825f41294..16a16b3d7 100644 --- a/src/Cooked/MockChain/Automation/GenerateTx/ReferenceInputs.hs +++ b/src/Cooked/MockChain/Automation/GenerateTx/ReferenceInputs.hs @@ -2,7 +2,7 @@ module Cooked.MockChain.Automation.GenerateTx.ReferenceInputs (toInsReference) where import Cardano.Api qualified as Cardano -import Cooked.MockChain.Effect.Read +import Cooked.MockChain.Effect.Read.Chain import Cooked.Skeleton import Data.Map qualified as Map import Data.Set qualified as Set @@ -17,7 +17,7 @@ import Polysemy.Error -- redeemers of the transaction, which can be gathered with -- 'txSkelReferenceInputsInRedeemers'. toInsReference :: - (Members '[MockChainRead, Error P.Ledger.ToCardanoError] effs) => + (Members '[MockChainReadChain, Error P.Ledger.ToCardanoError] effs) => TxSkel -> Sem effs (Cardano.TxInsReference Cardano.BuildTx Cardano.ConwayEra) toInsReference skel = do diff --git a/src/Cooked/MockChain/Automation/GenerateTx/Withdrawals.hs b/src/Cooked/MockChain/Automation/GenerateTx/Withdrawals.hs index 0727d021a..56a1dbf4c 100644 --- a/src/Cooked/MockChain/Automation/GenerateTx/Withdrawals.hs +++ b/src/Cooked/MockChain/Automation/GenerateTx/Withdrawals.hs @@ -2,10 +2,10 @@ module Cooked.MockChain.Automation.GenerateTx.Withdrawals (toWithdrawals) where import Cardano.Api qualified as Cardano -import Cardano.Node.Emulator.Internal.Node.Params qualified as Emulator import Control.Monad import Cooked.MockChain.Automation.GenerateTx.Witness -import Cooked.MockChain.Effect.Read +import Cooked.MockChain.Effect.Read.Chain +import Cooked.MockChain.Effect.Read.Conf import Cooked.MockChain.Runtime.Error import Cooked.Skeleton.User import Cooked.Skeleton.Withdrawal @@ -20,12 +20,12 @@ import Polysemy.Error -- | Takes a 'TxSkelWithdrawals' and transforms it into a 'Cardano.TxWithdrawals' toWithdrawals :: - (Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError] effs) => + (Members '[MockChainReadChain, MockChainReadConf, Error MockChainError, Error P.Ledger.ToCardanoError] effs) => TxSkelWithdrawals -> Sem effs (Cardano.TxWithdrawals Cardano.BuildTx Cardano.ConwayEra) toWithdrawals withdrawals | withdrawals == mempty = return Cardano.TxWithdrawalsNone toWithdrawals (view txSkelWithdrawalsListI -> withdrawals) = do - networkId <- Emulator.pNetworkId <$> getParams + networkId <- getNetworkId cardanoWithdrawals <- forM withdrawals $ \(Withdrawal user amount) -> do let coinAmount = maybe (Cardano.Coin 0) coerce amount (sCred, witness) <- case user of diff --git a/src/Cooked/MockChain/Automation/GenerateTx/Witness.hs b/src/Cooked/MockChain/Automation/GenerateTx/Witness.hs index fc60ec8a3..d881911ce 100644 --- a/src/Cooked/MockChain/Automation/GenerateTx/Witness.hs +++ b/src/Cooked/MockChain/Automation/GenerateTx/Witness.hs @@ -6,7 +6,7 @@ module Cooked.MockChain.Automation.GenerateTx.Witness where import Cardano.Api qualified as Cardano -import Cooked.MockChain.Effect.Read +import Cooked.MockChain.Effect.Read.Chain import Cooked.MockChain.Runtime.Error import Cooked.Skeleton import Ledger.Address qualified as P.Ledger @@ -20,7 +20,7 @@ import Polysemy.Error -- | Translates a script and a reference script utxo into either a plutus script -- or a reference input containing the right script toPlutusScriptOrReferenceInput :: - (Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError] effs) => + (Members '[MockChainReadChain, Error MockChainError, Error P.Ledger.ToCardanoError] effs) => VScript -> Maybe Api.TxOutRef -> Sem effs (Cardano.PlutusScriptOrReferenceInput lang) @@ -41,7 +41,7 @@ toPlutusScriptOrReferenceInput (Script.toScriptHash -> scriptHash) (Just scriptO -- script. They will be filled out later on once the full body has been -- generated. So, for now, we temporarily leave them to 0. toScriptWitness :: - ( Members '[MockChainRead, Error MockChainError, Error P.Ledger.ToCardanoError] effs, + ( Members '[MockChainReadChain, Error MockChainError, Error P.Ledger.ToCardanoError] effs, ToVScript a ) => a -> diff --git a/src/Cooked/MockChain/Common.hs b/src/Cooked/MockChain/Common.hs index e8429773a..158d9168e 100644 --- a/src/Cooked/MockChain/Common.hs +++ b/src/Cooked/MockChain/Common.hs @@ -6,10 +6,20 @@ module Cooked.MockChain.Common Collaterals, Utxo, Utxos, + BodyContent, + Body, + Transaction, + SubmissionFailures, + ExUnitsFailures, ) where +import Cardano.Api qualified as Cardano +import Cardano.Ledger.Alonzo.Plutus.Evaluate qualified as Alonzo +import Cardano.Ledger.Conway qualified as Conway +import Cardano.Ledger.Conway.Rules qualified as Conway import Cooked.Skeleton.Output +import Data.Map (Map) import Data.Set (Set) import PlutusLedgerApi.V3 qualified as Api @@ -29,4 +39,21 @@ type Collaterals = (CollateralIns, Maybe TxSkelOut) type Utxo = (Api.TxOutRef, TxSkelOut) -- | An alias for lists of `Utxo` -type Utxos = [Utxo] +type Utxos = Map Api.TxOutRef TxSkelOut + +-- | An alias for a transaction body content +type BodyContent = Cardano.TxBodyContent Cardano.BuildTx Cardano.ConwayEra + +-- | An alias for a transaction body +type Body = Cardano.TxBody Cardano.ConwayEra + +-- | An alias for errors occurring when computing execution units. These contain +-- Phase2 failures, but also errors uncovered when building a proper context to +-- execute the scripts. +type ExUnitsFailures = Map Cardano.ScriptWitnessIndex (Alonzo.TransactionScriptFailure Conway.ConwayEra) + +-- | An alias for errors occurring at submission +type SubmissionFailures = [Conway.ConwayLedgerPredFailure Conway.ConwayEra] + +-- | An alias for a Cardano transaction +type Transaction = Cardano.Tx Cardano.ConwayEra diff --git a/src/Cooked/MockChain/Effect/Log.hs b/src/Cooked/MockChain/Effect/Log.hs index 4c03d113d..b270bb90d 100644 --- a/src/Cooked/MockChain/Effect/Log.hs +++ b/src/Cooked/MockChain/Effect/Log.hs @@ -8,6 +8,7 @@ -- user's perspective, use `Cooked.MockChain.Effect.Misc.note` instead. module Cooked.MockChain.Effect.Log ( -- * Logging events + TxValidity (..), MockChainLogEntry (..), -- * Logging effect @@ -26,6 +27,17 @@ import PlutusLedgerApi.V3 qualified as Api import Polysemy import Polysemy.Writer +-- | The validity of a transaction +data TxValidity + = -- | The transaction is valid, we store the number of inputs and outputs + Valid Int Int + | -- | The transaction is invalid in phase 1 (no ledger change) + InvalidPhase1 + | -- | The transaction is invalid in phase 2, we store the number of collateral + -- inputs and return collateral outputs + InvalidPhase2 Int Int + deriving (Show) + -- | Events logged when processing transaction skeletons data MockChainLogEntry = -- | Logging a Skeleton as it is submitted by the user. @@ -33,9 +45,9 @@ data MockChainLogEntry | -- | Logging a Skeleton as it has been adjusted by the balancing mechanism, -- alongside fee, and possible collateral utxos and return collateral user. MCLogAdjustedTxSkel TxSkel Fee (Maybe Collaterals) - | -- | Logging the successful validation of a new transaction, with its id and - -- number of produced outputs. - MCLogNewTx Api.TxId Integer + | -- | Logging the production of a new transaction, with its ID as well as its + -- validity. + MCLogNewTx Api.TxId TxValidity | -- | Logging the fact that utxos provided by the user for balancing have to be -- discarded for a specific reason. MCLogDiscardedUtxos Integer String @@ -52,6 +64,12 @@ data MockChainLogEntry MCLogAutoFilledConstitution Api.ScriptHash | -- | Logging the automatic adjustment of a min ada amount MCLogAdjustedTxSkelOut TxSkelOut Api.Lovelace + | -- | Logging the existence of failures uncovered during the computation of + -- execution units, when they're not treated as fatal. + MCELogExUnitsFailures ExUnitsFailures + | -- | Logging the existence of failures uncovered during submission, when + -- they're not treated as fatal. + MCELogSubmissionFailures SubmissionFailures deriving (Show) -- | An effect to allow logging of mockchain events diff --git a/src/Cooked/MockChain/Effect/Read.hs b/src/Cooked/MockChain/Effect/Read.hs deleted file mode 100644 index d73d3a6d2..000000000 --- a/src/Cooked/MockChain/Effect/Read.hs +++ /dev/null @@ -1,404 +0,0 @@ -{-# LANGUAGE TemplateHaskell #-} - --- | This module exposes primitives to query the current state of the --- blockchain. -module Cooked.MockChain.Effect.Read - ( -- * The `MockChainRead` effect - MockChainRead, - runMockChainRead, - - -- * Queries related to protocol parameters - getParams, - govActionDeposit, - dRepDeposit, - stakeAddressDeposit, - stakePoolDeposit, - - -- * Queries related to `Cooked.Skeleton.TxSkel` - txSkelDepositedValueInCertificates, - txSkelDepositedValueInProposals, - txSkelAllScripts, - txSkelInputScripts, - txSkelInputValue, - - -- * Queries related to timing - currentSlot, - currentMSRange, - getEnclosingSlot, - slotRangeBefore, - slotRangeAfter, - slotToMSRange, - - -- * Queries related to fetching UTxOs - allUtxos, - utxosAt, - txSkelOutByRef, - utxosFromCardanoTx, - lookupUtxos, - previewByRef, - viewByRef, - - -- * Other queries - getConstitutionScript, - getCurrentReward, - ) -where - -import Cardano.Api qualified as Cardano -import Cardano.Ledger.Conway.Core qualified as Conway -import Cardano.Node.Emulator.Internal.Node qualified as Emulator -import Control.Lens qualified as Lens -import Control.Monad -import Cooked.MockChain.Automation.GenerateTx.Credential (toStakeCredential) -import Cooked.MockChain.Common -import Cooked.MockChain.Runtime.Error -import Cooked.MockChain.Runtime.State -import Cooked.Skeleton -import Data.Coerce (coerce) -import Data.Map (Map) -import Data.Map qualified as Map -import Data.Maybe -import Ledger.Slot qualified as P.Ledger -import Ledger.Tx qualified as P.Ledger -import Ledger.Tx.CardanoAPI qualified as P.Ledger -import Optics.Core -import Plutus.Script.Utils.Address qualified as Script -import PlutusLedgerApi.V3 qualified as Api -import Polysemy -import Polysemy.Error -import Polysemy.Fail -import Polysemy.State - --- | An effect that offers primitives to query the current state of the --- mockchain. As its name suggests, this effect is read-only and does not alter --- the state in any way. -data MockChainRead :: Effect where - GetParams :: MockChainRead m Emulator.Params - TxSkelOutByRef :: Api.TxOutRef -> MockChainRead m TxSkelOut - CurrentSlot :: MockChainRead m P.Ledger.Slot - AllUtxos :: MockChainRead m Utxos - UtxosAt :: (Script.ToCredential a) => a -> MockChainRead m Utxos - GetConstitutionScript :: MockChainRead m (Maybe VScript) - GetCurrentReward :: (Script.ToCredential c) => c -> MockChainRead m (Maybe Api.Lovelace) - -makeSem_ ''MockChainRead - --- | The interpretation for read-only effect in the blockchain state -runMockChainRead :: - forall effs a. - ( Members - '[ State MockChainState, - Error P.Ledger.ToCardanoError, - Error MockChainError - ] - effs - ) => - Sem (MockChainRead : effs) a -> - Sem effs a -runMockChainRead = interpret $ \case - GetParams -> gets mcstParams - TxSkelOutByRef oRef -> do - res <- gets $ Map.lookup oRef . mcstOutputs - case res of - Just (txSkelOut, True) -> return txSkelOut - _ -> throw $ MCEUnknownOutRef oRef - AllUtxos -> fetchUtxos $ const True - UtxosAt (Script.toCredential -> cred) -> fetchUtxos $ (== cred) . Script.toCredential - CurrentSlot -> gets $ view $ mcstLedgerStateL % to Emulator.getSlot - GetConstitutionScript -> gets $ view mcstConstitutionL - GetCurrentReward (Script.toCredential -> cred) -> do - stakeCredential <- toStakeCredential cred - gets $ - preview $ - mcstLedgerStateL - % to (Emulator.getReward stakeCredential) - % _Just - % to coerce - where - fetchUtxos decide = - gets $ - toListOf $ - mcstOutputsL - % to Map.toList - % traversed - % filtered (snd . snd) - % filtered (decide . fst . snd) - % to (fmap fst) - --- | Returns the emulator parameters, including protocol parameters -getParams :: - (Member MockChainRead effs) => - Sem effs Emulator.Params - --- | Retrieves the required governance action deposit amount -govActionDeposit :: - (Member MockChainRead effs) => - Sem effs Api.Lovelace -govActionDeposit = - getParams - <&> Api.Lovelace - . Cardano.unCoin - . Lens.view Conway.ppGovActionDepositL - . Emulator.emulatorPParams - --- | Retrieves the required drep deposit amount -dRepDeposit :: - (Member MockChainRead effs) => - Sem effs Api.Lovelace -dRepDeposit = - getParams - <&> Api.Lovelace - . Cardano.unCoin - . Lens.view Conway.ppDRepDepositL - . Emulator.emulatorPParams - --- | Retrieves the required stake address deposit amount -stakeAddressDeposit :: - (Member MockChainRead effs) => - Sem effs Api.Lovelace -stakeAddressDeposit = - getParams - <&> Api.Lovelace - . Cardano.unCoin - . Lens.view Conway.ppKeyDepositL - . Emulator.emulatorPParams - --- | Retrieves the required stake pool deposit amount -stakePoolDeposit :: - (Member MockChainRead effs) => - Sem effs Api.Lovelace -stakePoolDeposit = - getParams - <&> Api.Lovelace - . Cardano.unCoin - . Lens.view Conway.ppPoolDepositL - . Emulator.emulatorPParams - --- | Retrieves the total amount of lovelace deposited in certificates in this --- skeleton. Note that unregistering a staking address or a dRep lead to a --- negative deposit (a withdrawal, in fact) which means this function can return --- a negative amount of lovelace, which is intended. The deposited amounts are --- dictated by the current protocol parameters, and computed as such. -txSkelDepositedValueInCertificates :: - (Member MockChainRead effs) => - TxSkel -> - Sem effs Api.Lovelace -txSkelDepositedValueInCertificates txSkel = do - sDep <- stakeAddressDeposit - dDep <- dRepDeposit - pDep <- stakePoolDeposit - return $ - foldOf - ( txSkelCertificatesL - % traversed - % to - ( \case - TxSkelCertificate _ StakingRegister {} -> sDep - TxSkelCertificate _ StakingRegisterDelegate {} -> sDep - TxSkelCertificate _ StakingUnRegister {} -> -sDep - TxSkelCertificate _ DRepRegister {} -> dDep - TxSkelCertificate _ DRepUnRegister {} -> -dDep - TxSkelCertificate _ PoolRegister {} -> pDep - -- There is no special case for 'PoolRetire' because the deposit - -- is given back to the reward account. - _ -> Api.Lovelace 0 - ) - ) - txSkel - --- | Retrieves the total amount of lovelace deposited in proposals in this --- skeleton (equal to `govActionDeposit` times the number of proposals) -txSkelDepositedValueInProposals :: - (Member MockChainRead effs) => - TxSkel -> - Sem effs Api.Lovelace -txSkelDepositedValueInProposals TxSkel {txSkelProposals} = - govActionDeposit - <&> Api.Lovelace - . (toInteger (length txSkelProposals) *) - . Api.getLovelace - --- | Returns all scripts involved in this 'TxSkel' -txSkelAllScripts :: - (Member MockChainRead effs) => - TxSkel -> - Sem effs [VScript] -txSkelAllScripts txSkel = do - txSkelSpendingScripts <- txSkelInputScripts txSkel - return $ - toListOf (txSkelRedeemedScriptsT % userVScriptL) txSkel - <> txSkelSpendingScripts - --- | Returns all scripts which guard transaction inputs -txSkelInputScripts :: - (Member MockChainRead effs) => - TxSkel -> - Sem effs [VScript] -txSkelInputScripts = - fmap catMaybes - . mapM (previewByRef (txSkelOutOwnerL % userVScriptAT)) - . Map.keys - . txSkelInputs - --- | look up the UTxOs the transaction consumes, and sum their values. -txSkelInputValue :: - (Member MockChainRead effs) => - TxSkel -> - Sem effs Api.Value -txSkelInputValue = - fmap mconcat - . mapM (viewByRef txSkelOutValueL) - . Map.keys - . txSkelInputs - --- | Returns the current slot -currentSlot :: - (Member MockChainRead effs) => - Sem effs P.Ledger.Slot - --- | Returns the closed ms interval corresponding to the current slot -currentMSRange :: - (Members '[MockChainRead, Fail] effs) => - Sem effs (Api.POSIXTime, Api.POSIXTime) -currentMSRange = slotToMSRange =<< currentSlot - --- | Return the slot that contains the given time. See 'slotToMSRange' for --- some satisfied equational properties. -getEnclosingSlot :: - (Member MockChainRead effs) => - Api.POSIXTime -> - Sem effs P.Ledger.Slot -getEnclosingSlot t = - getParams - <&> (`Emulator.posixTimeToEnclosingSlot` t) - . Emulator.pSlotConfig - --- | The infinite range of slots ending before or at the given time -slotRangeBefore :: - (Members '[MockChainRead, Fail] effs) => - Api.POSIXTime -> - Sem effs P.Ledger.SlotRange -slotRangeBefore t = do - n <- getEnclosingSlot t - (_, b) <- slotToMSRange n - -- If the given time @t@ happens to be the last ms of its slot, we can include - -- the whole slot. Otherwise, the only way to be sure that the returned slot - -- range contains no time after @t@ is to go to the preceding slot. - return $ Api.to $ if t == b then n else n - 1 - --- | The infinite range of slots starting after or at the given time -slotRangeAfter :: - (Members '[MockChainRead, Fail] effs) => - Api.POSIXTime -> - Sem effs P.Ledger.SlotRange -slotRangeAfter t = do - n <- getEnclosingSlot t - (a, _) <- slotToMSRange n - return $ Api.from $ if t == a then n else n + 1 - --- | Returns the closed ms interval corresponding to the slot with the given --- number. It holds that --- --- > slotToMSRange (getEnclosingSlot t) == (a, b) ==> a <= t <= b --- --- and --- --- > slotToMSRange n == (a, b) ==> getEnclosingSlot a == n && getEnclosingSlot b == n --- --- and --- --- > slotToMSRange n == (a, b) ==> getEnclosingSlot (a-1) == n-1 && getEnclosingSlot (b+1) == n+1 -slotToMSRange :: - ( Members '[MockChainRead, Fail] effs, - Integral i - ) => - i -> - Sem effs (Api.POSIXTime, Api.POSIXTime) -slotToMSRange (fromIntegral -> slot) = do - slotConfig <- Emulator.pSlotConfig <$> getParams - case Emulator.slotToPOSIXTimeRange slotConfig slot of - Api.Interval - (Api.LowerBound (Api.Finite l) leftclosed) - (Api.UpperBound (Api.Finite r) rightclosed) -> - return - ( if leftclosed then l else l + 1, - if rightclosed then r else r - 1 - ) - _ -> fail "Unexpected unbounded slot: please report a bug at https://github.com/tweag/cooked-validators/issues" - --- | Returns a list of all currently known outputs -allUtxos :: - (Member MockChainRead effs) => - Sem effs Utxos - --- | Returns a list of all UTxOs at a certain address. -utxosAt :: - ( Member MockChainRead effs, - Script.ToCredential cred - ) => - cred -> - Sem effs Utxos - --- | Returns an output given a reference to it -txSkelOutByRef :: - (Member MockChainRead effs) => - Api.TxOutRef -> - Sem effs TxSkelOut - --- | Retrieves the ordered list of outputs of the given "CardanoTx". --- --- This is useful when writing endpoints and/or traces to fetch utxos of --- interest right from the start and avoid querying the chain for them --- afterwards using 'allUtxos' or similar functions. -utxosFromCardanoTx :: - (Member MockChainRead effs) => - P.Ledger.CardanoTx -> - Sem effs [(Api.TxOutRef, TxSkelOut)] -utxosFromCardanoTx = - mapM (\txOutRef -> (txOutRef,) <$> txSkelOutByRef txOutRef) - . fmap (P.Ledger.fromCardanoTxIn . snd) - . P.Ledger.getCardanoTxOutRefs - --- | Go through all of the 'Api.TxOutRef's in the list and look them up in the --- state of the blockchain, throwing an error if one of them cannot be resolved. -lookupUtxos :: - (Member MockChainRead effs) => - [Api.TxOutRef] -> - Sem effs (Map Api.TxOutRef TxSkelOut) -lookupUtxos = - foldM - (\m oRef -> flip (Map.insert oRef) m <$> txSkelOutByRef oRef) - Map.empty - --- | Retrieves an output and views a specific element out of it -viewByRef :: - ( Member MockChainRead effs, - Is g A_Getter - ) => - Optic' g is TxSkelOut c -> - Api.TxOutRef -> - Sem effs c -viewByRef optic = (view optic <$>) . txSkelOutByRef - --- | Retrieves an output and previews a specific element out of it -previewByRef :: - ( Member MockChainRead effs, - Is af An_AffineFold - ) => - Optic' af is TxSkelOut c -> - Api.TxOutRef -> - Sem effs (Maybe c) -previewByRef optic = (preview optic <$>) . txSkelOutByRef - --- | Gets the current official constitution script -getConstitutionScript :: - (Member MockChainRead effs) => - Sem effs (Maybe VScript) - --- | Gets the current reward associated with a credential -getCurrentReward :: - ( Member MockChainRead effs, - Script.ToCredential c - ) => - c -> - Sem effs (Maybe Api.Lovelace) diff --git a/src/Cooked/MockChain/Effect/Read/Chain.hs b/src/Cooked/MockChain/Effect/Read/Chain.hs new file mode 100644 index 000000000..2cc69064e --- /dev/null +++ b/src/Cooked/MockChain/Effect/Read/Chain.hs @@ -0,0 +1,352 @@ +-- | This module exposes the user-facing primitives to query the current state +-- of the blockchain, such as the available UTxOs, and the current constitution +-- or rewards. Time-related queries live in the separate +-- 'Cooked.MockChain.Effect.Time.MockChainTime' effect. The lower-level +-- configuration primitives (protocol parameters, network id, era history, system +-- start) live in the internal +-- 'Cooked.MockChain.Effect.Read.Conf.MockChainReadConf' effect, which this +-- effect relies on during its own interpretation. +module Cooked.MockChain.Effect.Read.Chain + ( -- * The 'MockChainReadChain' effect + MockChainReadChain, + + -- * 'MockChainReadChain' interpreters + runMockChainReadChainEmul, + runMockChainReadChainNode, + + -- * Queries related to `Cooked.Skeleton.TxSkel` + txSkelAllScripts, + txSkelInputScripts, + txSkelInputValue, + + -- * Queries related to fetching UTxOs + allUtxos, + utxosAt, + txSkelOutByRef, + utxosFromCardanoTx, + lookupUtxos, + previewByRef, + viewByRef, + + -- * Query fetching the current reward amount + getCurrentReward, + + -- * Query fetching the current full constitution script + getConstitutionScript, + ) +where + +import Cardano.Api qualified as Cardano +import Cardano.Api.Ledger qualified as Cardano hiding (TxIn) +import Cardano.Node.Emulator.Internal.Node qualified as Emulator +import Control.Monad +import Cooked.MockChain.Automation.GenerateTx.Credential +import Cooked.MockChain.Common +import Cooked.MockChain.Effect.Read.Conf +import Cooked.MockChain.Runtime.Error +import Cooked.MockChain.Runtime.State +import Cooked.Skeleton +import Data.Coerce (coerce) +import Data.Map (Map) +import Data.Map qualified as Map +import Data.Map.Optics (toMapOf) +import Data.Maybe +import Data.Maybe.Strict +import Data.Set qualified as Set +import Ledger.Address qualified as P.Ledger +import Ledger.Tx qualified as P.Ledger +import Ledger.Tx.CardanoAPI qualified as P.Ledger +import Optics.Core +import Plutus.Script.Utils.Address qualified as Script +import Plutus.Script.Utils.Scripts qualified as Script +import PlutusLedgerApi.V3 qualified as Api +import Polysemy +import Polysemy.Error +import Polysemy.Reader +import Polysemy.State + +-- | An effect that offers primitives to query the current state of the +-- mockchain. As its name suggests, this effect is read-only and does not alter +-- the state in any way. This is the user-facing read effect; its interpreters +-- rely on the internal +-- 'Cooked.MockChain.Effect.Read.Conf.MockChainReadConf' effect to resolve the +-- fixed chain configuration. +data MockChainReadChain :: Effect where + TxSkelOutByRef :: Api.TxOutRef -> MockChainReadChain m TxSkelOut + AllUtxos :: MockChainReadChain m Utxos + UtxosAt :: (Script.ToAddress a) => a -> MockChainReadChain m Utxos + GetConstitutionScript :: MockChainReadChain m (Maybe VScript) + GetCurrentReward :: (Script.ToCredential c) => c -> MockChainReadChain m (Maybe Api.Lovelace) + +makeSem_ ''MockChainReadChain + +-- | Returns all scripts involved in this 'TxSkel' +txSkelAllScripts :: + (Member MockChainReadChain effs) => + TxSkel -> + Sem effs [VScript] +txSkelAllScripts txSkel = do + txSkelSpendingScripts <- txSkelInputScripts txSkel + return $ + toListOf (txSkelRedeemedScriptsT % userVScriptL) txSkel + <> txSkelSpendingScripts + +-- | Returns all scripts which guard transaction inputs +txSkelInputScripts :: + (Member MockChainReadChain effs) => + TxSkel -> + Sem effs [VScript] +txSkelInputScripts = + fmap catMaybes + . mapM (previewByRef (txSkelOutOwnerL % userVScriptAT)) + . Map.keys + . txSkelInputs + +-- | look up the UTxOs the transaction consumes, and sum their values. +txSkelInputValue :: + (Member MockChainReadChain effs) => + TxSkel -> + Sem effs Api.Value +txSkelInputValue = + fmap mconcat + . mapM (viewByRef txSkelOutValueL) + . Map.keys + . txSkelInputs + +-- | Returns a list of all currently known outputs +allUtxos :: + (Member MockChainReadChain effs) => + Sem effs Utxos + +-- | Returns a list of all UTxOs at a certain address. +utxosAt :: + ( Member MockChainReadChain effs, + Script.ToAddress cred + ) => + cred -> + Sem effs Utxos + +-- | Returns an output given a reference to it +txSkelOutByRef :: + (Member MockChainReadChain effs) => + Api.TxOutRef -> + Sem effs TxSkelOut + +-- | Retrieves the ordered list of outputs of the given "CardanoTx". +-- +-- This is useful when writing endpoints and/or traces to fetch utxos of +-- interest right from the start and avoid querying the chain for them +-- afterwards using 'allUtxos' or similar functions. +utxosFromCardanoTx :: + (Member MockChainReadChain effs) => + P.Ledger.CardanoTx -> + Sem effs [(Api.TxOutRef, TxSkelOut)] +utxosFromCardanoTx = + mapM (\txOutRef -> (txOutRef,) <$> txSkelOutByRef txOutRef) + . fmap (P.Ledger.fromCardanoTxIn . snd) + . P.Ledger.getCardanoTxOutRefs + +-- | Go through all of the 'Api.TxOutRef's in the list and look them up in the +-- state of the blockchain, throwing an error if one of them cannot be resolved. +lookupUtxos :: + (Member MockChainReadChain effs) => + [Api.TxOutRef] -> + Sem effs (Map Api.TxOutRef TxSkelOut) +lookupUtxos = + foldM + (\m oRef -> flip (Map.insert oRef) m <$> txSkelOutByRef oRef) + Map.empty + +-- | Retrieves an output and views a specific element out of it +viewByRef :: + ( Member MockChainReadChain effs, + Is g A_Getter + ) => + Optic' g is TxSkelOut c -> + Api.TxOutRef -> + Sem effs c +viewByRef optic = (view optic <$>) . txSkelOutByRef + +-- | Retrieves an output and previews a specific element out of it +previewByRef :: + ( Member MockChainReadChain effs, + Is af An_AffineFold + ) => + Optic' af is TxSkelOut c -> + Api.TxOutRef -> + Sem effs (Maybe c) +previewByRef optic = (preview optic <$>) . txSkelOutByRef + +-- | Gets the current official constitution script +getConstitutionScript :: + (Member MockChainReadChain effs) => + Sem effs (Maybe VScript) + +-- | Gets the current reward associated with a credential +getCurrentReward :: + ( Member MockChainReadChain effs, + Script.ToCredential c + ) => + c -> + Sem effs (Maybe Api.Lovelace) + +-- | The interpretation for read-only effect with a stored 'EmulatorState' and +-- 'ChainIndex' +runMockChainReadChainEmul :: + forall effs a. + ( Members + '[ State EmulatorState, + State ChainIndex, + Error P.Ledger.ToCardanoError, + Error MockChainError + ] + effs + ) => + Sem (MockChainReadChain : effs) a -> + Sem effs a +runMockChainReadChainEmul = interpret $ \case + TxSkelOutByRef oRef -> do + res <- gets $ Map.lookup oRef . chainIndexOutputs + case res of + Just (txSkelOut, True) -> return txSkelOut + _ -> throw $ MCEUnknownOutRef oRef + AllUtxos -> fetchUtxos $ const True + UtxosAt (Script.toAddress -> addr) -> fetchUtxos $ (== addr) . Script.toAddress + GetConstitutionScript -> gets $ view chainIndexConstitutionL + GetCurrentReward (Script.toCredential -> cred) -> do + stakeCredential <- toStakeCredential cred + gets $ + preview $ + emulatorStateLedgerStateL + % to (Emulator.getReward stakeCredential) + % _Just + % to coerce + where + fetchUtxos decide = + gets $ + toMapOf $ + chainIndexOutputsL + % itraversed + % filtered snd + % filtered (decide . fst) + % to fst + +-- | Interpret the `MockChainReadChain` effect by talking to a deployed node +-- through a `Cardano.LocalNodeConnectInfo` (socket path and network id) +-- provided via a `Reader`, running in a stack featuring @IO@ (via `Embed`). The +-- fixed chain configuration is resolved through the internal +-- 'Cooked.MockChain.Effect.Read.Conf.MockChainReadConf' effect. +runMockChainReadChainNode :: + forall effs a. + ( Members + '[ Embed IO, + MockChainReadConf, + Error Cardano.UnsupportedNtcVersionError, + Error Cardano.EraMismatch, + Error Cardano.AcquiringFailure, + Error P.Ledger.ToCardanoError, + Error MockChainError, + Reader Cardano.LocalNodeConnectInfo, + State ChainIndex + ] + effs + ) => + Sem (MockChainReadChain : effs) a -> + Sem effs a +runMockChainReadChainNode = interpret $ \case + AllUtxos -> queryUtxosAndHandleErrors Cardano.QueryUTxOWhole + UtxosAt (Script.toAddress -> addr) -> do + networkId <- getNetworkId + (Cardano.AddressInEra _ cAddr) <- fromEither $ P.Ledger.toCardanoAddressInEra networkId addr + queryUtxosAndHandleErrors $ Cardano.QueryUTxOByAddress $ Set.singleton $ Cardano.toAddressAny cAddr + TxSkelOutByRef oRef -> do + txIn <- fromEither $ P.Ledger.toCardanoTxIn oRef + utxo <- queryUtxosAndHandleErrors $ Cardano.QueryUTxOByTxIn $ Set.singleton txIn + maybe (throw $ MCEUnknownOutRef oRef) return $ Map.lookup oRef utxo + GetConstitutionScript -> do + -- We retrieve the official optional script hash of the current constitution + Cardano.Constitution _ mScriptHash <- + queryAndHandleErrors $ Cardano.queryConstitution Cardano.ConwayEraOnwardsConway + -- We retrieve the optional constitution already stored in the chain index + mStoredConstitution <- gets chainIndexConstitution + -- We inspect the current option constitution script hash + case mScriptHash of + -- There is no official constitution (should not happen). We just set our + -- own constitution to @Nothing@ accordingly. + SNothing -> do + modify' $ set chainIndexConstitutionL Nothing + return Nothing + -- There is an official constitution, and it matches the stored one, which + -- we directly return. + SJust (Cardano.ScriptHash -> scriptHash) + | Just storedConstitution <- mStoredConstitution, + Script.toScriptHash scriptHash == Script.toScriptHash storedConstitution -> + return $ Just storedConstitution + -- There is an official constitution, and it does not match the stored one + -- (it has changed, or it's the first time it's been queried). We fetch + -- the actual constitution from a reference script at its own address, + -- where it should live, according to a governance convention. We store + -- the script we find there after verifying its hash, and return it. + SJust (Cardano.ScriptHash -> scriptHash) -> do + networkId <- getNetworkId + utxo <- + queryUtxosAndHandleErrors $ + Cardano.QueryUTxOByAddress $ + Set.singleton $ + Cardano.AddressShelley $ + Cardano.makeShelleyAddress + networkId + (Cardano.PaymentCredentialByScript scriptHash) + Cardano.NoStakeAddress + let newConstitution = + listToMaybe $ + [ script + | (_, preview txSkelOutReferenceScriptAT -> Just script) <- Map.toList utxo, + Script.toScriptHash script == Script.toScriptHash scriptHash + ] + modify' $ set chainIndexConstitutionL newConstitution + return newConstitution + GetCurrentReward (Script.toCredential -> cred) -> do + networkId <- getNetworkId + stakeCred <- toStakeCredential cred + (rewards, _) <- + queryAndHandleErrors $ + Cardano.queryStakeAddresses + Cardano.ShelleyBasedEraConway + (Set.singleton (Cardano.fromShelleyStakeCredential stakeCred)) + networkId + return $ Api.Lovelace . Cardano.unCoin <$> Map.lookup (Cardano.StakeAddress (Cardano.toShelleyNetwork networkId) stakeCred) rewards + where + -- Fetches the local node info, embeds a query in IO and handles errors + query q = do + conn <- ask + response <- embed $ Cardano.executeLocalStateQueryExpr conn Cardano.VolatileTip q + fromEither response + -- Handles one more layer of errors from the response of a query + queryAndHandleError q = query q >>= fromEither + -- Handles a second layer of error from the response of a query + queryAndHandleErrors q = queryAndHandleError q >>= fromEither + -- Queries the Utxos present on-chain, handling the errors, and returns the + -- query result in terms of @Utxos@, updated with the known chain index. + queryUtxosAndHandleErrors utxoFilter = do + utxo <- queryAndHandleErrors $ Cardano.queryUtxo Cardano.ShelleyBasedEraConway utxoFilter + knownUtxos <- gets chainIndexOutputs + return $ + Map.mapWithKey + (\oRef txSkelOut -> maybe txSkelOut fst $ Map.lookup oRef knownUtxos) + (Map.mapKeysMonotonic P.Ledger.fromCardanoTxIn $ convertUtxo <$> Cardano.unUTxO utxo) + convertUtxo :: Cardano.TxOut Cardano.CtxUTxO Cardano.ConwayEra -> TxSkelOut + convertUtxo (Cardano.TxOut (P.Ledger.toPlutusAddress -> (Api.Address cred stCred)) val dat refScript) = + TxSkelOut + (review userCredentialI cred) + stCred + ( dat & \case + Cardano.TxOutDatumNone -> NoTxSkelOutDatum + Cardano.TxOutDatumHash _ hash -> + SomeTxSkelOutDatumHash $ Api.DatumHash $ Api.toBuiltin $ Cardano.serialiseToRawBytes hash + Cardano.TxOutDatumInline _ datum -> + SomeTxSkelOutDatum (P.Ledger.fromCardanoScriptData datum) Inline + ) + (P.Ledger.fromCardanoValue $ P.Ledger.fromCardanoTxOutValue val) + False + (P.Ledger.fromCardanoReferenceScript refScript) diff --git a/src/Cooked/MockChain/Effect/Read/Conf.hs b/src/Cooked/MockChain/Effect/Read/Conf.hs new file mode 100644 index 000000000..847057973 --- /dev/null +++ b/src/Cooked/MockChain/Effect/Read/Conf.hs @@ -0,0 +1,212 @@ +-- | This module exposes internal, configuration-level primitives to query the +-- fixed configuration of the chain, such as its protocol parameters, network +-- id, era history and system start. These primitives are not meant to be used +-- directly when writing traces: they are an implementation detail backing the +-- user-facing 'Cooked.MockChain.Effect.Read.Chain.MockChainReadChain' effect, +-- and they are deliberately not re-exported through the 'Cooked.MockChain' +-- umbrella module. +module Cooked.MockChain.Effect.Read.Conf + ( -- * The 'MockChainReadConf' effect + MockChainReadConf, + + -- * 'MockChainReadConf' interpreters + runMockChainReadConfEmul, + runMockChainReadConfNode, + + -- * Queries related to protocol parameters + getParams, + getNetworkId, + govActionDeposit, + dRepDeposit, + stakeAddressDeposit, + stakePoolDeposit, + + -- * Queries related to time configuration + getEraHistory, + getSystemStart, + + -- * Queries related to `Cooked.Skeleton.TxSkel` deposits + txSkelDepositedValueInCertificates, + txSkelDepositedValueInProposals, + ) +where + +import Cardano.Api qualified as Cardano +import Cardano.Ledger.Conway qualified as Conway +import Cardano.Ledger.Conway.Core qualified as Conway +import Cardano.Ledger.Core qualified as C.Ledger +import Cardano.Ledger.Shelley.API qualified as Shelley +import Cardano.Node.Emulator.Internal.Node qualified as Emulator +import Cardano.Slotting.Time qualified as Time +import Control.Lens qualified as Lens +import Cooked.MockChain.Runtime.State +import Cooked.Skeleton +import Data.Functor +import Optics.Core +import PlutusLedgerApi.V3 qualified as Api +import Polysemy +import Polysemy.Error +import Polysemy.Reader +import Polysemy.State + +-- | An effect that offers primitives to query the fixed configuration of the +-- chain (protocol parameters, network id, era history and system start). As its +-- name suggests, this effect is read-only and does not alter the state in any +-- way. It is internal to the library and backs the user-facing +-- 'Cooked.MockChain.Effect.Read.Chain.MockChainReadChain' effect. +data MockChainReadConf :: Effect where + GetParams :: MockChainReadConf m (C.Ledger.PParams Conway.ConwayEra) + GetNetworkId :: MockChainReadConf m Cardano.NetworkId + GetEraHistory :: MockChainReadConf m Cardano.EraHistory + GetSystemStart :: MockChainReadConf m Time.SystemStart + +makeSem_ ''MockChainReadConf + +-- | The interpretation for the configuration effect with a stored +-- 'EmulatorState' +runMockChainReadConfEmul :: + (Member (State EmulatorState) effs) => + Sem (MockChainReadConf : effs) a -> + Sem effs a +runMockChainReadConfEmul = interpret $ \case + GetParams -> gets $ Emulator.pEmulatorPParams . emulatorStateParams + GetNetworkId -> gets $ Emulator.pNetworkId . emulatorStateParams + GetEraHistory -> gets $ Emulator.emulatorEraHistory . emulatorStateParams + GetSystemStart -> gets $ Shelley.systemStart . Emulator.emulatorGlobals . emulatorStateParams + +-- | Interpret the `MockChainReadConf` effect by talking to a deployed node +-- through a `Cardano.LocalNodeConnectInfo` (socket path and network id) provided +-- via a `Reader`, running in a stack featuring @IO@ (via `Embed`). +runMockChainReadConfNode :: + ( Members + '[ Embed IO, + Error Cardano.UnsupportedNtcVersionError, + Error Cardano.EraMismatch, + Error Cardano.AcquiringFailure, + Reader Cardano.LocalNodeConnectInfo + ] + effs + ) => + Sem (MockChainReadConf : effs) a -> + Sem effs a +runMockChainReadConfNode = interpret $ \case + GetParams -> queryAndHandleErrors $ Cardano.queryProtocolParameters Cardano.ShelleyBasedEraConway + GetNetworkId -> asks Cardano.localNodeNetworkId + GetEraHistory -> queryAndHandleError Cardano.queryEraHistory + GetSystemStart -> queryAndHandleError Cardano.querySystemStart + where + -- Fetches the local node info, embeds a query in IO and handles errors + query q = do + conn <- ask + response <- embed $ Cardano.executeLocalStateQueryExpr conn Cardano.VolatileTip q + fromEither response + -- Handles one more layer of errors from the response of a query + queryAndHandleError q = query q >>= fromEither + -- Handles a second layer of error from the response of a query + queryAndHandleErrors q = queryAndHandleError q >>= fromEither + +-- | Returns the emulator parameters, including protocol parameters +getParams :: + (Member MockChainReadConf effs) => + Sem effs (C.Ledger.PParams Conway.ConwayEra) + +-- | Returns the network id of the current chain +getNetworkId :: + (Member MockChainReadConf effs) => + Sem effs Cardano.NetworkId + +-- | Returns the era history of the chain, which notably allows converting slots +-- into epochs (see 'Cardano.slotToEpoch'). +getEraHistory :: + (Member MockChainReadConf effs) => + Sem effs Cardano.EraHistory + +-- | Returns the system start time of the chain, that is the UTC time at which +-- the first slot begins. +getSystemStart :: + (Member MockChainReadConf effs) => + Sem effs Time.SystemStart + +-- | Retrieves the required governance action deposit amount +govActionDeposit :: + (Member MockChainReadConf effs) => + Sem effs Api.Lovelace +govActionDeposit = + getParams + <&> Api.Lovelace + . Cardano.unCoin + . Lens.view Conway.ppGovActionDepositL + +-- | Retrieves the required drep deposit amount +dRepDeposit :: + (Member MockChainReadConf effs) => + Sem effs Api.Lovelace +dRepDeposit = + getParams + <&> Api.Lovelace + . Cardano.unCoin + . Lens.view Conway.ppDRepDepositL + +-- | Retrieves the required stake address deposit amount +stakeAddressDeposit :: + (Member MockChainReadConf effs) => + Sem effs Api.Lovelace +stakeAddressDeposit = + getParams + <&> Api.Lovelace + . Cardano.unCoin + . Lens.view Conway.ppKeyDepositL + +-- | Retrieves the required stake pool deposit amount +stakePoolDeposit :: + (Member MockChainReadConf effs) => + Sem effs Api.Lovelace +stakePoolDeposit = + getParams + <&> Api.Lovelace + . Cardano.unCoin + . Lens.view Conway.ppPoolDepositL + +-- | Retrieves the total amount of lovelace deposited in certificates in this +-- skeleton. Note that unregistering a staking address or a dRep lead to a +-- negative deposit (a withdrawal, in fact) which means this function can return +-- a negative amount of lovelace, which is intended. The deposited amounts are +-- dictated by the current protocol parameters, and computed as such. +txSkelDepositedValueInCertificates :: + (Member MockChainReadConf effs) => + TxSkel -> + Sem effs Api.Lovelace +txSkelDepositedValueInCertificates txSkel = do + sDep <- stakeAddressDeposit + dDep <- dRepDeposit + pDep <- stakePoolDeposit + return $ + foldOf + ( txSkelCertificatesL + % traversed + % to + ( \case + TxSkelCertificate _ StakingRegister {} -> sDep + TxSkelCertificate _ StakingRegisterDelegate {} -> sDep + TxSkelCertificate _ StakingUnRegister {} -> -sDep + TxSkelCertificate _ DRepRegister {} -> dDep + TxSkelCertificate _ DRepUnRegister {} -> -dDep + TxSkelCertificate _ PoolRegister {} -> pDep + -- There is no special case for 'PoolRetire' because the deposit + -- is given back to the reward account. + _ -> Api.Lovelace 0 + ) + ) + txSkel + +-- | Retrieves the total amount of lovelace deposited in proposals in this +-- skeleton (equal to `govActionDeposit` times the number of proposals) +txSkelDepositedValueInProposals :: + (Member MockChainReadConf effs) => + TxSkel -> + Sem effs Api.Lovelace +txSkelDepositedValueInProposals TxSkel {txSkelProposals} = + govActionDeposit + <&> Api.Lovelace + . (toInteger (length txSkelProposals) *) + . Api.getLovelace diff --git a/src/Cooked/MockChain/Effect/Submission.hs b/src/Cooked/MockChain/Effect/Submission.hs new file mode 100644 index 000000000..5193e42b1 --- /dev/null +++ b/src/Cooked/MockChain/Effect/Submission.hs @@ -0,0 +1,99 @@ +{-# LANGUAGE TemplateHaskell #-} + +-- | This module exposes the 'MockChainSubmit' effect, which is responsible for +-- submitting a Cardano transaction for validation. +module Cooked.MockChain.Effect.Submission + ( -- * The 'MockChainSubmit' effect + MockChainSubmit (..), + submitTransaction, + + -- * Interpretation functions + runMockChainSubmitEmul, + runMockChainSubmitNode, + ) +where + +import Cardano.Api qualified as Cardano +import Cardano.Ledger.Shelley.API.Mempool qualified as Shelley +import Cardano.Node.Emulator.Internal.Node qualified as Emulator +import Cooked.MockChain.Common +import Cooked.MockChain.Effect.Read.Conf +import Cooked.MockChain.Runtime.State +import Data.Foldable.Extra +import Ledger.Orphans () +import Optics.Core +import Polysemy +import Polysemy.Error +import Polysemy.Fail +import Polysemy.Reader +import Polysemy.State + +-- | An effect allow to submit a transaction for validation +data MockChainSubmit :: Effect where + SubmitTransaction :: Transaction -> MockChainSubmit m SubmissionFailures + +makeSem_ ''MockChainSubmit + +-- | Submits a transaction for validation, returning a (possibly empty) list of +-- submission failures. +submitTransaction :: + (Member MockChainSubmit effs) => + Transaction -> + Sem effs SubmissionFailures + +-- | Interprets the `MockChainSubmit` effect on an emulator +runMockChainSubmitEmul :: + forall effs a. + (Member (State EmulatorState) effs) => + Sem (MockChainSubmit : effs) a -> + Sem effs a +runMockChainSubmitEmul = interpret $ \case + SubmitTransaction cardanoTx -> do + -- To run transaction validation we need a minimal ledger state + eLedgerState <- gets emulatorStateLedgerState + -- And the emulator params + params <- gets emulatorStateParams + -- We run the transaction validation through the emulator + let (newELedgerState, submissionFailures) = case Emulator.validateAndApplyTx params eLedgerState cardanoTx of + Left (Shelley.ApplyTxError errs) -> (newELedgerState, toList errs) + Right (newELedgerState', _) -> (newELedgerState', []) + -- We update the index with the utxos consumed and produced by the tx + modify' $ set emulatorStateLedgerStateL newELedgerState + -- We return the validation result + return submissionFailures + +-- | Interprets the `MockChainSubmit` effect by submitting the generated +-- transaction to a deployed node through a `Cardano.LocalNodeConnectInfo` +-- (socket path and network id) provided via a `Reader`, running in a stack +-- featuring @IO@ (via `Embed`). +runMockChainSubmitNode :: + forall effs a. + ( Members + '[ Embed IO, + Error Cardano.EraMismatch, + MockChainReadConf, + Reader Cardano.LocalNodeConnectInfo, + Fail + ] + effs + ) => + Sem (MockChainSubmit : effs) a -> + Sem effs a +runMockChainSubmitNode = interpret $ \case + SubmitTransaction cardanoTx -> do + -- We retrieve the local node connection info. + conn <- ask + -- We submit the transaction to the node + result <- embed $ Cardano.submitTxToNodeLocal conn $ Cardano.TxInMode Cardano.ShelleyBasedEraConway cardanoTx + -- We disect the result the node sends us + case result of + Cardano.SubmitFail + ( Cardano.TxValidationErrorInCardanoMode + (Cardano.ShelleyTxValidationError Cardano.ShelleyBasedEraConway (Shelley.ApplyTxError err)) + ) -> return $ toList err + -- Somehow, the error does not correspond to the proper era, should be unreachable + Cardano.SubmitFail (Cardano.TxValidationErrorInCardanoMode _) -> fail "TxValidationErrorInCardanoMode: Unreachable case" + -- There is an era mismatch between the ledger era and the transaction era + Cardano.SubmitFail (Cardano.TxValidationEraMismatch eraMismatch) -> throw eraMismatch + -- The submission was successful (no phase 1 error) + Cardano.SubmitSuccess -> return [] diff --git a/src/Cooked/MockChain/Effect/Time.hs b/src/Cooked/MockChain/Effect/Time.hs new file mode 100644 index 000000000..46abd4063 --- /dev/null +++ b/src/Cooked/MockChain/Effect/Time.hs @@ -0,0 +1,238 @@ +-- | This module exposes the user-facing primitives to query and manipulate the +-- current time of the blockchain, expressed in terms of slots and POSIX time. +-- It regroups the time-related read primitives (the current slot, the +-- conversions between slots and POSIX time ranges) as well as the primitives to +-- wait for a given slot or time. The lower-level configuration primitives (era +-- history and system start) live in the internal +-- 'Cooked.MockChain.Effect.Read.Conf.MockChainReadConf' effect, which the node +-- interpreter of this effect relies on. +module Cooked.MockChain.Effect.Time + ( -- * The 'MockChainTime' effect + MockChainTime, + + -- * 'MockChainTime' interpreters + runMockChainTimeEmul, + runMockChainTimeNode, + + -- * Queries related to the current time + currentSlot, + currentMSRange, + getEnclosingSlot, + slotRangeBefore, + slotRangeAfter, + slotToMSRange, + + -- * Modifications of the current time + waitNSlots, + awaitSlot, + awaitEnclosingSlot, + waitNMSFromSlotLowerBound, + waitNMSFromSlotUpperBound, + ) +where + +import Cardano.Api qualified as Cardano +import Cardano.Node.Emulator.Internal.Node qualified as Emulator +import Cardano.Slotting.Time qualified as Time +import Control.Concurrent (threadDelay) +import Control.Lens qualified as Lens +import Control.Monad +import Cooked.MockChain.Effect.Read.Conf +import Cooked.MockChain.Runtime.State +import Data.Time.Clock +import Data.Time.Clock.POSIX +import Ledger.Slot qualified as P.Ledger +import Optics.Core +import PlutusLedgerApi.V3 qualified as Api +import Polysemy +import Polysemy.Error +import Polysemy.Fail +import Polysemy.Reader +import Polysemy.State + +-- | An effect that offers primitives to query, convert, and wait on the current +-- time of the mockchain. The read-only primitives ('currentSlot', +-- 'slotToMSRange', 'getEnclosingSlot') do not alter the state, while the waiting +-- primitive ('waitNSlots') advances the current slot. +data MockChainTime :: Effect where + CurrentSlot :: MockChainTime m P.Ledger.Slot + SlotToMSRange :: P.Ledger.Slot -> MockChainTime m (Api.POSIXTime, Api.POSIXTime) + GetEnclosingSlot :: Api.POSIXTime -> MockChainTime m P.Ledger.Slot + WaitNSlots :: Integer -> MockChainTime m P.Ledger.Slot + +makeSem_ ''MockChainTime + +-- | Returns the current slot +currentSlot :: + (Member MockChainTime effs) => + Sem effs P.Ledger.Slot + +-- | Returns the closed ms interval corresponding to the slot with the given +-- number. +slotToMSRange :: + (Members '[MockChainTime, Fail] effs) => + P.Ledger.Slot -> + Sem effs (Api.POSIXTime, Api.POSIXTime) + +-- | Returns the closed ms interval corresponding to the current slot +currentMSRange :: + (Members '[MockChainTime, Fail] effs) => + Sem effs (Api.POSIXTime, Api.POSIXTime) +currentMSRange = slotToMSRange =<< currentSlot + +-- | Return the slot that contains the given time. See 'slotToMSRange' for +-- some satisfied equational properties. +getEnclosingSlot :: + (Member MockChainTime effs) => + Api.POSIXTime -> + Sem effs P.Ledger.Slot + +-- | The infinite range of slots ending before or at the given time +slotRangeBefore :: + (Members '[MockChainTime, Fail] effs) => + Api.POSIXTime -> + Sem effs P.Ledger.SlotRange +slotRangeBefore t = do + n <- getEnclosingSlot t + (_, b) <- slotToMSRange n + -- If the given time @t@ happens to be the last ms of its slot, we can include + -- the whole slot. Otherwise, the only way to be sure that the returned slot + -- range contains no time after @t@ is to go to the preceding slot. + return $ Api.to $ if t == b then n else n - 1 + +-- | The infinite range of slots starting after or at the given time +slotRangeAfter :: + (Members '[MockChainTime, Fail] effs) => + Api.POSIXTime -> + Sem effs P.Ledger.SlotRange +slotRangeAfter t = do + n <- getEnclosingSlot t + (a, _) <- slotToMSRange n + return $ Api.from $ if t == a then n else n + 1 + +-- | Waits a certain number of slots and returns the new slot +waitNSlots :: + (Member MockChainTime effs) => + Integer -> + Sem effs P.Ledger.Slot + +-- | Wait for a certain slot, or throws an error if the slot is already past +awaitSlot :: (Member MockChainTime effs) => P.Ledger.Slot -> Sem effs P.Ledger.Slot +awaitSlot (P.Ledger.Slot targetSlot) = do + P.Ledger.Slot now <- currentSlot + waitNSlots (targetSlot - now) + +-- | Waits until the current slot becomes greater or equal to the slot +-- containing the given POSIX time. Note that that it might not wait for +-- anything if the current slot is large enough. +awaitEnclosingSlot :: (Member MockChainTime effs) => Api.POSIXTime -> Sem effs P.Ledger.Slot +awaitEnclosingSlot time = getEnclosingSlot time >>= awaitSlot + +-- | Wait a given number of ms from the lower bound of the current slot and +-- returns the current slot after waiting. +waitNMSFromSlotLowerBound :: (Members '[MockChainTime, Fail] effs) => Integer -> Sem effs P.Ledger.Slot +waitNMSFromSlotLowerBound duration = currentMSRange >>= awaitEnclosingSlot . (+ fromIntegral duration) . fst + +-- | Wait a given number of ms from the upper bound of the current slot and +-- returns the current slot after waiting. +waitNMSFromSlotUpperBound :: (Members '[MockChainTime, Fail] effs) => Integer -> Sem effs P.Ledger.Slot +waitNMSFromSlotUpperBound duration = currentMSRange >>= awaitEnclosingSlot . (+ fromIntegral duration) . snd + +-- | The interpretation for the time effect with a stored 'EmulatorState' +runMockChainTimeEmul :: + forall effs a. + ( Members + '[ State EmulatorState, + Fail + ] + effs + ) => + Sem (MockChainTime : effs) a -> + Sem effs a +runMockChainTimeEmul = interpret $ \case + CurrentSlot -> gets $ view $ emulatorStateLedgerStateL % to Emulator.getSlot + SlotToMSRange slot -> do + slotConfig <- gets $ Emulator.pSlotConfig . emulatorStateParams + case Emulator.slotToPOSIXTimeRange slotConfig slot of + Api.Interval + (Api.LowerBound (Api.Finite l) leftclosed) + (Api.UpperBound (Api.Finite r) rightclosed) -> + return + ( if leftclosed then l else l + 1, + if rightclosed then r else r - 1 + ) + _ -> fail "Unexpected unbounded slot: please report a bug at https://github.com/tweag/cooked-validators/issues" + GetEnclosingSlot t -> gets $ (`Emulator.posixTimeToEnclosingSlot` t) . Emulator.pSlotConfig . emulatorStateParams + WaitNSlots n -> do + cs <- gets $ Emulator.getSlot . emulatorStateLedgerState + -- Waiting for a non-positive number of slots does not change the current + -- slot, and we simply return it unchanged. + if n <= 0 + then return cs + else do + let newSlot = cs + fromIntegral n + modify' $ over emulatorStateLedgerStateL $ Lens.set Emulator.elsSlotL $ fromIntegral newSlot + return newSlot + +-- | Interpret the `MockChainTime` effect by talking to a deployed node through a +-- `Cardano.LocalNodeConnectInfo` (socket path and network id) provided via a +-- `Reader`, running in a stack featuring @IO@ (via `Embed`). Waiting is +-- performed by suspending the thread for the appropriate amount of time. The +-- fixed chain configuration is resolved through the internal +-- 'Cooked.MockChain.Effect.Read.Conf.MockChainReadConf' effect. +runMockChainTimeNode :: + forall effs a. + ( Members + '[ Embed IO, + MockChainReadConf, + Error Cardano.PastHorizonException, + Reader Cardano.LocalNodeConnectInfo + ] + effs + ) => + Sem (MockChainTime : effs) a -> + Sem effs a +runMockChainTimeNode = interpret $ \case + CurrentSlot -> getNodeSlot + SlotToMSRange slot -> slotToMS slot + GetEnclosingSlot t -> do + eraHistory <- getEraHistory + systemStart <- getSystemStart + let relTime = Time.toRelativeTime systemStart $ posixTimeToUTC t + fromSlotNo <$> fromEither (Cardano.getSlotForRelativeTime relTime eraHistory) + WaitNSlots n -> do + P.Ledger.Slot cs <- getNodeSlot + let target = P.Ledger.Slot $ cs + n + -- We compute the POSIX time at which the target slot begins and suspend the + -- thread until we reach it, if it lies in the future. + (Api.POSIXTime targetMS, _) <- slotToMS target + nowUTC <- embed getCurrentTime + let diff = diffUTCTime (posixTimeToUTC (Api.POSIXTime targetMS)) nowUTC + when (diff > 0) $ embed $ threadDelay $ round $ diff * 1000000 + return $ P.Ledger.Slot $ max cs (cs + n) + where + -- Retrieves the current slot from the local node chain tip + getNodeSlot :: Sem effs P.Ledger.Slot + getNodeSlot = do + conn <- ask + chainTip <- embed $ Cardano.getLocalChainTip conn + return $ case chainTip of + Cardano.ChainTipAtGenesis -> P.Ledger.Slot 0 + (Cardano.ChainTip slotNo _ _) -> fromSlotNo slotNo + -- Converts a slot into the closed POSIX ms interval it spans + slotToMS :: P.Ledger.Slot -> Sem effs (Api.POSIXTime, Api.POSIXTime) + slotToMS slot = do + eraHistory <- getEraHistory + systemStart <- getSystemStart + (relStart, slotLen) <- fromEither $ Cardano.getProgress (toSlotNo slot) eraHistory + let startUTC = Time.fromRelativeTime systemStart relStart + endUTC = Time.getSlotLength slotLen `addUTCTime` startUTC + return (utcToPOSIXTime startUTC, utcToPOSIXTime endUTC - 1) + -- Converts a Plutus slot to a Cardano slot + toSlotNo = Cardano.SlotNo . fromInteger . P.Ledger.getSlot + -- Converts a Cardano slot to a Plutus slot + fromSlotNo (Cardano.SlotNo w) = P.Ledger.Slot (toInteger w) + -- Converts a POSIX time to a UTC time + posixTimeToUTC = posixSecondsToUTCTime . fromRational . (/ 1000) . toRational . Api.getPOSIXTime + -- Converts a UTC time to a POSIX time + utcToPOSIXTime = Api.POSIXTime . round . (1000 *) . utcTimeToPOSIXSeconds diff --git a/src/Cooked/MockChain/Effect/Validation.hs b/src/Cooked/MockChain/Effect/Validation.hs new file mode 100644 index 000000000..131aeb8e4 --- /dev/null +++ b/src/Cooked/MockChain/Effect/Validation.hs @@ -0,0 +1,166 @@ +{-# LANGUAGE TemplateHaskell #-} + +-- | This module exposes the `MockChainValidate` effect, which is responsible +-- for turning a `Cooked.Skeleton.TxSkel` into an actual transaction and +-- submitting it to the emulated ledger. This includes running the whole +-- adjustment pipeline (auto-filling, balancing and transaction generation) and +-- updating the mockchain state based on the validation outcome. +module Cooked.MockChain.Effect.Validation + ( -- * The `MockChainValidate` effect + MockChainValidate (..), + validateTxSkel, + validateTxSkel', + validateTxSkelL, + validateTxSkel_, + + -- * Interpreting the effect + runMockChainValidate, + ) +where + +import Cardano.Api qualified as Cardano +import Control.Monad +import Cooked.MockChain.Automation +import Cooked.MockChain.Common +import Cooked.MockChain.Effect.Log +import Cooked.MockChain.Effect.Read.Chain +import Cooked.MockChain.Effect.Read.Conf +import Cooked.MockChain.Effect.Submission +import Cooked.MockChain.Runtime.Error +import Cooked.MockChain.Runtime.State +import Cooked.Skeleton +import Data.Foldable.Extra +import Data.Map.Strict qualified as Map +import Data.Set qualified as Set +import Ledger.Orphans () +import Ledger.Tx qualified as P.Ledger +import Ledger.Tx.CardanoAPI qualified as P.Ledger +import Optics.Core +import PlutusLedgerApi.V3 qualified as Api +import Polysemy +import Polysemy.Error +import Polysemy.Fail +import Polysemy.State + +-- | An effect that offers the ability to submit a 'TxSkel' throughout the +-- modification and validation pipeline. Technically, this effect is not needed +-- from a semantical perspective, as all of this could already be expressed in +-- 'MockChainSubmit', however, we want this effect to exist on its own to be +-- eligible to be modified by tweaks. +data MockChainValidate :: Effect where + ValidateTxSkel :: TxSkel -> MockChainValidate m (ExtendedTxSkel, SubmissionFailures, Transaction, Utxos) + +makeSem_ ''MockChainValidate + +-- | Generates, balances and validates a transaction from a skeleton. Returns +-- the extended skeleton, generated transaction and the new produced outputs. +validateTxSkel :: + (Member MockChainValidate effs) => + TxSkel -> + Sem effs (ExtendedTxSkel, SubmissionFailures, Transaction, Utxos) + +-- | Same as `validateTxSkel`, but only returns the generated UTxOs +validateTxSkel' :: + (Member MockChainValidate effs) => + TxSkel -> + Sem effs Utxos +validateTxSkel' = fmap (view _4) . validateTxSkel + +-- | Same as `validateTxSkel'`, but only returns the list of produced +-- 'Api.TxOutRef' +validateTxSkelL :: + (Member MockChainValidate effs) => + TxSkel -> + Sem effs [Api.TxOutRef] +validateTxSkelL = fmap (toList . Map.keysSet) . validateTxSkel' + +-- | Same as `validateTxSkel`, but discards the returned transaction +validateTxSkel_ :: + (Member MockChainValidate effs) => + TxSkel -> + Sem effs () +validateTxSkel_ = void . validateTxSkel + +-- | Interpretes the 'MockChainValidate' effects in terms of other effects, in +-- particular 'MockChainSubmit'. +runMockChainValidate :: + ( Members + '[ MockChainLog, + MockChainReadChain, + MockChainReadConf, + MockChainSubmit, + Error P.Ledger.ToCardanoError, + Error MockChainError, + State ChainIndex, + Fail + ] + effs + ) => + Sem (MockChainValidate : effs) a -> + Sem effs a +runMockChainValidate = interpret $ \case + ValidateTxSkel txSkel -> do + -- We fetch the skeleton options + let TxSkelOpts {..} = txSkelOpts txSkel + -- We log the submission of the new skeleton + logEvent $ MCLogSubmittedTxSkel txSkel + -- We run the automation pipeline on the original skeleton + eSkel@(ExtendedTxSkel finalTxSkel fee mCollaterals txBody exUnitsFailures) <- runAutomationPipeline txSkel + -- We log the adjusted skeleton + logEvent $ MCLogAdjustedTxSkel finalTxSkel fee mCollaterals + -- We handle the execution units failures when applicable + when (notNull exUnitsFailures) $ + if txSkelOptHaltOnExUnitsFailures + -- If requested, we treat them as fatal, ending the run + then throw $ MCEExUnitsFailures exUnitsFailures + -- Otherwise, we just log them + else logEvent $ MCELogExUnitsFailures exUnitsFailures + -- We build the Cardano transaction, and apply on it the modification in the + -- skeleton option + let cardanoTx = txSkelOptModTx $ txSignatoriesAndBodyToCardanoTx (view txSkelSignatoriesL finalTxSkel) txBody + -- We wrap it for plutus-ledger usage + let pCardanoTx = P.Ledger.CardanoTx cardanoTx Cardano.ShelleyBasedEraConway + -- We compute the id of the new transaction + let txId = P.Ledger.fromCardanoTxId $ P.Ledger.getCardanoTxId pCardanoTx + -- We submit the transaction for validation + submissionFailures <- submitTransaction cardanoTx + -- We handle the submission failures when applicable + when (notNull submissionFailures) $ + if txSkelOptHaltOnSubmissionFailures + -- If requested, we treat them as fatal, ending the run + then throw $ MCESubmissionFailures submissionFailures + -- Otherwise, we just log them + else logEvent $ MCELogSubmissionFailures submissionFailures + -- We compute the set of consumed outputs and new outputs, based on the + -- validity of the transaction, producing some validity logs in the process. + (consumedInputs, newOutputs) <- + if + -- the transaction is valid, the index is modified based on the regular + -- inputs and outputs of the transaction. + | null submissionFailures && null exUnitsFailures -> do + let inputs = Map.keysSet $ txSkelInputs finalTxSkel + outputs = fromCardanoIndex (P.Ledger.getCardanoTxProducedOutputs pCardanoTx) $ txSkelOutputs finalTxSkel + logEvent $ MCLogNewTx txId $ Valid (length inputs) (Map.size outputs) + return (inputs, outputs) + -- the transaction fails in phase 1, the index remains unchanged. + | notNull submissionFailures -> do + logEvent $ MCLogNewTx txId InvalidPhase1 + return (Set.empty, Map.empty) + -- the transaction fails in phase 2, but no collaterals were + -- provided. This is an unreachable case. + | Nothing <- mCollaterals -> + fail + "Unreachable case when processing validation result, please report a bug at https://github.com/tweag/cooked-validators/issues" + -- the transaction fails in phase 2, and collaterals are provided, the + -- index is modified based on the collateral inputs and outputs of the + -- transaction. + | Just (colIns, retCol) <- mCollaterals -> do + let outputs = fromCardanoIndex (P.Ledger.getCardanoTxProducedReturnCollateral pCardanoTx) $ toList retCol + logEvent $ MCLogNewTx txId $ InvalidPhase2 (length colIns) (Map.size outputs) + return (colIns, outputs) + -- We update the index with the consumed and produced outputs + modify' $ removeOutputs consumedInputs + modify' $ addOutputs $ Map.toList newOutputs + return (eSkel, submissionFailures, cardanoTx, newOutputs) + where + fromCardanoIndex index = Map.fromList . zip (P.Ledger.fromCardanoTxIn . fst <$> Map.toList index) diff --git a/src/Cooked/MockChain/Effect/Write.hs b/src/Cooked/MockChain/Effect/Write.hs index 0cbdd3c7d..7f6206ef7 100644 --- a/src/Cooked/MockChain/Effect/Write.hs +++ b/src/Cooked/MockChain/Effect/Write.hs @@ -1,24 +1,12 @@ {-# LANGUAGE TemplateHaskell #-} --- | This module exposes primitives to update the current state of the --- blockchain, including by sending transactions for validation. +-- | This module exposes primitives to manually (and artificially) update the +-- current state of the blockchain. module Cooked.MockChain.Effect.Write ( -- * The `MockChainWrite` effect MockChainWrite (..), runMockChainWrite, - -- * Modifications of the current time - waitNSlots, - awaitSlot, - awaitEnclosingSlot, - waitNMSFromSlotLowerBound, - waitNMSFromSlotUpperBound, - - -- * Sending `Cooked.Skeleton.TxSkel`s for validation - validateTxSkel, - validateTxSkel', - validateTxSkel_, - -- * Other operations setParams, setConstitutionScript, @@ -32,41 +20,31 @@ import Cardano.Api.Ledger qualified as Cardano import Cardano.Node.Emulator.Internal.Node qualified as Emulator import Control.Lens qualified as Lens import Control.Monad -import Cooked.MockChain.Automation.AutoFilling.Constitution import Cooked.MockChain.Automation.AutoFilling.MinAda -import Cooked.MockChain.Automation.AutoFilling.ReferenceScripts -import Cooked.MockChain.Automation.AutoFilling.Withdrawals -import Cooked.MockChain.Automation.Balancing import Cooked.MockChain.Automation.GenerateTx.Body import Cooked.MockChain.Automation.GenerateTx.Output import Cooked.MockChain.Common import Cooked.MockChain.Effect.Log -import Cooked.MockChain.Effect.Read +import Cooked.MockChain.Effect.Read.Chain +import Cooked.MockChain.Effect.Read.Conf import Cooked.MockChain.Runtime.Error import Cooked.MockChain.Runtime.State import Cooked.Skeleton -import Cooked.Tweak.Common -import Cooked.Tweak.Query import Data.Map.Strict qualified as Map import Ledger.Index qualified as P.Ledger import Ledger.Orphans () -import Ledger.Slot qualified as P.Ledger import Ledger.Tx qualified as P.Ledger import Ledger.Tx.CardanoAPI qualified as P.Ledger import Optics.Core import Plutus.Script.Utils.Scripts qualified as Script -import PlutusLedgerApi.V3 qualified as Api import Polysemy import Polysemy.Error -import Polysemy.Fail import Polysemy.State -- | An effect that offers all the primitives that are performing modifications -- on the blockchain state. data MockChainWrite :: Effect where - WaitNSlots :: Integer -> MockChainWrite m P.Ledger.Slot SetParams :: Emulator.Params -> MockChainWrite m () - ValidateTxSkel :: TxSkel -> MockChainWrite m (P.Ledger.CardanoTx, Utxos) SetConstitutionScript :: (ToVScript s) => s -> MockChainWrite m () ForceOutputs :: [TxSkelOut] -> MockChainWrite m Utxos @@ -76,12 +54,13 @@ makeSem_ ''MockChainWrite runMockChainWrite :: forall effs a. ( Members - '[ State MockChainState, + '[ State EmulatorState, + State ChainIndex, Error P.Ledger.ToCardanoError, Error MockChainError, MockChainLog, - MockChainRead, - Fail + MockChainReadChain, + MockChainReadConf ] effs ) => @@ -89,194 +68,38 @@ runMockChainWrite :: Sem effs a runMockChainWrite = interpret $ \case SetParams params -> do - modify $ set mcstParamsL params - modify $ over mcstLedgerStateL $ Emulator.updateStateParams params - WaitNSlots n -> do - cs <- gets (Emulator.getSlot . mcstLedgerState) - if - | n == 0 -> return cs - | n > 0 -> do - let newSlot = cs + fromIntegral n - modify' (over mcstLedgerStateL $ Lens.set Emulator.elsSlotL $ fromIntegral newSlot) - return newSlot - | otherwise -> throw $ MCEPastSlot cs (cs + fromIntegral n) + modify $ set emulatorStateParamsL params + modify $ over emulatorStateLedgerStateL $ Emulator.updateStateParams params SetConstitutionScript (toVScript -> cScript) -> do - modify' (mcstConstitutionL ?~ cScript) + modify' $ chainIndexConstitutionL ?~ cScript modify' $ - over mcstLedgerStateL $ - Lens.set Emulator.elsConstitutionScriptL $ - (Cardano.SJust . Cardano.toShelleyScriptHash . Script.toCardanoScriptHash) - cScript + over emulatorStateLedgerStateL $ + Lens.set + Emulator.elsConstitutionScriptL + (Cardano.SJust $ Cardano.toShelleyScriptHash $ Script.toCardanoScriptHash cScript) ForceOutputs outputs -> do - -- We retrieve the protocol parameters - params <- getParams - -- The emulator takes for granted transactions with a single pseudo input, - -- which we build to force transaction validation - let input = - ( Cardano.genesisUTxOPseudoTxIn (Emulator.pNetworkId params) $ - Cardano.GenesisUTxOKeyHash $ - Cardano.KeyHash "23d51e91ae5adc7ae801e9de4cd54175fb7464ec2680b25686bbb194", - Cardano.BuildTxWith $ Cardano.KeyWitness Cardano.KeyWitnessForSpending - ) -- We adjust the outputs for the minimal required ADA if needed outputsMinAda <- mapM toTxSkelOutWithMinAda outputs -- We transform these outputs to Cardano outputs outputs' <- mapM toCardanoTxOut outputsMinAda - -- We create our transaction body, which only consists of the dummy input - -- and the outputs to force, and make a transaction out of it. + -- We create our transaction body, composed of the forced outputs cardanoTx <- - P.Ledger.CardanoEmulatorEraTx . txSignatoriesAndBodyToCardanoTx [] - <$> fromEither - ( Emulator.createTransactionBody params $ - P.Ledger.CardanoBuildTx - ( P.Ledger.emptyTxBodyContent - { Cardano.txOuts = outputs', - Cardano.txIns = [input] - } - ) - ) + P.Ledger.CardanoEmulatorEraTx . (`Cardano.Tx` []) + <$> txBodyContentToTxBody (P.Ledger.emptyTxBodyContent {Cardano.txOuts = outputs'}) -- We need to adjust our internal state to account for the forced - -- transaction. We begin by computing the new map of outputs. - let outputsMap = - Map.fromList $ - zipWith - (\x y -> (x, (y, True))) - (P.Ledger.fromCardanoTxIn . snd <$> P.Ledger.getCardanoTxOutRefs cardanoTx) - outputsMinAda + -- transaction. We begin by computing the new outputs. + let outputsList = zip (P.Ledger.fromCardanoTxIn . snd <$> P.Ledger.getCardanoTxOutRefs cardanoTx) outputsMinAda -- We update the index, which effectively receives the new utxos - modify' - ( over mcstLedgerStateL $ - Lens.over - Emulator.elsUtxoL - ( P.Ledger.fromPlutusIndex - . P.Ledger.insert cardanoTx - . P.Ledger.toPlutusIndex - ) - ) + modify' $ + over emulatorStateLedgerStateL $ + Lens.over Emulator.elsUtxoL $ + P.Ledger.fromPlutusIndex + . P.Ledger.insert cardanoTx + . P.Ledger.toPlutusIndex -- We update our internal map by adding the new outputs - modify' (over mcstOutputsL (<> outputsMap)) + modify' $ addOutputs outputsList -- Finally, we return the created utxos - return $ Map.toList (fst <$> outputsMap) - ValidateTxSkel skel -> fmap snd $ runTweak skel $ do - -- We retrieve the current skeleton options - TxSkelOpts {..} <- viewTweak txSkelOptsL - -- We log the submission of the new skeleton - viewTweak simple >>= logEvent . MCLogSubmittedTxSkel - -- We retrieve the current parameters - oldParams <- getParams - -- We compute the optionally modified parameters - let newParams = txSkelOptModParams oldParams - -- We change the parameters for the duration of the validation process - modify $ set mcstParamsL newParams - modify $ over mcstLedgerStateL $ Emulator.updateStateParams newParams - -- We ensure that the outputs have the required minimal amount of ada, when - -- requested in the skeleton options - autoFillMinAda - -- We retrieve the official constitution script and attach it to each - -- proposal that requires it, if it's not empty - autoFillConstitution - -- We add reference scripts in the various redeemers of the skeleton, when - -- they can be found in the index and are allowed to be auto filled - autoFillReferenceScripts - -- We attach the reward amount to withdrawals when applicable - autoFillWithdrawalAmounts - -- We balance the skeleton when requested in the skeleton option, and get - -- the associated fee, collateral inputs and return collateral user - ExtendedTxSkel finalTxSkel fee mCollaterals body <- viewTweak simple >>= balanceTxSkel - -- We log the adjusted skeleton - logEvent $ MCLogAdjustedTxSkel finalTxSkel fee mCollaterals - -- We generate the transaction asscoiated with the skeleton, and apply on it - -- the modifications from the skeleton options - signatories <- viewTweak txSkelSignatoriesL - let cardanoTx = P.Ledger.CardanoEmulatorEraTx $ txSkelOptModTx $ txSignatoriesAndBodyToCardanoTx signatories body - -- To run transaction validation we need a minimal ledger state - eLedgerState <- gets mcstLedgerState - -- We finally run the emulated validation. We update our internal state - -- based on the validation result, and throw an error if this fails. If at - -- some point we want to allows mockchain runs with validation errors, the - -- caller will need to catch those errors and do something with them. - newOutputs <- case Emulator.validateCardanoTx newParams eLedgerState cardanoTx of - -- In case of a phase 1 error, we give back the same index - (_, P.Ledger.FailPhase1 _ err) -> throw $ MCEValidationError P.Ledger.Phase1 err - (newELedgerState, P.Ledger.FailPhase2 _ err _) | Just (colInputs, mRetColOutput) <- mCollaterals -> do - -- We update the emulated ledger state - modify' (set mcstLedgerStateL newELedgerState) - -- We remove the collateral utxos from our own stored outputs - forM_ colInputs $ modify' . removeOutput - -- We add the returned collateral to our outputs when it exists - case (mRetColOutput, Map.toList $ P.Ledger.getCardanoTxProducedReturnCollateral cardanoTx) of - (Nothing, []) -> return () - (Just retColOutput, [(txIn, _)]) -> modify' $ addOutput (P.Ledger.fromCardanoTxIn txIn) retColOutput - _ -> fail "Unreachable case when processing return collaterals, please report a bug at https://github.com/tweag/cooked-validators/issues" - -- We throw a mockchain error - throw $ MCEValidationError P.Ledger.Phase2 err - -- In case of success, we update the index with all inputs and outputs - -- contained in the transaction - (newELedgerState, P.Ledger.Success {}) -> do - -- We update the index with the utxos consumed and produced by the tx - modify' (set mcstLedgerStateL newELedgerState) - -- We retrieve the utxos created by the transaction - let utxos = P.Ledger.fromCardanoTxIn . snd <$> P.Ledger.getCardanoTxOutRefs cardanoTx - -- We combine them with their corresponding `TxSkelOut` - let newOutputs = zip utxos (txSkelOutputs finalTxSkel) - -- We add the news utxos to the state - forM_ newOutputs $ modify' . uncurry addOutput - -- And remove the old ones - forM_ (Map.toList $ txSkelInputs finalTxSkel) $ modify' . removeOutput . fst - -- We return the newly created outputs - return newOutputs - -- This is a theoretical unreachable case. Since we fail in Phase 2, it - -- means the transaction involved script, and thus we must have generated - -- collaterals. - (_, P.Ledger.FailPhase2 {}) - | Nothing <- mCollaterals -> - fail "Unreachable case when processing validation result, please report a bug at https://github.com/tweag/cooked-validators/issues" - -- We apply a change of slot when requested in the options - when txSkelOptAutoSlotIncrease $ modify' (over mcstLedgerStateL Emulator.nextSlot) - -- We return the parameters to their original state - modify $ set mcstParamsL oldParams - modify $ over mcstLedgerStateL $ Emulator.updateStateParams oldParams - -- We log the validated transaction - logEvent $ MCLogNewTx (P.Ledger.fromCardanoTxId $ P.Ledger.getCardanoTxId cardanoTx) (fromIntegral $ length $ P.Ledger.getCardanoTxOutRefs cardanoTx) - -- We return the validated transaction - return (cardanoTx, newOutputs) - --- | Waits a certain number of slots and returns the new slot -waitNSlots :: (Member MockChainWrite effs) => Integer -> Sem effs P.Ledger.Slot - --- | Wait for a certain slot, or throws an error if the slot is already past -awaitSlot :: (Members '[MockChainRead, MockChainWrite] effs) => P.Ledger.Slot -> Sem effs P.Ledger.Slot -awaitSlot (P.Ledger.Slot targetSlot) = do - P.Ledger.Slot now <- currentSlot - waitNSlots (targetSlot - now) - --- | Waits until the current slot becomes greater or equal to the slot --- containing the given POSIX time. Note that that it might not wait for --- anything if the current slot is large enough. -awaitEnclosingSlot :: (Members '[MockChainRead, MockChainWrite] effs) => Api.POSIXTime -> Sem effs P.Ledger.Slot -awaitEnclosingSlot time = getEnclosingSlot time >>= awaitSlot - --- | Wait a given number of ms from the lower bound of the current slot and --- returns the current slot after waiting. -waitNMSFromSlotLowerBound :: (Members '[MockChainRead, MockChainWrite, Fail] effs) => Integer -> Sem effs P.Ledger.Slot -waitNMSFromSlotLowerBound duration = currentMSRange >>= awaitEnclosingSlot . (+ fromIntegral duration) . fst - --- | Wait a given number of ms from the upper bound of the current slot and --- returns the current slot after waiting. -waitNMSFromSlotUpperBound :: (Members '[MockChainRead, MockChainWrite, Fail] effs) => Integer -> Sem effs P.Ledger.Slot -waitNMSFromSlotUpperBound duration = currentMSRange >>= awaitEnclosingSlot . (+ fromIntegral duration) . snd - --- | Generates, balances and validates a transaction from a skeleton, and --- returns the validated transaction, alongside the created UTxOs. -validateTxSkel :: (Member MockChainWrite effs) => TxSkel -> Sem effs (P.Ledger.CardanoTx, Utxos) - --- | Same as `validateTxSkel`, but only returns the generated UTxOs -validateTxSkel' :: (Members '[MockChainRead, MockChainWrite] effs) => TxSkel -> Sem effs Utxos -validateTxSkel' = fmap snd . validateTxSkel - --- | Same as `validateTxSkel`, but discards the returned transaction -validateTxSkel_ :: (Member MockChainWrite effs) => TxSkel -> Sem effs () -validateTxSkel_ = void . validateTxSkel + return $ Map.fromList outputsList -- | Updates the current parameters setParams :: (Member MockChainWrite effs) => Emulator.Params -> Sem effs () diff --git a/src/Cooked/MockChain/Run/Instances.hs b/src/Cooked/MockChain/Run/Instances.hs index 2fad07196..0b247aa70 100644 --- a/src/Cooked/MockChain/Run/Instances.hs +++ b/src/Cooked/MockChain/Run/Instances.hs @@ -50,7 +50,11 @@ where import Cooked.Ltl import Cooked.MockChain.Effect.Log import Cooked.MockChain.Effect.Misc -import Cooked.MockChain.Effect.Read +import Cooked.MockChain.Effect.Read.Chain +import Cooked.MockChain.Effect.Read.Conf +import Cooked.MockChain.Effect.Submission +import Cooked.MockChain.Effect.Time +import Cooked.MockChain.Effect.Validation import Cooked.MockChain.Effect.Write import Cooked.MockChain.Run.Runnable import Cooked.MockChain.Run.Tweak @@ -68,8 +72,10 @@ import Polysemy.Writer -- | The most direct stack of effects to run a mockchain type DirectEffs = - '[ MockChainWrite, - MockChainRead, + '[ MockChainValidate, + MockChainWrite, + MockChainReadChain, + MockChainTime, MockChainMisc, Fail ] @@ -78,35 +84,50 @@ type DirectEffs = type DirectMockChain a = Sem DirectEffs a instance RunnableMockChain DirectEffs where - runMockChain mcst = + runMockChain emInit ciInit = (: []) . run . runWriter . runMockChainLog fromLogEntry - . runState mcst + . runState ciInit + . runState emInit . runError . runToCardanoErrorInMockChainError . runFailInMockChainError . runMockChainMisc fromAlias fromNote fromAssert - . runMockChainRead + . runMockChainReadConfEmul + . runMockChainTimeEmul + . runMockChainReadChainEmul . runMockChainWrite + . runMockChainSubmitEmul + . runMockChainValidate + . insertAt @1 + @'[ MockChainSubmit + ] + . insertAt @7 + @'[ Error P.Ledger.ToCardanoError, + Error MockChainError, + State EmulatorState, + State ChainIndex, + MockChainLog, + Writer MockChainJournal + ] . insertAt @4 - @[ Error P.Ledger.ToCardanoError, - Error MockChainError, - State MockChainState, - MockChainLog, - Writer MockChainJournal - ] + @'[ MockChainReadConf + ] -- | A stack of effects aimed at being used as modifications for a -- `FullMockChain` computation type FullTweakEffs = '[ MockChainMisc, - MockChainRead, + MockChainReadChain, + MockChainTime, + MockChainReadConf, Fail, Error P.Ledger.ToCardanoError, Error MockChainError, - State MockChainState, + State EmulatorState, + State ChainIndex, MockChainLog, Writer MockChainJournal ] @@ -118,15 +139,19 @@ type FullTweak a = TypedTweak FullTweakEffs a -- addition of all the lower level effects required to interpret it. type FullEffs = '[ ModifyGlobally (UntypedTweak FullTweakEffs), + MockChainValidate, MockChainWrite, ModifyLocally (UntypedTweak FullTweakEffs), State [Ltl (UntypedTweak FullTweakEffs)], MockChainMisc, - MockChainRead, + MockChainReadChain, + MockChainTime, + MockChainReadConf, Fail, Error P.Ledger.ToCardanoError, Error MockChainError, - State MockChainState, + State EmulatorState, + State ChainIndex, MockChainLog, Writer MockChainJournal, NonDet @@ -136,21 +161,29 @@ type FullEffs = type FullMockChain a = Sem FullEffs a instance RunnableMockChain FullEffs where - runMockChain mcst = + runMockChain emInit ciInit = run . runNonDet . runWriter . runMockChainLog fromLogEntry - . runState mcst + . runState ciInit + . runState emInit . runError . runToCardanoErrorInMockChainError . runFailInMockChainError - . runMockChainRead + . runMockChainReadConfEmul + . runMockChainTimeEmul + . runMockChainReadChainEmul . runMockChainMisc fromAlias fromNote fromAssert . evalState [] . runModifyLocally . runMockChainWrite - . reinterpretMockChainWriteWithTweak @FullTweakEffs + . runMockChainSubmitEmul + . runMockChainValidate + . insertAt @1 + @'[ MockChainSubmit + ] + . reinterpretMockChainValidateWithTweak @FullTweakEffs . runModifyGlobally -- | A stack of effects aimed at being used as modifications for a @@ -158,7 +191,8 @@ instance RunnableMockChain FullEffs where type ExtendedStagedTweakEffs extraEff = '[ extraEff, MockChainMisc, - MockChainRead, + MockChainReadChain, + MockChainTime, Fail ] @@ -170,10 +204,12 @@ type ExtendedStagedTweak extraEff a = TypedTweak (ExtendedStagedTweakEffs extraE -- `ExtendedStagedTweakEffs` type ExtendedStagedEffs extraEff = '[ ModifyGlobally (UntypedTweak (ExtendedStagedTweakEffs extraEff)), + MockChainValidate, MockChainWrite, extraEff, MockChainMisc, - MockChainRead, + MockChainReadChain, + MockChainTime, Fail, NonDet ] @@ -188,34 +224,46 @@ class InterpretAlone eff where runInterpretAlone :: Sem (eff : effs) a -> Sem effs a instance (InterpretAlone extraEff) => RunnableMockChain (ExtendedStagedEffs extraEff) where - runMockChain mcst = + runMockChain emInit ciInit = run . runNonDet . runWriter . runMockChainLog fromLogEntry - . runState mcst + . runState ciInit + . runState emInit . runError . runToCardanoErrorInMockChainError . runFailInMockChainError - . runMockChainRead + . runMockChainReadConfEmul + . runMockChainTimeEmul + . runMockChainReadChainEmul . runMockChainMisc fromAlias fromNote fromAssert . runInterpretAlone . evalState [] . runModifyLocally . runMockChainWrite - . insertAt @7 - @[ Error P.Ledger.ToCardanoError, - Error MockChainError, - State MockChainState, - MockChainLog, - Writer MockChainJournal - ] - . reinterpretMockChainWriteWithTweak @(ExtendedStagedTweakEffs extraEff) + . runMockChainSubmitEmul + . runMockChainValidate + . insertAt @1 + @'[ MockChainSubmit + ] + . insertAt @10 + @'[ Error P.Ledger.ToCardanoError, + Error MockChainError, + State EmulatorState, + State ChainIndex, + MockChainLog, + Writer MockChainJournal + ] + . reinterpretMockChainValidateWithTweak @(ExtendedStagedTweakEffs extraEff) + . insertAt @8 + @'[ MockChainReadConf + ] . runModifyGlobally - . insertAt @2 - @[ ModifyLocally (UntypedTweak (ExtendedStagedTweakEffs extraEff)), - State [Ltl (UntypedTweak (ExtendedStagedTweakEffs extraEff))] - ] + . insertAt @3 + @'[ ModifyLocally (UntypedTweak (ExtendedStagedTweakEffs extraEff)), + State [Ltl (UntypedTweak (ExtendedStagedTweakEffs extraEff))] + ] -- | A stack of effects aimed at being used as modifications for a -- `StagedMockChain` computation diff --git a/src/Cooked/MockChain/Run/Runnable.hs b/src/Cooked/MockChain/Run/Runnable.hs index 6f7667abf..46bed059e 100644 --- a/src/Cooked/MockChain/Run/Runnable.hs +++ b/src/Cooked/MockChain/Run/Runnable.hs @@ -72,7 +72,7 @@ distributionFromList = foldl' (\x (user, values) -> x <> map (receives user . Va -- | Raw return type of running a mockchain type RawMockChainReturn a = - (MockChainJournal, (MockChainState, Either MockChainError a)) + (MockChainJournal, (ChainIndex, (EmulatorState, Either MockChainError a))) -- | The returned type when running a mockchain. This is both a reorganizing and -- filtering of the natural returned type `RawMockChainReturn`. @@ -96,14 +96,16 @@ type FunOnMockChainResult a b = RawMockChainReturn a -> b -- | Building a `MockChainReturn` from a `RawMockChainReturn` unRawMockChainReturn :: FunOnMockChainResult a (MockChainReturn a) -unRawMockChainReturn (journal, (st, val)) = - MockChainReturn val (mcstOutputs st) (mcstToUtxoState st) journal +unRawMockChainReturn (journal, (chainIndex, (_emulatorState, val))) = + MockChainReturn val (chainIndexOutputs chainIndex) (chainIndexToUtxoState chainIndex) journal -- | Configuration from which to run a mockchain data MockChainConf a b where MockChainConf :: - { -- | The initial state from which to run the mockchain - mccInitialState :: MockChainState, + { -- | The initial emulator state from which to run the mockchain + mccInitialEmulatorState :: EmulatorState, + -- | The initial chain index from which to run the mockchain + mccInitialChainIndex :: ChainIndex, -- | The initial payments to issue in the run mccInitialDistribution :: InitialDistribution, -- | The function to apply on the results of the run @@ -111,16 +113,16 @@ data MockChainConf a b where } -> MockChainConf a b --- | The default `MockChainConf`, which uses the default initial state and +-- | The default `MockChainConf`, which uses the default initial states and -- initial distribution, and returns a refined `MockChainReturn` mockChainConfTemplate :: MockChainConf a (MockChainReturn a) -mockChainConfTemplate = MockChainConf def def unRawMockChainReturn +mockChainConfTemplate = MockChainConf def def def unRawMockChainReturn -- | The class of effects that represent a mockchain run class RunnableMockChain effs where - -- | Runs a computation from an initial `MockChainState`, while returning a - -- list of `RawMockChainReturn` - runMockChain :: MockChainState -> Sem effs a -> [RawMockChainReturn a] + -- | Runs a computation from an initial `EmulatorState` and `ChainIndex`, + -- while returning a list of `RawMockChainReturn` + runMockChain :: EmulatorState -> ChainIndex -> Sem effs a -> [RawMockChainReturn a] -- | Runs a `RunnableMockChain` from an initial `MockChainConf` runMockChainFromConf :: @@ -130,9 +132,9 @@ runMockChainFromConf :: MockChainConf a b -> Sem effs a -> [b] -runMockChainFromConf (MockChainConf initState initDist funOnResult) currentRun = +runMockChainFromConf (MockChainConf emInitState ciInitState initDist funOnResult) currentRun = fmap funOnResult $ - runMockChain initState $ + runMockChain emInitState ciInitState $ forceOutputs initDist >> currentRun -- | Runs a `RunnableMockChain` from an initial distribution diff --git a/src/Cooked/MockChain/Run/Tweak.hs b/src/Cooked/MockChain/Run/Tweak.hs index bb88f19c8..0c5d520a6 100644 --- a/src/Cooked/MockChain/Run/Tweak.hs +++ b/src/Cooked/MockChain/Run/Tweak.hs @@ -2,7 +2,7 @@ -- of modifying transaction skeleton before sending them for validation. module Cooked.MockChain.Run.Tweak ( -- * Modifying mockchain runs using tweaks - reinterpretMockChainWriteWithTweak, + reinterpretMockChainValidateWithTweak, -- * Tweaks geared for 'Cooked.Skeleton.TxSkel' modifications TypedTweak, @@ -20,9 +20,8 @@ where import Control.Monad import Cooked.Ltl -import Cooked.MockChain.Effect.Write +import Cooked.MockChain.Effect.Validation import Cooked.Tweak.Common -import Data.Coerce import Polysemy import Polysemy.Internal import Polysemy.NonDet @@ -37,7 +36,7 @@ data UntypedTweak tweakEffs where -- | Applies a 'Tweak' to every step in a trace where it is applicable, -- branching at any such locations. The tweak must apply at least once. somewhere :: - (Members '[ModifyGlobally (UntypedTweak tweakEffs)] effs) => + (Member (ModifyGlobally (UntypedTweak tweakEffs)) effs) => TypedTweak tweakEffs b -> Sem effs a -> Sem effs a @@ -46,7 +45,7 @@ somewhere = modifyLtl . ltlEventually . LtlAtom . UntypedTweak -- | Applies a 'Tweak' to every transaction in a given trace. Fails if the tweak -- fails anywhere in the trace. everywhere :: - (Members '[ModifyGlobally (UntypedTweak tweakEffs)] effs) => + (Member (ModifyGlobally (UntypedTweak tweakEffs)) effs) => TypedTweak tweakEffs b -> Sem effs a -> Sem effs a @@ -55,7 +54,7 @@ everywhere = modifyLtl . ltlAlways . LtlAtom . UntypedTweak -- | Ensures a given 'Tweak' can never successfully be applied in a computation, -- and leaves the computation unchanged. nowhere :: - (Members '[ModifyGlobally (UntypedTweak tweakEffs)] effs) => + (Member (ModifyGlobally (UntypedTweak tweakEffs)) effs) => TypedTweak tweakEffs b -> Sem effs a -> Sem effs a @@ -64,7 +63,7 @@ nowhere = modifyLtl . ltlNever . LtlAtom . UntypedTweak -- | Apply a given 'Tweak' at every location in a computation where it does not -- fail, which might never occur. whenAble :: - (Members '[ModifyGlobally (UntypedTweak tweakEffs)] effs) => + (Member (ModifyGlobally (UntypedTweak tweakEffs)) effs) => TypedTweak tweakEffs b -> Sem effs a -> Sem effs a @@ -76,7 +75,7 @@ whenAble = modifyLtl . ltlWhenPossible . LtlAtom . UntypedTweak -- See also `Cooked.Tweak.Labels.labelled` to select transactions based on -- labels instead of their index. there :: - (Members '[ModifyGlobally (UntypedTweak tweakEffs)] effs) => + (Member (ModifyGlobally (UntypedTweak tweakEffs)) effs) => Integer -> TypedTweak tweakEffs b -> Sem effs a -> @@ -94,15 +93,16 @@ there n = modifyLtl . ltlDelay n . LtlAtom . UntypedTweak -- given @arguments@. Then `withTweak` says "I want to modify the transaction -- returned by this endpoint in the following way". withTweak :: - (Members '[ModifyGlobally (UntypedTweak tweakEffs)] effs) => + (Member (ModifyGlobally (UntypedTweak tweakEffs)) effs) => Sem effs a -> TypedTweak tweakEffs b -> Sem effs a withTweak = flip (there 0) --- | Reinterpretes `MockChainWrite` in itself, when the `ModifyLocally` effect --- exists in the stack, applying the relevant modifications in the process. -reinterpretMockChainWriteWithTweak :: +-- | Reinterpretes `MockChainValidate` in itself, when the `ModifyLocally` +-- effect exists in the stack, applying the relevant modifications in the +-- process. +reinterpretMockChainValidateWithTweak :: forall tweakEffs effs a. ( Members '[ ModifyLocally (UntypedTweak tweakEffs), @@ -111,9 +111,9 @@ reinterpretMockChainWriteWithTweak :: effs, Subsume tweakEffs effs ) => - Sem (MockChainWrite : effs) a -> - Sem (MockChainWrite : effs) a -reinterpretMockChainWriteWithTweak = reinterpret @MockChainWrite $ \case + Sem (MockChainValidate : effs) a -> + Sem (MockChainValidate : effs) a +reinterpretMockChainValidateWithTweak = reinterpret @MockChainValidate $ \case ValidateTxSkel skel -> do requirements <- getRequirements let sumTweak :: TypedTweak tweakEffs () = @@ -130,4 +130,3 @@ reinterpretMockChainWriteWithTweak = reinterpret @MockChainWrite $ \case requirements newTxSkel <- raise $ subsume_ $ fst <$> runTweak skel sumTweak validateTxSkel newTxSkel - a -> send $ coerce a diff --git a/src/Cooked/MockChain/Runtime/Error.hs b/src/Cooked/MockChain/Runtime/Error.hs index d65ca9570..98cdee7ee 100644 --- a/src/Cooked/MockChain/Runtime/Error.hs +++ b/src/Cooked/MockChain/Runtime/Error.hs @@ -10,9 +10,8 @@ module Cooked.MockChain.Runtime.Error ) where +import Cooked.MockChain.Common import Cooked.Skeleton.User -import Ledger.Index qualified as P.Ledger -import Ledger.Slot qualified as P.Ledger import Ledger.Tx qualified as P.Ledger import PlutusLedgerApi.V3 qualified as Api import Polysemy @@ -40,8 +39,10 @@ data BalancingError -- | Errors that can be produced by the blockchain data MockChainError - = -- | Validation errors, either in Phase 1 or Phase 2 - MCEValidationError P.Ledger.ValidationPhase P.Ledger.ValidationError + = -- | Failures occurring while computing execution units + MCEExUnitsFailures ExUnitsFailures + | -- | Failures occurring while submitting the transaction for validation + MCESubmissionFailures SubmissionFailures | -- | Balancing errors MCEBalancingError BalancingError | -- | Translating a skeleton element to its Cardano counterpart failed @@ -50,10 +51,14 @@ data MockChainError MCEWrongReferenceScriptError Api.TxOutRef Api.ScriptHash (Maybe Api.ScriptHash) | -- | A UTxO is missing from the mockchain state MCEUnknownOutRef Api.TxOutRef - | -- | A jump in time would result in a past slot - MCEPastSlot P.Ledger.Slot P.Ledger.Slot | -- | An attempt to invoke an unsupported feature has been made MCEUnsupportedFeature String + | -- | An attempt to spend a script output whose datum is only known by its + -- hash, which does not provide the datum content required by the witness + MCESpendingHashOnlyDatum Api.TxOutRef Api.DatumHash + | -- | An attempt to spend a script output whose script is only known by its + -- hash, without providing the full script through a matching reference input + MCESpendingHashOnlyScript Api.TxOutRef Api.ScriptHash | -- | Used to provide 'MonadFail' instances. MCEFailure String deriving (Show, Eq) diff --git a/src/Cooked/MockChain/Runtime/State.hs b/src/Cooked/MockChain/Runtime/State.hs index 2c0f6afe7..55abdad60 100644 --- a/src/Cooked/MockChain/Runtime/State.hs +++ b/src/Cooked/MockChain/Runtime/State.hs @@ -1,9 +1,18 @@ --- | This module exposes the internal state in which our direct simulation is --- run (`MockChainState`), as well as a restricted and simplified version --- (`UtxoState`). The latter only consists of Utxos with a focus on who owns --- those Utxos. You can see this as having some sort of an "account" view of the --- ledger state, which typically does not exist in Cardano. This is useful for --- two reasons: +-- | This module exposes the two independent pieces of state in which our direct +-- simulation is run: +-- +-- - `EmulatorState`, which gathers the emulator-specific data (the emulator +-- `Emulator.Params` and the `Emulator.EmulatedLedgerState`). This is only +-- relevant when running against the emulated ledger. +-- +-- - `ChainIndex`, which gathers the backend-agnostic data (the map of known +-- outputs and the current constitution script). This piece of state is also +-- meaningful for the node backend, which keeps its own local `ChainIndex`. +-- +-- It also exposes a restricted and simplified view (`UtxoState`). The latter +-- only consists of Utxos with a focus on who owns those Utxos. You can see this +-- as having some sort of an "account" view of the ledger state, which typically +-- does not exist in Cardano. This is useful for two reasons: -- -- - For printing purposes, where it is much more convenient to see the available -- assets as "who owns what" rather than as a set of mixed Utxos. @@ -12,19 +21,24 @@ -- needed. For instance, properties such as "does Alice indeed owns 3 XXX -- tokens at the end of this run?" become much easier to express. module Cooked.MockChain.Runtime.State - ( -- * `MockChainState` and associated optics - MockChainState (..), - mcstParamsL, - mcstLedgerStateL, - mcstOutputsL, - mcstConstitutionL, - mcstMOutputL, - - -- * Helpers to add or remove outputs from a `MockChainState` + ( -- * `EmulatorState` and associated optics + EmulatorState (..), + emulatorStateParamsL, + emulatorStateLedgerStateL, + + -- * `ChainIndex` and associated optics + ChainIndex (..), + chainIndexOutputsL, + chainIndexConstitutionL, + chainIndexMOutputL, + + -- * Helpers to add or remove outputs from a `ChainIndex` addOutput, + addOutputs, removeOutput, + removeOutputs, - -- * `UtxoState`: A simplified, address-focused view on a `MockChainState` + -- * `UtxoState`: A simplified, address-focused view on a `ChainIndex` UtxoPayloadDatum (..), utxoPayloadDatumKindAT, utxoPayloadDatumTypedAT, @@ -43,8 +57,8 @@ module Cooked.MockChain.Runtime.State -- * Querying the assets owned by a given address holdsInState, - -- * Transforming a `MockChainState` into an `UtxoState` - mcstToUtxoState, + -- * Transforming a `ChainIndex` into an `UtxoState` + chainIndexToUtxoState, ) where @@ -63,50 +77,73 @@ import Plutus.Script.Utils.Address qualified as Script import PlutusLedgerApi.V1.Value qualified as Api import PlutusLedgerApi.V3 qualified as Api --- | The state used to run the simulation in 'Cooked.MockChain.Direct' -data MockChainState where - MockChainState :: +-- | The emulator-specific state used to run the simulation in +-- 'Cooked.MockChain.Direct'. It only makes sense when running against the +-- emulated ledger. +data EmulatorState where + EmulatorState :: { -- | The parameters of the emulated blockchain - mcstParams :: Emulator.Params, + emulatorStateParams :: Emulator.Params, -- | The ledger state of the emulated blockchain - mcstLedgerState :: Emulator.EmulatedLedgerState, - -- | Associates to each 'Api.TxOutRef' the 'TxSkelOut' that produced it, + emulatorStateLedgerState :: Emulator.EmulatedLedgerState + } -> + EmulatorState + deriving (Show) + +-- | Focuses on the parameters of an 'EmulatorState' +makeLensesFor [("emulatorStateParams", "emulatorStateParamsL")] ''EmulatorState + +-- | Focuses on the ledger state of an 'EmulatorState' +makeLensesFor [("emulatorStateLedgerState", "emulatorStateLedgerStateL")] ''EmulatorState + +instance Default EmulatorState where + def = EmulatorState def (Emulator.initialState def) + +-- | The backend-agnostic state used to run the simulation. It gathers the map +-- of known outputs and the current constitution script. It is also meaningful +-- for the node backend, which keeps its own local 'ChainIndex'. +data ChainIndex where + ChainIndex :: + { -- | Associates to each 'Api.TxOutRef' the 'TxSkelOut' that produced it, -- alongside a boolean to state whether this UTxO is still present in the -- index ('True') or has already been consumed ('False'). - mcstOutputs :: Map Api.TxOutRef (TxSkelOut, Bool), + chainIndexOutputs :: Map Api.TxOutRef (TxSkelOut, Bool), -- | The constitution script to be used with proposals - mcstConstitution :: Maybe VScript + chainIndexConstitution :: Maybe VScript } -> - MockChainState + ChainIndex deriving (Show) --- | Focuses on the parameters of a 'MockChainState' -makeLensesFor [("mcstParams", "mcstParamsL")] ''MockChainState +-- | Focuses on the outputs of a 'ChainIndex' +makeLensesFor [("chainIndexOutputs", "chainIndexOutputsL")] ''ChainIndex --- | Focuses on the ledger state of a 'MockChainState' -makeLensesFor [("mcstLedgerState", "mcstLedgerStateL")] ''MockChainState +-- | Focuses on the constitution script of a 'ChainIndex' +makeLensesFor [("chainIndexConstitution", "chainIndexConstitutionL")] ''ChainIndex --- | Focuses on the outputs of a 'MockChainState' -makeLensesFor [("mcstOutputs", "mcstOutputsL")] ''MockChainState +instance Default ChainIndex where + def = ChainIndex Map.empty Nothing --- | Focuses on the constitution script of a 'MockChainState' -makeLensesFor [("mcstConstitution", "mcstConstitutionL")] ''MockChainState +-- | Accesses a given available Utxo from a `ChainIndex` +chainIndexMOutputL :: Api.TxOutRef -> Lens' ChainIndex (Maybe TxSkelOut) +chainIndexMOutputL oRef = chainIndexOutputsL % at oRef % iso (fmap fst) (fmap (,True)) -instance Default MockChainState where - def = MockChainState def (Emulator.initialState def) Map.empty Nothing +-- | Stores an output in a 'ChainIndex' +addOutput :: Api.TxOutRef -> TxSkelOut -> ChainIndex -> ChainIndex +addOutput oRef = set (chainIndexMOutputL oRef) . Just --- | Accesses a given available Utxo from a `MockChainState` -mcstMOutputL :: Api.TxOutRef -> Lens' MockChainState (Maybe TxSkelOut) -mcstMOutputL oRef = mcstOutputsL % at oRef % iso (fmap fst) (fmap (,True)) +-- | Stores a list of outputs in a 'ChainIndex' +addOutputs :: [(Api.TxOutRef, TxSkelOut)] -> ChainIndex -> ChainIndex +addOutputs outputs chainIndex = + foldl (\index (oRef, output) -> addOutput oRef output index) chainIndex outputs --- | Stores an output in a 'MockChainState' -addOutput :: Api.TxOutRef -> TxSkelOut -> MockChainState -> MockChainState -addOutput oRef = set (mcstMOutputL oRef) . Just - --- | Removes an output from the 'MockChainState'. This does not actually remove +-- | Removes an output from the 'ChainIndex'. This does not actually remove -- it from the map, but instead marks its availability to @False@ -removeOutput :: Api.TxOutRef -> MockChainState -> MockChainState -removeOutput oRef = set (mcstOutputsL % at oRef % _Just % _2) False +removeOutput :: Api.TxOutRef -> ChainIndex -> ChainIndex +removeOutput oRef = set (chainIndexOutputsL % at oRef % _Just % _2) False + +-- | Removes several outputs from a 'ChainIndex' using 'removeOutput' each time +removeOutputs :: (Foldable t) => t Api.TxOutRef -> ChainIndex -> ChainIndex +removeOutputs l index = foldl (flip removeOutput) index l -- | A simplified version of a 'Cooked.Skeleton.Datum.TxSkelOutDatum' which only -- stores the actual datum and whether it is hashed (@True@) or inline @@ -116,6 +153,7 @@ removeOutput oRef = set (mcstOutputsL % at oRef % _Just % _2) False data UtxoPayloadDatum where NoUtxoPayloadDatum :: UtxoPayloadDatum SomeUtxoPayloadDatum :: (DatumConstrs dat) => dat -> Bool -> UtxoPayloadDatum + UtxoPayloadDatumHash :: Api.DatumHash -> UtxoPayloadDatum -- | Focuses on the optional hashed flag of a 'UtxoPayloadDatum' utxoPayloadDatumKindAT :: AffineTraversal' UtxoPayloadDatum Bool @@ -124,11 +162,13 @@ utxoPayloadDatumKindAT = ( \case NoUtxoPayloadDatum -> Left NoUtxoPayloadDatum SomeUtxoPayloadDatum _ b -> Right b + UtxoPayloadDatumHash _ -> Right True ) ( flip ( \kind -> \case NoUtxoPayloadDatum -> NoUtxoPayloadDatum SomeUtxoPayloadDatum content _ -> SomeUtxoPayloadDatum content kind + datum@(UtxoPayloadDatumHash _) -> datum ) ) @@ -146,6 +186,7 @@ utxoPayloadDatumTypedAT = ( \content -> \case NoUtxoPayloadDatum -> NoUtxoPayloadDatum SomeUtxoPayloadDatum _ kind -> SomeUtxoPayloadDatum content kind + UtxoPayloadDatumHash _ -> SomeUtxoPayloadDatum content True ) ) @@ -159,6 +200,9 @@ instance Ord UtxoPayloadDatum where (SomeUtxoPayloadDatum (Api.toBuiltinData -> dat) b) (SomeUtxoPayloadDatum (Api.toBuiltinData -> dat') b') = compare (dat, b) (dat', b') + compare SomeUtxoPayloadDatum {} _ = LT + compare _ SomeUtxoPayloadDatum {} = GT + compare (UtxoPayloadDatumHash hash) (UtxoPayloadDatumHash hash') = compare hash hash' instance Eq UtxoPayloadDatum where dat == dat' = compare dat dat' == EQ @@ -253,10 +297,10 @@ holdsInState (Script.toAddress -> address) = maybe mempty utxoPayloadSetTotal . utxoPayloadSetTotal :: UtxoPayloadSet -> Api.Value utxoPayloadSetTotal = foldOf (utxoPayloadSetListI % folded % utxoPayloadValueL) --- | Builds a 'UtxoState' from a 'MockChainState' -mcstToUtxoState :: MockChainState -> UtxoState -mcstToUtxoState = - List.foldl' extractPayload mempty . Map.toList . mcstOutputs +-- | Builds a 'UtxoState' from a 'ChainIndex' +chainIndexToUtxoState :: ChainIndex -> UtxoState +chainIndexToUtxoState = + List.foldl' extractPayload mempty . Map.toList . chainIndexOutputs where extractPayload :: UtxoState -> (Api.TxOutRef, (TxSkelOut, Bool)) -> UtxoState extractPayload utxoState (txOutRef, (txSkelOut, bool)) = @@ -269,6 +313,7 @@ mcstToUtxoState = ( case view txSkelOutDatumL txSkelOut of NoTxSkelOutDatum -> NoUtxoPayloadDatum SomeTxSkelOutDatum content kind -> SomeUtxoPayloadDatum content (kind /= Inline) + SomeTxSkelOutDatumHash hash -> UtxoPayloadDatumHash hash ) (preview txSkelOutReferenceScriptHashAF txSkelOut) ] diff --git a/src/Cooked/MockChain/Testing.hs b/src/Cooked/MockChain/Testing.hs index 4fe651bc8..b591b87bf 100644 --- a/src/Cooked/MockChain/Testing.hs +++ b/src/Cooked/MockChain/Testing.hs @@ -85,6 +85,7 @@ module Cooked.MockChain.Testing ) where +import Cardano.Ledger.Alonzo.Plutus.Evaluate qualified as Alonzo import Control.Exception qualified as E import Control.Monad import Cooked.MockChain.Effect.Log @@ -96,11 +97,10 @@ import Cooked.MockChain.Runtime.State import Cooked.Pretty import Data.Default import Data.List (isInfixOf) +import Data.Map qualified as Map import Data.Set qualified as Set import Data.Text qualified as T -import Ledger.Index qualified as P.Ledger import Plutus.Script.Utils.Address qualified as Script -import PlutusLedgerApi.V1.Scripts qualified as Api import PlutusLedgerApi.V1.Value qualified as Api import Polysemy import Test.QuickCheck qualified as QC @@ -291,7 +291,7 @@ type LogProp prop = PrettyCookedOpts -> [MockChainLogEntry] -> prop type StateProp prop = PrettyCookedOpts -> UtxoState -> prop -- | Type of trace runners -type Runner effs a b = MockChainState -> InitialDistribution -> Sem effs a -> [MockChainReturn b] +type Runner effs a b = EmulatorState -> ChainIndex -> InitialDistribution -> Sem effs a -> [MockChainReturn b] -- | Data structure to test a mockchain trace. @a@ is the return typed of the -- tested trace, @prop@ is the domain in which the properties live. This is not @@ -301,8 +301,10 @@ data Test effs a b prop = Test testTrace :: Sem effs a, -- | The runner of the trace, possibly changing the return type testRunner :: Runner effs a b, - -- | The initial state from which the trace should be run - testInitState :: MockChainState, + -- | The initial emulator state from which the trace should be run + testInitEmulatorState :: EmulatorState, + -- | The initial chain index from which the trace should be run + testInitChainIndex :: ChainIndex, -- | The initial distribution from which the trace should be run testInitDist :: InitialDistribution, -- | The requirement on the number of results @@ -330,7 +332,7 @@ testToProp :: Test effs a b prop -> prop testToProp Test {..} = - let results = testRunner testInitState testInitDist testTrace + let results = testRunner testInitEmulatorState testInitChainIndex testInitDist testTrace in testSizeProp (toInteger (length results)) .&&. testAll ( \ret@(MockChainReturn outcome _ state (MockChainJournal mcLog names _ assertions)) -> @@ -405,7 +407,8 @@ mustSucceedTest' runner trace = Test { testTrace = trace, testRunner = runner, - testInitState = def, + testInitEmulatorState = def, + testInitChainIndex = def, testInitDist = def, testSizeProp = isAtLeastOfSize 1, testFailureProp = \_ _ _ _ -> testFailureMsg "💀 Unexpected failure!", @@ -421,8 +424,8 @@ mustSucceedTest :: ) => Sem effs a -> Test effs a a prop -mustSucceedTest = mustSucceedTest' $ \initState initDist -> - runMockChainFromConf $ MockChainConf initState initDist unRawMockChainReturn +mustSucceedTest = mustSucceedTest' $ \emInitState ciInitState initDist -> + runMockChainFromConf $ MockChainConf emInitState ciInitState initDist unRawMockChainReturn -- | A test template which expects a failure from a trace. See -- `mustSucceedTest'` for more information on its intended usage. @@ -435,7 +438,8 @@ mustFailTest' runner trace = Test { testTrace = trace, testRunner = runner, - testInitState = def, + testInitEmulatorState = def, + testInitChainIndex = def, testInitDist = def, testSizeProp = const testSuccess, testFailureProp = \_ _ _ _ -> testSuccess, @@ -451,8 +455,8 @@ mustFailTest :: ) => Sem effs a -> Test effs a a prop -mustFailTest = mustFailTest' $ \initState initDist -> - runMockChainFromConf $ MockChainConf initState initDist unRawMockChainReturn +mustFailTest = mustFailTest' $ \emInitState ciInitState initDist -> + runMockChainFromConf $ MockChainConf emInitState ciInitState initDist unRawMockChainReturn -- * Appending elements (in particular requirements) to existing tests @@ -555,11 +559,20 @@ withErrorProp test errorProp = withFailureProp test (\_ _ err _ -> errorProp err -- * Specific properties around failures +-- | Whether a script failure is a genuine Plutus (phase 2) evaluation failure. +-- Only the 'Alonzo.ValidationFailure' constructor is considered a phase 2 +-- failure; every other constructor is treated as a phase 1 failure. +isValidationFailure :: Alonzo.TransactionScriptFailure era -> Bool +isValidationFailure Alonzo.ValidationFailure {} = True +isValidationFailure _ = False + -- | A property to ensure a phase 1 failure isPhase1Failure :: (IsProp prop) => FailureProp prop -isPhase1Failure _ _ (MCEValidationError P.Ledger.Phase1 _) _ = testSuccess +isPhase1Failure _ _ (MCESubmissionFailures _) _ = testSuccess +isPhase1Failure _ _ (MCEExUnitsFailures failures) _ + | not (any isValidationFailure (Map.elems failures)) = testSuccess isPhase1Failure pcOpts _ e _ = testFailureMsg $ "Expected phase 1 evaluation failure, got: " @@ -569,7 +582,8 @@ isPhase1Failure pcOpts _ e _ = isPhase2Failure :: (IsProp prop) => FailureProp prop -isPhase2Failure _ _ (MCEValidationError P.Ledger.Phase2 _) _ = testSuccess +isPhase2Failure _ _ (MCEExUnitsFailures failures) _ + | any isValidationFailure (Map.elems failures) = testSuccess isPhase2Failure pcOpts _ e _ = testFailureMsg $ "Expected phase 2 evaluation failure, got: " @@ -580,9 +594,10 @@ isPhase1FailureWithMsg :: (IsProp prop) => String -> FailureProp prop -isPhase1FailureWithMsg s _ _ (MCEValidationError P.Ledger.Phase1 (P.Ledger.CardanoLedgerValidationError text)) _ - | s `isInfixOf` T.unpack text = - testSuccess +isPhase1FailureWithMsg s _ _ (MCESubmissionFailures failures) _ + | any (isInfixOf s . show) failures = testSuccess +isPhase1FailureWithMsg s _ _ (MCEExUnitsFailures failures) _ + | any (\f -> not (isValidationFailure f) && s `isInfixOf` show f) (Map.elems failures) = testSuccess isPhase1FailureWithMsg _ pcOpts _ e _ = testFailureMsg $ "Expected phase 1 evaluation failure with constrained messages, got: " @@ -593,9 +608,8 @@ isPhase2FailureWithMsg :: (IsProp prop) => String -> FailureProp prop -isPhase2FailureWithMsg s _ _ (MCEValidationError P.Ledger.Phase2 (P.Ledger.ScriptFailure (Api.EvaluationError texts _))) _ - | any (isInfixOf s . T.unpack) texts = - testSuccess +isPhase2FailureWithMsg s _ _ (MCEExUnitsFailures failures) _ + | not $ null [text | Alonzo.ValidationFailure _ _ logs _ <- Map.elems failures, (T.unpack -> text) <- logs, s `isInfixOf` text] = testSuccess isPhase2FailureWithMsg _ pcOpts _ e _ = testFailureMsg $ "Expected phase 2 evaluation failure with constrained messages, got: " diff --git a/src/Cooked/MockChain/UtxoSearch.hs b/src/Cooked/MockChain/UtxoSearch.hs index 09e82d43a..38ac57998 100644 --- a/src/Cooked/MockChain/UtxoSearch.hs +++ b/src/Cooked/MockChain/UtxoSearch.hs @@ -8,12 +8,13 @@ module Cooked.MockChain.UtxoSearch beginSearchPure, -- * Processing search result + RefinedOutputsList, UtxoSearchResult, - getOutputs, + utxosSearchResultUtxosI, + getUtxos, getOutputsAndExtracts, getExtracts, getTxOutRefs, - getTxOutRefsAndOutputs, -- * Basic UTxO searches utxosAtSearch, @@ -42,26 +43,35 @@ module Cooked.MockChain.UtxoSearch ) where -import Control.Monad (filterM, forM) +import Control.Monad (foldM) import Cooked.Families hiding (Member) import Cooked.MockChain.Common -import Cooked.MockChain.Effect.Read +import Cooked.MockChain.Effect.Read.Chain import Cooked.Skeleton.Datum import Cooked.Skeleton.Output import Cooked.Skeleton.Value -import Data.Functor -import Data.Maybe +import Data.Map (Map) +import Data.Map qualified as Map +import Data.Set import Optics.Core import Optics.Core.Extras import Plutus.Script.Utils.Address qualified as Script import Plutus.Script.Utils.Scripts qualified as Script import PlutusLedgerApi.V3 qualified as Api import Polysemy +import Witherable + +-- | An heterogeneous list starting with a 'TxSkelOut' +type RefinedOutputsList elems = HList (TxSkelOut ': elems) -- | Raw result of a `UtxoSearch`. We store the `Api.TxOutRef` of the output, -- alongside an heterogeneous list starting with the output in question, -- followed by any element that was extracted during the search. -type UtxoSearchResult elems = [(Api.TxOutRef, HList (TxSkelOut ': elems))] +type UtxoSearchResult elems = Map Api.TxOutRef (RefinedOutputsList elems) + +-- | An isomorphisms between `Utxos` and search results with no extra element. +utxosSearchResultUtxosI :: Iso' (UtxoSearchResult '[]) Utxos +utxosSearchResultUtxosI = iso (fmap hHead) (fmap hSingleton) -- | A `UtxoSearch` is a computation that returns a list of UTxOs alongside -- their `TxSkelOut` counterpart and a list of other elements retrieved from the @@ -73,7 +83,7 @@ type UtxoSearch effs elems = Sem effs (UtxoSearchResult elems) beginSearch :: Sem effs Utxos -> UtxoSearch effs '[] -beginSearch = fmap (fmap (fmap (`HCons` HEmpty))) +beginSearch = fmap $ review utxosSearchResultUtxosI -- | Same as `beginSearch` with a pure input beginSearchPure :: @@ -82,40 +92,33 @@ beginSearchPure :: beginSearchPure = beginSearch . return -- | Retrieves the `TxSkelOut`s from a `UtxoSearchResult` -getOutputs :: +getUtxos :: Sem effs (UtxoSearchResult elems) -> - Sem effs [TxSkelOut] -getOutputs = fmap (fmap (hHead . snd)) + Sem effs Utxos +getUtxos = fmap (fmap hHead) -- | Retrieves the `TxSkelOut`s from a `UtxoSearchResult` alongside the -- extracted elements getOutputsAndExtracts :: Sem effs (UtxoSearchResult elems) -> - Sem effs [(TxSkelOut, HList elems)] -getOutputsAndExtracts = - fmap (fmap (\(_, HCons output l) -> (output, l))) + Sem effs [RefinedOutputsList elems] +getOutputsAndExtracts = fmap Map.elems -- | Retrieves the extracted elements from a `UtxoSearchResult` getExtracts :: Sem effs (UtxoSearchResult elems) -> Sem effs [HList elems] -getExtracts = fmap (fmap (hTail . snd)) +getExtracts = fmap (Map.elems . fmap hTail) -- | Retrieves the `Api.TxOutRef`s from a `UtxoSearchResult` getTxOutRefs :: Sem effs (UtxoSearchResult elems) -> - Sem effs [Api.TxOutRef] -getTxOutRefs = fmap (fmap fst) - --- | Retrieves both the `Api.TxOutRef`s and `TxSkelOut`s from a `UtxoSearchResult` -getTxOutRefsAndOutputs :: - Sem effs (UtxoSearchResult elems) -> - Sem effs Utxos -getTxOutRefsAndOutputs = fmap (fmap (\(oRef, HCons output _) -> (oRef, output))) + Sem effs (Set Api.TxOutRef) +getTxOutRefs = fmap Map.keysSet -- | Searches for utxos at a given address with a given filter utxosAtSearch :: - (Member MockChainRead effs, Script.ToCredential pkh) => + (Member MockChainReadChain effs, Script.ToAddress pkh) => pkh -> (UtxoSearch effs '[] -> UtxoSearch effs els) -> UtxoSearch effs els @@ -123,40 +126,39 @@ utxosAtSearch pkh filters = filters $ beginSearch $ utxosAt pkh -- | Searches for all the known utxos with a given filter allUtxosSearch :: - (Member MockChainRead effs) => + (Member MockChainReadChain effs) => (UtxoSearch effs '[] -> UtxoSearch effs els) -> UtxoSearch effs els allUtxosSearch filters = filters $ beginSearch allUtxos -- | Searches for utxos belonging to a given list with a given filter txSkelOutByRefSearch :: - (Member MockChainRead effs) => - [Api.TxOutRef] -> + (Member MockChainReadChain effs) => + Set Api.TxOutRef -> (UtxoSearch effs '[] -> UtxoSearch effs els) -> UtxoSearch effs els txSkelOutByRefSearch utxos filters = - filters $ beginSearch (zip utxos <$> mapM txSkelOutByRef utxos) + filters $ + foldM + (\acc oRef -> (\x -> Map.insert oRef (hSingleton x) acc) <$> txSkelOutByRef oRef) + Map.empty + utxos -- | Searches for utxos belonging to a given list with no filter txSkelOutByRefSearch' :: - (Member MockChainRead effs) => - [Api.TxOutRef] -> + (Member MockChainReadChain effs) => + Set Api.TxOutRef -> UtxoSearch effs '[] txSkelOutByRefSearch' = (`txSkelOutByRefSearch` id) --- | Extracts a new element from the currently selected outputs, filtering in --- the process out utxos for which this element is not available +-- | Extracts a new element from the currently selected outputs, filtering out +-- in the process utxos for which this element is not available extract :: (TxSkelOut -> Sem effs (Maybe b)) -> UtxoSearch effs els -> UtxoSearch effs (b ': els) -extract extractFun comp = do - resl <- comp - resl' <- forM resl $ - \(oRef, HCons txSkelOut other) -> do - res <- extractFun txSkelOut - return $ res <&> (\x -> (oRef, HCons txSkelOut (HCons x other))) - return $ catMaybes resl' +extract extractFun = + (>>= witherM (\(HCons txSkelOut es) -> fmap (HCons txSkelOut . (`HCons` es)) <$> extractFun txSkelOut)) -- | Same as `extract`, but with a pure extraction function extractPure :: @@ -201,7 +203,7 @@ ensure :: UtxoSearch effs els -> UtxoSearch effs els ensure filterF comp = - comp >>= filterM (filterF . hHead . snd) + comp >>= filterA (filterF . hHead) -- | Same as `ensure`, but with a pure predicate ensurePure :: diff --git a/src/Cooked/Pretty/MockChain.hs b/src/Cooked/Pretty/MockChain.hs index b3260106b..45435884c 100644 --- a/src/Cooked/Pretty/MockChain.hs +++ b/src/Cooked/Pretty/MockChain.hs @@ -86,8 +86,10 @@ instance PrettyCooked BalancingError where ] instance PrettyCooked MockChainError where - prettyCookedOpt opts (MCEValidationError plutusPhase plutusError) = - PP.vsep ["Validation error " <+> prettyCookedOpt opts plutusPhase, PP.indent 2 (prettyCookedOpt opts plutusError)] + prettyCookedOpt opts (MCEExUnitsFailures failures) = + prettyItemize opts "Execution units failures:" "-" (PP.viaShow <$> Map.elems failures :: [DocCooked]) + prettyCookedOpt opts (MCESubmissionFailures failures) = + prettyItemize opts "Submission failures:" "-" (PP.viaShow <$> failures :: [DocCooked]) prettyCookedOpt opts (MCEBalancingError err) = prettyCookedOpt opts err prettyCookedOpt _ (MCEToCardanoError cardanoError) = "Transaction generation error:" <+> PP.pretty cardanoError @@ -100,11 +102,17 @@ instance PrettyCooked MockChainError where <+> "but instead got:" <+> (case got of Nothing -> "none"; Just sHash -> prettyHash opts sHash) prettyCookedOpt _ (MCEUnsupportedFeature feature) = "Unsupported feature:" <+> PP.pretty feature - prettyCookedOpt _ (MCEPastSlot current target) = - "Unable to move back in time; current slot:" - <+> PP.viaShow current - <+> "; target slot:" - <+> PP.viaShow target + prettyCookedOpt opts (MCESpendingHashOnlyDatum txOutRef datumHash) = + "Unable to spend the following output, whose datum is only known by its hash:" + <+> prettyCookedOpt opts txOutRef + <+> "with datum hash:" + <+> prettyHash opts datumHash + prettyCookedOpt opts (MCESpendingHashOnlyScript txOutRef scriptHash) = + "Unable to spend the following output, whose script is only known by its hash:" + <+> prettyCookedOpt opts txOutRef + <+> "with script hash:" + <+> prettyHash opts scriptHash + <+> "; the full script must be provided through a matching reference input." prettyCookedOpt _ (MCEFailure msg) = "Failed with:" <+> PP.pretty msg instance PrettyCooked (Contextualized [MockChainLogEntry]) where @@ -145,14 +153,30 @@ instance PrettyCooked (Contextualized MockChainLogEntry) where mCollaterals ) ) - prettyCookedOpt opts (Contextualized _ (MCLogNewTx txId nb)) = + prettyCookedOpt opts (Contextualized _ (MCLogNewTx txId validity)) = prettyItemize opts - "New transaction successfully validated:" + "New transaction produced:" "-" - [ "Transaction id:" <+> prettyHash opts txId, - "Number of new outputs:" <+> PP.pretty nb - ] + ( ("Transaction id:" <+> prettyHash opts txId) + : case validity of + Valid nbInputs nbOutputs -> + [ "Validity: valid", + "Number of consumed inputs:" <+> PP.pretty nbInputs, + "Number of new outputs:" <+> PP.pretty nbOutputs + ] + InvalidPhase1 -> + ["Validity: invalid in phase 1 (no ledger change)"] + InvalidPhase2 nbColInputs nbRetColOutputs -> + [ "Validity: invalid in phase 2", + "Number of consumed collateral inputs:" <+> PP.pretty nbColInputs, + "Number of return collateral outputs:" <+> PP.pretty nbRetColOutputs + ] + ) + prettyCookedOpt opts (Contextualized _ (MCELogExUnitsFailures failures)) = + prettyItemize opts "Warning: execution units failures:" "-" (PP.viaShow <$> Map.elems failures :: [DocCooked]) + prettyCookedOpt opts (Contextualized _ (MCELogSubmissionFailures failures)) = + prettyItemize opts "Warning: submission failures:" "-" (PP.viaShow <$> failures :: [DocCooked]) prettyCookedOpt opts (Contextualized _ (MCLogDiscardedUtxos n s)) = prettyItemize @[DocCooked] opts @@ -263,6 +287,7 @@ instance PrettyCookedList UtxoPayloadSet where splitDatum :: UtxoPayloadDatum -> Maybe (DocCooked, Bool) splitDatum NoUtxoPayloadDatum = Nothing splitDatum (SomeUtxoPayloadDatum dat b) = Just (prettyCookedOpt opts dat, b) + splitDatum (UtxoPayloadDatumHash hash) = Just (prettyHash opts hash, True) newtype CollateralInput = CollateralInput {unCollateralInput :: Api.TxOutRef} diff --git a/src/Cooked/Pretty/Skeleton.hs b/src/Cooked/Pretty/Skeleton.hs index 9cc812e9b..cfd961a0b 100644 --- a/src/Cooked/Pretty/Skeleton.hs +++ b/src/Cooked/Pretty/Skeleton.hs @@ -5,11 +5,9 @@ module Cooked.Pretty.Skeleton (Contextualized (..)) where import Cooked.Pretty.Class -import Cooked.Pretty.Options import Cooked.Pretty.Plutus () import Cooked.Skeleton import Cooked.Wallet (Wallet) -import Data.Default import Data.Map (Map) import Data.Map qualified as Map import Data.Maybe (catMaybes) @@ -66,6 +64,7 @@ instance PrettyCooked TxSkelCertificate where instance PrettyCookedList (User req mode) where prettyCookedOptListMaybe opt (UserPubKey (Script.toPubKeyHash -> pkh)) = [Just ("User" <+> prettyHash opt pkh)] prettyCookedOptListMaybe opt (UserScript (toVScript -> vScript)) = [Just ("Script" <+> prettyHash opt vScript)] + prettyCookedOptListMaybe opt (UserScriptHash sHash) = [Just ("Script" <+> prettyHash opt sHash)] prettyCookedOptListMaybe opt (UserRedeemedScript (toVScript -> script) red) = Just (prettyHash opt script) : prettyCookedOptListMaybe opt red @@ -277,41 +276,41 @@ instance PrettyCookedMaybe TxSkelOutDatum where <> prettyHash opts (Api.toBuiltinData dat) <> "):" <+> PP.align (prettyCookedOpt opts dat) + prettyCookedOptMaybe opts (SomeTxSkelOutDatumHash hash) = + Just $ "Datum (hash only)" <+> "(" <> prettyHash opts hash <> ")" --- | Pretty-print a list of transaction skeleton options, only printing an --- option if its value is non-default. +-- | Pretty-print a list of transaction skeleton options, printing every option +-- (except the opaque transaction modification). instance PrettyCookedList TxSkelOpts where - prettyCookedOptListMaybe + prettyCookedOptList opts ( TxSkelOpts - txSkelOptAutoSlotIncrease _ txSkelOptBalancingPolicy txSkelOptFeePolicy txSkelOptBalanceOutputPolicy txSkelOptBalancingUtxos - _ txSkelOptCollateralUtxos - txSkelOptDeferFailures txSkelOptMaxNbOfBalancingUtxos + txSkelOptHaltOnExUnitsFailures + txSkelOptHaltOnSubmissionFailures ) = - [ prettyIfNot True prettyAutoSlotIncrease txSkelOptAutoSlotIncrease, - prettyIfNot def prettyBalanceOutputPolicy txSkelOptBalanceOutputPolicy, - prettyIfNot def prettyBalanceFeePolicy txSkelOptFeePolicy, - prettyIfNot def prettyBalancingPolicy txSkelOptBalancingPolicy, - prettyIfNot def prettyBalancingUtxos txSkelOptBalancingUtxos, - prettyIfNot def prettyCollateralUtxos txSkelOptCollateralUtxos, - prettyIfNot False (const "Defer Phase 2 failures during balancing") txSkelOptDeferFailures, - ("Limit the number of balancing Utxos to " <>) . PP.pretty <$> txSkelOptMaxNbOfBalancingUtxos + [ prettyBalanceOutputPolicy txSkelOptBalanceOutputPolicy, + prettyBalanceFeePolicy txSkelOptFeePolicy, + prettyBalancingPolicy txSkelOptBalancingPolicy, + prettyBalancingUtxos txSkelOptBalancingUtxos, + prettyCollateralUtxos txSkelOptCollateralUtxos, + prettyMaxNbOfBalancingUtxos txSkelOptMaxNbOfBalancingUtxos, + prettyHaltOnFailures "computing execution units" txSkelOptHaltOnExUnitsFailures, + prettyHaltOnFailures "submission" txSkelOptHaltOnSubmissionFailures ] where - prettyIfNot :: (Eq a) => a -> (a -> DocCooked) -> a -> Maybe DocCooked - prettyIfNot defaultValue f x - | x == defaultValue && not (pcOptPrintDefaultTxSkelOpts opts) = Nothing - | otherwise = Just $ f x - prettyAutoSlotIncrease :: Bool -> DocCooked - prettyAutoSlotIncrease True = "Automatic slot increase" - prettyAutoSlotIncrease False = "No automatic slot increase" + prettyMaxNbOfBalancingUtxos :: Maybe Integer -> DocCooked + prettyMaxNbOfBalancingUtxos Nothing = "No limit on the number of balancing Utxos" + prettyMaxNbOfBalancingUtxos (Just n) = "Limit the number of balancing Utxos to " <> PP.pretty n + prettyHaltOnFailures :: DocCooked -> Bool -> DocCooked + prettyHaltOnFailures step False = "Proceed after failures while" <+> step + prettyHaltOnFailures step True = "Halt after failures while" <+> step prettyBalanceOutputPolicy :: BalanceOutputPolicy -> DocCooked prettyBalanceOutputPolicy AdjustExistingOutput = "Balance policy: Adjust existing outputs" prettyBalanceOutputPolicy DontAdjustExistingOutput = "Balance policy: Don't adjust existing outputs" diff --git a/src/Cooked/Skeleton.hs b/src/Cooked/Skeleton.hs index 9f58e1b96..17dde5520 100644 --- a/src/Cooked/Skeleton.hs +++ b/src/Cooked/Skeleton.hs @@ -43,6 +43,8 @@ module Cooked.Skeleton -- * Smart constructor txSkelTemplate, + txSkelEmulatorTemplate, + txSkelNodeTemplate, -- * Utilities txSkelKnownTxOutRefs, @@ -66,7 +68,6 @@ import Cooked.Skeleton.User as X import Cooked.Skeleton.ValidityRange as X import Cooked.Skeleton.Value as X import Cooked.Skeleton.Withdrawal as X -import Data.Default import Data.Map (Map) import Data.Map qualified as Map import Data.Set (Set) @@ -241,12 +242,13 @@ txSkelRedeemersT = txSkelSpendingRedeemersT `adjoin` (txSkelRedeemedScriptsT % userRedeemerL) --- | A convenience template of an empty transaction skeleton. -txSkelTemplate :: TxSkel -txSkelTemplate = +-- | A convenience template of an empty transaction skeleton, parameterized by +-- the transaction options to use. +txSkelTemplate :: TxSkelOpts -> TxSkel +txSkelTemplate opts = TxSkel { txSkelLabels = mempty, - txSkelOpts = def, + txSkelOpts = opts, txSkelMints = mempty, txSkelValidityRange = Api.always, txSkelSignatories = mempty, @@ -258,6 +260,16 @@ txSkelTemplate = txSkelCertificates = mempty } +-- | A convenience template of an empty transaction skeleton, using options +-- tailored for the emulator backend ('txSkelOptsEmulatorTemplate'). +txSkelEmulatorTemplate :: TxSkel +txSkelEmulatorTemplate = txSkelTemplate txSkelOptsEmulatorTemplate + +-- | A convenience template of an empty transaction skeleton, using options +-- tailored for a deployed node backend ('txSkelOptsNodeTemplate'). +txSkelNodeTemplate :: TxSkel +txSkelNodeTemplate = txSkelTemplate txSkelOptsNodeTemplate + -- | All 'Api.TxOutRef's in reference inputs from redeemers txSkelReferenceInputsInRedeemers :: TxSkel -> Set Api.TxOutRef txSkelReferenceInputsInRedeemers = diff --git a/src/Cooked/Skeleton/Datum.hs b/src/Cooked/Skeleton/Datum.hs index f606fe384..0a1fbd4b2 100644 --- a/src/Cooked/Skeleton/Datum.hs +++ b/src/Cooked/Skeleton/Datum.hs @@ -18,6 +18,7 @@ module Cooked.Skeleton.Datum txSkelOutDatumDatumAF, txSkelOutDatumDatumHashAF, txSkelOutDatumOutputDatumG, + txSkelOutDatumOutputDatumI, ) where @@ -75,16 +76,19 @@ datumKindResolvedP = -- | Datums to be placed in 'Cooked.Skeleton.TxSkel' outputs, which are either -- empty, or composed of a datum content and its placement data TxSkelOutDatum where - -- | use no datum + -- | Don't use any datum NoTxSkelOutDatum :: TxSkelOutDatum - -- | use some datum content and associated placement + -- | Use some datum content with a datum kind SomeTxSkelOutDatum :: (DatumConstrs dat) => dat -> DatumKind -> TxSkelOutDatum + -- | Use some datum hash only + SomeTxSkelOutDatumHash :: Api.DatumHash -> TxSkelOutDatum deriving instance Show TxSkelOutDatum instance Eq TxSkelOutDatum where NoTxSkelOutDatum == NoTxSkelOutDatum = True (SomeTxSkelOutDatum (Api.toBuiltinData -> dat) b) == (SomeTxSkelOutDatum (Api.toBuiltinData -> dat') b') = (dat, b) == (dat', b') + (SomeTxSkelOutDatumHash hash) == (SomeTxSkelOutDatumHash hash') = hash == hash' _ == _ = False instance Ord TxSkelOutDatum where @@ -95,6 +99,9 @@ instance Ord TxSkelOutDatum where (SomeTxSkelOutDatum (Api.toBuiltinData -> dat) b) (SomeTxSkelOutDatum (Api.toBuiltinData -> dat') b') = compare (dat, b) (dat', b') + compare SomeTxSkelOutDatum {} _ = LT + compare _ SomeTxSkelOutDatum {} = GT + compare (SomeTxSkelOutDatumHash hash) (SomeTxSkelOutDatumHash hash') = compare hash hash' -- * Optics working on 'TxSkelOutDatum' @@ -105,11 +112,13 @@ txSkelOutDatumKindAT = ( \case NoTxSkelOutDatum -> Left NoTxSkelOutDatum SomeTxSkelOutDatum _ kind -> Right kind + SomeTxSkelOutDatumHash _ -> Right (Hashed NotResolved) ) ( flip ( \kind -> \case NoTxSkelOutDatum -> NoTxSkelOutDatum SomeTxSkelOutDatum content _ -> SomeTxSkelOutDatum content kind + datum@(SomeTxSkelOutDatumHash _) -> datum ) ) @@ -135,6 +144,7 @@ txSkelOutDatumTypedAT = ( \content -> \case NoTxSkelOutDatum -> NoTxSkelOutDatum SomeTxSkelOutDatum _ kind -> SomeTxSkelOutDatum content kind + SomeTxSkelOutDatumHash _ -> SomeTxSkelOutDatum content (Hashed NotResolved) ) ) @@ -144,7 +154,12 @@ txSkelOutDatumDatumAF = txSkelOutDatumTypedAT % to Api.Datum -- | Retrieves the optional 'Api.DatumHash' of a 'TxSkelOutDatum' txSkelOutDatumDatumHashAF :: AffineFold TxSkelOutDatum Api.DatumHash -txSkelOutDatumDatumHashAF = txSkelOutDatumDatumAF % to Script.datumHash +txSkelOutDatumDatumHashAF = + afolding + ( \case + SomeTxSkelOutDatumHash hash -> Just hash + datum -> Script.datumHash <$> preview txSkelOutDatumDatumAF datum + ) -- | Retrieves the 'Api.OutputDatum' of a 'TxSkelOutDatum' txSkelOutDatumOutputDatumG :: Getter TxSkelOutDatum Api.OutputDatum @@ -154,3 +169,18 @@ instance Script.ToOutputDatum TxSkelOutDatum where toOutputDatum NoTxSkelOutDatum = Api.NoOutputDatum toOutputDatum (SomeTxSkelOutDatum datum Inline) = Api.OutputDatum $ Api.Datum $ Api.toBuiltinData datum toOutputDatum (SomeTxSkelOutDatum datum _) = Api.OutputDatumHash $ Script.datumHash $ Api.Datum $ Api.toBuiltinData datum + toOutputDatum (SomeTxSkelOutDatumHash hash) = Api.OutputDatumHash hash + +-- | An isomorphism betwean our 'TxSkelOutDatum' and Plutus +-- 'Api.OutputDatum'. The existence of this function does not mean that both +-- share the same expressiveness. In only means that there exists a sensible way +-- to convert one into the other, and vice versa. +txSkelOutDatumOutputDatumI :: Iso' TxSkelOutDatum Api.OutputDatum +txSkelOutDatumOutputDatumI = + iso + Script.toOutputDatum + ( \case + Api.OutputDatum (Api.Datum bData) -> SomeTxSkelOutDatum bData Inline + Api.NoOutputDatum -> NoTxSkelOutDatum + Api.OutputDatumHash dHash -> SomeTxSkelOutDatumHash dHash + ) diff --git a/src/Cooked/Skeleton/Option.hs b/src/Cooked/Skeleton/Option.hs index 05d961e69..bf956370a 100644 --- a/src/Cooked/Skeleton/Option.hs +++ b/src/Cooked/Skeleton/Option.hs @@ -13,24 +13,23 @@ module Cooked.Skeleton.Option -- * Optics txSkelOptModTxL, - txSkelOptAutoSlotIncreaseL, txSkelOptBalancingPolicyL, txSkelOptBalanceOutputPolicyL, txSkelOptFeePolicyL, txSkelOptBalancingUtxosL, - txSkelOptModParamsL, txSkelOptCollateralUtxosL, - txSkelOptDeferPhase2FailuresDuringBalancingL, txSkelOptMaxNbOfBalancingUtxosL, + txSkelOptHaltOnExUnitsFailuresL, + txSkelOptHaltOnSubmissionFailuresL, -- * Utilities txSkelOptAddModTx, - txSkelOptAddModParams, + txSkelOptsEmulatorTemplate, + txSkelOptsNodeTemplate, ) where -import Cardano.Api qualified as Cardano -import Cardano.Node.Emulator qualified as Emulator +import Cooked.MockChain.Common import Data.Default import Data.Set (Set) import Data.Typeable @@ -40,7 +39,12 @@ import Plutus.Script.Utils.Address qualified as Script import PlutusLedgerApi.V3 qualified as Api -- | Set of constraints that need to be satisfied by users in options -type UserConstraints pkh = (Script.ToPubKeyHash pkh, Show pkh, Eq pkh, Typeable pkh) +type UserConstraints pkh = + ( Script.ToPubKeyHash pkh, + Show pkh, + Eq pkh, + Typeable pkh + ) -- | What fee policy to use in the transaction. data FeePolicy @@ -133,29 +137,17 @@ instance Default CollateralUtxos where -- | Set of options to modify the behavior of generating and validating some -- transaction. data TxSkelOpts = TxSkelOpts - { -- | Whether to increase the slot counter automatically on transaction - -- submission. This is useful for modelling transactions that could be - -- submitted in parallel in reality, so there should be no explicit ordering - -- of what comes first. - -- - -- Default is @True@. - txSkelOptAutoSlotIncrease :: Bool, - -- | Applies an arbitrary modification to a transaction after it has been - -- potentially adjusted and balanced. The name of this option contains - -- /unsafe/ to draw attention to the fact that modifying a transaction at - -- that stage might make it invalid. Still, this offers a hook for being - -- able to alter a transaction in unforeseen ways. It is mostly used to test - -- contracts that have been written for custom PABs. + { -- | Applies an arbitrary modification to a transaction after it has been + -- adjusted, balanced and generated. This offers a hook for being able to + -- alter a transaction in unforeseen ways. -- -- One interesting use of this function is to observe a transaction just -- before it is being sent for validation, with -- - -- > txSkelOptModTx = [RawModTx Debug.Trace.traceShowId] + -- > txSkelOptModTx = Debug.Trace.traceShowId -- - -- The leftmost function in the list is applied first. - -- - -- Default is @[]@. - txSkelOptModTx :: Cardano.Tx Cardano.ConwayEra -> Cardano.Tx Cardano.ConwayEra, + -- Default is @id@. + txSkelOptModTx :: Transaction -> Transaction, -- | Whether to balance the transaction or not, and which user should -- provide/reclaim the missing and surplus value. -- @@ -178,44 +170,11 @@ data TxSkelOpts = TxSkelOpts -- -- Default is 'BalancingUtxosFromBalancingUser'. txSkelOptBalancingUtxos :: BalancingUtxos, - -- | Apply an arbitrary modification to the protocol parameters that are - -- used to balance and submit the transaction. This is obviously a very - -- unsafe thing to do if you want to preserve compatibility with the actual - -- chain. It is useful mainly for testing purposes, when you might want to - -- use extremely big transactions or transactions that exhaust the maximum - -- execution budget. Such a thing could be accomplished with - -- - -- > txSkelOptModParams = Just $ ModParams increaseTransactionLimits - -- - -- for example. - -- - -- Default is 'Nothing'. - txSkelOptModParams :: Emulator.Params -> Emulator.Params, -- | Which utxos to use as collaterals. They can be given manually, or -- computed automatically from a given, or the balancing, user. -- -- Default is 'CollateralUtxosFromBalancingUser' txSkelOptCollateralUtxos :: CollateralUtxos, - -- | Whether to defer validation failures occurring during balancing - -- (specifically during the computation of execution units) to the actual - -- later submission of the transaction. - -- - -- When set to @False@: the phase 2 validation failures will be caught as - -- early as possible, typically during balancing when the execution units - -- are computed. This will shortcut the whole balancing process which - -- iterates the body generation, and thus increase performances (by 40%). As - -- a result, the balanced `Cooked.Skeleton.TxSkel` will never be computed - -- and thus will be absent from the log, which is the only downside. - -- - -- When set to @True@: the phase 2 validation errors will be ignored during - -- the balancing process. This will result in a worst performance (40%), but - -- will allow the log to display a balanced version of the failing - -- `Cooked.Skeleton.TxSkel`, which might be useful. Only use this when - -- debugging complicated phase 2 failures which require a precise view of - -- the balanced `Cooked.Skeleton.TxSkel` sent for validation. - -- - -- Default is `False` - txSkelOptDeferPhase2FailuresDuringBalancing :: Bool, -- | The optional maximum number of Utxos that can be used during -- balancing. The algorithm which selects Utxos when permorming balancing is -- greedy. In the default use case where the are only a few wallets and @@ -229,31 +188,40 @@ data TxSkelOpts = TxSkelOpts -- added in the inputs of the transaction, if such a Utxo exist. -- -- Default is @Nothing@ - txSkelOptMaxNbOfBalancingUtxos :: Maybe Integer + txSkelOptMaxNbOfBalancingUtxos :: Maybe Integer, + -- | Whether to halt the mockchain run when a failure occurs while computing + -- execution units during balancing (typically a phase 2 script failure + -- uncovered early). When 'False', such failures are only logged. + -- + -- Default is 'True' + txSkelOptHaltOnExUnitsFailures :: Bool, + -- | Whether to halt the mockchain run when a failure occurs while submitting + -- the transaction for validation. When 'False', such failures are only + -- logged. + -- + -- Default is 'True' + txSkelOptHaltOnSubmissionFailures :: Bool } -- | Comparing 'TxSkelOpts' is possible as long as we ignore modifications to the -- generated transaction and the parameters. instance Eq TxSkelOpts where - (TxSkelOpts slotIncrease _ balancingPol feePol balOutputPol balUtxos _ colUtxos deferFailures maxNbBalUtxos) - == (TxSkelOpts slotIncrease' _ balancingPol' feePol' balOutputPol' balUtxos' _ colUtxos' deferFailures' maxNbBalUtxos') = - slotIncrease == slotIncrease' - && balancingPol == balancingPol' + (TxSkelOpts _ balancingPol feePol balOutputPol balUtxos colUtxos maxNbBalUtxos exUnitsPol subPol) + == (TxSkelOpts _ balancingPol' feePol' balOutputPol' balUtxos' colUtxos' maxNbBalUtxos' exUnitsPol' subPol') = + balancingPol == balancingPol' && feePol == feePol' && balOutputPol == balOutputPol' && balUtxos == balUtxos' && colUtxos == colUtxos' - && deferFailures == deferFailures' && maxNbBalUtxos == maxNbBalUtxos' + && exUnitsPol == exUnitsPol' + && subPol == subPol' -- | Showing 'TxSkelOpts' is possible as long as we ignore modifications to the -- generated transaction and the parameters. instance Show TxSkelOpts where - show (TxSkelOpts slotIncrease _ balancingPol feePol balOutputPol balUtxos _ colUtxos deferFailures maxNbBalUtxos) = - show [show slotIncrease, show balancingPol, show feePol, show balOutputPol, show balUtxos, show colUtxos, show deferFailures, show maxNbBalUtxos] - --- | Focuses on the automatic slot increase option of a 'TxSkelOpts' -makeLensesFor [("txSkelOptAutoSlotIncrease", "txSkelOptAutoSlotIncreaseL")] ''TxSkelOpts + show (TxSkelOpts _ balancingPol feePol balOutputPol balUtxos colUtxos maxNbBalUtxos exUnitsPol subPol) = + show [show balancingPol, show feePol, show balOutputPol, show balUtxos, show colUtxos, show maxNbBalUtxos, show exUnitsPol, show subPol] -- | Focuses on the Cardano transaction modifications option of a 'TxSkelOpts' makeLensesFor [("txSkelOptModTx", "txSkelOptModTxL")] ''TxSkelOpts @@ -270,37 +238,57 @@ makeLensesFor [("txSkelOptBalanceOutputPolicy", "txSkelOptBalanceOutputPolicyL") -- | Focuses on the balancing utxos option of a 'TxSkelOpts' makeLensesFor [("txSkelOptBalancingUtxos", "txSkelOptBalancingUtxosL")] ''TxSkelOpts --- | Focuses on the changes to protocol parameters option of a 'TxSkelOpts' -makeLensesFor [("txSkelOptModParams", "txSkelOptModParamsL")] ''TxSkelOpts - -- | Focuses on the collateral utxos option of a 'TxSkelOpts' makeLensesFor [("txSkelOptCollateralUtxos", "txSkelOptCollateralUtxosL")] ''TxSkelOpts --- | Focuses on the deferring of the failures option of a 'TxSkelOpts' -makeLensesFor [("txSkelOptDeferPhase2FailuresDuringBalancing", "txSkelOptDeferPhase2FailuresDuringBalancingL")] ''TxSkelOpts - -- | Focuses on the max nb of balancing Utxos option of a 'TxSkelOpts' makeLensesFor [("txSkelOptMaxNbOfBalancingUtxos", "txSkelOptMaxNbOfBalancingUtxosL")] ''TxSkelOpts -instance Default TxSkelOpts where - def = - TxSkelOpts - { txSkelOptAutoSlotIncrease = True, - txSkelOptModTx = id, - txSkelOptBalancingPolicy = def, - txSkelOptBalanceOutputPolicy = def, - txSkelOptFeePolicy = def, - txSkelOptBalancingUtxos = def, - txSkelOptModParams = id, - txSkelOptCollateralUtxos = def, - txSkelOptDeferPhase2FailuresDuringBalancing = False, - txSkelOptMaxNbOfBalancingUtxos = Nothing - } +-- | Focuses on the halt-on-execution-units-failures option of a 'TxSkelOpts' +makeLensesFor [("txSkelOptHaltOnExUnitsFailures", "txSkelOptHaltOnExUnitsFailuresL")] ''TxSkelOpts + +-- | Focuses on the halt-on-submission-failures option of a 'TxSkelOpts' +makeLensesFor [("txSkelOptHaltOnSubmissionFailures", "txSkelOptHaltOnSubmissionFailuresL")] ''TxSkelOpts -- | Appends a transaction modification to the given 'TxSkelOpts' -txSkelOptAddModTx :: (Cardano.Tx Cardano.ConwayEra -> Cardano.Tx Cardano.ConwayEra) -> TxSkelOpts -> TxSkelOpts +txSkelOptAddModTx :: (Transaction -> Transaction) -> TxSkelOpts -> TxSkelOpts txSkelOptAddModTx modTx = over txSkelOptModTxL (modTx .) --- | Appends a parameters modification to the given 'TxSkelOpts' -txSkelOptAddModParams :: (Emulator.Params -> Emulator.Params) -> TxSkelOpts -> TxSkelOpts -txSkelOptAddModParams modParams = over txSkelOptModParamsL (modParams .) +-- | A sensible set of options when running against the emulator backend. The +-- emulator is local and deterministic and reports complete failures, so we opt +-- to halt on both execution-units and submission failures to surface problems +-- as early and as loudly as possible. Balancing is kept unbounded since the +-- emulator typically deals with only a handful of Utxos. +txSkelOptsEmulatorTemplate :: TxSkelOpts +txSkelOptsEmulatorTemplate = + TxSkelOpts + { txSkelOptModTx = id, + txSkelOptBalancingPolicy = def, + txSkelOptBalanceOutputPolicy = def, + txSkelOptFeePolicy = def, + txSkelOptBalancingUtxos = def, + txSkelOptCollateralUtxos = def, + txSkelOptMaxNbOfBalancingUtxos = Nothing, + txSkelOptHaltOnExUnitsFailures = True, + txSkelOptHaltOnSubmissionFailures = True + } + +-- | A sensible set of options when running against a deployed node backend. A +-- real node may reject transactions for reasons outside of the caller's +-- control, so we do not halt after execution-units and submission failures, +-- letting the mockchain run continue and surface issues through the log rather +-- than aborting. Balancing is capped to keep Utxo selection tractable when the +-- node exposes many candidate Utxos. +txSkelOptsNodeTemplate :: TxSkelOpts +txSkelOptsNodeTemplate = + TxSkelOpts + { txSkelOptModTx = id, + txSkelOptBalancingPolicy = def, + txSkelOptBalanceOutputPolicy = def, + txSkelOptFeePolicy = def, + txSkelOptBalancingUtxos = def, + txSkelOptCollateralUtxos = def, + txSkelOptMaxNbOfBalancingUtxos = Just 10, + txSkelOptHaltOnExUnitsFailures = False, + txSkelOptHaltOnSubmissionFailures = False + } diff --git a/src/Cooked/Skeleton/Output.hs b/src/Cooked/Skeleton/Output.hs index 02e6487ec..99126706c 100644 --- a/src/Cooked/Skeleton/Output.hs +++ b/src/Cooked/Skeleton/Output.hs @@ -149,6 +149,9 @@ instance IsTxSkelOutAllowedOwner Wallet where instance IsTxSkelOutAllowedOwner VScript where toPKHOrVScript = UserScript +instance IsTxSkelOutAllowedOwner Api.ScriptHash where + toPKHOrVScript = UserScriptHash + instance (Typeable a) => IsTxSkelOutAllowedOwner (Script.TypedValidator a) where toPKHOrVScript = UserScript diff --git a/src/Cooked/Skeleton/Proposal.hs b/src/Cooked/Skeleton/Proposal.hs index 90f8a0edb..e76de3128 100644 --- a/src/Cooked/Skeleton/Proposal.hs +++ b/src/Cooked/Skeleton/Proposal.hs @@ -19,7 +19,7 @@ module Cooked.Skeleton.Proposal simpleProposal, -- * Utilities - fillConstitution, + fillConstitutionWhenEmpty, ) where @@ -223,10 +223,10 @@ makeLensesFor [("txSkelProposalAnchor", "txSkelProposalAnchorL")] ''TxSkelPropos simpleProposal :: (Script.ToCredential cred, Typeable kind) => cred -> GovernanceAction kind -> TxSkelProposal simpleProposal cred action = TxSkelProposal cred action Nothing Nothing --- | Sets the constitution script with an empty redeemer when empty. This will --- not tamper with an existing constitution script and redeemer. -fillConstitution :: (ToVScript script, Typeable script) => script -> TxSkelProposal -> TxSkelProposal -fillConstitution constitution = +-- | Sets the constitution script with an empty redeemer. This will not tamper +-- with an existing constitution script and redeemer. +fillConstitutionWhenEmpty :: (ToVScript script, Typeable script) => script -> TxSkelProposal -> TxSkelProposal +fillConstitutionWhenEmpty constitution = over (txSkelProposalMConstitutionAT @IsScript) (maybe (Just $ UserRedeemedScript constitution emptyTxSkelRedeemer) Just) diff --git a/src/Cooked/Skeleton/User.hs b/src/Cooked/Skeleton/User.hs index aa3280108..1b8ce74dc 100644 --- a/src/Cooked/Skeleton/User.hs +++ b/src/Cooked/Skeleton/User.hs @@ -19,6 +19,7 @@ module Cooked.Skeleton.User -- * Optics userHashG, userCredentialG, + userCredentialI, userRedeemerAT, userVScriptAT, userScriptHashAF, @@ -83,6 +84,11 @@ data User :: UserKind -> UserMode -> Type where -- | A script user. This can be used whenever a script is needed, but only for -- the allocation mode. UserScript :: forall script kind. (kind ∈ '[IsScript, IsEither], ToVScript script, Typeable script) => script -> User kind Allocation + -- | A script user known only by its hash. This can be used whenever a script + -- is needed for the allocation mode but the full script is not available. + -- Spending an output owned by such a user requires providing the full script + -- through a reference input. + UserScriptHash :: forall kind. (kind ∈ '[IsScript, IsEither]) => Api.ScriptHash -> User kind Allocation -- | A script user with an associated redeemer. This can be used whenever a -- script is needed for redemption mode. UserRedeemedScript :: forall script kind. (kind ∈ [IsScript, IsEither], ToVScript script, Typeable script) => script -> TxSkelRedeemer -> User kind Redemption @@ -93,6 +99,7 @@ type Peer = User IsPubKey Allocation instance Show (User kind mode) where show (UserPubKey (Script.toPubKeyHash -> pkh)) = "UserPubKey " <> show pkh show (UserScript (toVScript -> vScript)) = "UserScript " <> show (Script.toScriptHash vScript) + show (UserScriptHash sHash) = "UserScriptHash " <> show sHash show (UserRedeemedScript (toVScript -> vScript) red) = "UserRedeemedScript " <> show (Script.toScriptHash vScript) <> " " <> show red instance Eq (User kind mode) where @@ -100,17 +107,24 @@ instance Eq (User kind mode) where pkh == pkh' (UserScript (Script.toScriptHash . toVScript -> sHash)) == (UserScript (Script.toScriptHash . toVScript -> sHash')) = sHash == sHash' + (UserScriptHash sHash) == (UserScriptHash sHash') = + sHash == sHash' (UserRedeemedScript (Script.toScriptHash . toVScript -> sHash) red) == (UserRedeemedScript (Script.toScriptHash . toVScript -> sHash') red') = sHash == sHash' && red == red' _ == _ = False instance Ord (User kind mode) where compare (UserPubKey {}) (UserScript {}) = LT + compare (UserPubKey {}) (UserScriptHash {}) = LT compare (UserPubKey {}) (UserRedeemedScript {}) = LT compare (UserScript {}) (UserPubKey {}) = GT + compare (UserScript {}) (UserScriptHash {}) = LT + compare (UserScriptHash {}) (UserPubKey {}) = GT + compare (UserScriptHash {}) (UserScript {}) = GT compare (UserRedeemedScript {}) (UserPubKey {}) = GT compare (UserPubKey (Script.toPubKeyHash -> pkh)) (UserPubKey (Script.toPubKeyHash -> pkh')) = compare pkh pkh' compare (UserScript (Script.toScriptHash . toVScript -> sh)) (UserScript (Script.toScriptHash . toVScript -> sh')) = compare sh sh' + compare (UserScriptHash sh) (UserScriptHash sh') = compare sh sh' compare (UserRedeemedScript (Script.toScriptHash . toVScript -> sh) red) (UserRedeemedScript (Script.toScriptHash . toVScript -> sh') red') = compare (sh, red) (sh', red') @@ -120,6 +134,7 @@ instance Script.ToPubKeyHash (User IsPubKey mode) where instance Script.ToCredential (User kind mode) where toCredential (UserPubKey (Script.toPubKeyHash -> pkh)) = Script.toCredential pkh toCredential (UserScript (toVScript -> vScript)) = Script.toCredential vScript + toCredential (UserScriptHash sHash) = Script.toCredential sHash toCredential (UserRedeemedScript (toVScript -> vScript) _) = Script.toCredential vScript instance Script.ToAddress (User kind mode) where @@ -134,6 +149,7 @@ userHashG = ( \case UserPubKey (Script.toPubKeyHash -> Api.PubKeyHash hs) -> hs UserScript (Script.toScriptHash . toVScript -> Api.ScriptHash hs) -> hs + UserScriptHash (Api.ScriptHash hs) -> hs UserRedeemedScript (Script.toScriptHash . toVScript -> Api.ScriptHash hs) _ -> hs ) @@ -144,6 +160,7 @@ userTypedAF = ( \case UserPubKey @user' pkh | Just Refl <- eqT @user @user' -> Just pkh UserScript @user' script | Just Refl <- eqT @user @user' -> Just script + UserScriptHash sHash | Just Refl <- eqT @user @Api.ScriptHash -> Just sHash UserRedeemedScript @user' script _ | Just Refl <- eqT @user @user' -> Just script _ -> Nothing ) @@ -159,6 +176,7 @@ userTypedScriptAT = ) ( \case UserScript _ -> UserScript + UserScriptHash _ -> UserScript UserRedeemedScript _ red -> (`UserRedeemedScript` red) ) @@ -178,10 +196,12 @@ userEitherScriptP = prism ( \case UserScript script -> UserScript script + UserScriptHash sHash -> UserScriptHash sHash UserRedeemedScript script red -> UserRedeemedScript script red ) ( \case UserScript script -> Right (UserScript script) + UserScriptHash sHash -> Right (UserScriptHash sHash) UserRedeemedScript script red -> Right (UserRedeemedScript script red) user -> Left user ) @@ -200,6 +220,16 @@ userEitherPubKeyP = userCredentialG :: Getter (User kind mode) Api.Credential userCredentialG = to Script.toCredential +-- | An isomorphism between an 'Api.Credential' and an allocation user +userCredentialI :: Iso' (User IsEither Allocation) Api.Credential +userCredentialI = + iso + (view userCredentialG) + ( \case + Api.ScriptCredential sHash -> UserScriptHash sHash + Api.PubKeyCredential pkh -> UserPubKey pkh + ) + -- | Focuses on the optional 'TxSkelRedeemer' of a 'User' userRedeemerAT :: AffineTraversal' (User kind mode) TxSkelRedeemer userRedeemerAT = @@ -230,7 +260,14 @@ userVScriptAT = -- | Retrieves the optional 'Api.ScriptHash' of a 'User' userScriptHashAF :: AffineFold (User kind mode) Api.ScriptHash -userScriptHashAF = userVScriptAT % to Script.toScriptHash +userScriptHashAF = + afolding + ( \case + UserScript (Script.toScriptHash . toVScript -> sHash) -> Just sHash + UserScriptHash sHash -> Just sHash + UserRedeemedScript (Script.toScriptHash . toVScript -> sHash) _ -> Just sHash + _ -> Nothing + ) -- | Focuses on the optional 'Api.PubKeyHash' of a 'User' userPubKeyHashAT :: AffineTraversal' (User kind mode) Api.PubKeyHash @@ -252,29 +289,15 @@ userPubKeyHashI = (\(UserPubKey (Script.toPubKeyHash -> pkh)) -> pkh) UserPubKey --- | Focuses on the 'VScript' of a script -userVScriptL :: Lens' (User IsScript mode) VScript -userVScriptL = - lens - ( \case - UserScript (toVScript -> vScript) -> vScript - UserRedeemedScript (toVScript -> vScript) _ -> vScript - ) - ( \case - UserScript _ -> UserScript - UserRedeemedScript _ red -> (`UserRedeemedScript` red) - ) - -- | Retrieves the 'Api.ScriptHash' of a script userScriptHashG :: Getter (User IsScript mode) Api.ScriptHash -userScriptHashG = userVScriptL % to Script.toScriptHash - --- | Focuses on the 'TxSkelRedeemer' of a script being redeemed -userRedeemerL :: Lens' (User IsScript Redemption) TxSkelRedeemer -userRedeemerL = - lens - (\(UserRedeemedScript _ red) -> red) - (\(UserRedeemedScript script _) -> UserRedeemedScript script) +userScriptHashG = + to + ( \case + UserScript (Script.toScriptHash . toVScript -> sHash) -> sHash + UserScriptHash sHash -> sHash + UserRedeemedScript (Script.toScriptHash . toVScript -> sHash) _ -> sHash + ) -- | An isomorphism between a @User IsScript Redemption@ and a pair of 'VScript' -- and 'TxSkelRedeemer' @@ -283,3 +306,11 @@ userScriptRedeemerI = iso (\(UserRedeemedScript (toVScript -> vScript) red) -> (vScript, red)) (uncurry UserRedeemedScript) + +-- | Focuses on the 'TxSkelRedeemer' of a script being redeemed +userRedeemerL :: Lens' (User IsScript Redemption) TxSkelRedeemer +userRedeemerL = userScriptRedeemerI % _2 + +-- | Focuses on the 'VScript' of a redeemed script +userVScriptL :: Lens' (User IsScript Redemption) VScript +userVScriptL = userScriptRedeemerI % _1 diff --git a/src/Cooked/Tweak/Guard.hs b/src/Cooked/Tweak/Guard.hs index 8d3e70eca..5cd73df4f 100644 --- a/src/Cooked/Tweak/Guard.hs +++ b/src/Cooked/Tweak/Guard.hs @@ -61,7 +61,7 @@ condTweak optic = (guardTweak optic >>) -- > -- > someEndpoint = do -- > ... --- > validateTxSkel' txSkelTemplate +-- > validateTxSkel' txSkelEmulatorTemplate -- > { txSkelLabels = -- > [ TxSkelLabel "InitialMinting" -- > , TxSkelLabel "AuctionWorkflow" @@ -84,7 +84,7 @@ labelled lbl = condTweak $ txSkelLabelsL % at (TxSkelLabel lbl) % _Just -- > -- > someEndpoint = do -- > ... --- > validateTxSkel' txSkelTemplate +-- > validateTxSkel' txSkelEmulatorTemplate -- > { txSkelLabels = -- > [ TxSkelLabel "InitialMinting" -- > , TxSkelLabel "AuctionWorkflow" diff --git a/tests/Spec/Attack/DatumHijacking.hs b/tests/Spec/Attack/DatumHijacking.hs index 25630b36e..44eae99d4 100644 --- a/tests/Spec/Attack/DatumHijacking.hs +++ b/tests/Spec/Attack/DatumHijacking.hs @@ -4,6 +4,7 @@ module Spec.Attack.DatumHijacking (tests) where import Cooked import Data.Map qualified as Map +import Data.Set qualified as Set import Optics.Core import Plutus.Attack.DatumHijacking import Plutus.Script.Utils.V3 qualified as Script @@ -22,7 +23,7 @@ instance PrettyCooked LockDatum where lockTxSkel :: Api.TxOutRef -> Script.MultiPurposeScript DHContract -> TxSkel lockTxSkel o v = - txSkelTemplate + txSkelEmulatorTemplate { txSkelInputs = Map.singleton o emptyTxSkelRedeemer, txSkelOutputs = [v `receives` InlineDatum FirstLock <&&> Value lockValue], txSkelSignatories = txSkelSignatoriesFromList [wallet 1] @@ -30,12 +31,12 @@ lockTxSkel o v = txLock :: Script.MultiPurposeScript DHContract -> StagedMockChain Api.TxOutRef txLock v = do - oref : _ <- getTxOutRefs $ utxosAtSearch (wallet 1) $ ensureAFoldIs (txSkelOutValueL % filtered (`Api.geq` lockValue)) - fst . head <$> validateTxSkel' (lockTxSkel oref v) + oRefs <- getTxOutRefs $ utxosAtSearch (wallet 1) $ ensureAFoldIs (txSkelOutValueL % filtered (`Api.geq` lockValue)) + head <$> validateTxSkelL (lockTxSkel (Set.elemAt 0 oRefs) v) relockTxSkel :: Script.MultiPurposeScript DHContract -> Api.TxOutRef -> TxSkel relockTxSkel v o = - txSkelTemplate + txSkelEmulatorTemplate { txSkelInputs = Map.singleton o $ someTxSkelRedeemer (), txSkelOutputs = [v `receives` InlineDatum SecondLock <&&> Value lockValue], txSkelSignatories = txSkelSignatoriesFromList [wallet 1] @@ -65,7 +66,7 @@ tests = value_10_000 = Script.lovelace 10000 value_9_999 = Script.lovelace 9999 inSkel = - txSkelTemplate + txSkelEmulatorTemplate { txSkelOutputs = [ carelessValidator `receives` InlineDatum SecondLock <&&> Value value_10_001, carelessValidator `receives` InlineDatum SecondLock <&&> Value value_9_999, diff --git a/tests/Spec/Attack/DatumTampering.hs b/tests/Spec/Attack/DatumTampering.hs index 8bd5d2dea..2ad99a7a2 100644 --- a/tests/Spec/Attack/DatumTampering.hs +++ b/tests/Spec/Attack/DatumTampering.hs @@ -17,7 +17,7 @@ alice = wallet 1 datumTamperingAttackTest :: TestTree datumTamperingAttackTest = testCase "datumTamperingAttack" $ - [ txSkelTemplate + [ txSkelEmulatorTemplate { txSkelLabels = Set.singleton $ TxSkelLabel $ DatumTamperingLabel [(52 :: Integer, 53 :: Integer)], txSkelOutputs = [ alice `receives` VisibleHashedDatum (52 :: Integer, 54 :: Integer), @@ -28,7 +28,7 @@ datumTamperingAttackTest = ] @=? (run . runNonDet) ( execTweak - txSkelTemplate + txSkelEmulatorTemplate { txSkelOutputs = [ alice `receives` VisibleHashedDatum (52 :: Integer, 53 :: Integer), alice `receives` Value (Script.lovelace 234), @@ -62,7 +62,7 @@ malformDatumAttackTest = ] ( (fmap allBuiltinData . run . runNonDet) ( execTweak - ( txSkelTemplate + ( txSkelEmulatorTemplate { txSkelOutputs = [ alice `receives` VisibleHashedDatum (52 :: Integer, 53 :: Integer), alice `receives` Value (Script.lovelace 234), diff --git a/tests/Spec/Attack/OutputsReordering.hs b/tests/Spec/Attack/OutputsReordering.hs index a496accdf..33a2e413d 100644 --- a/tests/Spec/Attack/OutputsReordering.hs +++ b/tests/Spec/Attack/OutputsReordering.hs @@ -10,7 +10,7 @@ import Test.Tasty.HUnit manyOutputsSkeleton :: TxSkel manyOutputsSkeleton = - txSkelTemplate + txSkelEmulatorTemplate { txSkelOutputs = (\n -> wallet n `receives` Value (Script.ada 10)) <$> [1 .. 5] } diff --git a/tests/Spec/Attack/PeerTampering.hs b/tests/Spec/Attack/PeerTampering.hs index 294f094d5..d30b4a1cc 100644 --- a/tests/Spec/Attack/PeerTampering.hs +++ b/tests/Spec/Attack/PeerTampering.hs @@ -20,7 +20,7 @@ pkh = Script.toPubKeyHash . wallet -- exercises both branches of 'txSkelAllocatedPeersT'. baseSkel :: TxSkel baseSkel = - txSkelTemplate + txSkelEmulatorTemplate { txSkelOutputs = [wallet 1 `receives` Value (Script.lovelace 3_000)], txSkelSignatories = txSkelSignatoriesFromList [wallet 1, wallet 2] } diff --git a/tests/Spec/Attack/RedeemerTampering.hs b/tests/Spec/Attack/RedeemerTampering.hs index a1c5af889..39ab6b947 100644 --- a/tests/Spec/Attack/RedeemerTampering.hs +++ b/tests/Spec/Attack/RedeemerTampering.hs @@ -23,7 +23,7 @@ oref = Api.TxOutRef (Api.TxId "") -- must leave untouched. baseSkel :: TxSkel baseSkel = - txSkelTemplate + txSkelEmulatorTemplate { txSkelInputs = Map.fromList [ (oref 0, someTxSkelRedeemer (10 :: Integer)), @@ -41,7 +41,7 @@ integerRedeemers = toListOf (txSkelSpendingRedeemersT % txSkelRedeemerTypedAT) -- to make their redeemers invisible to the redeemer traversals. certificateSkel :: TxSkelRedeemer -> TxSkel certificateSkel red = - txSkelTemplate + txSkelEmulatorTemplate { txSkelCertificates = [TxSkelCertificate (UserRedeemedScript (toVScript $ Script.trueMPScript @()) red) StakingRegister] } diff --git a/tests/Spec/Attack/TokenDuplication.hs b/tests/Spec/Attack/TokenDuplication.hs index 8a0551993..e92ec731b 100644 --- a/tests/Spec/Attack/TokenDuplication.hs +++ b/tests/Spec/Attack/TokenDuplication.hs @@ -19,7 +19,7 @@ dupTokenTrace pol tName amount recipient = do skel = let mints = review txSkelMintsListI [mint pol () tName amount] mintedValue = Script.toValue mints - in txSkelTemplate + in txSkelEmulatorTemplate { txSkelMints = mints, txSkelOutputs = [recipient `receives` Value mintedValue], txSkelSignatories = txSkelSignatoriesFromList [wallet 3] @@ -38,7 +38,7 @@ tests = ac1 = Api.assetClass (Script.toCurrencySymbol pol1) tName1 ac2 = Api.assetClass (Script.toCurrencySymbol pol2) tName2 skelIn = - txSkelTemplate + txSkelEmulatorTemplate { txSkelMints = review txSkelMintsListI @@ -59,7 +59,7 @@ tests = [ (Script.toCurrencySymbol pol1, tName1, v1 - 5), (Script.toCurrencySymbol pol2, tName2, v2 - 7) ] - in [ ( txSkelTemplate + in [ ( txSkelEmulatorTemplate { txSkelLabels = Set.singleton $ TxSkelLabel $ TokenDuplicationLabel increment, txSkelMints = review @@ -103,13 +103,13 @@ tests = ac1 = Api.assetClass (Script.toCurrencySymbol pol) tName1 ac2 = Api.assetClass (Script.toCurrencySymbol Script.trueMintingMPScript) (Api.TokenName "preExistingToken") skelIn = - txSkelTemplate + txSkelEmulatorTemplate { txSkelMints = review txSkelMintsListI [mint pol () tName1 1], txSkelOutputs = [wallet 1 `receives` Value (Api.assetClassValue ac1 1 <> Api.assetClassValue ac2 2)], txSkelSignatories = txSkelSignatoriesFromList [wallet 2] } skelExpected = - [ ( txSkelTemplate + [ ( txSkelEmulatorTemplate { txSkelLabels = Set.singleton $ TxSkelLabel $ TokenDuplicationLabel $ review (valueAssetClassAmountP pol tName1) 1, txSkelMints = review txSkelMintsListI [mint pol () tName1 2], txSkelOutputs = diff --git a/tests/Spec/Attack/ValidityTampering.hs b/tests/Spec/Attack/ValidityTampering.hs index b7764529f..d5b44c338 100644 --- a/tests/Spec/Attack/ValidityTampering.hs +++ b/tests/Spec/Attack/ValidityTampering.hs @@ -31,7 +31,7 @@ runValidityTampering initialRange params = fmap (view txSkelValidityRangeL) . run . runNonDet - . execTweak (txSkelTemplate {txSkelValidityRange = initialRange}) + . execTweak (txSkelEmulatorTemplate {txSkelValidityRange = initialRange}) $ validityTamperingAttack params tests :: TestTree diff --git a/tests/Spec/Balancing.hs b/tests/Spec/Balancing.hs index 0457f4740..3902c14e4 100644 --- a/tests/Spec/Balancing.hs +++ b/tests/Spec/Balancing.hs @@ -1,13 +1,12 @@ module Spec.Balancing where import Cooked -import Data.Default +import Data.List (isInfixOf) import Data.List qualified as List import Data.Map (Map) import Data.Map qualified as Map +import Data.Set (Set) import Data.Set qualified as Set -import Data.Text (isInfixOf) -import Ledger.Index qualified as P.Ledger import Optics.Core import Optics.Core.Extras import Plutus.Script.Utils.V3 qualified as Script @@ -37,13 +36,11 @@ initialDistributionBalancing = alice `receives` FixedValue (Script.ada 105 <> banana 2) <&&> VisibleHashedDatum () ] -type TestBalancingOutcome = (TxSkel, TxSkel, Fee, Maybe Collaterals, [Api.TxOutRef]) +type TestBalancingOutcome = (TxSkel, TxSkel, Fee, Maybe Collaterals, Set Api.TxOutRef) spendsScriptUtxo :: Bool -> FullMockChain (Map Api.TxOutRef TxSkelRedeemer) spendsScriptUtxo False = return Map.empty -spendsScriptUtxo True = do - (scriptOutRef, _) : _ <- utxosAt $ Script.trueSpendingMPScript @() - return $ Map.singleton scriptOutRef emptyTxSkelRedeemerNoAutoFill +spendsScriptUtxo True = fmap (const emptyTxSkelRedeemerNoAutoFill) <$> utxosAt (Script.trueSpendingMPScript @()) testingBalancingTemplate :: -- Value to pay to bob @@ -51,11 +48,11 @@ testingBalancingTemplate :: -- Value to pay back to alice Api.Value -> -- utxos to be spent - FullMockChain [Api.TxOutRef] -> + FullMockChain (Set Api.TxOutRef) -> -- utxos to be used for balancing - FullMockChain [Api.TxOutRef] -> + FullMockChain (Set Api.TxOutRef) -> -- utxos to be used for collaterals - FullMockChain [Api.TxOutRef] -> + FullMockChain (Set Api.TxOutRef) -> -- Whether to consum the script utxo Bool -> -- Option modifications @@ -70,34 +67,33 @@ testingBalancingTemplate toBobValue toAliceValue spendSearch balanceSearch colla additionalSpend <- spendsScriptUtxo consumeScriptUtxo let valueConstr = if adjust then Value else FixedValue skel = - txSkelTemplate + txSkelEmulatorTemplate { txSkelOutputs = List.filter ((/= mempty) . (^. txSkelOutValueL)) [ bob `receives` valueConstr toBobValue, alice `receives` valueConstr toAliceValue ], - txSkelInputs = additionalSpend <> Map.fromList ((,emptyTxSkelRedeemer) <$> toSpendUtxos), + txSkelInputs = additionalSpend <> Map.fromSet (const emptyTxSkelRedeemer) toSpendUtxos, txSkelOpts = optionsMod - def + txSkelOptsEmulatorTemplate { txSkelOptBalancingUtxos = if List.null toBalanceUtxos then BalancingUtxosFromBalancingUser - else BalancingUtxosFromSet $ Set.fromList toBalanceUtxos, + else BalancingUtxosFromSet toBalanceUtxos, txSkelOptCollateralUtxos = if List.null toCollateralUtxos then CollateralUtxosFromBalancingUser - else CollateralUtxosFromSet (Set.fromList toCollateralUtxos) alice + else CollateralUtxosFromSet toCollateralUtxos alice }, txSkelSignatories = txSkelSignatoriesFromList [alice] } - ExtendedTxSkel skel' fee mCols _ <- balanceTxSkel skel - validateTxSkel_ skel + (ExtendedTxSkel skel' fee mCols _ _, _, _, _) <- validateTxSkel skel nonOnlyValueUtxos <- aliceNonOnlyValueUtxos return (skel, skel', fee, mCols, nonOnlyValueUtxos) -aliceNonOnlyValueUtxos :: FullMockChain [Api.TxOutRef] +aliceNonOnlyValueUtxos :: FullMockChain (Set Api.TxOutRef) aliceNonOnlyValueUtxos = getTxOutRefs $ utxosAtSearch alice $ @@ -105,20 +101,20 @@ aliceNonOnlyValueUtxos = is txSkelOutReferenceScriptAT skel || is (txSkelOutDatumL % txSkelOutDatumKindAT) skel -aliceNAdaUtxos :: Integer -> FullMockChain [Api.TxOutRef] +aliceNAdaUtxos :: Integer -> FullMockChain (Set Api.TxOutRef) aliceNAdaUtxos n = getTxOutRefs $ utxosAtSearch alice $ ensureAFoldIs (txSkelOutValueL % valueLovelaceL % filtered (== Api.Lovelace (n * 1_000_000))) -aliceRefScriptUtxos :: FullMockChain [Api.TxOutRef] +aliceRefScriptUtxos :: FullMockChain (Set Api.TxOutRef) aliceRefScriptUtxos = getTxOutRefs $ utxosAtSearch alice $ ensureAFoldIs txSkelOutReferenceScriptAT -emptySearch :: FullMockChain [Api.TxOutRef] -emptySearch = return [] +emptySearch :: FullMockChain (Set Api.TxOutRef) +emptySearch = return Set.empty simplePaymentToBob :: Integer -> Integer -> Integer -> Integer -> Bool -> (TxSkelOpts -> TxSkelOpts) -> Bool -> FullMockChain TestBalancingOutcome simplePaymentToBob lv apples oranges bananas = @@ -141,13 +137,13 @@ bothPaymentsToBobAndAlice val = noBalanceMaxFee :: FullMockChain () noBalanceMaxFee = do maxFee <- snd <$> getMinAndMaxFee 0 - (txOutRef : _) <- aliceNAdaUtxos 30 + aliceORefs30Ada <- aliceNAdaUtxos 30 validateTxSkel_ $ - txSkelTemplate + txSkelEmulatorTemplate { txSkelOutputs = [bob `receives` Value (Script.lovelace (30_000_000 - maxFee))], - txSkelInputs = Map.singleton txOutRef emptyTxSkelRedeemer, + txSkelInputs = Map.fromSet (const emptyTxSkelRedeemer) aliceORefs30Ada, txSkelOpts = - def + txSkelOptsEmulatorTemplate { txSkelOptBalancingPolicy = DoNotBalance, txSkelOptFeePolicy = AutoFeeComputation }, @@ -157,33 +153,33 @@ noBalanceMaxFee = do balanceReduceFee :: FullMockChain (Integer, Integer, Integer, Integer) balanceReduceFee = do let skelAutoFee = - txSkelTemplate + txSkelEmulatorTemplate { txSkelOutputs = [bob `receives` Value (Script.ada 50)], txSkelSignatories = txSkelSignatoriesFromList [alice] } - ExtendedTxSkel skelBalanced feeBalanced mCols _ <- balanceTxSkel skelAutoFee - (feeBalanced', _) <- estimateTxSkelFee skelBalanced feeBalanced mCols + ExtendedTxSkel skelBalanced feeBalanced mCols _ _ <- balanceTxSkel skelAutoFee + (feeBalanced', _, _) <- estimateTxSkelFee skelBalanced feeBalanced mCols let skelManualFee = skelAutoFee { txSkelOpts = - def + txSkelOptsEmulatorTemplate { txSkelOptFeePolicy = ManualFee (feeBalanced - 1) } } - ExtendedTxSkel skelBalancedManual feeBalancedManual mColsManual _ <- balanceTxSkel skelManualFee - (feeBalancedManual', _) <- estimateTxSkelFee skelBalancedManual feeBalancedManual mColsManual + ExtendedTxSkel skelBalancedManual feeBalancedManual mColsManual _ _ <- balanceTxSkel skelManualFee + (feeBalancedManual', _, _) <- estimateTxSkelFee skelBalancedManual feeBalancedManual mColsManual return (feeBalanced, feeBalanced', feeBalancedManual, feeBalancedManual') reachingMagic :: FullMockChain () reachingMagic = do bananaOutRefs <- getTxOutRefs $ utxosAtSearch alice $ ensureAFoldIs (txSkelOutValueL % filtered (banana 1 `Api.leq`)) validateTxSkel_ $ - txSkelTemplate + txSkelEmulatorTemplate { txSkelOutputs = [bob `receives` Value (Script.ada 106 <> banana 12)], txSkelSignatories = txSkelSignatoriesFromList [alice], txSkelOpts = - def - { txSkelOptBalancingUtxos = BalancingUtxosFromSet (Set.fromList bananaOutRefs) + txSkelOptsEmulatorTemplate + { txSkelOptBalancingUtxos = BalancingUtxosFromSet bananaOutRefs } } @@ -222,15 +218,15 @@ failsAtBalancing (MCEBalancingError (NotEnoughFundForExtraMinAda {})) = testBool failsAtBalancing _ = testBool False failsWithTooLittleFee :: MockChainError -> Assertion -failsWithTooLittleFee (MCEValidationError P.Ledger.Phase1 (P.Ledger.CardanoLedgerValidationError text)) = testBool $ isInfixOf "FeeTooSmallUTxO" text +failsWithTooLittleFee (MCESubmissionFailures failures) = testBool $ any (isInfixOf "FeeTooSmallUTxO" . show) failures failsWithTooLittleFee _ = testBool False failsWithValueNotConserved :: MockChainError -> Assertion -failsWithValueNotConserved (MCEValidationError P.Ledger.Phase1 (P.Ledger.CardanoLedgerValidationError text)) = testBool $ isInfixOf "ValueNotConserved" text +failsWithValueNotConserved (MCESubmissionFailures failures) = testBool $ any (isInfixOf "ValueNotConserved" . show) failures failsWithValueNotConserved _ = testBool False failsWithEmptyTxIns :: MockChainError -> Assertion -failsWithEmptyTxIns (MCEValidationError P.Ledger.Phase1 (P.Ledger.CardanoLedgerValidationError text)) = testBool $ isInfixOf "InputSetEmptyUTxO" text +failsWithEmptyTxIns (MCESubmissionFailures failures) = testBool $ any (isInfixOf "InputSetEmptyUTxO" . show) failures failsWithEmptyTxIns _ = testBool False failsAtCollateralsWith :: Integer -> MockChainError -> Assertion @@ -457,7 +453,7 @@ tests = ( testingBalancingTemplate (Script.ada 142) mempty - ((fst <$>) <$> utxosAt alice) + (Map.keysSet <$> utxosAt alice) emptySearch (aliceNAdaUtxos 1) True @@ -641,7 +637,7 @@ tests = (apple 2 <> orange 5 <> banana 4) mempty emptySearch - ((fst <$>) <$> utxosAt alice) + (Map.keysSet <$> utxosAt alice) emptySearch False (setFixedFee 1_000_000) diff --git a/tests/Spec/BasicUsage.hs b/tests/Spec/BasicUsage.hs index 812ccf001..ed7a6c0ff 100644 --- a/tests/Spec/BasicUsage.hs +++ b/tests/Spec/BasicUsage.hs @@ -15,7 +15,7 @@ carrie = wallet 3 pkToPk :: Wallet -> Wallet -> Integer -> StagedMockChain () pkToPk sender recipient amount = validateTxSkel_ $ - txSkelTemplate + txSkelEmulatorTemplate { txSkelOutputs = [recipient `receives` Value (Script.ada amount)], txSkelSignatories = txSkelSignatoriesFromList [sender] } @@ -30,7 +30,7 @@ multiplePksToPks = mintingQuickValue :: StagedMockChain () mintingQuickValue = validateTxSkel_ - txSkelTemplate + txSkelEmulatorTemplate { txSkelMints = review txSkelMintsListI [mint (Script.trueMintingMPScript @()) () (Api.TokenName "banana") 10], txSkelOutputs = [alice `receives` Value (Script.multiPurposeScriptValue (Script.trueMintingMPScript @()) (Api.TokenName "banana") 10)], txSkelSignatories = txSkelSignatoriesFromList [alice] @@ -38,9 +38,9 @@ mintingQuickValue = payToAlwaysTrueValidator :: StagedMockChain Api.TxOutRef payToAlwaysTrueValidator = - fst . head - <$> ( validateTxSkel' $ - txSkelTemplate + head + <$> ( validateTxSkelL $ + txSkelEmulatorTemplate { txSkelOutputs = [Script.trueSpendingMPScript @() `receives` Value (Script.ada 10)], txSkelSignatories = txSkelSignatoriesFromList [alice] } @@ -50,7 +50,7 @@ consumeAlwaysTrueValidator :: StagedMockChain () consumeAlwaysTrueValidator = do outref <- payToAlwaysTrueValidator validateTxSkel_ $ - txSkelTemplate + txSkelEmulatorTemplate { txSkelInputs = Map.fromList [(outref, someTxSkelRedeemer ())], txSkelOutputs = [alice `receives` Value (Script.ada 10)], txSkelSignatories = txSkelSignatoriesFromList [alice] diff --git a/tests/Spec/Certificates.hs b/tests/Spec/Certificates.hs index 29022d8b9..0ca602721 100644 --- a/tests/Spec/Certificates.hs +++ b/tests/Spec/Certificates.hs @@ -16,7 +16,7 @@ publishCertificate :: TxSkelCertificate -> DirectMockChain () publishCertificate cert = do forceOutputs_ [alice `receives` Value (Script.ada 100)] validateTxSkel_ $ - txSkelTemplate + txSkelEmulatorTemplate { txSkelSignatories = txSkelSignatoriesFromList [alice], txSkelCertificates = [cert] } @@ -24,7 +24,7 @@ publishCertificate cert = do withdraw :: User IsEither Redemption -> DirectMockChain () withdraw user = validateTxSkel_ $ - txSkelTemplate + txSkelEmulatorTemplate { txSkelSignatories = txSkelSignatoriesFromList [alice], txSkelWithdrawals = review txSkelWithdrawalsListI [Withdrawal user Nothing] } diff --git a/tests/Spec/InitialDistribution.hs b/tests/Spec/InitialDistribution.hs index f67421d97..d74c5060b 100644 --- a/tests/Spec/InitialDistribution.hs +++ b/tests/Spec/InitialDistribution.hs @@ -29,15 +29,15 @@ getValueFromInitialDatum = do spendReferenceAlwaysTrueValidator :: DirectMockChain () spendReferenceAlwaysTrueValidator = do - [(referenceScriptTxOutRef, _)] <- utxosAt alice - ((scriptTxOutRef, _) : _) <- - validateTxSkel' $ - txSkelTemplate + (fst . Map.elemAt 0 -> referenceScriptTxOutRef) <- utxosAt alice + (scriptTxOutRef : _) <- + validateTxSkelL $ + txSkelEmulatorTemplate { txSkelOutputs = [Script.trueSpendingMPScript @() `receives` Value (Script.ada 2)], txSkelSignatories = txSkelSignatoriesFromList [bob] } validateTxSkel_ $ - txSkelTemplate + txSkelEmulatorTemplate { txSkelOutputs = [alice `receives` Value (Script.ada 2)], txSkelInputs = Map.singleton scriptTxOutRef $ TxSkelRedeemer () (Just referenceScriptTxOutRef) False, txSkelSignatories = txSkelSignatoriesFromList [bob] diff --git a/tests/Spec/InlineDatums.hs b/tests/Spec/InlineDatums.hs index 72af6fcd7..2227ba4b9 100644 --- a/tests/Spec/InlineDatums.hs +++ b/tests/Spec/InlineDatums.hs @@ -23,9 +23,9 @@ listUtxosTestTrace :: Script.Versioned Script.Validator -> DirectMockChain (Api.TxOutRef, TxSkelOut) listUtxosTestTrace useInlineDatum validator = - head + Map.elemAt 0 <$> validateTxSkel' - txSkelTemplate + txSkelEmulatorTemplate { txSkelOutputs = [validator `receives` (if useInlineDatum then InlineDatum else VisibleHashedDatum) FirstPaymentDatum], txSkelSignatories = txSkelSignatoriesFromList [wallet 1] } @@ -44,7 +44,7 @@ spendOutputTestTrace :: spendOutputTestTrace useInlineDatum validator = do (theTxOutRef, _) <- listUtxosTestTrace useInlineDatum validator validateTxSkel_ - txSkelTemplate + txSkelEmulatorTemplate { txSkelInputs = Map.singleton theTxOutRef $ someTxSkelRedeemer (), txSkelSignatories = txSkelSignatoriesFromList [wallet 1] } @@ -66,7 +66,7 @@ continuingOutputTestTrace :: continuingOutputTestTrace datumKindOnSecondPayment validator = do (theTxOutRef, theOutput) <- listUtxosTestTrace True validator validateTxSkel_ - txSkelTemplate + txSkelEmulatorTemplate { txSkelInputs = Map.singleton theTxOutRef $ someTxSkelRedeemer (), txSkelOutputs = [ validator diff --git a/tests/Spec/MinAda.hs b/tests/Spec/MinAda.hs index 5b0b31e85..0c4346425 100644 --- a/tests/Spec/MinAda.hs +++ b/tests/Spec/MinAda.hs @@ -1,6 +1,7 @@ module Spec.MinAda where import Cooked +import Data.Map qualified as Map import Optics.Core import Plutus.Script.Utils.Value qualified as Script import PlutusTx qualified @@ -24,9 +25,9 @@ instance PrettyCooked HeavyDatum where paymentWithMinAda :: DirectMockChain Integer paymentWithMinAda = do forceOutputs_ initialDistributionTemplate - view (txSkelOutValueL % valueLovelaceL % lovelaceIntegerI) . snd . (!! 0) + view (txSkelOutValueL % valueLovelaceL % lovelaceIntegerI) . snd . Map.elemAt 0 <$> validateTxSkel' - txSkelTemplate + txSkelEmulatorTemplate { txSkelOutputs = [wallet 2 `receives` VisibleHashedDatum heavyDatum], txSkelSignatories = txSkelSignatoriesFromList [wallet 1] } @@ -34,7 +35,7 @@ paymentWithMinAda = do paymentWithoutMinAda :: Integer -> DirectMockChain () paymentWithoutMinAda paidLovelaces = do validateTxSkel_ - txSkelTemplate + txSkelEmulatorTemplate { txSkelOutputs = [wallet 2 `receives` FixedValue (Script.lovelace paidLovelaces) <&&> VisibleHashedDatum heavyDatum], txSkelSignatories = txSkelSignatoriesFromList [wallet 1] } diff --git a/tests/Spec/MultiPurpose.hs b/tests/Spec/MultiPurpose.hs index 80b6116d5..969c3aa0b 100644 --- a/tests/Spec/MultiPurpose.hs +++ b/tests/Spec/MultiPurpose.hs @@ -25,9 +25,9 @@ bob = wallet 2 runScript :: StagedMockChain () runScript = do forceOutputs_ initialDistributionTemplate - [(oRef@(Api.TxOutRef txId _), _), (oRef', _), (oRef'', _)] <- - validateTxSkel' $ - txSkelTemplate + [oRef@(Api.TxOutRef txId _), oRef', oRef''] <- + validateTxSkelL $ + txSkelEmulatorTemplate { txSkelOutputs = [ alice `receives` Value (Script.ada 3), alice `receives` Value (Script.ada 5) @@ -40,13 +40,13 @@ runScript = do (mintSkel2, mintValue2, tn2) = mkMintSkel alice oRef' script (mintSkel3, mintValue3, tn3) = mkMintSkel bob oRef'' script - ((oRefScript, _) : _) <- validateTxSkel' mintSkel1 - ((oRefScript1, _) : _) <- validateTxSkel' mintSkel2 - ((oRefScript2, _) : _) <- validateTxSkel' mintSkel3 + (oRefScript : _) <- validateTxSkelL mintSkel1 + (oRefScript1 : _) <- validateTxSkelL mintSkel2 + (oRefScript2 : _) <- validateTxSkelL mintSkel3 - ((oRefScript1', _) : (oRefScript2', _) : _) <- - validateTxSkel' $ - txSkelTemplate + (oRefScript1' : oRefScript2' : _) <- + validateTxSkelL $ + txSkelEmulatorTemplate { txSkelSignatories = txSkelSignatoriesFromList [alice], txSkelInputs = HMap.fromList @@ -61,9 +61,9 @@ runScript = do txSkelMints = review txSkelMintsListI [burn script BurnToken tn1 1] } - ((oRefScript2'', _) : _) <- - validateTxSkel' $ - txSkelTemplate + (oRefScript2'' : _) <- + validateTxSkelL $ + txSkelEmulatorTemplate { txSkelSignatories = txSkelSignatoriesFromList [bob], txSkelInputs = HMap.fromList @@ -77,7 +77,7 @@ runScript = do } validateTxSkel_ $ - txSkelTemplate + txSkelEmulatorTemplate { txSkelSignatories = txSkelSignatoriesFromList [alice], txSkelInputs = HMap.singleton oRefScript2'' (someTxSkelRedeemer Close), txSkelMints = review txSkelMintsListI [burn script BurnToken tn3 1] @@ -88,7 +88,7 @@ runScript = do let tn = txOutRefToToken oRef mints = review txSkelMintsListI [mint script (MintToken oRef) tn 1] mintValue = Script.toValue mints - in ( txSkelTemplate + in ( txSkelEmulatorTemplate { txSkelInputs = HMap.singleton oRef emptyTxSkelRedeemer, txSkelMints = mints, txSkelOutputs = [script `receives` InlineDatum index <&&> Value mintValue], diff --git a/tests/Spec/ProposingScript.hs b/tests/Spec/ProposingScript.hs index 842e659ea..c232fa595 100644 --- a/tests/Spec/ProposingScript.hs +++ b/tests/Spec/ProposingScript.hs @@ -24,17 +24,17 @@ testProposingScript autoRefScript autoConstitution constitution mScript govActio forceOutputs_ initialDistributionTemplate setConstitutionScript constitution validateTxSkel_ $ - txSkelTemplate + txSkelEmulatorTemplate { txSkelOutputs = [alice `receives` ReferenceScript constitution], txSkelSignatories = txSkelSignatoriesFromList [alice] } validateTxSkel_ $ - txSkelTemplate + txSkelEmulatorTemplate { txSkelSignatories = txSkelSignatoriesFromList [alice], txSkelCertificates = [pubKeyCertificate alice $ StakingRegisterDelegate (Api.DelegVote Api.DRepAlwaysAbstain)] } validateTxSkel_ $ - txSkelTemplate + txSkelEmulatorTemplate { txSkelSignatories = txSkelSignatoriesFromList [alice], txSkelProposals = [ TxSkelProposal diff --git a/tests/Spec/ReferenceInputs.hs b/tests/Spec/ReferenceInputs.hs index 37aec79b1..c8111479d 100644 --- a/tests/Spec/ReferenceInputs.hs +++ b/tests/Spec/ReferenceInputs.hs @@ -15,9 +15,9 @@ instance PrettyCooked FooDatum where trace1 :: DirectMockChain () trace1 = do - (txOutRefFoo, _) : (txOutRefBar, _) : _ <- - validateTxSkel' - txSkelTemplate + txOutRefFoo : txOutRefBar : _ <- + validateTxSkelL + txSkelEmulatorTemplate { txSkelOutputs = [ fooTypedValidator `receives` Value (Script.ada 4) <&&> InlineDatum (FooDatum $ Script.toPubKeyHash $ wallet 3), barTypedValidator `receives` Value (Script.ada 5) @@ -25,7 +25,7 @@ trace1 = do txSkelSignatories = txSkelSignatoriesFromList [wallet 2] } validateTxSkel_ - txSkelTemplate + txSkelEmulatorTemplate { txSkelInputs = Map.singleton txOutRefBar $ someTxSkelRedeemer (), txSkelReferenceInputs = Set.singleton txOutRefFoo, txSkelOutputs = [wallet 4 `receives` Value (Script.ada 5)], @@ -34,9 +34,9 @@ trace1 = do trace2 :: DirectMockChain () trace2 = do - (refORef, _) : (scriptORef, _) : _ <- - validateTxSkel' - ( txSkelTemplate + refORef : scriptORef : _ <- + validateTxSkelL + ( txSkelEmulatorTemplate { txSkelOutputs = [ wallet 1 `receives` Value (Script.ada 2) <&&> VisibleHashedDatum (10 :: Integer), bazTypedValidator `receives` Value (Script.ada 10) @@ -45,7 +45,7 @@ trace2 = do } ) validateTxSkel_ $ - txSkelTemplate + txSkelEmulatorTemplate { txSkelSignatories = txSkelSignatoriesFromList [wallet 1], txSkelInputs = Map.singleton scriptORef (someTxSkelRedeemer ()), txSkelReferenceInputs = Set.singleton refORef diff --git a/tests/Spec/ReferenceScripts.hs b/tests/Spec/ReferenceScripts.hs index b92a46a51..6cad7f154 100644 --- a/tests/Spec/ReferenceScripts.hs +++ b/tests/Spec/ReferenceScripts.hs @@ -18,9 +18,9 @@ putRefScriptOnWalletOutput :: Script.Versioned Script.Validator -> DirectMockChain V3.TxOutRef putRefScriptOnWalletOutput recipient referenceScript = - fst . head - <$> validateTxSkel' - txSkelTemplate + head + <$> validateTxSkelL + txSkelEmulatorTemplate { txSkelOutputs = [recipient `receives` ReferenceScript referenceScript], txSkelSignatories = txSkelSignatoriesFromList [wallet 1] } @@ -30,9 +30,9 @@ putRefScriptOnScriptOutput :: Script.Versioned Script.Validator -> DirectMockChain V3.TxOutRef putRefScriptOnScriptOutput recipient referenceScript = - fst . head - <$> validateTxSkel' - txSkelTemplate + head + <$> validateTxSkelL + txSkelEmulatorTemplate { txSkelOutputs = [recipient `receives` ReferenceScript referenceScript], txSkelSignatories = txSkelSignatoriesFromList [wallet 1] } @@ -42,14 +42,14 @@ checkReferenceScriptOnOref :: V3.TxOutRef -> DirectMockChain () checkReferenceScriptOnOref expectedScriptHash refScriptOref = do - (oref, _) : _ <- - validateTxSkel' - txSkelTemplate + oref : _ <- + validateTxSkelL + txSkelEmulatorTemplate { txSkelOutputs = [requireRefScriptValidator expectedScriptHash `receives` Value (Script.ada 42)], txSkelSignatories = txSkelSignatoriesFromList [wallet 1] } validateTxSkel_ - txSkelTemplate + txSkelEmulatorTemplate { txSkelInputs = Map.singleton oref emptyTxSkelRedeemer, txSkelReferenceInputs = Set.singleton refScriptOref, txSkelSignatories = txSkelSignatoriesFromList [wallet 1] @@ -62,15 +62,15 @@ checkReferenceScriptOnOref expectedScriptHash refScriptOref = do useReferenceScript :: Wallet -> Bool -> Script.Versioned Script.Validator -> DirectMockChain P.Ledger.CardanoTx useReferenceScript spendingSubmitter consumeScriptOref theScript = do scriptOref <- putRefScriptOnWalletOutput (wallet 3) theScript - (oref, _) : _ <- - validateTxSkel' - txSkelTemplate + oref : _ <- + validateTxSkelL + txSkelEmulatorTemplate { txSkelOutputs = [theScript `receives` Value (Script.ada 42)], txSkelSignatories = txSkelSignatoriesFromList [wallet 1] } - fst + (\(_, _, tx, _) -> P.Ledger.CardanoEmulatorEraTx tx) <$> validateTxSkel - txSkelTemplate + txSkelEmulatorTemplate { txSkelInputs = Map.fromList $ (oref, TxSkelRedeemer () (Just scriptOref) False) @@ -81,23 +81,23 @@ useReferenceScript spendingSubmitter consumeScriptOref theScript = do useReferenceScriptInInputs :: Wallet -> Script.Versioned Script.Validator -> DirectMockChain () useReferenceScriptInInputs spendingSubmitter theScript = do scriptOref <- putRefScriptOnWalletOutput (wallet 1) theScript - (oref, _) : _ <- - validateTxSkel' - txSkelTemplate + oref : _ <- + validateTxSkelL + txSkelEmulatorTemplate { txSkelOutputs = [theScript `receives` Value (Script.ada 42)], txSkelSignatories = txSkelSignatoriesFromList [wallet 1] } validateTxSkel_ - txSkelTemplate + txSkelEmulatorTemplate { txSkelInputs = Map.fromList [(oref, TxSkelRedeemer () (Just scriptOref) False), (scriptOref, emptyTxSkelRedeemer)], txSkelSignatories = txSkelSignatoriesFromList [spendingSubmitter] } referenceMint :: Script.Versioned Script.MintingPolicy -> Script.Versioned Script.MintingPolicy -> Int -> Bool -> DirectMockChain () referenceMint mp1 mp2 n autoRefScript = do - ((!! n) -> (mpOutRef, _)) <- + (Map.elemAt n -> (mpOutRef, _)) <- validateTxSkel' $ - txSkelTemplate + txSkelEmulatorTemplate { txSkelOutputs = [ wallet 1 `receives` Value (Script.ada 2) <&&> ReferenceScript mp1, wallet 1 `receives` Value (Script.ada 10) @@ -105,7 +105,7 @@ referenceMint mp1 mp2 n autoRefScript = do txSkelSignatories = txSkelSignatoriesFromList [wallet 1] } validateTxSkel_ $ - txSkelTemplate + txSkelEmulatorTemplate { txSkelMints = review txSkelMintsListI @@ -148,16 +148,19 @@ tests = [ testCookedFromInitDistTemplate @DirectEffs "fail from transaction generation for missing reference scripts" $ mustFailTest ( do - consumedOref : _ <- getTxOutRefs $ utxosAtSearch (wallet 1) $ ensureAFoldIs (txSkelOutValueL % filtered (`Api.geq` Script.lovelace 42_000_000)) - (oref, _) : _ <- - validateTxSkel' - txSkelTemplate + (Set.elemAt 0 -> consumedOref) <- + getTxOutRefs $ + utxosAtSearch (wallet 1) $ + ensureAFoldIs (txSkelOutValueL % filtered (`Api.geq` Script.lovelace 42_000_000)) + oref : _ <- + validateTxSkelL + txSkelEmulatorTemplate { txSkelOutputs = [Script.alwaysSucceedValidatorVersioned `receives` Value (Script.ada 42)], txSkelInputs = Map.singleton consumedOref emptyTxSkelRedeemer, txSkelSignatories = txSkelSignatoriesFromList [wallet 1] } validateTxSkel_ - txSkelTemplate + txSkelEmulatorTemplate { txSkelInputs = Map.singleton oref (TxSkelRedeemer () (Just consumedOref) False), txSkelSignatories = txSkelSignatoriesFromList [wallet 1] } @@ -169,14 +172,14 @@ tests = mustFailTest ( do scriptOref <- putRefScriptOnWalletOutput (wallet 3) Script.alwaysFailValidatorVersioned - (oref, _) : _ <- - validateTxSkel' - txSkelTemplate + oref : _ <- + validateTxSkelL + txSkelEmulatorTemplate { txSkelOutputs = [Script.alwaysSucceedValidatorVersioned `receives` Value (Script.ada 42)], txSkelSignatories = txSkelSignatoriesFromList [wallet 1] } validateTxSkel_ - txSkelTemplate + txSkelEmulatorTemplate { txSkelInputs = Map.singleton oref (TxSkelRedeemer () (Just scriptOref) False), txSkelSignatories = txSkelSignatoriesFromList [wallet 1] } @@ -187,14 +190,14 @@ tests = testCookedFromInitDistTemplate "phase 1 - fail if using a reference script with 'someRedeemer'" $ mustFailInPhase1Test $ do scriptOref <- putRefScriptOnWalletOutput (wallet 3) Script.alwaysSucceedValidatorVersioned - (oref, _) : _ <- - validateTxSkel' - txSkelTemplate + oref : _ <- + validateTxSkelL + txSkelEmulatorTemplate { txSkelOutputs = [Script.alwaysSucceedValidatorVersioned `receives` Value (Script.ada 42)], txSkelSignatories = txSkelSignatoriesFromList [wallet 1] } validateTxSkel_ - txSkelTemplate + txSkelEmulatorTemplate { txSkelInputs = Map.singleton oref emptyTxSkelRedeemerNoAutoFill, txSkelReferenceInputs = Set.singleton scriptOref, txSkelSignatories = txSkelSignatoriesFromList [wallet 1] diff --git a/tests/Spec/Slot.hs b/tests/Spec/Slot.hs index f16b38306..c4d92125f 100644 --- a/tests/Spec/Slot.hs +++ b/tests/Spec/Slot.hs @@ -1,6 +1,6 @@ module Spec.Slot (tests) where -import Cooked.MockChain.Effect.Read +import Cooked.MockChain.Effect.Time import Cooked.MockChain.Runtime.Error import Cooked.MockChain.Runtime.State import Data.Default @@ -16,8 +16,9 @@ import Test.Tasty.QuickCheck runSlot :: Sem - '[ MockChainRead, - State MockChainState, + '[ MockChainTime, + State EmulatorState, + State ChainIndex, Fail, Error P.Ledger.ToCardanoError, Error MockChainError @@ -30,7 +31,8 @@ runSlot = . runToCardanoErrorInMockChainError . runFailInMockChainError . evalState def - . runMockChainRead + . evalState def + . runMockChainTimeEmul tests :: TestTree tests = diff --git a/tests/Spec/Tweak/Common.hs b/tests/Spec/Tweak/Common.hs index 49e96ccaa..afe3b491d 100644 --- a/tests/Spec/Tweak/Common.hs +++ b/tests/Spec/Tweak/Common.hs @@ -13,7 +13,7 @@ alice :: Wallet alice = wallet 1 mkSkel :: [Integer] -> TxSkel -mkSkel l = set txSkelOutputsL (receives alice . Value . Script.lovelace <$> l) txSkelTemplate +mkSkel l = set txSkelOutputsL (receives alice . Value . Script.lovelace <$> l) txSkelEmulatorTemplate tests :: TestTree tests = diff --git a/tests/Spec/Tweak/Labels.hs b/tests/Spec/Tweak/Labels.hs index 45b0bdaf7..ff5970795 100644 --- a/tests/Spec/Tweak/Labels.hs +++ b/tests/Spec/Tweak/Labels.hs @@ -17,7 +17,7 @@ carrie = wallet 3 payTo :: Wallet -> Integer -> StagedMockChain () payTo target amount = do validateTxSkel_ $ - txSkelTemplate + txSkelEmulatorTemplate { txSkelSignatories = txSkelSignatoriesFromList [alice], txSkelOutputs = [target `receives` Value (Script.ada amount)] } diff --git a/tests/Spec/Withdrawals.hs b/tests/Spec/Withdrawals.hs index 8ee6340e2..4c3d0ec8b 100644 --- a/tests/Spec/Withdrawals.hs +++ b/tests/Spec/Withdrawals.hs @@ -21,7 +21,7 @@ testWithdrawingScript userCertifying userRewarding mAmount = do forceOutputs_ [alice `receives` Value (Script.ada 100)] when (isJust userCertifying) $ validateTxSkel_ $ - txSkelTemplate + txSkelEmulatorTemplate { txSkelSignatories = txSkelSignatoriesFromList [alice], txSkelCertificates = [ TxSkelCertificate (fromJust userCertifying) $ @@ -31,7 +31,7 @@ testWithdrawingScript userCertifying userRewarding mAmount = do ] } validateTxSkel_ $ - txSkelTemplate + txSkelEmulatorTemplate { txSkelSignatories = txSkelSignatoriesFromList [alice], txSkelWithdrawals = txSkelWithdrawalsFromList [Withdrawal userRewarding (Api.Lovelace . (1_000_000 *) <$> mAmount)] }