From 579ecbe791e14918184de6801e1ed97dc094f763 Mon Sep 17 00:00:00 2001 From: mmontin Date: Thu, 4 Jul 2024 13:27:57 +0200 Subject: [PATCH 01/44] tests pass with new mockchain return type --- src/Cooked/MockChain.hs | 10 +-- src/Cooked/MockChain/BlockChain.hs | 12 ++++ src/Cooked/MockChain/Direct.hs | 52 +++++++++----- src/Cooked/MockChain/Staged.hs | 69 ++++--------------- src/Cooked/MockChain/Testing.hs | 4 +- src/Cooked/Pretty/Cooked.hs | 83 +++++++++++----------- src/Cooked/Pretty/Options.hs | 21 +++++- tests/Cooked/Attack/DatumHijackingSpec.hs | 8 +-- tests/Cooked/Attack/DoubleSatSpec.hs | 84 ++++++++++++----------- tests/Cooked/Attack/DupTokenSpec.hs | 8 +-- tests/Cooked/InlineDatumsSpec.hs | 4 +- tests/Cooked/MockChain/BlockChainSpec.hs | 6 +- tests/Cooked/Tweak/CommonSpec.hs | 83 +++++++++++----------- tests/Cooked/Tweak/OutPermutationsSpec.hs | 10 +-- tests/Cooked/Tweak/TamperDatumSpec.hs | 66 +++++++++--------- tests/Cooked/Tweak/ValidityRangeSpec.hs | 6 +- 16 files changed, 268 insertions(+), 258 deletions(-) diff --git a/src/Cooked/MockChain.hs b/src/Cooked/MockChain.hs index 78ba59c79..b5a7b3193 100644 --- a/src/Cooked/MockChain.hs +++ b/src/Cooked/MockChain.hs @@ -3,14 +3,10 @@ module Cooked.MockChain (module X) where import Cooked.MockChain.Balancing as X -import Cooked.MockChain.BlockChain as X -import Cooked.MockChain.Direct as X +import Cooked.MockChain.BlockChain as X hiding (BlockChainLogEntry) +import Cooked.MockChain.Direct as X hiding (MockChainReturn) import Cooked.MockChain.MinAda as X -import Cooked.MockChain.Staged as X hiding - ( MockChainLog, - MockChainLogEntry, - StagedMockChain, - ) +import Cooked.MockChain.Staged as X hiding (StagedMockChain) import Cooked.MockChain.Testing as X import Cooked.MockChain.UtxoSearch as X import Cooked.MockChain.UtxoState as X (UtxoState) diff --git a/src/Cooked/MockChain/BlockChain.hs b/src/Cooked/MockChain/BlockChain.hs index fcdf38e79..5003e9a1c 100644 --- a/src/Cooked/MockChain/BlockChain.hs +++ b/src/Cooked/MockChain/BlockChain.hs @@ -15,6 +15,7 @@ -- from the core definition of our blockchain. module Cooked.MockChain.BlockChain ( MockChainError (..), + BlockChainLogEntry (..), MonadBlockChainBalancing (..), MonadBlockChainWithoutValidation (..), MonadBlockChain (..), @@ -110,6 +111,12 @@ data MockChainError where FailWith :: String -> MockChainError deriving (Show, Eq) +data BlockChainLogEntry where + BCLogSubmittedTxSkel :: SkelContext -> TxSkel -> BlockChainLogEntry + BCLogNewTx :: Api.TxId -> BlockChainLogEntry + BCLogInfo :: String -> BlockChainLogEntry + BCLogFail :: String -> BlockChainLogEntry + -- | Contains methods needed for balancing. class (MonadFail m, MonadError MockChainError m) => MonadBlockChainBalancing m where -- | Returns the parameters of the chain. @@ -128,6 +135,9 @@ class (MonadFail m, MonadError MockChainError m) => MonadBlockChainBalancing m w -- | Returns an output given a reference to it txOutByRefLedger :: Api.TxOutRef -> m (Maybe Ledger.TxOut) + -- | Logs an event that occured during a BlockChain run + blockChainLog :: BlockChainLogEntry -> m () + class (MonadBlockChainBalancing m) => MonadBlockChainWithoutValidation m where -- | Returns a list of all currently known outputs. allUtxosLedger :: m [(Api.TxOutRef, Ledger.TxOut)] @@ -493,6 +503,7 @@ instance (MonadTrans t, MonadBlockChainBalancing m, Monad (t m), MonadError Mock utxosAtLedger = lift . utxosAtLedger txOutByRefLedger = lift . txOutByRefLedger datumFromHash = lift . datumFromHash + blockChainLog = lift . blockChainLog instance (MonadTrans t, MonadBlockChainWithoutValidation m, Monad (t m), MonadError MockChainError (AsTrans t m)) => MonadBlockChainWithoutValidation (AsTrans t m) where allUtxosLedger = lift allUtxosLedger @@ -536,6 +547,7 @@ instance (MonadBlockChainBalancing m) => MonadBlockChainBalancing (ListT m) wher utxosAtLedger = lift . utxosAtLedger txOutByRefLedger = lift . txOutByRefLedger datumFromHash = lift . datumFromHash + blockChainLog = lift . blockChainLog instance (MonadBlockChainWithoutValidation m) => MonadBlockChainWithoutValidation (ListT m) where allUtxosLedger = lift allUtxosLedger diff --git a/src/Cooked/MockChain/Direct.hs b/src/Cooked/MockChain/Direct.hs index c2a977eca..a92ad8f05 100644 --- a/src/Cooked/MockChain/Direct.hs +++ b/src/Cooked/MockChain/Direct.hs @@ -16,6 +16,7 @@ import Control.Monad.Except import Control.Monad.Identity import Control.Monad.Reader import Control.Monad.State.Strict +import Control.Monad.Writer import Cooked.Conversion.ToScript import Cooked.Conversion.ToScriptHash import Cooked.InitialDistribution @@ -97,6 +98,12 @@ data MockChainSt = MockChainSt } deriving (Show) +mcstToSkelContext :: MockChainSt -> SkelContext +mcstToSkelContext MockChainSt {..} = + SkelContext + (txOutV2FromLedger <$> getIndex mcstIndex) + (Map.map fst mcstDatums) + -- | Generating an emulated state for the emulator from a mockchain state and -- some parameters, based on a standard initial state mcstToEmulatedLedgerState :: MockChainSt -> Emulator.EmulatedLedgerState @@ -129,8 +136,8 @@ instance Eq MockChainSt where ] newtype MockChainT m a = MockChainT - {unMockChain :: StateT MockChainSt (ExceptT MockChainError m) a} - deriving newtype (Functor, Applicative, MonadState MockChainSt, MonadError MockChainError) + {unMockChain :: (StateT MockChainSt (ExceptT MockChainError (WriterT [BlockChainLogEntry] m))) a} + deriving newtype (Functor, Applicative, MonadState MockChainSt, MonadError MockChainError, MonadWriter [BlockChainLogEntry]) type MockChain = MockChainT Identity @@ -140,13 +147,15 @@ instance (Monad m) => Monad (MockChainT m) where MockChainT x >>= f = MockChainT $ x >>= unMockChain . f instance (Monad m) => MonadFail (MockChainT m) where - fail = throwError . FailWith + fail s = do + blockChainLog $ BCLogFail s + throwError $ FailWith s instance MonadTrans MockChainT where - lift = MockChainT . lift . lift + lift = MockChainT . lift . lift . lift instance (Monad m, Alternative m) => Alternative (MockChainT m) where - empty = MockChainT $ StateT $ const $ ExceptT empty + empty = MockChainT $ StateT $ const $ ExceptT $ WriterT empty (<|>) = combineMockChainT (<|>) combineMockChainT :: @@ -157,15 +166,17 @@ combineMockChainT :: MockChainT m x combineMockChainT f ma mb = MockChainT $ StateT $ \s -> - let resA = runExceptT $ runStateT (unMockChain ma) s - resB = runExceptT $ runStateT (unMockChain mb) s - in ExceptT $ f resA resB + let resA = runWriterT $ runExceptT $ runStateT (unMockChain ma) s + resB = runWriterT $ runExceptT $ runStateT (unMockChain mb) s + in ExceptT $ WriterT $ f resA resB + +type MockChainReturn a b = (Either MockChainError (a, b), [BlockChainLogEntry]) mapMockChainT :: - (m (Either MockChainError (a, MockChainSt)) -> n (Either MockChainError (b, MockChainSt))) -> + (m (MockChainReturn a MockChainSt) -> n (MockChainReturn b MockChainSt)) -> MockChainT m a -> MockChainT n b -mapMockChainT f = MockChainT . mapStateT (mapExceptT f) . unMockChain +mapMockChainT f = MockChainT . mapStateT (mapExceptT (mapWriterT f)) . unMockChain -- | Executes a 'MockChainT' from some initial state; does /not/ convert the -- 'MockChainSt' into a 'UtxoState'. @@ -173,8 +184,8 @@ runMockChainTRaw :: (Monad m) => MockChainSt -> MockChainT m a -> - m (Either MockChainError (a, MockChainSt)) -runMockChainTRaw i0 = runExceptT . flip runStateT i0 . unMockChain + m (MockChainReturn a MockChainSt) +runMockChainTRaw i0 = runWriterT . runExceptT . flip runStateT i0 . unMockChain -- | Executes a 'MockChainT' from an initial state set up with the given initial -- value distribution. Similar to 'runMockChainT', uses the default @@ -184,25 +195,25 @@ runMockChainTFrom :: (Monad m) => InitialDistribution -> MockChainT m a -> - m (Either MockChainError (a, UtxoState)) -runMockChainTFrom i0 = fmap (fmap $ second mcstToUtxoState) . runMockChainTRaw (mockChainSt0From i0) + m (MockChainReturn a UtxoState) +runMockChainTFrom i0 s = first (right (second mcstToUtxoState)) <$> runMockChainTRaw (mockChainSt0From i0) s -- | Executes a 'MockChainT' from the canonical initial state and environment. -- The canonical environment uses the default 'SlotConfig' and -- @Cooked.Wallet.wallet 1@ as the sole wallet signing transactions. -runMockChainT :: (Monad m) => MockChainT m a -> m (Either MockChainError (a, UtxoState)) +runMockChainT :: (Monad m) => MockChainT m a -> m (MockChainReturn a UtxoState) runMockChainT = runMockChainTFrom def -- | See 'runMockChainTRaw' -runMockChainRaw :: MockChain a -> Either MockChainError (a, MockChainSt) +runMockChainRaw :: MockChain a -> MockChainReturn a MockChainSt runMockChainRaw = runIdentity . runMockChainTRaw def -- | See 'runMockChainTFrom' -runMockChainFrom :: InitialDistribution -> MockChain a -> Either MockChainError (a, UtxoState) +runMockChainFrom :: InitialDistribution -> MockChain a -> MockChainReturn a UtxoState runMockChainFrom i0 = runIdentity . runMockChainTFrom i0 -- | See 'runMockChainT' -runMockChain :: MockChain a -> Either MockChainError (a, UtxoState) +runMockChain :: MockChain a -> MockChainReturn a UtxoState runMockChain = runIdentity . runMockChainT -- * Canonical initial values @@ -316,6 +327,7 @@ instance (Monad m) => MonadBlockChainBalancing (MockChainT m) where txOutByRefLedger outref = gets $ Map.lookup outref . getIndex . mcstIndex datumFromHash datumHash = (txSkelOutUntypedDatum <=< Just . fst <=< Map.lookup datumHash) <$> gets mcstDatums utxosAtLedger addr = filter ((addr ==) . outputAddress . txOutV2FromLedger . snd) <$> allUtxosLedger + blockChainLog l = tell [l] instance (Monad m) => MonadBlockChainWithoutValidation (MockChainT m) where allUtxosLedger = gets $ Map.toList . getIndex . mcstIndex @@ -325,6 +337,8 @@ instance (Monad m) => MonadBlockChainWithoutValidation (MockChainT m) where instance (Monad m) => MonadBlockChain (MockChainT m) where validateTxSkel skelUnbal = do + -- We log the submitted skeleton + gets mcstToSkelContext >>= blockChainLog . (`BCLogSubmittedTxSkel` skelUnbal) -- We retrieve the current parameters oldParams <- getParams -- We compute the optionally modified parameters @@ -386,6 +400,8 @@ instance (Monad m) => MonadBlockChain (MockChainT m) where modify' (\st -> st {mcstCurrentSlot = mcstCurrentSlot st + 1}) -- We return the parameters to their original state setParams oldParams + -- We log the validated transaction + blockChainLog $ BCLogNewTx (Ledger.fromCardanoTxId $ Ledger.getCardanoTxId cardanoTx) -- We return the validated transaction return cardanoTx where diff --git a/src/Cooked/MockChain/Staged.hs b/src/Cooked/MockChain/Staged.hs index a61a6cff9..9f14917f7 100644 --- a/src/Cooked/MockChain/Staged.hs +++ b/src/Cooked/MockChain/Staged.hs @@ -6,8 +6,6 @@ module Cooked.MockChain.Staged ( interpretAndRunWith, interpretAndRun, - MockChainLogEntry (..), - MockChainLog (..), StagedMockChain, runTweakFrom, MonadModalBlockChain, @@ -26,7 +24,6 @@ import Control.Monad (MonadPlus (..), msum) import Control.Monad.Except import Control.Monad.Reader import Control.Monad.State -import Control.Monad.Trans.Writer (WriterT (runWriterT), tell) import Cooked.Ltl import Cooked.MockChain.BlockChain import Cooked.MockChain.Direct @@ -34,10 +31,8 @@ import Cooked.MockChain.UtxoState import Cooked.Skeleton import Cooked.Tweak.Common import Data.Default -import Data.Map qualified as Map import Ledger.Slot qualified as Ledger import Ledger.Tx qualified as Ledger -import Ledger.Tx.CardanoAPI qualified as Ledger import Plutus.Script.Utils.Scripts qualified as Script import PlutusLedgerApi.V3 qualified as Api @@ -49,30 +44,15 @@ import PlutusLedgerApi.V3 qualified as Api interpretAndRunWith :: (forall m. (Monad m) => MockChainT m a -> m res) -> StagedMockChain a -> - [(res, MockChainLog)] -interpretAndRunWith f smc = runWriterT $ f $ interpret smc + [res] +interpretAndRunWith f smc = f $ interpret smc -interpretAndRun :: - StagedMockChain a -> - [(Either MockChainError (a, UtxoState), MockChainLog)] +interpretAndRun :: StagedMockChain a -> [MockChainReturn a UtxoState] interpretAndRun = interpretAndRunWith runMockChainT -data MockChainLogEntry - = MCLogSubmittedTxSkel SkelContext TxSkel - | MCLogNewTx Api.TxId - | MCLogFail String - -newtype MockChainLog = MockChainLog {unMockChainLog :: [MockChainLogEntry]} - -instance Semigroup MockChainLog where - MockChainLog x <> MockChainLog y = MockChainLog $ x <> y - -instance Monoid MockChainLog where - mempty = MockChainLog [] - -- | The semantic domain in which 'StagedMockChain' gets interpreted; see the -- 'interpret' function for more. -type InterpMockChain = MockChainT (WriterT MockChainLog []) +type InterpMockChain = MockChainT [] -- | The 'interpret' function gives semantics to our traces. One -- 'StagedMockChain' computation yields a potential list of 'MockChainT' @@ -99,6 +79,7 @@ data MockChainBuiltin a where AllUtxosLedger :: MockChainBuiltin [(Api.TxOutRef, Ledger.TxOut)] UtxosAtLedger :: Api.Address -> MockChainBuiltin [(Api.TxOutRef, Ledger.TxOut)] ValidatorFromHash :: Script.ValidatorHash -> MockChainBuiltin (Maybe (Script.Versioned Script.Validator)) + BlockChainLog :: BlockChainLogEntry -> MockChainBuiltin () -- | The empty set of traces Empty :: MockChainBuiltin a -- | The union of two sets of traces @@ -132,33 +113,17 @@ instance InterpLtl (UntypedTweak InterpMockChain) MockChainBuiltin InterpMockCha interpBuiltin (ValidateTxSkel skel) = get >>= msum - . map (uncurry interpretAndTell) + . map (uncurry interpretNow) . nowLaterList where - interpretAndTell :: + interpretNow :: UntypedTweak InterpMockChain -> [Ltl (UntypedTweak InterpMockChain)] -> StateT [Ltl (UntypedTweak InterpMockChain)] InterpMockChain Ledger.CardanoTx - interpretAndTell (UntypedTweak now) later = do - mcst <- lift get - let managedTxOuts = getIndex . mcstIndex $ mcst - managedDatums = mcstDatums mcst + interpretNow (UntypedTweak now) later = do (_, skel') <- lift $ runTweakInChain now skel - lift $ - lift $ - tell $ - MockChainLog - [ MCLogSubmittedTxSkel - (SkelContext (txOutV2FromLedger <$> managedTxOuts) $ Map.map fst managedDatums) - skel' - ] - tx <- validateTxSkel skel' - lift $ - lift $ - tell $ - MockChainLog [MCLogNewTx (Ledger.fromCardanoTxId $ Ledger.getCardanoTxId tx)] put later - return tx + validateTxSkel skel' interpBuiltin (TxOutByRefLedger o) = txOutByRefLedger o interpBuiltin GetCurrentSlot = currentSlot interpBuiltin (AwaitSlot s) = awaitSlot s @@ -168,23 +133,18 @@ instance InterpLtl (UntypedTweak InterpMockChain) MockChainBuiltin InterpMockCha interpBuiltin (UtxosAtLedger address) = utxosAtLedger address interpBuiltin Empty = mzero interpBuiltin (Alt l r) = interpLtl l `mplus` interpLtl r - interpBuiltin (Fail msg) = do - lift $ lift $ tell $ MockChainLog [MCLogFail msg] - fail msg + interpBuiltin (Fail msg) = fail msg interpBuiltin (ThrowError err) = throwError err interpBuiltin (CatchError act handler) = catchError (interpLtl act) (interpLtl . handler) + interpBuiltin (BlockChainLog entry) = blockChainLog entry -- ** Helpers to run tweaks for use in tests for tweaks -runTweak :: Tweak InterpMockChain a -> TxSkel -> [Either MockChainError (a, TxSkel)] +runTweak :: Tweak InterpMockChain a -> TxSkel -> [MockChainReturn a TxSkel] runTweak = runTweakFrom def -runTweakFrom :: MockChainSt -> Tweak InterpMockChain a -> TxSkel -> [Either MockChainError (a, TxSkel)] -runTweakFrom mcst tweak skel = - map (right fst . fst) - . runWriterT - . runMockChainTRaw mcst - $ runTweakInChain tweak skel +runTweakFrom :: MockChainSt -> Tweak InterpMockChain a -> TxSkel -> [MockChainReturn a TxSkel] +runTweakFrom mcst tweak = map (first (right fst)) . runMockChainTRaw mcst . runTweakInChain tweak -- ** Modalities @@ -241,6 +201,7 @@ instance MonadBlockChainBalancing StagedMockChain where txOutByRefLedger = singletonBuiltin . TxOutByRefLedger utxosAtLedger = singletonBuiltin . UtxosAtLedger validatorFromHash = singletonBuiltin . ValidatorFromHash + blockChainLog = singletonBuiltin . BlockChainLog instance MonadBlockChainWithoutValidation StagedMockChain where allUtxosLedger = singletonBuiltin AllUtxosLedger diff --git a/src/Cooked/MockChain/Testing.hs b/src/Cooked/MockChain/Testing.hs index afa3f39bd..89d4735b6 100644 --- a/src/Cooked/MockChain/Testing.hs +++ b/src/Cooked/MockChain/Testing.hs @@ -148,7 +148,7 @@ testAllSatisfiesFrom :: prop testAllSatisfiesFrom pcOpts f = testSatisfiesFrom' (testAll go) where - go :: (Either MockChainError (a, UtxoState), MockChainLog) -> prop + go :: MockChainReturn a UtxoState -> prop go (prop, mcLog) = testCounterexample (renderString (prettyCookedOpt pcOpts) mcLog) (f prop) -- | Asserts that the given 'StagedMockChain' produces exactly two outcomes, @@ -227,7 +227,7 @@ testOneEquivClass pcOpts rel = testSatisfiesFrom' $ \case -- predicates. Check 'testAllSatisfiesFrom' or 'testBinaryRelatedBy' for -- examples on using this. testSatisfiesFrom' :: - ([(Either MockChainError (a, UtxoState), MockChainLog)] -> prop) -> + ([MockChainReturn a UtxoState] -> prop) -> InitialDistribution -> StagedMockChain a -> prop diff --git a/src/Cooked/Pretty/Cooked.hs b/src/Cooked/Pretty/Cooked.hs index 6ef15fd3a..75d5a39ce 100644 --- a/src/Cooked/Pretty/Cooked.hs +++ b/src/Cooked/Pretty/Cooked.hs @@ -31,8 +31,8 @@ where import Cooked.Conversion import Cooked.MockChain.BlockChain +import Cooked.MockChain.Direct import Cooked.MockChain.GenerateTx -import Cooked.MockChain.Staged import Cooked.MockChain.UtxoState import Cooked.Output import Cooked.Pretty.Class @@ -124,51 +124,50 @@ instance (Show a) => PrettyCooked (a, UtxoState) where "-" ["Returns:" <+> PP.viaShow res, prettyCookedOpt opts state] -instance (Show a) => PrettyCooked (Either MockChainError (a, UtxoState)) where - prettyCookedOpt opts (Left err) = "🔴" <+> prettyCookedOpt opts err - prettyCookedOpt opts (Right endState) = "🟢" <+> prettyCookedOpt opts endState +prettyBlockChainEntries :: PrettyCookedOpts -> [BlockChainLogEntry] -> DocCooked +prettyBlockChainEntries opts entries = + prettyItemize + "MockChain run:" + "-" + (prettyCookedOpt opts <$> entries) + +instance (Show a) => PrettyCooked (MockChainReturn a UtxoState) where + prettyCookedOpt opts (res, entries) = prettyLogWith $ + case res of + Left err -> "🔴" <+> prettyCookedOpt opts err + Right (a, s) -> "🟢" <+> prettyCookedOpt opts (a, s) + where + prettyLogWith :: DocCooked -> DocCooked + prettyLogWith inner = + case pcOptLog opts of + PCOptLogNone -> inner + PCOptLogNoInfo -> + prettyItemize + "End result:" + "-" + [prettyBlockChainEntries opts (filter (\case BCLogInfo _ -> False; _ -> True) entries), inner] + PCOptLogAll -> + prettyItemize + "End result:" + "-" + [prettyBlockChainEntries opts entries, inner] -- | This pretty prints a 'MockChainLog' that usually consists of the list of -- validated or submitted transactions. In the log, we know a transaction has -- been validated if the 'MCLogSubmittedTxSkel' is followed by a 'MCLogNewTx'. -instance PrettyCooked MockChainLog where - prettyCookedOpt opts = - prettyEnumerate "MockChain run:" "." - . go [] - . unMockChainLog - where - -- In order to avoid printing 'MockChainLogValidateTxSkel' then - -- 'MockChainLogNewTx' as two different items, we combine them into one - -- single 'DocCooked' - go :: [DocCooked] -> [MockChainLogEntry] -> [DocCooked] - go - acc - ( MCLogSubmittedTxSkel skelContext skel - : MCLogNewTx txId - : entries - ) - | pcOptPrintTxHashes opts = - go - ( "Validated" - <+> PP.parens ("TxId:" <+> prettyCookedOpt opts txId) - <+> prettyTxSkel opts skelContext skel - : acc - ) - entries - | otherwise = go ("Validated" <+> prettyTxSkel opts skelContext skel : acc) entries - go - acc - ( MCLogSubmittedTxSkel skelContext skel - : entries - ) = - go ("Submitted" <+> prettyTxSkel opts skelContext skel : acc) entries - go acc (MCLogFail msg : entries) = - go ("Fail:" <+> PP.pretty msg : acc) entries - -- This case is not supposed to occur because it should follow a - -- 'MCLogSubmittedTxSkel' - go acc (MCLogNewTx txId : entries) = - go ("New transaction:" <+> prettyCookedOpt opts txId : acc) entries - go acc [] = reverse acc +instance PrettyCooked BlockChainLogEntry where + prettyCookedOpt opts (BCLogSubmittedTxSkel skelContext skel) = "Submitted:" <+> prettyTxSkel opts skelContext skel + prettyCookedOpt _ (BCLogFail msg) = "Fail:" <+> PP.pretty msg + prettyCookedOpt opts (BCLogNewTx txId) = "New transaction:" <+> prettyCookedOpt opts txId + prettyCookedOpt _ (BCLogInfo info) = "Info:" <+> PP.pretty info + +-- go acc (MCLogFail msg : entries) = +-- go ("Fail:" <+> PP.pretty msg : acc) entries +-- -- This case is not supposed to occur because it should follow a +-- -- 'MCLogSubmittedTxSkel' +-- go acc (MCLogNewTx txId : entries) = +-- go ("New transaction:" <+> prettyCookedOpt opts txId : acc) entries +-- go acc [] = reverse acc prettyTxSkel :: PrettyCookedOpts -> SkelContext -> TxSkel -> DocCooked prettyTxSkel opts skelContext (TxSkel lbl txopts mints signers validityRange ins insReference outs proposals) = diff --git a/src/Cooked/Pretty/Options.hs b/src/Cooked/Pretty/Options.hs index 602a65009..dfc6db987 100644 --- a/src/Cooked/Pretty/Options.hs +++ b/src/Cooked/Pretty/Options.hs @@ -4,6 +4,7 @@ module Cooked.Pretty.Options ( PrettyCookedOpts (..), PrettyCookedHashOpts (..), PCOptTxOutRefs (..), + PCOptLog (..), hashNamesFromList, defaultHashNames, ) @@ -31,7 +32,9 @@ data PrettyCookedOpts = PrettyCookedOpts -- @53_000_000@ instead of @53000000@. By default: True pcOptNumericUnderscores :: Bool, -- | Options related to printing hashes - pcOptHashes :: PrettyCookedHashOpts + pcOptHashes :: PrettyCookedHashOpts, + -- | What kind of log to print + pcOptLog :: PCOptLog } deriving (Eq, Show) @@ -42,9 +45,23 @@ instance Default PrettyCookedOpts where pcOptPrintTxOutRefs = PCOptTxOutRefsHidden, pcOptPrintDefaultTxOpts = False, pcOptNumericUnderscores = True, - pcOptHashes = def + pcOptHashes = def, + pcOptLog = def } +-- | What log to display +data PCOptLog + = -- | No logging at all + PCOptLogNone + | -- | All logging except for infos, default option + PCOptLogNoInfo + | -- | All logging, for debugging purpose + PCOptLogAll + deriving (Eq, Show) + +instance Default PCOptLog where + def = PCOptLogNoInfo + -- | Whether to print transaction outputs references. data PCOptTxOutRefs = -- | Hide them diff --git a/tests/Cooked/Attack/DatumHijackingSpec.hs b/tests/Cooked/Attack/DatumHijackingSpec.hs index 89dd0cfbb..9f86b29b4 100644 --- a/tests/Cooked/Attack/DatumHijackingSpec.hs +++ b/tests/Cooked/Attack/DatumHijackingSpec.hs @@ -201,14 +201,14 @@ tests = ], txSkelSigners = [wallet 1] } - in [ testCase "no modified transactions if no interesting outputs to steal" $ [] @=? skelOut mempty (const True), + in [ testCase "no modified transactions if no interesting outputs to steal" $ [] @=? fst <$> skelOut mempty (const True), testCase "one modified transaction for one interesting output" $ [ Right ( [ConcreteOutput val1 Nothing (TxSkelOutInlineDatum SecondLock) x3 Nothing], skelExpected thief val1 ) ] - @=? skelOut x2 (0 ==), + @=? fst <$> skelOut x2 (0 ==), testCase "two modified transactions for two interesting outputs" $ [ Right ( [ ConcreteOutput val1 Nothing (TxSkelOutInlineDatum SecondLock) x3 Nothing, @@ -217,14 +217,14 @@ tests = skelExpected thief thief ) ] - @=? skelOut x2 (const True), + @=? fst <$> skelOut x2 (const True), testCase "select second interesting output to get one modified transaction" $ [ Right ( [ConcreteOutput val1 Nothing (TxSkelOutInlineDatum SecondLock) x2 Nothing], skelExpected val1 thief ) ] - @=? skelOut x2 (1 ==) + @=? fst <$> skelOut x2 (1 ==) ], testCase "careful validator" $ testFails diff --git a/tests/Cooked/Attack/DoubleSatSpec.hs b/tests/Cooked/Attack/DoubleSatSpec.hs index 68a641a72..64384aa07 100644 --- a/tests/Cooked/Attack/DoubleSatSpec.hs +++ b/tests/Cooked/Attack/DoubleSatSpec.hs @@ -121,7 +121,7 @@ customInitDist = -- | Utxos generated from the initial distribution aUtxo1, aUtxo2, aUtxo3, aUtxo4, bUtxo1, bUtxo2 :: (Api.TxOutRef, Api.TxOut) (aUtxo1, aUtxo2, aUtxo3, aUtxo4, bUtxo1, bUtxo2) = - case runMockChainFrom customInitDist $ do + case fst $ runMockChainFrom customInitDist $ do [a1, a2, a3, a4] <- runUtxoSearch $ utxosAtSearch aValidator [b1, b2] <- runUtxoSearch $ utxosAtSearch bValidator return (a1, a2, a3, a4, b1, b2) of @@ -160,46 +160,48 @@ tests = -- on the focused input 'aValidator' UTxO. skelsOut :: ([Api.TxOutRef] -> [[Api.TxOutRef]]) -> [(ARedeemer, Api.TxOutRef)] -> [TxSkel] skelsOut splitMode aInputs = - mapMaybe (\case Right (_, skel') -> Just skel'; _ -> Nothing) $ - runTweakFrom - (mockChainSt0From customInitDist) - ( doubleSatAttack - splitMode - (txSkelInsL % itraversed) -- we know that every 'TxOutRef' in the inputs points to a UTxO that the 'aValidator' owns - ( \aOref _aRedeemer -> do - bUtxos <- runUtxoSearch $ scriptOutputsSearch bValidator - if - | aOref == fst aUtxo1 -> - return - [ (txSkelSomeRedeemer ARedeemer2, toDelta bOref $ txSkelSomeRedeemer BRedeemer1) - | (bOref, bOut) <- bUtxos, - outputValue bOut == Script.lovelaceValueOf 123 -- not satisfied by any UTxO in 'dsTestMockChain' - ] - | aOref == fst aUtxo2 -> - return - [ (txSkelSomeRedeemer ARedeemer2, toDelta bOref $ txSkelSomeRedeemer BRedeemer1) - | (bOref, _) <- bUtxos, - bOref == fst bUtxo1 - ] - | aOref == fst aUtxo3 -> - return $ - concatMap - ( \(bOref, _) -> - if - | bOref == fst bUtxo1 -> - [(txSkelSomeRedeemer ARedeemer2, toDelta bOref $ txSkelSomeRedeemer BRedeemer1)] - | bOref == fst bUtxo2 -> - [ (txSkelSomeRedeemer ARedeemer2, toDelta bOref $ txSkelSomeRedeemer BRedeemer1), - (txSkelSomeRedeemer ARedeemer3, toDelta bOref $ txSkelSomeRedeemer BRedeemer2) - ] - | otherwise -> [] - ) - bUtxos - | otherwise -> return [] - ) - (wallet 6) - ) - (skelIn aInputs) + mapMaybe + ((\case Right (_, skel') -> Just skel'; _ -> Nothing) . fst) + ( runTweakFrom + (mockChainSt0From customInitDist) + ( doubleSatAttack + splitMode + (txSkelInsL % itraversed) -- we know that every 'TxOutRef' in the inputs points to a UTxO that the 'aValidator' owns + ( \aOref _aRedeemer -> do + bUtxos <- runUtxoSearch $ scriptOutputsSearch bValidator + if + | aOref == fst aUtxo1 -> + return + [ (txSkelSomeRedeemer ARedeemer2, toDelta bOref $ txSkelSomeRedeemer BRedeemer1) + | (bOref, bOut) <- bUtxos, + outputValue bOut == Script.lovelaceValueOf 123 -- not satisfied by any UTxO in 'dsTestMockChain' + ] + | aOref == fst aUtxo2 -> + return + [ (txSkelSomeRedeemer ARedeemer2, toDelta bOref $ txSkelSomeRedeemer BRedeemer1) + | (bOref, _) <- bUtxos, + bOref == fst bUtxo1 + ] + | aOref == fst aUtxo3 -> + return $ + concatMap + ( \(bOref, _) -> + if + | bOref == fst bUtxo1 -> + [(txSkelSomeRedeemer ARedeemer2, toDelta bOref $ txSkelSomeRedeemer BRedeemer1)] + | bOref == fst bUtxo2 -> + [ (txSkelSomeRedeemer ARedeemer2, toDelta bOref $ txSkelSomeRedeemer BRedeemer1), + (txSkelSomeRedeemer ARedeemer3, toDelta bOref $ txSkelSomeRedeemer BRedeemer2) + ] + | otherwise -> [] + ) + bUtxos + | otherwise -> return [] + ) + (wallet 6) + ) + (skelIn aInputs) + ) where toDelta :: Api.TxOutRef -> TxSkelRedeemer -> DoubleSatDelta toDelta oref howSpent = (Map.singleton oref howSpent, [], mempty) diff --git a/tests/Cooked/Attack/DupTokenSpec.hs b/tests/Cooked/Attack/DupTokenSpec.hs index b15dd49c5..2f6866eb3 100644 --- a/tests/Cooked/Attack/DupTokenSpec.hs +++ b/tests/Cooked/Attack/DupTokenSpec.hs @@ -105,11 +105,11 @@ tests = ) ] in [ testCase "add one token in every asset class" $ - skelExpected 6 8 @=? skelOut (\_ n -> n + 1), + skelExpected 6 8 @=? fst <$> skelOut (\_ n -> n + 1), testCase "no modified transaction if no increase in value specified" $ - [] @=? skelOut (\_ n -> n), + [] @=? fst <$> skelOut (\_ n -> n), testCase "add tokens depending on the asset class" $ - skelExpected 10 7 @=? skelOut (\ac n -> if ac == ac1 then n + 5 else n) + skelExpected 10 7 @=? fst <$> skelOut (\ac n -> if ac == ac1 then n + 5 else n) ], testCase "careful minting policy" $ let tName = Script.tokenName "MockToken" @@ -155,5 +155,5 @@ tests = ) ] skelOut = runTweak (dupTokenAttack (\_ i -> i + 1) attacker) skelIn - in skelExpected @=? skelOut + in skelExpected @=? fst <$> skelOut ] diff --git a/tests/Cooked/InlineDatumsSpec.hs b/tests/Cooked/InlineDatumsSpec.hs index df39f0e80..52ae2433a 100644 --- a/tests/Cooked/InlineDatumsSpec.hs +++ b/tests/Cooked/InlineDatumsSpec.hs @@ -177,7 +177,7 @@ tests = let theValidator = inputDatumValidator True in [ testCase "the datum is retrieved correctly" $ assertBool "... it's not" $ - case runMockChain (listUtxosTestTrace True theValidator >> allUtxos) of + case fst $ runMockChain (listUtxosTestTrace True theValidator >> allUtxos) of Right (utxos, _endState) -> case mapMaybe ((outputOutputDatum <$>) . isScriptOutputFrom theValidator . snd) utxos of [Api.OutputDatum _] -> True @@ -185,7 +185,7 @@ tests = _ -> False, testCase "the datum hash is retrieved correctly" $ assertBool "... it's not" $ - case runMockChain (listUtxosTestTrace False theValidator >> allUtxos) of + case fst $ runMockChain (listUtxosTestTrace False theValidator >> allUtxos) of Right (utxos, _endState) -> case mapMaybe ((outputOutputDatum <$>) . isScriptOutputFrom theValidator . snd) utxos of [Api.OutputDatumHash _] -> True diff --git a/tests/Cooked/MockChain/BlockChainSpec.hs b/tests/Cooked/MockChain/BlockChainSpec.hs index faacacdf2..0bd506cf3 100644 --- a/tests/Cooked/MockChain/BlockChainSpec.hs +++ b/tests/Cooked/MockChain/BlockChainSpec.hs @@ -15,7 +15,7 @@ tests = "time handling" [ testProperty "bounds computed by slotToTimeInterval are included in slot" $ \n -> - case runMockChain $ do + case fst $ runMockChain $ do (l, r) <- slotToTimeInterval $ Ledger.Slot n Ledger.Slot nl <- getEnclosingSlot l Ledger.Slot nr <- getEnclosingSlot r @@ -24,7 +24,7 @@ tests = Right ((nl, nr), _) -> nl == n && nr == n, testProperty "bounds computed by slotToTimeInterval are maximal" $ \n -> - case runMockChain $ do + case fst $ runMockChain $ do (l, r) <- slotToTimeInterval $ Ledger.Slot n Ledger.Slot nl <- getEnclosingSlot (l - 1) Ledger.Slot nr <- getEnclosingSlot (r + 1) @@ -32,7 +32,7 @@ tests = Left _err -> False Right ((nl, nr), _) -> nl == n - 1 && nr == n + 1, testProperty "time is always included in enclosing slot" $ - \t -> case runMockChain $ slotToTimeInterval =<< getEnclosingSlot (Api.POSIXTime t) of + \t -> case fst $ runMockChain $ slotToTimeInterval =<< getEnclosingSlot (Api.POSIXTime t) of Left _err -> False Right ((Api.POSIXTime a, Api.POSIXTime b), _) -> a <= t && a <= b ] diff --git a/tests/Cooked/Tweak/CommonSpec.hs b/tests/Cooked/Tweak/CommonSpec.hs index 0be654572..43f0ded04 100644 --- a/tests/Cooked/Tweak/CommonSpec.hs +++ b/tests/Cooked/Tweak/CommonSpec.hs @@ -22,35 +22,38 @@ tests = let skel = mkSkel [123, 234, 345] in [ testCase "return empty list and don't change anything if no applicable modifications" $ -- this one is a regression test [Right ([], skel)] - @=? runTweak - ( overMaybeSelectingTweak - (txSkelOutsL % traversed % txSkelOutValueL) - (const Nothing) - (const True) - ) - skel, + @=? fst + <$> runTweak + ( overMaybeSelectingTweak + (txSkelOutsL % traversed % txSkelOutValueL) + (const Nothing) + (const True) + ) + skel, testCase "select applied modification by index" $ [Right ([Script.lovelaceValueOf 345], mkSkel [123, 234, 789])] - @=? runTweak - ( overMaybeSelectingTweak - (txSkelOutsL % traversed % txSkelOutValueL) - ( \value -> - if value `Script.geq` Script.lovelaceValueOf 200 - then Just $ Script.lovelaceValueOf 789 - else Nothing - ) - (== 1) - ) - skel, + @=? fst + <$> runTweak + ( overMaybeSelectingTweak + (txSkelOutsL % traversed % txSkelOutValueL) + ( \value -> + if value `Script.geq` Script.lovelaceValueOf 200 + then Just $ Script.lovelaceValueOf 789 + else Nothing + ) + (== 1) + ) + skel, testCase "return unmodified foci in the right order" $ [Right ([Script.lovelaceValueOf 123, Script.lovelaceValueOf 345], mkSkel [789, 234, 789])] - @=? runTweak - ( overMaybeSelectingTweak - (txSkelOutsL % traversed % txSkelOutValueL) - (const $ Just $ Script.lovelaceValueOf 789) - (`elem` [0, 2]) - ) - skel + @=? fst + <$> runTweak + ( overMaybeSelectingTweak + (txSkelOutsL % traversed % txSkelOutValueL) + (const $ Just $ Script.lovelaceValueOf 789) + (`elem` [0, 2]) + ) + skel ], testGroup "combineModsTweak" $ let skelIn = mkSkel [0, 0, 0] @@ -87,13 +90,14 @@ tests = skelOut 2 2 1, skelOut 2 2 2 ] - ( runTweak - ( combineModsTweak - (tail . subsequences) - (txSkelOutsL % itraversed % txSkelOutValueL % adaL) - (\i x -> return [(x + 1, i), (x + 2, i)]) - ) - skelIn + ( fst + <$> runTweak + ( combineModsTweak + (tail . subsequences) + (txSkelOutsL % itraversed % txSkelOutValueL % adaL) + (\i x -> return [(x + 1, i), (x + 2, i)]) + ) + skelIn ), testCase "separate modifications" $ assertSameSets @@ -105,13 +109,14 @@ tests = skelOut 0 0 1, skelOut 0 0 2 ] - ( runTweak - ( combineModsTweak - (map (: [])) - (txSkelOutsL % itraversed % txSkelOutValueL % adaL) - (\i x -> return [(x + 1, i), (x + 2, i)]) - ) - skelIn + ( fst + <$> runTweak + ( combineModsTweak + (map (: [])) + (txSkelOutsL % itraversed % txSkelOutValueL % adaL) + (\i x -> return [(x + 1, i), (x + 2, i)]) + ) + skelIn ) ] ] diff --git a/tests/Cooked/Tweak/OutPermutationsSpec.hs b/tests/Cooked/Tweak/OutPermutationsSpec.hs index f8a4e6c41..897a2a88e 100644 --- a/tests/Cooked/Tweak/OutPermutationsSpec.hs +++ b/tests/Cooked/Tweak/OutPermutationsSpec.hs @@ -67,23 +67,23 @@ tests = in [ testCase "KeepIdentity (Just 2)" $ assertSameSets (map (Right . ((),)) [skel a b c, skel b a c]) - (runTweak (allOutPermutsTweak $ KeepIdentity $ Just 2) $ skel a b c), + (fst <$> runTweak (allOutPermutsTweak $ KeepIdentity $ Just 2) (skel a b c)), testCase "KeepIdentity Nothing" $ assertSameSets (map (Right . ((),)) [skel a b c, skel a c b, skel b a c, skel b c a, skel c a b, skel c b a]) - (runTweak (allOutPermutsTweak $ KeepIdentity Nothing) $ skel a b c), + (fst <$> runTweak (allOutPermutsTweak $ KeepIdentity Nothing) (skel a b c)), testCase "OmitIdentity (Just 2)" $ assertSameSets [Right ((), skel b a c)] - (runTweak (allOutPermutsTweak $ OmitIdentity $ Just 2) $ skel a b c), + (fst <$> runTweak (allOutPermutsTweak $ OmitIdentity $ Just 2) (skel a b c)), testCase "OmitIdentity Nothing" $ assertSameSets (map (Right . ((),)) [skel a c b, skel b a c, skel b c a, skel c a b, skel c b a]) - (runTweak (allOutPermutsTweak $ OmitIdentity Nothing) $ skel a b c) + (fst <$> runTweak (allOutPermutsTweak $ OmitIdentity Nothing) (skel a b c)) ], testGroup "tests for a single random outputs permutation:" $ let l = (\i -> paysPK (wallet i) $ Script.lovelaceValueOf 123) <$> [1 .. 5] - runs = txSkelOuts . snd <$> rights ((\i -> runTweak (singleOutPermutTweak i) txSkelTemplate {txSkelOuts = l}) =<< [1 .. 5]) + runs = txSkelOuts . snd <$> rights (fst <$> ((\i -> runTweak (singleOutPermutTweak i) txSkelTemplate {txSkelOuts = l}) =<< [1 .. 5])) in [ testCase "All permutations contain the correct elements" $ mapM_ (assertSameSets l) runs, testCase "All permutations are different from the initial distribution" $ diff --git a/tests/Cooked/Tweak/TamperDatumSpec.hs b/tests/Cooked/Tweak/TamperDatumSpec.hs index b1cd22980..212279468 100644 --- a/tests/Cooked/Tweak/TamperDatumSpec.hs +++ b/tests/Cooked/Tweak/TamperDatumSpec.hs @@ -32,18 +32,19 @@ tamperDatumTweakTest = } ) ] - @=? runTweak - ( tamperDatumTweak @(Integer, Integer) - (\(x, y) -> if y == 77 then Nothing else Just (x, y + 1)) - ) - ( txSkelTemplate - { txSkelOuts = - [ paysPK alice (Script.lovelaceValueOf 789) `withDatum` (52 :: Integer, 53 :: Integer), - paysPK alice (Script.lovelaceValueOf 234) `withDatum` (), - paysPK alice (Script.lovelaceValueOf 567) `withDatum` (76 :: Integer, 77 :: Integer) - ] - } - ) + @=? fst + <$> runTweak + ( tamperDatumTweak @(Integer, Integer) + (\(x, y) -> if y == 77 then Nothing else Just (x, y + 1)) + ) + ( txSkelTemplate + { txSkelOuts = + [ paysPK alice (Script.lovelaceValueOf 789) `withDatum` (52 :: Integer, 53 :: Integer), + paysPK alice (Script.lovelaceValueOf 234) `withDatum` (), + paysPK alice (Script.lovelaceValueOf 567) `withDatum` (76 :: Integer, 77 :: Integer) + ] + } + ) malformDatumTweakTest :: TestTree malformDatumTweakTest = @@ -72,26 +73,27 @@ malformDatumTweakTest = txSkelWithDatums1And4 (52 :: Integer, 53 :: Integer) (84 :: Integer, ()), -- datum1 untouched, datum4 changed txSkelWithDatums1And4 (52 :: Integer, 53 :: Integer) False -- datum1 untouched, datum4 changed ] - ( runTweak - ( malformDatumTweak @(Integer, Integer) - ( \(x, y) -> - if y == 77 - then [] - else - [ PlutusTx.toBuiltinData (x, ()), - PlutusTx.toBuiltinData False - ] - ) - ) - ( txSkelTemplate - { txSkelOuts = - [ paysPK alice (Script.lovelaceValueOf 789) `withDatum` (52 :: Integer, 53 :: Integer), - paysPK alice (Script.lovelaceValueOf 234) `withDatum` (), - paysPK alice (Script.lovelaceValueOf 567) `withDatum` (76 :: Integer, 77 :: Integer), - paysPK alice (Script.lovelaceValueOf 567) `withDatum` (84 :: Integer, 85 :: Integer) - ] - } - ) + ( fst + <$> runTweak + ( malformDatumTweak @(Integer, Integer) + ( \(x, y) -> + if y == 77 + then [] + else + [ PlutusTx.toBuiltinData (x, ()), + PlutusTx.toBuiltinData False + ] + ) + ) + ( txSkelTemplate + { txSkelOuts = + [ paysPK alice (Script.lovelaceValueOf 789) `withDatum` (52 :: Integer, 53 :: Integer), + paysPK alice (Script.lovelaceValueOf 234) `withDatum` (), + paysPK alice (Script.lovelaceValueOf 567) `withDatum` (76 :: Integer, 77 :: Integer), + paysPK alice (Script.lovelaceValueOf 567) `withDatum` (84 :: Integer, 85 :: Integer) + ] + } + ) ) tests :: TestTree diff --git a/tests/Cooked/Tweak/ValidityRangeSpec.hs b/tests/Cooked/Tweak/ValidityRangeSpec.hs index e9ecf0e8d..b94d3c9c7 100644 --- a/tests/Cooked/Tweak/ValidityRangeSpec.hs +++ b/tests/Cooked/Tweak/ValidityRangeSpec.hs @@ -62,7 +62,7 @@ tests :: TestTree tests = testGroup "Validity range tweaks" - [ testCase "Validity inclusion" $ fst . head . rights $ runTweak checkIsValidDuring txSkelTemplate, - testCase "Validity intersection" $ fst . head . rights $ runTweak checkAddToValidityRange txSkelTemplate, - testCase "Time shifting in validity range" $ fst . head . rights $ runTweak checkMoveCurrentSlot txSkelTemplate + [ testCase "Validity inclusion" $ fst . head . rights $ fst <$> runTweak checkIsValidDuring txSkelTemplate, + testCase "Validity intersection" $ fst . head . rights $ fst <$> runTweak checkAddToValidityRange txSkelTemplate, + testCase "Time shifting in validity range" $ fst . head . rights $ fst <$> runTweak checkMoveCurrentSlot txSkelTemplate ] From fc36439a1dde19b24423d840a4370490ae130daf Mon Sep 17 00:00:00 2001 From: mmontin Date: Fri, 5 Jul 2024 00:13:21 +0200 Subject: [PATCH 02/44] more log levels --- src/Cooked/MockChain/BlockChain.hs | 15 +++++++++++- src/Cooked/MockChain/Direct.hs | 4 ++- src/Cooked/Pretty/Cooked.hs | 39 +++++++++++++++--------------- src/Cooked/Pretty/Options.hs | 17 ++++++++++--- 4 files changed, 49 insertions(+), 26 deletions(-) diff --git a/src/Cooked/MockChain/BlockChain.hs b/src/Cooked/MockChain/BlockChain.hs index 5003e9a1c..5aaeddc43 100644 --- a/src/Cooked/MockChain/BlockChain.hs +++ b/src/Cooked/MockChain/BlockChain.hs @@ -52,6 +52,7 @@ module Cooked.MockChain.BlockChain validateTxSkel', txSkelProposalsDeposit, govActionDeposit, + blockChainLogEntryToInt, ) where @@ -78,6 +79,7 @@ import Data.Kind import Data.Map (Map) import Data.Map qualified as Map import Data.Maybe +import Data.Set (Set) import Ledger.Index qualified as Ledger import Ledger.Slot qualified as Ledger import Ledger.Tx qualified as Ledger @@ -99,6 +101,7 @@ data MockChainError where -- | Thrown when not enough collateral are provided. Built upon the fee, the -- percentage and the expected minimal collateral value. MCENoSuitableCollateral :: Integer -> Integer -> Api.Value -> MockChainError + -- | Thrown when an error occured during transaction generation MCEGenerationError :: GenerateTxError -> MockChainError -- | Thrown when an output reference should be in the state of the mockchain, -- but isn't. @@ -113,9 +116,19 @@ data MockChainError where data BlockChainLogEntry where BCLogSubmittedTxSkel :: SkelContext -> TxSkel -> BlockChainLogEntry + BCLogAdjustedTxSkel :: SkelContext -> TxSkel -> Integer -> Set Api.TxOutRef -> Wallet -> BlockChainLogEntry BCLogNewTx :: Api.TxId -> BlockChainLogEntry + BCLogError :: String -> BlockChainLogEntry + BCLogWarning :: String -> BlockChainLogEntry BCLogInfo :: String -> BlockChainLogEntry - BCLogFail :: String -> BlockChainLogEntry + +blockChainLogEntryToInt :: BlockChainLogEntry -> Integer +blockChainLogEntryToInt BCLogSubmittedTxSkel {} = 3 +blockChainLogEntryToInt BCLogAdjustedTxSkel {} = 3 +blockChainLogEntryToInt BCLogNewTx {} = 3 +blockChainLogEntryToInt BCLogError {} = 3 +blockChainLogEntryToInt BCLogWarning {} = 2 +blockChainLogEntryToInt BCLogInfo {} = 1 -- | Contains methods needed for balancing. class (MonadFail m, MonadError MockChainError m) => MonadBlockChainBalancing m where diff --git a/src/Cooked/MockChain/Direct.hs b/src/Cooked/MockChain/Direct.hs index a92ad8f05..a3cf340ca 100644 --- a/src/Cooked/MockChain/Direct.hs +++ b/src/Cooked/MockChain/Direct.hs @@ -148,7 +148,7 @@ instance (Monad m) => Monad (MockChainT m) where instance (Monad m) => MonadFail (MockChainT m) where fail s = do - blockChainLog $ BCLogFail s + blockChainLog $ BCLogError s throwError $ FailWith s instance MonadTrans MockChainT where @@ -351,6 +351,8 @@ instance (Monad m) => MonadBlockChain (MockChainT m) where -- We balance the skeleton when requested in the skeleton option, and get -- the associated fee, collateral inputs and return collateral wallet (skel, fee, collateralIns, returnCollateralWallet) <- balanceTxSkel minAdaSkelUnbal + -- We log the adjusted skeleton + gets mcstToSkelContext >>= \ctx -> blockChainLog $ BCLogAdjustedTxSkel ctx skel fee collateralIns returnCollateralWallet -- We retrieve data that will be used in the transaction generation process: -- datums, validators and various kinds of inputs. This idea is to provide a -- rich-enough context for the transaction generation to succeed. diff --git a/src/Cooked/Pretty/Cooked.hs b/src/Cooked/Pretty/Cooked.hs index 75d5a39ce..7d9af66dd 100644 --- a/src/Cooked/Pretty/Cooked.hs +++ b/src/Cooked/Pretty/Cooked.hs @@ -132,33 +132,32 @@ prettyBlockChainEntries opts entries = (prettyCookedOpt opts <$> entries) instance (Show a) => PrettyCooked (MockChainReturn a UtxoState) where - prettyCookedOpt opts (res, entries) = prettyLogWith $ - case res of - Left err -> "🔴" <+> prettyCookedOpt opts err - Right (a, s) -> "🟢" <+> prettyCookedOpt opts (a, s) - where - prettyLogWith :: DocCooked -> DocCooked - prettyLogWith inner = - case pcOptLog opts of - PCOptLogNone -> inner - PCOptLogNoInfo -> - prettyItemize - "End result:" - "-" - [prettyBlockChainEntries opts (filter (\case BCLogInfo _ -> False; _ -> True) entries), inner] - PCOptLogAll -> - prettyItemize - "End result:" - "-" - [prettyBlockChainEntries opts entries, inner] + prettyCookedOpt opts (res, entries) = + let inner = case res of + Left err -> "🔴" <+> prettyCookedOpt opts err + Right (a, s) -> "🟢" <+> prettyCookedOpt opts (a, s) + in -- We only keep log entries above the required level + case filter ((pcOptLogToInt (pcOptLog opts) <=) . blockChainLogEntryToInt) entries of + [] -> inner + subEntries -> prettyItemize "End result:" "-" [prettyBlockChainEntries opts subEntries, inner] -- | This pretty prints a 'MockChainLog' that usually consists of the list of -- validated or submitted transactions. In the log, we know a transaction has -- been validated if the 'MCLogSubmittedTxSkel' is followed by a 'MCLogNewTx'. instance PrettyCooked BlockChainLogEntry where prettyCookedOpt opts (BCLogSubmittedTxSkel skelContext skel) = "Submitted:" <+> prettyTxSkel opts skelContext skel - prettyCookedOpt _ (BCLogFail msg) = "Fail:" <+> PP.pretty msg + prettyCookedOpt opts (BCLogAdjustedTxSkel skelContext skel fee collaterals returnWallet) = + prettyItemize + "Adjusted:" + "-" + [ "Fee:" <+> prettyCookedOpt opts fee, + "Collaterals:" <+> prettyCookedOpt opts (Set.toList collaterals), + "Return collateral wallet:" <+> prettyCookedOpt opts (walletPKHash returnWallet), + prettyTxSkel opts skelContext skel + ] prettyCookedOpt opts (BCLogNewTx txId) = "New transaction:" <+> prettyCookedOpt opts txId + prettyCookedOpt _ (BCLogError err) = "Fail:" <+> PP.pretty err + prettyCookedOpt _ (BCLogWarning warn) = "Warning:" <+> PP.pretty warn prettyCookedOpt _ (BCLogInfo info) = "Info:" <+> PP.pretty info -- go acc (MCLogFail msg : entries) = diff --git a/src/Cooked/Pretty/Options.hs b/src/Cooked/Pretty/Options.hs index dfc6db987..37b12d9a5 100644 --- a/src/Cooked/Pretty/Options.hs +++ b/src/Cooked/Pretty/Options.hs @@ -7,6 +7,7 @@ module Cooked.Pretty.Options PCOptLog (..), hashNamesFromList, defaultHashNames, + pcOptLogToInt, ) where @@ -53,14 +54,22 @@ instance Default PrettyCookedOpts where data PCOptLog = -- | No logging at all PCOptLogNone - | -- | All logging except for infos, default option - PCOptLogNoInfo - | -- | All logging, for debugging purpose + | -- | Log errors and special log entries + PCOptLogSpecial + | -- | Same as `PCOptLogSpecial`, but with warnings + PCOptLogWarning + | -- | Log everything PCOptLogAll deriving (Eq, Show) instance Default PCOptLog where - def = PCOptLogNoInfo + def = PCOptLogAll + +pcOptLogToInt :: PCOptLog -> Integer +pcOptLogToInt PCOptLogNone = 4 +pcOptLogToInt PCOptLogSpecial = 3 +pcOptLogToInt PCOptLogWarning = 2 +pcOptLogToInt PCOptLogAll = 1 -- | Whether to print transaction outputs references. data PCOptTxOutRefs From 38c0718a0733cb4e58f171bb473d26e35a618636 Mon Sep 17 00:00:00 2001 From: mmontin Date: Fri, 5 Jul 2024 00:57:09 +0200 Subject: [PATCH 03/44] logging removal of unusable balancing utxos --- src/Cooked/MockChain/Balancing.hs | 22 ++++++++++++++++++---- src/Cooked/Pretty/Cooked.hs | 8 ++++---- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/Cooked/MockChain/Balancing.hs b/src/Cooked/MockChain/Balancing.hs index 910422baa..61034bb80 100644 --- a/src/Cooked/MockChain/Balancing.hs +++ b/src/Cooked/MockChain/Balancing.hs @@ -7,6 +7,7 @@ import Cardano.Api.Ledger qualified as Cardano import Cardano.Api.Shelley qualified as Cardano import Cardano.Node.Emulator.Internal.Node.Params qualified as Emulator import Cardano.Node.Emulator.Internal.Node.Validation qualified as Emulator +import Control.Monad import Control.Monad.Except import Cooked.Conversion import Cooked.MockChain.BlockChain @@ -83,10 +84,23 @@ balanceTxSkel skelUnbal@TxSkel {..} = do Just bWallet -> do -- The balancing should be performed. We collect the balancing utxos and -- filter out those already used in the unbalanced skeleton. - (filter ((`notElem` txSkelKnownTxOutRefs skelUnbal) . fst) -> balancingUtxos) <- - runUtxoSearch $ case txOptBalancingUtxos txSkelOpts of - BalancingUtxosFromBalancingWallet -> onlyValueOutputsAtSearch bWallet `filterWithAlways` outputTxOut - BalancingUtxosFromSet utxos -> txOutByRefSearch (Set.toList utxos) `filterWithPure` isPKOutput `filterWithAlways` outputTxOut + candidateBalancingUtxos <- + case txOptBalancingUtxos txSkelOpts of + BalancingUtxosFromBalancingWallet -> runUtxoSearch $ onlyValueOutputsAtSearch bWallet `filterWithAlways` outputTxOut + BalancingUtxosFromSet utxos -> do + bUtxos <- runUtxoSearch $ txOutByRefSearch (Set.toList utxos) + let (pkUtxos, length -> scriptUtxosNb) = partition (isJust . isPKOutput . snd) bUtxos + unless (scriptUtxosNb == 0) $ + blockChainLog $ + BCLogWarning $ + show scriptUtxosNb <> " utxos belonging to scripts have been provided for balancing and are ignored." + return pkUtxos + let (balancingUtxos, length -> knownUtxosNb) = partition ((`notElem` txSkelKnownTxOutRefs skelUnbal) . fst) candidateBalancingUtxos + unless (knownUtxosNb == 0) $ + blockChainLog $ + BCLogWarning $ + show knownUtxosNb <> " utxos already used in the skeleton have been provided for balancing and are ignored." + case txOptFeePolicy txSkelOpts of -- If fees are left for us to compute, we run a dichotomic search. This -- is full auto mode, the most powerful but time-consuming. diff --git a/src/Cooked/Pretty/Cooked.hs b/src/Cooked/Pretty/Cooked.hs index 7d9af66dd..0e1bf244d 100644 --- a/src/Cooked/Pretty/Cooked.hs +++ b/src/Cooked/Pretty/Cooked.hs @@ -150,10 +150,10 @@ instance PrettyCooked BlockChainLogEntry where prettyItemize "Adjusted:" "-" - [ "Fee:" <+> prettyCookedOpt opts fee, - "Collaterals:" <+> prettyCookedOpt opts (Set.toList collaterals), - "Return collateral wallet:" <+> prettyCookedOpt opts (walletPKHash returnWallet), - prettyTxSkel opts skelContext skel + [ prettyTxSkel opts skelContext skel, + "Fee: Lovelace" <+> prettyCookedOpt opts fee, + "Collateral inputs:" <+> prettyCookedOpt opts (Set.toList collaterals), + "Return collateral wallet:" <+> prettyCookedOpt opts (walletPKHash returnWallet) ] prettyCookedOpt opts (BCLogNewTx txId) = "New transaction:" <+> prettyCookedOpt opts txId prettyCookedOpt _ (BCLogError err) = "Fail:" <+> PP.pretty err From b525aeb7f36707e972f50918bcfecc0dfbeb8f71 Mon Sep 17 00:00:00 2001 From: mmontin Date: Mon, 8 Jul 2024 19:37:29 +0200 Subject: [PATCH 04/44] improving logging in balancing --- src/Cooked/MockChain/Balancing.hs | 39 +++++++++++++++++-------------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/src/Cooked/MockChain/Balancing.hs b/src/Cooked/MockChain/Balancing.hs index 61034bb80..abed19761 100644 --- a/src/Cooked/MockChain/Balancing.hs +++ b/src/Cooked/MockChain/Balancing.hs @@ -43,8 +43,8 @@ type BalancingOutputs = [(Api.TxOutRef, Api.TxOut)] -- | This is the main entry point of our balancing mechanism. This function -- takes a skeleton and returns a (possibly) balanced skeleton alongside the -- associated fee, collateral inputs and return collateral wallet. The options --- from the skeleton control whether it should be balanced, and how to compute its --- associated elements. +-- from the skeleton control whether it should be balanced, and how to compute +-- its associated elements. balanceTxSkel :: (MonadBlockChainBalancing m) => TxSkel -> m (TxSkel, Fee, Collaterals, Wallet) balanceTxSkel skelUnbal@TxSkel {..} = do -- We retrieve the possible balancing wallet. Any extra payment will be @@ -82,24 +82,23 @@ balanceTxSkel skelUnbal@TxSkel {..} = do ManualFee fee' -> fee' in (skelUnbal,fee,) <$> collateralInsFromFees fee collateralIns returnCollateralWallet Just bWallet -> do - -- The balancing should be performed. We collect the balancing utxos and - -- filter out those already used in the unbalanced skeleton. - candidateBalancingUtxos <- + -- The balancing should be performed. We collect the candidates balancing + -- utxos based on the associated policy + balancingUtxos <- case txOptBalancingUtxos txSkelOpts of BalancingUtxosFromBalancingWallet -> runUtxoSearch $ onlyValueOutputsAtSearch bWallet `filterWithAlways` outputTxOut - BalancingUtxosFromSet utxos -> do - bUtxos <- runUtxoSearch $ txOutByRefSearch (Set.toList utxos) - let (pkUtxos, length -> scriptUtxosNb) = partition (isJust . isPKOutput . snd) bUtxos - unless (scriptUtxosNb == 0) $ - blockChainLog $ - BCLogWarning $ - show scriptUtxosNb <> " utxos belonging to scripts have been provided for balancing and are ignored." - return pkUtxos - let (balancingUtxos, length -> knownUtxosNb) = partition ((`notElem` txSkelKnownTxOutRefs skelUnbal) . fst) candidateBalancingUtxos - unless (knownUtxosNb == 0) $ - blockChainLog $ - BCLogWarning $ - show knownUtxosNb <> " utxos already used in the skeleton have been provided for balancing and are ignored." + BalancingUtxosFromSet utxos -> + -- We resolve the given set of utxos + runUtxoSearch (txOutByRefSearch (Set.toList utxos)) + >>= + -- We filter out those belonging to scripts, while throwing a + -- warning if any was actually discarded. + filterAndWarn (isJust . isPKOutput . snd) "utxos belonging to scripts have been provided for balancing and are discarded." + -- We filter the candidate utxos by removing those already present in the + -- skeleton, throwing a warning if any was actually discarder + >>= filterAndWarn + ((`notElem` txSkelKnownTxOutRefs skelUnbal) . fst) + "utxos already used in the skeleton have been provided for balancing and are discarded." case txOptFeePolicy txSkelOpts of -- If fees are left for us to compute, we run a dichotomic search. This @@ -114,6 +113,10 @@ balanceTxSkel skelUnbal@TxSkel {..} = do return (attemptedSkel, fee, adjustedCollateralIns) return (txSkelBal, fee, adjustedCollateralIns, returnCollateralWallet) + where + filterAndWarn f s l + | (ok, length -> koLength) <- partition f l = + unless (koLength == 0) (blockChainLog $ BCLogWarning $ show koLength <> " " <> s) >> return ok -- | This computes the minimum and maximum possible fee a transaction can cost -- based on the current protocol parameters From 98b5e810ee5c50f145e995f7d4f1f7104e612fab Mon Sep 17 00:00:00 2001 From: mmontin Date: Wed, 10 Jul 2024 11:55:38 +0200 Subject: [PATCH 05/44] new log version with dedicated constructors --- src/Cooked/MockChain.hs | 2 +- src/Cooked/MockChain/Balancing.hs | 11 +++------ src/Cooked/MockChain/BlockChain.hs | 39 ++++++++++-------------------- src/Cooked/MockChain/Direct.hs | 18 ++++++-------- src/Cooked/MockChain/Staged.hs | 6 ++--- src/Cooked/Pretty/Cooked.hs | 27 ++++++--------------- src/Cooked/Pretty/Options.hs | 29 +++------------------- src/Cooked/Skeleton.hs | 1 + 8 files changed, 41 insertions(+), 92 deletions(-) diff --git a/src/Cooked/MockChain.hs b/src/Cooked/MockChain.hs index b5a7b3193..65903ff85 100644 --- a/src/Cooked/MockChain.hs +++ b/src/Cooked/MockChain.hs @@ -3,7 +3,7 @@ module Cooked.MockChain (module X) where import Cooked.MockChain.Balancing as X -import Cooked.MockChain.BlockChain as X hiding (BlockChainLogEntry) +import Cooked.MockChain.BlockChain as X hiding (MockChainLogEntry) import Cooked.MockChain.Direct as X hiding (MockChainReturn) import Cooked.MockChain.MinAda as X import Cooked.MockChain.Staged as X hiding (StagedMockChain) diff --git a/src/Cooked/MockChain/Balancing.hs b/src/Cooked/MockChain/Balancing.hs index abed19761..9765fa442 100644 --- a/src/Cooked/MockChain/Balancing.hs +++ b/src/Cooked/MockChain/Balancing.hs @@ -90,15 +90,12 @@ balanceTxSkel skelUnbal@TxSkel {..} = do BalancingUtxosFromSet utxos -> -- We resolve the given set of utxos runUtxoSearch (txOutByRefSearch (Set.toList utxos)) - >>= -- We filter out those belonging to scripts, while throwing a -- warning if any was actually discarded. - filterAndWarn (isJust . isPKOutput . snd) "utxos belonging to scripts have been provided for balancing and are discarded." + >>= filterAndWarn (isJust . isPKOutput . snd) "They belong to scripts." -- We filter the candidate utxos by removing those already present in the -- skeleton, throwing a warning if any was actually discarder - >>= filterAndWarn - ((`notElem` txSkelKnownTxOutRefs skelUnbal) . fst) - "utxos already used in the skeleton have been provided for balancing and are discarded." + >>= filterAndWarn ((`notElem` txSkelKnownTxOutRefs skelUnbal) . fst) "They are already used in the skeleton." case txOptFeePolicy txSkelOpts of -- If fees are left for us to compute, we run a dichotomic search. This @@ -115,8 +112,8 @@ balanceTxSkel skelUnbal@TxSkel {..} = do return (txSkelBal, fee, adjustedCollateralIns, returnCollateralWallet) where filterAndWarn f s l - | (ok, length -> koLength) <- partition f l = - unless (koLength == 0) (blockChainLog $ BCLogWarning $ show koLength <> " " <> s) >> return ok + | (ok, toInteger . length -> koLength) <- partition f l = + unless (koLength == 0) (publish $ MCLogDiscardedUtxos koLength s) >> return ok -- | This computes the minimum and maximum possible fee a transaction can cost -- based on the current protocol parameters diff --git a/src/Cooked/MockChain/BlockChain.hs b/src/Cooked/MockChain/BlockChain.hs index 5aaeddc43..2d8e7f02b 100644 --- a/src/Cooked/MockChain/BlockChain.hs +++ b/src/Cooked/MockChain/BlockChain.hs @@ -15,7 +15,7 @@ -- from the core definition of our blockchain. module Cooked.MockChain.BlockChain ( MockChainError (..), - BlockChainLogEntry (..), + MockChainLogEntry (..), MonadBlockChainBalancing (..), MonadBlockChainWithoutValidation (..), MonadBlockChain (..), @@ -52,7 +52,6 @@ module Cooked.MockChain.BlockChain validateTxSkel', txSkelProposalsDeposit, govActionDeposit, - blockChainLogEntryToInt, ) where @@ -89,12 +88,11 @@ import Optics.Core import Plutus.Script.Utils.Scripts qualified as Script import PlutusLedgerApi.V3 qualified as Api --- * BlockChain monad +-- * MockChain errors -- | The errors that can be produced by the 'MockChainT' monad data MockChainError where - -- FIXME, maybe the validation phase can be deduced from the nature of the - -- error + -- | Validation errors, either in Phase 1 or Phase 2 MCEValidationError :: Ledger.ValidationPhase -> Ledger.ValidationError -> MockChainError -- | Thrown when the balancing wallet does not have enough funds MCEUnbalanceable :: Wallet -> Api.Value -> TxSkel -> MockChainError @@ -103,8 +101,7 @@ data MockChainError where MCENoSuitableCollateral :: Integer -> Integer -> Api.Value -> MockChainError -- | Thrown when an error occured during transaction generation MCEGenerationError :: GenerateTxError -> MockChainError - -- | Thrown when an output reference should be in the state of the mockchain, - -- but isn't. + -- | Thrown when an output reference is missing from the mockchain state MCEUnknownOutRefError :: String -> Api.TxOutRef -> MockChainError -- | Same as 'MCEUnknownOutRefError' for validators. MCEUnknownValidator :: String -> Script.ValidatorHash -> MockChainError @@ -114,25 +111,15 @@ data MockChainError where FailWith :: String -> MockChainError deriving (Show, Eq) -data BlockChainLogEntry where - BCLogSubmittedTxSkel :: SkelContext -> TxSkel -> BlockChainLogEntry - BCLogAdjustedTxSkel :: SkelContext -> TxSkel -> Integer -> Set Api.TxOutRef -> Wallet -> BlockChainLogEntry - BCLogNewTx :: Api.TxId -> BlockChainLogEntry - BCLogError :: String -> BlockChainLogEntry - BCLogWarning :: String -> BlockChainLogEntry - BCLogInfo :: String -> BlockChainLogEntry - -blockChainLogEntryToInt :: BlockChainLogEntry -> Integer -blockChainLogEntryToInt BCLogSubmittedTxSkel {} = 3 -blockChainLogEntryToInt BCLogAdjustedTxSkel {} = 3 -blockChainLogEntryToInt BCLogNewTx {} = 3 -blockChainLogEntryToInt BCLogError {} = 3 -blockChainLogEntryToInt BCLogWarning {} = 2 -blockChainLogEntryToInt BCLogInfo {} = 1 +data MockChainLogEntry where + MCLogSubmittedTxSkel :: SkelContext -> TxSkel -> MockChainLogEntry + MCLogAdjustedTxSkel :: SkelContext -> TxSkel -> Integer -> Set Api.TxOutRef -> Wallet -> MockChainLogEntry + MCLogNewTx :: Api.TxId -> MockChainLogEntry + MCLogDiscardedUtxos :: Integer -> String -> MockChainLogEntry -- | Contains methods needed for balancing. class (MonadFail m, MonadError MockChainError m) => MonadBlockChainBalancing m where - -- | Returns the parameters of the chain. + -- | Returns the emulator parameters, including protocol parameters getParams :: m Emulator.Params -- | Returns a list of all UTxOs at a certain address. @@ -149,7 +136,7 @@ class (MonadFail m, MonadError MockChainError m) => MonadBlockChainBalancing m w txOutByRefLedger :: Api.TxOutRef -> m (Maybe Ledger.TxOut) -- | Logs an event that occured during a BlockChain run - blockChainLog :: BlockChainLogEntry -> m () + publish :: MockChainLogEntry -> m () class (MonadBlockChainBalancing m) => MonadBlockChainWithoutValidation m where -- | Returns a list of all currently known outputs. @@ -516,7 +503,7 @@ instance (MonadTrans t, MonadBlockChainBalancing m, Monad (t m), MonadError Mock utxosAtLedger = lift . utxosAtLedger txOutByRefLedger = lift . txOutByRefLedger datumFromHash = lift . datumFromHash - blockChainLog = lift . blockChainLog + publish = lift . publish instance (MonadTrans t, MonadBlockChainWithoutValidation m, Monad (t m), MonadError MockChainError (AsTrans t m)) => MonadBlockChainWithoutValidation (AsTrans t m) where allUtxosLedger = lift allUtxosLedger @@ -560,7 +547,7 @@ instance (MonadBlockChainBalancing m) => MonadBlockChainBalancing (ListT m) wher utxosAtLedger = lift . utxosAtLedger txOutByRefLedger = lift . txOutByRefLedger datumFromHash = lift . datumFromHash - blockChainLog = lift . blockChainLog + publish = lift . publish instance (MonadBlockChainWithoutValidation m) => MonadBlockChainWithoutValidation (ListT m) where allUtxosLedger = lift allUtxosLedger diff --git a/src/Cooked/MockChain/Direct.hs b/src/Cooked/MockChain/Direct.hs index a3cf340ca..01c08fef6 100644 --- a/src/Cooked/MockChain/Direct.hs +++ b/src/Cooked/MockChain/Direct.hs @@ -136,8 +136,8 @@ instance Eq MockChainSt where ] newtype MockChainT m a = MockChainT - {unMockChain :: (StateT MockChainSt (ExceptT MockChainError (WriterT [BlockChainLogEntry] m))) a} - deriving newtype (Functor, Applicative, MonadState MockChainSt, MonadError MockChainError, MonadWriter [BlockChainLogEntry]) + {unMockChain :: (StateT MockChainSt (ExceptT MockChainError (WriterT [MockChainLogEntry] m))) a} + deriving newtype (Functor, Applicative, MonadState MockChainSt, MonadError MockChainError, MonadWriter [MockChainLogEntry]) type MockChain = MockChainT Identity @@ -147,9 +147,7 @@ instance (Monad m) => Monad (MockChainT m) where MockChainT x >>= f = MockChainT $ x >>= unMockChain . f instance (Monad m) => MonadFail (MockChainT m) where - fail s = do - blockChainLog $ BCLogError s - throwError $ FailWith s + fail = throwError . FailWith instance MonadTrans MockChainT where lift = MockChainT . lift . lift . lift @@ -170,7 +168,7 @@ combineMockChainT f ma mb = MockChainT $ resB = runWriterT $ runExceptT $ runStateT (unMockChain mb) s in ExceptT $ WriterT $ f resA resB -type MockChainReturn a b = (Either MockChainError (a, b), [BlockChainLogEntry]) +type MockChainReturn a b = (Either MockChainError (a, b), [MockChainLogEntry]) mapMockChainT :: (m (MockChainReturn a MockChainSt) -> n (MockChainReturn b MockChainSt)) -> @@ -327,7 +325,7 @@ instance (Monad m) => MonadBlockChainBalancing (MockChainT m) where txOutByRefLedger outref = gets $ Map.lookup outref . getIndex . mcstIndex datumFromHash datumHash = (txSkelOutUntypedDatum <=< Just . fst <=< Map.lookup datumHash) <$> gets mcstDatums utxosAtLedger addr = filter ((addr ==) . outputAddress . txOutV2FromLedger . snd) <$> allUtxosLedger - blockChainLog l = tell [l] + publish l = tell [l] instance (Monad m) => MonadBlockChainWithoutValidation (MockChainT m) where allUtxosLedger = gets $ Map.toList . getIndex . mcstIndex @@ -338,7 +336,7 @@ instance (Monad m) => MonadBlockChainWithoutValidation (MockChainT m) where instance (Monad m) => MonadBlockChain (MockChainT m) where validateTxSkel skelUnbal = do -- We log the submitted skeleton - gets mcstToSkelContext >>= blockChainLog . (`BCLogSubmittedTxSkel` skelUnbal) + gets mcstToSkelContext >>= publish . (`MCLogSubmittedTxSkel` skelUnbal) -- We retrieve the current parameters oldParams <- getParams -- We compute the optionally modified parameters @@ -352,7 +350,7 @@ instance (Monad m) => MonadBlockChain (MockChainT m) where -- the associated fee, collateral inputs and return collateral wallet (skel, fee, collateralIns, returnCollateralWallet) <- balanceTxSkel minAdaSkelUnbal -- We log the adjusted skeleton - gets mcstToSkelContext >>= \ctx -> blockChainLog $ BCLogAdjustedTxSkel ctx skel fee collateralIns returnCollateralWallet + gets mcstToSkelContext >>= \ctx -> publish $ MCLogAdjustedTxSkel ctx skel fee collateralIns returnCollateralWallet -- We retrieve data that will be used in the transaction generation process: -- datums, validators and various kinds of inputs. This idea is to provide a -- rich-enough context for the transaction generation to succeed. @@ -403,7 +401,7 @@ instance (Monad m) => MonadBlockChain (MockChainT m) where -- We return the parameters to their original state setParams oldParams -- We log the validated transaction - blockChainLog $ BCLogNewTx (Ledger.fromCardanoTxId $ Ledger.getCardanoTxId cardanoTx) + publish $ MCLogNewTx (Ledger.fromCardanoTxId $ Ledger.getCardanoTxId cardanoTx) -- We return the validated transaction return cardanoTx where diff --git a/src/Cooked/MockChain/Staged.hs b/src/Cooked/MockChain/Staged.hs index 9f14917f7..17016929f 100644 --- a/src/Cooked/MockChain/Staged.hs +++ b/src/Cooked/MockChain/Staged.hs @@ -79,7 +79,7 @@ data MockChainBuiltin a where AllUtxosLedger :: MockChainBuiltin [(Api.TxOutRef, Ledger.TxOut)] UtxosAtLedger :: Api.Address -> MockChainBuiltin [(Api.TxOutRef, Ledger.TxOut)] ValidatorFromHash :: Script.ValidatorHash -> MockChainBuiltin (Maybe (Script.Versioned Script.Validator)) - BlockChainLog :: BlockChainLogEntry -> MockChainBuiltin () + Publish :: MockChainLogEntry -> MockChainBuiltin () -- | The empty set of traces Empty :: MockChainBuiltin a -- | The union of two sets of traces @@ -136,7 +136,7 @@ instance InterpLtl (UntypedTweak InterpMockChain) MockChainBuiltin InterpMockCha interpBuiltin (Fail msg) = fail msg interpBuiltin (ThrowError err) = throwError err interpBuiltin (CatchError act handler) = catchError (interpLtl act) (interpLtl . handler) - interpBuiltin (BlockChainLog entry) = blockChainLog entry + interpBuiltin (Publish entry) = publish entry -- ** Helpers to run tweaks for use in tests for tweaks @@ -201,7 +201,7 @@ instance MonadBlockChainBalancing StagedMockChain where txOutByRefLedger = singletonBuiltin . TxOutByRefLedger utxosAtLedger = singletonBuiltin . UtxosAtLedger validatorFromHash = singletonBuiltin . ValidatorFromHash - blockChainLog = singletonBuiltin . BlockChainLog + publish = singletonBuiltin . Publish instance MonadBlockChainWithoutValidation StagedMockChain where allUtxosLedger = singletonBuiltin AllUtxosLedger diff --git a/src/Cooked/Pretty/Cooked.hs b/src/Cooked/Pretty/Cooked.hs index 0e1bf244d..6cee78e6c 100644 --- a/src/Cooked/Pretty/Cooked.hs +++ b/src/Cooked/Pretty/Cooked.hs @@ -124,29 +124,20 @@ instance (Show a) => PrettyCooked (a, UtxoState) where "-" ["Returns:" <+> PP.viaShow res, prettyCookedOpt opts state] -prettyBlockChainEntries :: PrettyCookedOpts -> [BlockChainLogEntry] -> DocCooked -prettyBlockChainEntries opts entries = - prettyItemize - "MockChain run:" - "-" - (prettyCookedOpt opts <$> entries) - instance (Show a) => PrettyCooked (MockChainReturn a UtxoState) where prettyCookedOpt opts (res, entries) = - let inner = case res of + let mcLog = prettyItemize "MockChain run log:" "-" (prettyCookedOpt opts <$> entries) + mcEndResult = case res of Left err -> "🔴" <+> prettyCookedOpt opts err Right (a, s) -> "🟢" <+> prettyCookedOpt opts (a, s) - in -- We only keep log entries above the required level - case filter ((pcOptLogToInt (pcOptLog opts) <=) . blockChainLogEntryToInt) entries of - [] -> inner - subEntries -> prettyItemize "End result:" "-" [prettyBlockChainEntries opts subEntries, inner] + in prettyItemizeNoTitle "-" $ if pcOptLog opts then [mcLog, mcEndResult] else [mcEndResult] -- | This pretty prints a 'MockChainLog' that usually consists of the list of -- validated or submitted transactions. In the log, we know a transaction has -- been validated if the 'MCLogSubmittedTxSkel' is followed by a 'MCLogNewTx'. -instance PrettyCooked BlockChainLogEntry where - prettyCookedOpt opts (BCLogSubmittedTxSkel skelContext skel) = "Submitted:" <+> prettyTxSkel opts skelContext skel - prettyCookedOpt opts (BCLogAdjustedTxSkel skelContext skel fee collaterals returnWallet) = +instance PrettyCooked MockChainLogEntry where + prettyCookedOpt opts (MCLogSubmittedTxSkel skelContext skel) = "Submitted:" <+> prettyTxSkel opts skelContext skel + prettyCookedOpt opts (MCLogAdjustedTxSkel skelContext skel fee collaterals returnWallet) = prettyItemize "Adjusted:" "-" @@ -155,10 +146,8 @@ instance PrettyCooked BlockChainLogEntry where "Collateral inputs:" <+> prettyCookedOpt opts (Set.toList collaterals), "Return collateral wallet:" <+> prettyCookedOpt opts (walletPKHash returnWallet) ] - prettyCookedOpt opts (BCLogNewTx txId) = "New transaction:" <+> prettyCookedOpt opts txId - prettyCookedOpt _ (BCLogError err) = "Fail:" <+> PP.pretty err - prettyCookedOpt _ (BCLogWarning warn) = "Warning:" <+> PP.pretty warn - prettyCookedOpt _ (BCLogInfo info) = "Info:" <+> PP.pretty info + prettyCookedOpt opts (MCLogNewTx txId) = "New transaction:" <+> prettyCookedOpt opts txId + prettyCookedOpt opts (MCLogDiscardedUtxos n s) = prettyCookedOpt opts n <+> "balancing utxos were discarded:" <+> PP.pretty s -- go acc (MCLogFail msg : entries) = -- go ("Fail:" <+> PP.pretty msg : acc) entries diff --git a/src/Cooked/Pretty/Options.hs b/src/Cooked/Pretty/Options.hs index 37b12d9a5..8e630ed92 100644 --- a/src/Cooked/Pretty/Options.hs +++ b/src/Cooked/Pretty/Options.hs @@ -4,10 +4,8 @@ module Cooked.Pretty.Options ( PrettyCookedOpts (..), PrettyCookedHashOpts (..), PCOptTxOutRefs (..), - PCOptLog (..), hashNamesFromList, defaultHashNames, - pcOptLogToInt, ) where @@ -34,8 +32,8 @@ data PrettyCookedOpts = PrettyCookedOpts pcOptNumericUnderscores :: Bool, -- | Options related to printing hashes pcOptHashes :: PrettyCookedHashOpts, - -- | What kind of log to print - pcOptLog :: PCOptLog + -- | Whether to display the log + pcOptLog :: Bool } deriving (Eq, Show) @@ -47,30 +45,9 @@ instance Default PrettyCookedOpts where pcOptPrintDefaultTxOpts = False, pcOptNumericUnderscores = True, pcOptHashes = def, - pcOptLog = def + pcOptLog = True } --- | What log to display -data PCOptLog - = -- | No logging at all - PCOptLogNone - | -- | Log errors and special log entries - PCOptLogSpecial - | -- | Same as `PCOptLogSpecial`, but with warnings - PCOptLogWarning - | -- | Log everything - PCOptLogAll - deriving (Eq, Show) - -instance Default PCOptLog where - def = PCOptLogAll - -pcOptLogToInt :: PCOptLog -> Integer -pcOptLogToInt PCOptLogNone = 4 -pcOptLogToInt PCOptLogSpecial = 3 -pcOptLogToInt PCOptLogWarning = 2 -pcOptLogToInt PCOptLogAll = 1 - -- | Whether to print transaction outputs references. data PCOptTxOutRefs = -- | Hide them diff --git a/src/Cooked/Skeleton.hs b/src/Cooked/Skeleton.hs index 23e415a40..b504a7ab6 100644 --- a/src/Cooked/Skeleton.hs +++ b/src/Cooked/Skeleton.hs @@ -1051,6 +1051,7 @@ data SkelContext = SkelContext { skelContextTxOuts :: Map Api.TxOutRef Api.TxOut, skelContextTxSkelOutDatums :: Map Api.DatumHash TxSkelOutDatum } + deriving (Show, Eq) -- | Returns the full value contained in the skeleton outputs txSkelValueInOutputs :: TxSkel -> Api.Value From 47f54a506b5daee738d6706364b0f0878df6463e Mon Sep 17 00:00:00 2001 From: mmontin Date: Wed, 10 Jul 2024 12:02:43 +0200 Subject: [PATCH 06/44] changing item --- src/Cooked/Pretty/Cooked.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cooked/Pretty/Cooked.hs b/src/Cooked/Pretty/Cooked.hs index 6cee78e6c..4ca784258 100644 --- a/src/Cooked/Pretty/Cooked.hs +++ b/src/Cooked/Pretty/Cooked.hs @@ -130,7 +130,7 @@ instance (Show a) => PrettyCooked (MockChainReturn a UtxoState) where mcEndResult = case res of Left err -> "🔴" <+> prettyCookedOpt opts err Right (a, s) -> "🟢" <+> prettyCookedOpt opts (a, s) - in prettyItemizeNoTitle "-" $ if pcOptLog opts then [mcLog, mcEndResult] else [mcEndResult] + in prettyItemizeNoTitle "*" $ if pcOptLog opts then [mcLog, mcEndResult] else [mcEndResult] -- | This pretty prints a 'MockChainLog' that usually consists of the list of -- validated or submitted transactions. In the log, we know a transaction has From 2ff0e461553a4bedd99d869bda4eb2dfcef066b1 Mon Sep 17 00:00:00 2001 From: mmontin Date: Wed, 10 Jul 2024 15:38:43 +0200 Subject: [PATCH 07/44] integrating comments, adding comments and more readable bullets --- src/Cooked/MockChain/BlockChain.hs | 12 ++++++++++++ src/Cooked/Pretty/Cooked.hs | 20 ++++++-------------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/Cooked/MockChain/BlockChain.hs b/src/Cooked/MockChain/BlockChain.hs index 2d8e7f02b..01828985d 100644 --- a/src/Cooked/MockChain/BlockChain.hs +++ b/src/Cooked/MockChain/BlockChain.hs @@ -111,10 +111,22 @@ data MockChainError where FailWith :: String -> MockChainError deriving (Show, Eq) +-- * MockChain logs + +-- | This represents the specific events that should be logged when processing +-- transactions. If a new kind of event arises, then a new constructor should be +-- provided here. data MockChainLogEntry where + -- | Logging a Skeleton as it is submitted by the user. MCLogSubmittedTxSkel :: SkelContext -> TxSkel -> MockChainLogEntry + -- | Logging a Skeleton as it has been adjusted by the balancing mechanism, + -- alongside fee, collateral utxos and return collateral wallet. MCLogAdjustedTxSkel :: SkelContext -> TxSkel -> Integer -> Set Api.TxOutRef -> Wallet -> MockChainLogEntry + -- | Logging the appearance of a new transaction, after a skeleton has been + -- successfully sent for validation. MCLogNewTx :: Api.TxId -> MockChainLogEntry + -- | Logging the fact that utxos provided by the user for balancing have to be + -- discarded for a specific reason. MCLogDiscardedUtxos :: Integer -> String -> MockChainLogEntry -- | Contains methods needed for balancing. diff --git a/src/Cooked/Pretty/Cooked.hs b/src/Cooked/Pretty/Cooked.hs index 4ca784258..b2d3d4e63 100644 --- a/src/Cooked/Pretty/Cooked.hs +++ b/src/Cooked/Pretty/Cooked.hs @@ -126,41 +126,33 @@ instance (Show a) => PrettyCooked (a, UtxoState) where instance (Show a) => PrettyCooked (MockChainReturn a UtxoState) where prettyCookedOpt opts (res, entries) = - let mcLog = prettyItemize "MockChain run log:" "-" (prettyCookedOpt opts <$> entries) + let mcLog = "📘" <+> prettyItemize "MockChain run log:" "⁍" (prettyCookedOpt opts <$> entries) mcEndResult = case res of Left err -> "🔴" <+> prettyCookedOpt opts err Right (a, s) -> "🟢" <+> prettyCookedOpt opts (a, s) - in prettyItemizeNoTitle "*" $ if pcOptLog opts then [mcLog, mcEndResult] else [mcEndResult] + in PP.vsep $ if pcOptLog opts then [mcLog, mcEndResult] else [mcEndResult] -- | This pretty prints a 'MockChainLog' that usually consists of the list of -- validated or submitted transactions. In the log, we know a transaction has -- been validated if the 'MCLogSubmittedTxSkel' is followed by a 'MCLogNewTx'. instance PrettyCooked MockChainLogEntry where - prettyCookedOpt opts (MCLogSubmittedTxSkel skelContext skel) = "Submitted:" <+> prettyTxSkel opts skelContext skel + prettyCookedOpt opts (MCLogSubmittedTxSkel skelContext skel) = prettyItemize "Submitted:" "-" [prettyTxSkel opts skelContext skel] prettyCookedOpt opts (MCLogAdjustedTxSkel skelContext skel fee collaterals returnWallet) = prettyItemize "Adjusted:" "-" [ prettyTxSkel opts skelContext skel, "Fee: Lovelace" <+> prettyCookedOpt opts fee, - "Collateral inputs:" <+> prettyCookedOpt opts (Set.toList collaterals), - "Return collateral wallet:" <+> prettyCookedOpt opts (walletPKHash returnWallet) + prettyItemize "Collateral inputs:" "-" (prettyCookedOpt opts <$> Set.toList collaterals), + "Return collateral target:" <+> prettyCookedOpt opts (walletPKHash returnWallet) ] prettyCookedOpt opts (MCLogNewTx txId) = "New transaction:" <+> prettyCookedOpt opts txId prettyCookedOpt opts (MCLogDiscardedUtxos n s) = prettyCookedOpt opts n <+> "balancing utxos were discarded:" <+> PP.pretty s --- go acc (MCLogFail msg : entries) = --- go ("Fail:" <+> PP.pretty msg : acc) entries --- -- This case is not supposed to occur because it should follow a --- -- 'MCLogSubmittedTxSkel' --- go acc (MCLogNewTx txId : entries) = --- go ("New transaction:" <+> prettyCookedOpt opts txId : acc) entries --- go acc [] = reverse acc - prettyTxSkel :: PrettyCookedOpts -> SkelContext -> TxSkel -> DocCooked prettyTxSkel opts skelContext (TxSkel lbl txopts mints signers validityRange ins insReference outs proposals) = prettyItemize - "transaction skeleton:" + "Transaction skeleton:" "-" ( catMaybes [ prettyItemizeNonEmpty "Labels:" "-" (prettyCookedOpt opts <$> Set.toList lbl), From 73d8ab22a08fc850eec6c45aafb21a65dcae00f2 Mon Sep 17 00:00:00 2001 From: mmontin Date: Wed, 10 Jul 2024 16:27:17 +0200 Subject: [PATCH 08/44] fixing the bug where collateral inputs were not resolved --- src/Cooked/Pretty/Cooked.hs | 46 +++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/src/Cooked/Pretty/Cooked.hs b/src/Cooked/Pretty/Cooked.hs index b2d3d4e63..2c5f43536 100644 --- a/src/Cooked/Pretty/Cooked.hs +++ b/src/Cooked/Pretty/Cooked.hs @@ -143,7 +143,7 @@ instance PrettyCooked MockChainLogEntry where "-" [ prettyTxSkel opts skelContext skel, "Fee: Lovelace" <+> prettyCookedOpt opts fee, - prettyItemize "Collateral inputs:" "-" (prettyCookedOpt opts <$> Set.toList collaterals), + prettyItemize "Collateral inputs:" "-" (prettyCollateralIn opts skelContext <$> Set.toList collaterals), "Return collateral target:" <+> prettyCookedOpt opts (walletPKHash returnWallet) ] prettyCookedOpt opts (MCLogNewTx txId) = "New transaction:" <+> prettyCookedOpt opts txId @@ -348,29 +348,35 @@ prettyTxSkelOut opts (Pays output) = prettyTxSkelOutDatumMaybe :: PrettyCookedOpts -> TxSkelOutDatum -> Maybe DocCooked prettyTxSkelOutDatumMaybe _ TxSkelOutNoDatum = Nothing prettyTxSkelOutDatumMaybe opts txSkelOutDatum@(TxSkelOutInlineDatum _) = - Just $ - "Datum (inlined):" - <+> PP.align (prettyCookedOpt opts txSkelOutDatum) + Just $ "Datum (inlined):" <+> PP.align (prettyCookedOpt opts txSkelOutDatum) prettyTxSkelOutDatumMaybe opts txSkelOutDatum = - Just $ - "Datum (hashed):" - <+> PP.align (prettyCookedOpt opts txSkelOutDatum) + Just $ "Datum (hashed):" <+> PP.align (prettyCookedOpt opts txSkelOutDatum) + +prettyTxOutRefM :: PrettyCookedOpts -> SkelContext -> Api.TxOutRef -> Maybe (DocCooked, DocCooked, [DocCooked]) +prettyTxOutRefM opts skelContext txOutRef = + ( \(output, txSkelOutDatum) -> + ( prettyCookedOpt opts (outputAddress output), + prettyCookedOpt opts (outputValue output), + catMaybes + [ prettyTxSkelOutDatumMaybe opts txSkelOutDatum, + getReferenceScriptDoc opts output + ] + ) + ) + <$> lookupOutput skelContext txOutRef + +prettyCollateralIn :: PrettyCookedOpts -> SkelContext -> Api.TxOutRef -> DocCooked +prettyCollateralIn opts skelContext txOutRef = + case prettyTxOutRefM opts skelContext txOutRef of + Nothing -> prettyCookedOpt opts txOutRef <+> "(non resolved)" + Just (addressDoc, valueDoc, otherDocs) -> prettyItemize ("Belonging to" <+> addressDoc) "-" (valueDoc : otherDocs) prettyTxSkelIn :: PrettyCookedOpts -> SkelContext -> (Api.TxOutRef, TxSkelRedeemer) -> DocCooked -prettyTxSkelIn opts skelContext (txOutRef, txSkelRedeemer) = do - case lookupOutput skelContext txOutRef of +prettyTxSkelIn opts skelContext (txOutRef, txSkelRedeemer) = + case prettyTxOutRefM opts skelContext txOutRef of Nothing -> "Spends" <+> prettyCookedOpt opts txOutRef <+> "(non resolved)" - Just (output, txSkelOutDatum) -> - prettyItemize - ("Spends from" <+> prettyCookedOpt opts (outputAddress output)) - "-" - ( prettyCookedOpt opts (outputValue output) - : prettyTxSkelRedeemer opts txSkelRedeemer - <> catMaybes - [ prettyTxSkelOutDatumMaybe opts txSkelOutDatum, - getReferenceScriptDoc opts output - ] - ) + Just (addressDoc, valueDoc, otherDocs) -> + prettyItemize ("Spends from" <+> addressDoc) "-" (valueDoc : prettyTxSkelRedeemer opts txSkelRedeemer <> otherDocs) prettyTxSkelInReference :: PrettyCookedOpts -> SkelContext -> Api.TxOutRef -> Maybe DocCooked prettyTxSkelInReference opts skelContext txOutRef = do From a94db0a8f72f2248abb7d2737c87d0bbf311f396 Mon Sep 17 00:00:00 2001 From: mmontin Date: Wed, 10 Jul 2024 16:32:39 +0200 Subject: [PATCH 09/44] CHANGELOG.md --- CHANGELOG.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97ad33523..f1a349d8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,13 @@ - `toInitDistWithMinAda` and `unsafeToInitDistWithMinAda` to ensure the initial distribution only provides outputs with the required minimal ada based on default parameters. - +- PrettyCooked option `pcOptLog`, which is a boolean, to turn on or off the log + display in the pretty printer. The default value is `True`. + ### Removed - `positivePart` and `negativePart` in `ValueUtils.hs`. Replaced by `Api.split`. +- Redundant logging of errors in mockchain runs. ### Changed @@ -22,6 +25,11 @@ constructors: `txSkelSomeRedeemer`, `txSkelEmptyRedeemer`, `txSkelSomeRedeemerAndReferenceScript`, `txSkelEmptyRedeemerAndReferenceScript`. +- Logging has been reworked: + * it is no longer limited to `StagedMockChain` runs + * it is now a component of `MonadBlockChainBalancing` + * it can be turned on/off in skeleton options + * it now displays the discarding of utxos during balancing. ### Fixed From f2cfce6f98815fc85f1c06f2df0f6fe317654b81 Mon Sep 17 00:00:00 2001 From: mmontin Date: Sun, 28 Jul 2024 02:50:08 +0200 Subject: [PATCH 10/44] integrating review comments --- CHANGELOG.md | 3 ++- src/Cooked/MockChain.hs | 2 +- src/Cooked/Pretty/Cooked.hs | 16 ++++++++++------ src/Cooked/Pretty/Options.hs | 4 ++-- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1a349d8d..c4189d309 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,8 +28,9 @@ - Logging has been reworked: * it is no longer limited to `StagedMockChain` runs * it is now a component of `MonadBlockChainBalancing` - * it can be turned on/off in skeleton options + * it can be turned on/off in pretty-printing options * it now displays the discarding of utxos during balancing. + * it is not visible from outside of `cooked-validators` ### Fixed diff --git a/src/Cooked/MockChain.hs b/src/Cooked/MockChain.hs index 65903ff85..a980de21e 100644 --- a/src/Cooked/MockChain.hs +++ b/src/Cooked/MockChain.hs @@ -3,7 +3,7 @@ module Cooked.MockChain (module X) where import Cooked.MockChain.Balancing as X -import Cooked.MockChain.BlockChain as X hiding (MockChainLogEntry) +import Cooked.MockChain.BlockChain as X hiding (MockChainLogEntry, publish) import Cooked.MockChain.Direct as X hiding (MockChainReturn) import Cooked.MockChain.MinAda as X import Cooked.MockChain.Staged as X hiding (StagedMockChain) diff --git a/src/Cooked/Pretty/Cooked.hs b/src/Cooked/Pretty/Cooked.hs index e92c2d25c..36ec31017 100644 --- a/src/Cooked/Pretty/Cooked.hs +++ b/src/Cooked/Pretty/Cooked.hs @@ -49,6 +49,7 @@ import Data.Set qualified as Set import Optics.Core import Plutus.Script.Utils.Ada qualified as Script import Plutus.Script.Utils.Scripts qualified as Script +import Plutus.Script.Utils.Value qualified as Script import PlutusLedgerApi.V3 qualified as Api import Prettyprinter ((<+>)) import Prettyprinter qualified as PP @@ -129,7 +130,7 @@ instance (Show a) => PrettyCooked (MockChainReturn a UtxoState) where mcEndResult = case res of Left err -> "🔴" <+> prettyCookedOpt opts err Right (a, s) -> "🟢" <+> prettyCookedOpt opts (a, s) - in PP.vsep $ if pcOptLog opts then [mcLog, mcEndResult] else [mcEndResult] + in PP.vsep $ if pcOptPrintLog opts then [mcLog, mcEndResult] else [mcEndResult] -- | This pretty prints a 'MockChainLog' that usually consists of the list of -- validated or submitted transactions. In the log, we know a transaction has @@ -141,7 +142,7 @@ instance PrettyCooked MockChainLogEntry where "Adjusted:" "-" [ prettyTxSkel opts skelContext skel, - "Fee: Lovelace" <+> prettyCookedOpt opts fee, + "Fee:" <+> prettyCookedOpt opts (Script.lovelace fee), prettyItemize "Collateral inputs:" "-" (prettyCollateralIn opts skelContext <$> Set.toList collaterals), "Return collateral target:" <+> prettyCookedOpt opts (walletPKHash returnWallet) ] @@ -351,8 +352,11 @@ prettyTxSkelOutDatumMaybe opts txSkelOutDatum@(TxSkelOutInlineDatum _) = prettyTxSkelOutDatumMaybe opts txSkelOutDatum = Just $ "Datum (hashed):" <+> PP.align (prettyCookedOpt opts txSkelOutDatum) -prettyTxOutRefM :: PrettyCookedOpts -> SkelContext -> Api.TxOutRef -> Maybe (DocCooked, DocCooked, [DocCooked]) -prettyTxOutRefM opts skelContext txOutRef = +-- | Resolves a "TxOutRef" from a given context, builds a doc cooked for its +-- address and value, and also builds a possibly empty list for its datum and +-- reference script when they exist. +utxoToPartsAsDocCooked :: PrettyCookedOpts -> SkelContext -> Api.TxOutRef -> Maybe (DocCooked, DocCooked, [DocCooked]) +utxoToPartsAsDocCooked opts skelContext txOutRef = ( \(output, txSkelOutDatum) -> ( prettyCookedOpt opts (outputAddress output), prettyCookedOpt opts (outputValue output), @@ -366,13 +370,13 @@ prettyTxOutRefM opts skelContext txOutRef = prettyCollateralIn :: PrettyCookedOpts -> SkelContext -> Api.TxOutRef -> DocCooked prettyCollateralIn opts skelContext txOutRef = - case prettyTxOutRefM opts skelContext txOutRef of + case utxoToPartsAsDocCooked opts skelContext txOutRef of Nothing -> prettyCookedOpt opts txOutRef <+> "(non resolved)" Just (addressDoc, valueDoc, otherDocs) -> prettyItemize ("Belonging to" <+> addressDoc) "-" (valueDoc : otherDocs) prettyTxSkelIn :: PrettyCookedOpts -> SkelContext -> (Api.TxOutRef, TxSkelRedeemer) -> DocCooked prettyTxSkelIn opts skelContext (txOutRef, txSkelRedeemer) = - case prettyTxOutRefM opts skelContext txOutRef of + case utxoToPartsAsDocCooked opts skelContext txOutRef of Nothing -> "Spends" <+> prettyCookedOpt opts txOutRef <+> "(non resolved)" Just (addressDoc, valueDoc, otherDocs) -> prettyItemize ("Spends from" <+> addressDoc) "-" (valueDoc : prettyTxSkelRedeemer opts txSkelRedeemer <> otherDocs) diff --git a/src/Cooked/Pretty/Options.hs b/src/Cooked/Pretty/Options.hs index f9d998ea3..b87525bd4 100644 --- a/src/Cooked/Pretty/Options.hs +++ b/src/Cooked/Pretty/Options.hs @@ -33,7 +33,7 @@ data PrettyCookedOpts = PrettyCookedOpts -- | Options related to printing hashes pcOptHashes :: PrettyCookedHashOpts, -- | Whether to display the log - pcOptLog :: Bool + pcOptPrintLog :: Bool } deriving (Eq, Show) @@ -45,7 +45,7 @@ instance Default PrettyCookedOpts where pcOptPrintDefaultTxOpts = False, pcOptNumericUnderscores = True, pcOptHashes = def, - pcOptLog = True + pcOptPrintLog = True } -- | Whether to print transaction outputs references. From 8764a1aaf7eedc0adc49151cad07397850da6e36 Mon Sep 17 00:00:00 2001 From: mmontin Date: Sun, 28 Jul 2024 11:53:53 +0200 Subject: [PATCH 11/44] typo --- src/Cooked/MockChain/Balancing.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cooked/MockChain/Balancing.hs b/src/Cooked/MockChain/Balancing.hs index 462b74433..deffd976b 100644 --- a/src/Cooked/MockChain/Balancing.hs +++ b/src/Cooked/MockChain/Balancing.hs @@ -94,7 +94,7 @@ balanceTxSkel skelUnbal@TxSkel {..} = do -- warning if any was actually discarded. >>= filterAndWarn (isJust . isPKOutput . snd) "They belong to scripts." -- We filter the candidate utxos by removing those already present in the - -- skeleton, throwing a warning if any was actually discarder + -- skeleton, throwing a warning if any was actually discarded >>= filterAndWarn ((`notElem` txSkelKnownTxOutRefs skelUnbal) . fst) "They are already used in the skeleton." case txOptFeePolicy txSkelOpts of From 798c8c3e12a34f08bf95b26763840f533a12ec03 Mon Sep 17 00:00:00 2001 From: mmontin Date: Sun, 28 Jul 2024 12:29:19 +0200 Subject: [PATCH 12/44] removing useless instances --- src/Cooked/Skeleton.hs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Cooked/Skeleton.hs b/src/Cooked/Skeleton.hs index b504a7ab6..23e415a40 100644 --- a/src/Cooked/Skeleton.hs +++ b/src/Cooked/Skeleton.hs @@ -1051,7 +1051,6 @@ data SkelContext = SkelContext { skelContextTxOuts :: Map Api.TxOutRef Api.TxOut, skelContextTxSkelOutDatums :: Map Api.DatumHash TxSkelOutDatum } - deriving (Show, Eq) -- | Returns the full value contained in the skeleton outputs txSkelValueInOutputs :: TxSkel -> Api.Value From 492d057b9d4a209a12c2ed9127ac9d27fcf8dbe2 Mon Sep 17 00:00:00 2001 From: mmontin Date: Sun, 7 Jul 2024 22:07:29 +0200 Subject: [PATCH 13/44] wip --- src/Cooked/MockChain/Balancing.hs | 49 +++++++++++++++---------- src/Cooked/MockChain/GenerateTx/Body.hs | 16 ++++++-- tests/Cooked/BalancingSpec.hs | 16 +++++++- 3 files changed, 57 insertions(+), 24 deletions(-) diff --git a/src/Cooked/MockChain/Balancing.hs b/src/Cooked/MockChain/Balancing.hs index deffd976b..f2ee56c43 100644 --- a/src/Cooked/MockChain/Balancing.hs +++ b/src/Cooked/MockChain/Balancing.hs @@ -1,7 +1,12 @@ -- | This module handles auto-balancing of transaction skeleton. This includes -- computation of fees and collaterals because their computation cannot be -- separated from the balancing. -module Cooked.MockChain.Balancing (balanceTxSkel, getMinAndMaxFee, estimateTxSkelFee) where +module Cooked.MockChain.Balancing + ( balanceTxSkel, + getMinAndMaxFee, + estimateTxSkelFee, + ) +where import Cardano.Api.Ledger qualified as Cardano import Cardano.Api.Shelley qualified as Cardano @@ -61,9 +66,9 @@ balanceTxSkel skelUnbal@TxSkel {..} = do -- single transaction fee, which we retrieve. (minFee, maxFee) <- getMinAndMaxFee - -- We collect collateral inputs. They might be directly provided in the - -- skeleton, or should be retrieved from a given wallet. They are associated - -- with a return collateral wallet, which we retrieve as well. + -- We collect collateral inputs candidates. They might be directly provided in + -- the skeleton, or should be retrieved from a given wallet. They are + -- associated with a return collateral wallet, which we retrieve as well. (collateralIns, returnCollateralWallet) <- case txOptCollateralUtxos txSkelOpts of CollateralUtxosFromBalancingWallet -> case balancingWallet of Nothing -> fail "Can't select collateral utxos from a balancing wallet because it does not exist." @@ -71,6 +76,11 @@ balanceTxSkel skelUnbal@TxSkel {..} = do CollateralUtxosFromWallet cWallet -> (,cWallet) . Set.fromList . map fst <$> runUtxoSearch (onlyValueOutputsAtSearch cWallet) CollateralUtxosFromSet utxos rWallet -> return (utxos, rWallet) + -- The transaction will only require collaterals when involving scripts. + requireCollaterals <- do + insValidators <- txSkelInputValidators skelUnbal + return $ not $ Map.null txSkelMints && null (mapMaybe txSkelProposalWitness txSkelProposals) && Map.null insValidators + -- At this point, the presence (or absence) of balancing wallet dictates -- whether the transaction should be automatically balanced or not. (txSkelBal, fee, adjustedCollateralIns) <- case balancingWallet of @@ -80,7 +90,7 @@ balanceTxSkel skelUnbal@TxSkel {..} = do let fee = case txOptFeePolicy txSkelOpts of AutoFeeComputation -> maxFee ManualFee fee' -> fee' - in (skelUnbal,fee,) <$> collateralInsFromFees fee collateralIns returnCollateralWallet + in (skelUnbal,fee,) <$> collateralInsFromFees requireCollaterals fee collateralIns returnCollateralWallet Just bWallet -> do -- The balancing should be performed. We collect the candidates balancing -- utxos based on the associated policy @@ -101,11 +111,11 @@ balanceTxSkel skelUnbal@TxSkel {..} = do -- If fees are left for us to compute, we run a dichotomic search. This -- is full auto mode, the most powerful but time-consuming. AutoFeeComputation -> - computeFeeAndBalance bWallet minFee maxFee collateralIns balancingUtxos returnCollateralWallet skelUnbal + computeFeeAndBalance requireCollaterals bWallet minFee maxFee collateralIns balancingUtxos returnCollateralWallet skelUnbal -- If fee are provided manually, we adjust the collaterals and the -- skeleton around them directly. ManualFee fee -> do - adjustedCollateralIns <- collateralInsFromFees fee collateralIns returnCollateralWallet + adjustedCollateralIns <- collateralInsFromFees requireCollaterals fee collateralIns returnCollateralWallet attemptedSkel <- computeBalancedTxSkel bWallet balancingUtxos skelUnbal fee return (attemptedSkel, fee, adjustedCollateralIns) @@ -142,22 +152,22 @@ getMinAndMaxFee = do -- | 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 :: (MonadBlockChainBalancing m) => Wallet -> Fee -> Fee -> Collaterals -> BalancingOutputs -> Wallet -> TxSkel -> m (TxSkel, Fee, Collaterals) -computeFeeAndBalance _ minFee maxFee _ _ _ _ +computeFeeAndBalance :: (MonadBlockChainBalancing m) => Bool -> Wallet -> Fee -> Fee -> Collaterals -> BalancingOutputs -> Wallet -> TxSkel -> m (TxSkel, Fee, Collaterals) +computeFeeAndBalance _ _ minFee maxFee _ _ _ _ | minFee > maxFee = throwError $ FailWith "Unreachable case, please report a bug at https://github.com/tweag/cooked-validators/issues" -computeFeeAndBalance balancingWallet minFee maxFee collateralIns balancingUtxos returnCollateralWallet skel +computeFeeAndBalance requireCollaterals balancingWallet minFee maxFee collateralIns balancingUtxos returnCollateralWallet skel | minFee == maxFee = do -- The fee interval is reduced to a single element, we balance around it - (adjustedCollateralIns, attemptedSkel) <- attemptBalancingAndCollaterals balancingWallet collateralIns balancingUtxos returnCollateralWallet minFee skel + (adjustedCollateralIns, attemptedSkel) <- attemptBalancingAndCollaterals requireCollaterals balancingWallet collateralIns balancingUtxos returnCollateralWallet minFee skel return (attemptedSkel, minFee, adjustedCollateralIns) -computeFeeAndBalance balancingWallet minFee maxFee collateralIns balancingUtxos returnCollateralWallet skel +computeFeeAndBalance requireCollaterals balancingWallet minFee maxFee collateralIns balancingUtxos returnCollateralWallet skel | fee <- (minFee + maxFee) `div` 2 = do -- The fee interval is larger than a single element. We attempt to balance -- around its central point, which can fail due to missing value in -- balancing utxos or collateral utxos. attemptedBalancing <- catchError - (Just <$> attemptBalancingAndCollaterals balancingWallet collateralIns balancingUtxos returnCollateralWallet fee skel) + (Just <$> attemptBalancingAndCollaterals requireCollaterals balancingWallet collateralIns balancingUtxos returnCollateralWallet fee skel) $ \case -- If it fails, and the remaining fee interval is not reduced to the -- current fee attempt, we return `Nothing` which signifies that we @@ -192,13 +202,13 @@ computeFeeAndBalance balancingWallet minFee maxFee collateralIns balancingUtxos -- fee of the input skeleton. _ -> (minFee, newFee) - computeFeeAndBalance balancingWallet newMinFee newMaxFee collateralIns balancingUtxos returnCollateralWallet skel + computeFeeAndBalance requireCollaterals balancingWallet newMinFee newMaxFee collateralIns balancingUtxos returnCollateralWallet skel -- | Helper function to group the two real steps of the balancing: balance a -- skeleton around a given fee, and compute the associated collateral inputs -attemptBalancingAndCollaterals :: (MonadBlockChainBalancing m) => Wallet -> Collaterals -> BalancingOutputs -> Wallet -> Fee -> TxSkel -> m (Collaterals, TxSkel) -attemptBalancingAndCollaterals balancingWallet collateralIns balancingUtxos returnCollateralWallet fee skel = do - adjustedCollateralIns <- collateralInsFromFees fee collateralIns returnCollateralWallet +attemptBalancingAndCollaterals :: (MonadBlockChainBalancing m) => Bool -> Wallet -> Collaterals -> BalancingOutputs -> Wallet -> Fee -> TxSkel -> m (Collaterals, TxSkel) +attemptBalancingAndCollaterals requireCollaterals balancingWallet collateralIns balancingUtxos returnCollateralWallet fee skel = do + adjustedCollateralIns <- collateralInsFromFees requireCollaterals fee collateralIns returnCollateralWallet attemptedSkel <- computeBalancedTxSkel balancingWallet balancingUtxos skel fee return (adjustedCollateralIns, attemptedSkel) @@ -206,8 +216,8 @@ attemptBalancingAndCollaterals balancingWallet collateralIns balancingUtxos retu -- accounting for the ratio to respect between fees and total collaterals, the -- min ada requirements in the associated return collateral and the maximum -- number of collateral inputs authorized by protocol parameters. -collateralInsFromFees :: (MonadBlockChainBalancing m) => Fee -> Collaterals -> Wallet -> m Collaterals -collateralInsFromFees fee collateralIns returnCollateralWallet = do +collateralInsFromFees :: (MonadBlockChainBalancing m) => Bool -> Fee -> Collaterals -> Wallet -> m Collaterals +collateralInsFromFees True fee collateralIns returnCollateralWallet = do -- We retrieve the max number of collateral inputs, with a default of 10. In -- practice this will be around 3. nbMax <- toInteger . fromMaybe 10 . Cardano.protocolParamMaxCollateralInputs . Emulator.pProtocolParams <$> getParams @@ -225,6 +235,7 @@ collateralInsFromFees fee collateralIns returnCollateralWallet = do let noSuitableCollateralError = MCENoSuitableCollateral fee percentage totalCollateral -- Retrieving and returning the best candidate as a utxo set Set.fromList . fst <$> getOptimalCandidate candidatesRaw returnCollateralWallet noSuitableCollateralError +collateralInsFromFees False _ _ _ = return Set.empty -- | The main computing function for optimal balancing and collaterals. It -- computes the subsets of a set of UTxOs that sum up to a certain target. It diff --git a/src/Cooked/MockChain/GenerateTx/Body.hs b/src/Cooked/MockChain/GenerateTx/Body.hs index bbfb958be..3ac292d96 100644 --- a/src/Cooked/MockChain/GenerateTx/Body.hs +++ b/src/Cooked/MockChain/GenerateTx/Body.hs @@ -10,11 +10,13 @@ import Cooked.MockChain.GenerateTx.Input qualified as Input import Cooked.MockChain.GenerateTx.Mint qualified as Mint import Cooked.MockChain.GenerateTx.Output qualified as Output import Cooked.MockChain.GenerateTx.Proposal qualified as Proposal +import Cooked.Output import Cooked.Skeleton import Cooked.Wallet import Data.Bifunctor import Data.Map (Map) import Data.Map qualified as Map +import Data.Maybe import Data.Set (Set) import Ledger.Address qualified as Ledger import Ledger.Tx qualified as Ledger @@ -63,11 +65,19 @@ txSkelToBodyContent skel@TxSkel {..} | txSkelReferenceInputs <- txSkelReferenceT "txSkelToBodyContent: Unable to translate reference inputs." (Cardano.TxInsReference Cardano.BabbageEraOnwardsConway) $ mapM Ledger.toCardanoTxIn txSkelReferenceInputs - (txInsCollateral, txTotalCollateral, txReturnCollateral) <- liftTxGen Collateral.toCollateralTriplet + (txInsCollateral, txTotalCollateral, txReturnCollateral) <- do + refs <- asks managedTxOuts + txOuts <- forM (Map.keys txSkelIns) $ flip (throwOnLookup "txSkelToBodyContent: Unable to resolve input utxo.") refs + if not $ + null (mapMaybe isScriptOutput txOuts) + && Map.null txSkelMints + && null (mapMaybe txSkelProposalWitness txSkelProposals) + then liftTxGen Collateral.toCollateralTriplet + else return (Cardano.TxInsCollateralNone, Cardano.TxTotalCollateralNone, Cardano.TxReturnCollateralNone) txOuts <- mapM (liftTxGen . Output.toCardanoTxOut) txSkelOuts (txValidityLowerBound, txValidityUpperBound) <- throwOnToCardanoError - "txSkelToBodyContent: Unable to translate transaction validity range" + "txSkelToBodyContent: Unable to translate transaction validity range." $ Ledger.toCardanoValidityRange txSkelValidityRange txMintValue <- liftTxGen $ Mint.toMintValue txSkelMints txExtraKeyWits <- @@ -89,7 +99,7 @@ txSkelToBodyContent skel@TxSkel {..} | txSkelReferenceInputs <- txSkelReferenceT txCertificates = Cardano.TxCertificatesNone -- That's what plutus-apps does as well txUpdateProposal = Cardano.TxUpdateProposalNone -- That's what plutus-apps does as well txScriptValidity = Cardano.TxScriptValidityNone -- That's what plutus-apps does as well - txVotingProcedures = Nothing -- TODO, same as above + txVotingProcedures = Nothing return Cardano.TxBodyContent {..} -- | Generates a transaction for a skeleton. We first generate a body and we diff --git a/tests/Cooked/BalancingSpec.hs b/tests/Cooked/BalancingSpec.hs index dcf08c95b..2b4d81097 100644 --- a/tests/Cooked/BalancingSpec.hs +++ b/tests/Cooked/BalancingSpec.hs @@ -80,7 +80,11 @@ testingBalancingTemplate toBobValue toAliceValue spendSearch balanceSearch colla then CollateralUtxosFromBalancingWallet else CollateralUtxosFromSet (Set.fromList toCollateralUtxos) alice }, - txSkelSigners = [alice] + txSkelSigners = [alice], + txSkelProposals = + [ simpleTxSkelProposal alice (TxGovActionTreasuryWithdrawals Map.empty) + `withWitness` (alwaysTrueProposingValidator, TxSkelNoRedeemer) + ] } (skel', fee, cols, _) <- balanceTxSkel skel void $ validateTxSkel skel @@ -132,6 +136,10 @@ noBalanceMaxFee = do txOptFeePolicy = AutoFeeComputation, txOptCollateralUtxos = CollateralUtxosFromSet (Set.singleton txOutRef) alice }, + txSkelProposals = + [ simpleTxSkelProposal alice (TxGovActionTreasuryWithdrawals Map.empty) + `withWitness` (alwaysTrueProposingValidator, TxSkelNoRedeemer) + ], txSkelSigners = [alice] } @@ -166,7 +174,11 @@ reachingMagic = do txSkelOpts = def { txOptBalancingUtxos = BalancingUtxosFromSet (Set.fromList bananaOutRefs) - } + }, + txSkelProposals = + [ simpleTxSkelProposal alice (TxGovActionTreasuryWithdrawals Map.empty) + `withWitness` (alwaysTrueProposingValidator, TxSkelNoRedeemer) + ] } type ResProp prop = TestBalancingOutcome -> prop From 817c777023f27833ad2182cfc365b4c9a671e5d9 Mon Sep 17 00:00:00 2001 From: mmontin Date: Wed, 10 Jul 2024 18:45:34 +0200 Subject: [PATCH 14/44] reverting balancingspec --- tests/Cooked/BalancingSpec.hs | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/tests/Cooked/BalancingSpec.hs b/tests/Cooked/BalancingSpec.hs index 2b4d81097..dcf08c95b 100644 --- a/tests/Cooked/BalancingSpec.hs +++ b/tests/Cooked/BalancingSpec.hs @@ -80,11 +80,7 @@ testingBalancingTemplate toBobValue toAliceValue spendSearch balanceSearch colla then CollateralUtxosFromBalancingWallet else CollateralUtxosFromSet (Set.fromList toCollateralUtxos) alice }, - txSkelSigners = [alice], - txSkelProposals = - [ simpleTxSkelProposal alice (TxGovActionTreasuryWithdrawals Map.empty) - `withWitness` (alwaysTrueProposingValidator, TxSkelNoRedeemer) - ] + txSkelSigners = [alice] } (skel', fee, cols, _) <- balanceTxSkel skel void $ validateTxSkel skel @@ -136,10 +132,6 @@ noBalanceMaxFee = do txOptFeePolicy = AutoFeeComputation, txOptCollateralUtxos = CollateralUtxosFromSet (Set.singleton txOutRef) alice }, - txSkelProposals = - [ simpleTxSkelProposal alice (TxGovActionTreasuryWithdrawals Map.empty) - `withWitness` (alwaysTrueProposingValidator, TxSkelNoRedeemer) - ], txSkelSigners = [alice] } @@ -174,11 +166,7 @@ reachingMagic = do txSkelOpts = def { txOptBalancingUtxos = BalancingUtxosFromSet (Set.fromList bananaOutRefs) - }, - txSkelProposals = - [ simpleTxSkelProposal alice (TxGovActionTreasuryWithdrawals Map.empty) - `withWitness` (alwaysTrueProposingValidator, TxSkelNoRedeemer) - ] + } } type ResProp prop = TestBalancingOutcome -> prop From c59f14cc5f687d2c4d37cd7c6fd450f288bffcdd Mon Sep 17 00:00:00 2001 From: mmontin Date: Wed, 10 Jul 2024 19:09:22 +0200 Subject: [PATCH 15/44] starting to consume scripts in balancing spec, to be continued --- tests/Cooked/BalancingSpec.hs | 81 +++++++++++++++++++++-------------- 1 file changed, 48 insertions(+), 33 deletions(-) diff --git a/tests/Cooked/BalancingSpec.hs b/tests/Cooked/BalancingSpec.hs index dcf08c95b..536628aca 100644 --- a/tests/Cooked/BalancingSpec.hs +++ b/tests/Cooked/BalancingSpec.hs @@ -7,6 +7,7 @@ import Cooked.MockChain.GenerateTx import Cooked.MockChain.Staged import Data.Default import Data.List qualified as List +import Data.Map (Map) import Data.Map qualified as Map import Data.Maybe import Data.Set @@ -33,7 +34,8 @@ banana = permanentValue "banana" initialDistributionBalancing :: InitialDistribution initialDistributionBalancing = InitialDistribution - [ paysPK alice (Script.ada 2 <> apple 3), + [ paysScript (alwaysTrueValidator @MockContract) () (Script.ada 42), + paysPK alice (Script.ada 2 <> apple 3), paysPK alice (Script.ada 25), paysPK alice (Script.ada 40 <> orange 6), paysPK alice (Script.ada 8), @@ -45,6 +47,12 @@ initialDistributionBalancing = type TestBalancingOutcome = (TxSkel, TxSkel, Integer, Set Api.TxOutRef, [Api.TxOutRef]) +spendsScriptUtxo :: (MonadBlockChain m) => Bool -> m (Map Api.TxOutRef TxSkelRedeemer) +spendsScriptUtxo False = return Map.empty +spendsScriptUtxo True = do + (scriptOutRef, _) : _ <- runUtxoSearch $ utxosAtSearch $ alwaysTrueValidator @MockContract + return $ Map.singleton scriptOutRef txSkelEmptyRedeemer + testingBalancingTemplate :: (MonadBlockChain m) => -- Value to pay to bob @@ -57,17 +65,20 @@ testingBalancingTemplate :: UtxoSearch m b -> -- Search for utxos to be used for collaterals UtxoSearch m c -> + -- Whether to consum the script utxo + Bool -> -- Option modifications (TxOpts -> TxOpts) -> m TestBalancingOutcome -testingBalancingTemplate toBobValue toAliceValue spendSearch balanceSearch collateralSearch optionsMod = do +testingBalancingTemplate toBobValue toAliceValue spendSearch balanceSearch collateralSearch consumeScriptUtxo optionsMod = do ((fst <$>) -> toSpendUtxos) <- runUtxoSearch spendSearch ((fst <$>) -> toBalanceUtxos) <- runUtxoSearch balanceSearch ((fst <$>) -> toCollateralUtxos) <- runUtxoSearch collateralSearch + additionalSpend <- spendsScriptUtxo consumeScriptUtxo let skel = txSkelTemplate { txSkelOuts = List.filter ((/= mempty) . (^. txSkelOutValueL)) [paysPK bob toBobValue, paysPK alice toAliceValue], - txSkelIns = Map.fromList $ (,txSkelEmptyRedeemer) <$> toSpendUtxos, + txSkelIns = additionalSpend <> Map.fromList ((,txSkelEmptyRedeemer) <$> toSpendUtxos), txSkelOpts = optionsMod def @@ -99,7 +110,7 @@ aliceRefScriptUtxos = utxosAtSearch alice `filterWithPred` \o -> isJust (Api.txO emptySearch :: (MonadBlockChain m) => UtxoSearch m Api.TxOut emptySearch = ListT.fromFoldable [] -simplePaymentToBob :: (MonadBlockChain m) => Integer -> Integer -> Integer -> Integer -> (TxOpts -> TxOpts) -> m TestBalancingOutcome +simplePaymentToBob :: (MonadBlockChain m) => Integer -> Integer -> Integer -> Integer -> Bool -> (TxOpts -> TxOpts) -> m TestBalancingOutcome simplePaymentToBob lv apples oranges bananas = testingBalancingTemplate (Script.lovelace lv <> apple apples <> orange oranges <> banana bananas) @@ -108,7 +119,7 @@ simplePaymentToBob lv apples oranges bananas = emptySearch emptySearch -bothPaymentsToBobAndAlice :: (MonadBlockChain m) => Integer -> (TxOpts -> TxOpts) -> m TestBalancingOutcome +bothPaymentsToBobAndAlice :: (MonadBlockChain m) => Integer -> Bool -> (TxOpts -> TxOpts) -> m TestBalancingOutcome bothPaymentsToBobAndAlice val = testingBalancingTemplate (Script.lovelace val) @@ -238,23 +249,27 @@ tests = [ testBalancingFailsWith "Balancing does not occur when not requested, fails with empty inputs" failsWithEmptyTxIns - (simplePaymentToBob 20_000_000 0 0 0 (setCollateralWallet alice . setDontBalance . setFixedFee 1_000_000)), + (simplePaymentToBob 20_000_000 0 0 0 False (setCollateralWallet alice . setDontBalance . setFixedFee 1_000_000)), testBalancingFailsWith "Balancing does not occur when not requested, fails with too small inputs" failsWithValueNotConserved - (testingBalancingTemplate (Script.ada 50) mempty (aliceNAdaUtxos 8) emptySearch emptySearch (setCollateralWallet alice . setDontBalance . setFixedFee 1_000_000)), + (testingBalancingTemplate (Script.ada 50) mempty (aliceNAdaUtxos 8) emptySearch emptySearch False (setCollateralWallet alice . setDontBalance . setFixedFee 1_000_000)), testBalancingSucceedsWith - "It is still possible to balance the transaction by hand" - [hasFee 1_000_000, insNb 1, additionalOutsNb 0, colInsNb 1, retOutsNb 3] - (testingBalancingTemplate (Script.ada 7) mempty (aliceNAdaUtxos 8) emptySearch emptySearch (setCollateralWallet alice . setDontBalance . setFixedFee 1_000_000)), + "It is possible to balance the transaction by hand without collaterals" + [hasFee 1_000_000, insNb 1, additionalOutsNb 0, colInsNb 0, retOutsNb 3] + (testingBalancingTemplate (Script.ada 7) mempty (aliceNAdaUtxos 8) emptySearch emptySearch False (setCollateralWallet alice . setDontBalance . setFixedFee 1_000_000)), + testBalancingSucceedsWith + "It is also possible to balance the transaction by hand with collaterals" + [hasFee 1_000_000, insNb 2, additionalOutsNb 0, colInsNb 1, retOutsNb 3] + (testingBalancingTemplate (Script.ada 49) mempty (aliceNAdaUtxos 8) emptySearch emptySearch True (setCollateralWallet alice . setDontBalance . setFixedFee 1_000_000)), testBalancingFailsWith "A collateral wallet needs to be provided when auto balancing is enabled" failsLackOfCollateralWallet - (testingBalancingTemplate (Script.ada 7) mempty (aliceNAdaUtxos 8) emptySearch emptySearch (setDontBalance . setFixedFee 1_000_000)), + (testingBalancingTemplate (Script.ada 7) mempty (aliceNAdaUtxos 8) emptySearch emptySearch False (setDontBalance . setFixedFee 1_000_000)), testBalancingSucceedsWith "We can also directly give a set of collateral utxos" [hasFee 1_000_000, insNb 1, additionalOutsNb 0, colInsNb 1, retOutsNb 3] - (testingBalancingTemplate (Script.ada 7) mempty (aliceNAdaUtxos 8) emptySearch (aliceNAdaUtxos 8) (setDontBalance . setFixedFee 1_000_000)) + (testingBalancingTemplate (Script.ada 7) mempty (aliceNAdaUtxos 8) emptySearch (aliceNAdaUtxos 8) False (setDontBalance . setFixedFee 1_000_000)) ], testGroup "Manual balancing with auto fee" @@ -266,7 +281,7 @@ tests = [ testBalancingSucceedsWith "We can auto balance a transaction with auto fee" [insNb 1, additionalOutsNb 1, colInsNb 1, retOutsNb 3] - (simplePaymentToBob 20_000_000 0 0 0 id), + (simplePaymentToBob 20_000_000 0 0 0 False id), testCase "Auto fee are minimal: less fee will lead to strictly smaller fee than Cardano's estimate" $ testSucceedsFrom' def @@ -274,20 +289,20 @@ tests = initialDistributionBalancing balanceReduceFee, testCase "The auto-fee process can sometimes recover from a temporary balancing error..." $ - testSucceedsFrom def initialDistributionBalancing (simplePaymentToBob 103_500_000 0 0 0 id), + testSucceedsFrom def initialDistributionBalancing (simplePaymentToBob 103_500_000 0 0 0 False id), testCase "... but not always" $ - testFailsFrom def failsAtBalancing initialDistributionBalancing (simplePaymentToBob 104_000_000 0 0 0 id), + testFailsFrom def failsAtBalancing initialDistributionBalancing (simplePaymentToBob 104_000_000 0 0 0 False id), testCase "The auto-fee process can recover from a temporary collateral error..." $ testSucceedsFrom def initialDistributionBalancing - (testingBalancingTemplate (Script.ada 100) mempty emptySearch emptySearch (aliceNAdaUtxos 2) id), + (testingBalancingTemplate (Script.ada 100) mempty emptySearch emptySearch (aliceNAdaUtxos 2) False id), testCase "... but not always" $ testFailsFrom def failsAtCollaterals initialDistributionBalancing - (testingBalancingTemplate (Script.ada 100) mempty (utxosAtSearch alice) emptySearch (aliceNAdaUtxos 1) id), + (testingBalancingTemplate (Script.ada 100) mempty (utxosAtSearch alice) emptySearch (aliceNAdaUtxos 1) False id), testCase "Reaching magical spot with the exact balance during auto fee computation" $ testSucceedsFrom def @@ -299,66 +314,66 @@ tests = [ testBalancingSucceedsWith "We can use a single utxo for balancing purpose" [hasFee 1_000_000, insNb 1, additionalOutsNb 1, colInsNb 1, retOutsNb 3] - (simplePaymentToBob 20_000_000 0 0 0 (setFixedFee 1_000_000)), + (simplePaymentToBob 20_000_000 0 0 0 False (setFixedFee 1_000_000)), testBalancingSucceedsWith "We can use several utxos for balancing with ridiculously high fee" [hasFee 40_000_000, insNb 3, additionalOutsNb 1, colInsNb 3, retOutsNb 3] - (simplePaymentToBob 20_000_000 0 0 0 (setFixedFee 40_000_000)), + (simplePaymentToBob 20_000_000 0 0 0 False (setFixedFee 40_000_000)), testBalancingFailsWith "We cannot balance with too little fee" failsWithTooLittleFee - (simplePaymentToBob 20_000_000 0 0 0 (setFixedFee 150_000)), + (simplePaymentToBob 20_000_000 0 0 0 False (setFixedFee 150_000)), testBalancingFailsWith "Fee are rightfully included in the balancing process, which fails when they are too high" (failsAtBalancingWith (Script.ada 1) alice) - (simplePaymentToBob 100_000_000 0 0 0 (setFixedFee 6_000_000)), + (simplePaymentToBob 100_000_000 0 0 0 False (setFixedFee 6_000_000)), testBalancingFailsWith "Collaterals are rightfully included in the balancing process, which fails when they are too high" (failsAtCollateralsWith 80_000_000) - (simplePaymentToBob 6_000_000 0 0 0 (setFixedFee 80_000_000)), + (simplePaymentToBob 6_000_000 0 0 0 False (setFixedFee 80_000_000)), testBalancingSucceedsWith "Exactly the right amount leads to no output change" [hasFee 2_000_000, insNb 3, additionalOutsNb 0, colInsNb 1, retOutsNb 3] - (simplePaymentToBob 65_000_000 3 6 0 (setFixedFee 2_000_000)), + (simplePaymentToBob 65_000_000 3 6 0 False (setFixedFee 2_000_000)), testBalancingSucceedsWith "It still leads to no output change when requesting a new output" [hasFee 2_000_000, insNb 3, additionalOutsNb 0, colInsNb 1, retOutsNb 3] - (simplePaymentToBob 65_000_000 3 6 0 (setDontAdjustOutput . setFixedFee 2_000_000)), + (simplePaymentToBob 65_000_000 3 6 0 False (setDontAdjustOutput . setFixedFee 2_000_000)), testBalancingSucceedsWith "1 lovelace more than the exact right amount leads to an additional output" [hasFee 2_000_000, insNb 3, additionalOutsNb 1, colInsNb 1, retOutsNb 3] - (simplePaymentToBob 65_000_001 3 6 0 (setFixedFee 2_000_000)), + (simplePaymentToBob 65_000_001 3 6 0 False (setFixedFee 2_000_000)), testBalancingSucceedsWith "1 lovelace less than the exact right amount leads to an additional output to account for minAda" [hasFee 2_000_000, insNb 3, additionalOutsNb 1, colInsNb 1, retOutsNb 3] - (simplePaymentToBob 65_000_001 3 6 0 (setFixedFee 2_000_000)), + (simplePaymentToBob 65_000_001 3 6 0 False (setFixedFee 2_000_000)), testBalancingSucceedsWith "We can merge assets to an existing outputs at the balancing wallet address" [hasFee 2_000_000, insNb 1, additionalOutsNb 0, colInsNb 1, retOutsNb 3] - (bothPaymentsToBobAndAlice 6_000_000 (setFixedFee 2_000_000)), + (bothPaymentsToBobAndAlice 6_000_000 False (setFixedFee 2_000_000)), testBalancingSucceedsWith "We can create a new output at the balancing wallet address even if one already exists" [hasFee 2_000_000, insNb 1, additionalOutsNb 1, colInsNb 1, retOutsNb 3] - (bothPaymentsToBobAndAlice 6_000_000 (setFixedFee 2_000_000 . setDontAdjustOutput)), + (bothPaymentsToBobAndAlice 6_000_000 False (setFixedFee 2_000_000 . setDontAdjustOutput)), testBalancingSucceedsWith "We can balance transactions with non-Script.ada assets" [hasFee 2_000_000, insNb 1, additionalOutsNb 1, colInsNb 1, retOutsNb 3] - (simplePaymentToBob 0 0 5 0 (setFixedFee 2_000_000 . setEnsureMinAda)), + (simplePaymentToBob 0 0 5 0 False (setFixedFee 2_000_000 . setEnsureMinAda)), testBalancingSucceedsWith "Successful balancing with multiple assets" [hasFee 1_000_000, insNb 2, additionalOutsNb 1, colInsNb 1, retOutsNb 3] - (simplePaymentToBob 0 2 5 0 (setEnsureMinAda . setFixedFee 1_000_000)), + (simplePaymentToBob 0 2 5 0 False (setEnsureMinAda . setFixedFee 1_000_000)), testBalancingFailsWith "Unsuccessful balancing with multiple assets in non value only utxos" (failsAtBalancingWith (banana 4) alice) - (simplePaymentToBob 0 2 5 4 (setEnsureMinAda . setFixedFee 1_000_000)), + (simplePaymentToBob 0 2 5 4 False (setEnsureMinAda . setFixedFee 1_000_000)), testBalancingSucceedsWith "Successful balancing with multiple assets and explicit utxo set, reference script is lost" [hasFee 1_000_000, insNb 3, additionalOutsNb 1, colInsNb 1, retOutsNb 2] - (testingBalancingTemplate (apple 2 <> orange 5 <> banana 4) mempty emptySearch (utxosAtSearch alice) emptySearch (setEnsureMinAda . setFixedFee 1_000_000)), + (testingBalancingTemplate (apple 2 <> orange 5 <> banana 4) mempty emptySearch (utxosAtSearch alice) emptySearch False (setEnsureMinAda . setFixedFee 1_000_000)), testBalancingSucceedsWith "Successful balancing with excess initial consumption" [hasFee 1_000_000, insNb 5, additionalOutsNb 1, colInsNb 1, retOutsNb 3] - (testingBalancingTemplate mempty mempty (onlyValueOutputsAtSearch alice) emptySearch emptySearch (setFixedFee 1_000_000)) + (testingBalancingTemplate mempty mempty (onlyValueOutputsAtSearch alice) emptySearch emptySearch False (setFixedFee 1_000_000)) ] ] From 96b3250e6c08fb7878369e2f401d36433c13760d Mon Sep 17 00:00:00 2001 From: mmontin Date: Thu, 11 Jul 2024 12:39:24 +0200 Subject: [PATCH 16/44] reworking empty collaterals --- src/Cooked/MockChain/Balancing.hs | 100 ++++--- src/Cooked/MockChain/BlockChain.hs | 2 +- src/Cooked/MockChain/Direct.hs | 10 +- src/Cooked/MockChain/GenerateTx.hs | 16 +- src/Cooked/MockChain/GenerateTx/Body.hs | 15 +- src/Cooked/MockChain/GenerateTx/Collateral.hs | 118 ++++---- tests/Cooked/BalancingSpec.hs | 275 +++++++++++++++--- tests/Cooked/ShowBSSpec.hs | 2 +- 8 files changed, 373 insertions(+), 165 deletions(-) diff --git a/src/Cooked/MockChain/Balancing.hs b/src/Cooked/MockChain/Balancing.hs index f2ee56c43..8f2f649e4 100644 --- a/src/Cooked/MockChain/Balancing.hs +++ b/src/Cooked/MockChain/Balancing.hs @@ -43,14 +43,17 @@ type Fee = Integer type Collaterals = Set Api.TxOutRef +type MCollaterals = Maybe (Collaterals, Wallet) + type BalancingOutputs = [(Api.TxOutRef, Api.TxOut)] -- | This is the main entry point of our balancing mechanism. This function -- takes a skeleton and returns a (possibly) balanced skeleton alongside the --- associated fee, collateral inputs and return collateral wallet. The options --- from the skeleton control whether it should be balanced, and how to compute --- its associated elements. -balanceTxSkel :: (MonadBlockChainBalancing m) => TxSkel -> m (TxSkel, Fee, Collaterals, Wallet) +-- associated fee, collateral inputs and return collateral wallet, which might +-- be empty when no script is involved in the transaction. The options from the +-- skeleton control whether it should be balanced, and how to compute its +-- associated elements. +balanceTxSkel :: (MonadBlockChainBalancing m) => TxSkel -> m (TxSkel, Fee, MCollaterals) balanceTxSkel skelUnbal@TxSkel {..} = do -- We retrieve the possible balancing wallet. Any extra payment will be -- redirected to them, and utxos will be taken from their wallet if associated @@ -68,29 +71,34 @@ balanceTxSkel skelUnbal@TxSkel {..} = do -- We collect collateral inputs candidates. They might be directly provided in -- the skeleton, or should be retrieved from a given wallet. They are - -- associated with a return collateral wallet, which we retrieve as well. - (collateralIns, returnCollateralWallet) <- case txOptCollateralUtxos txSkelOpts of - CollateralUtxosFromBalancingWallet -> case balancingWallet of - Nothing -> fail "Can't select collateral utxos from a balancing wallet because it does not exist." - Just bWallet -> (,bWallet) . Set.fromList . map fst <$> runUtxoSearch (onlyValueOutputsAtSearch bWallet) - CollateralUtxosFromWallet cWallet -> (,cWallet) . Set.fromList . map fst <$> runUtxoSearch (onlyValueOutputsAtSearch cWallet) - CollateralUtxosFromSet utxos rWallet -> return (utxos, rWallet) - - -- The transaction will only require collaterals when involving scripts. - requireCollaterals <- do - insValidators <- txSkelInputValidators skelUnbal - return $ not $ Map.null txSkelMints && null (mapMaybe txSkelProposalWitness txSkelProposals) && Map.null insValidators + -- associated with a return collateral wallet, which we retrieve as well. All + -- of this is wrapped in a `Maybe` type to represent the case when the + -- transaction does not involve script and should not have any kind of + -- collaterals attached to it. + mCollaterals <- do + -- We retrieve the various kinds of scripts + spendingScripts <- txSkelInputValidators skelUnbal + -- The transaction will only require collaterals when involving scripts + if Map.null txSkelMints && null (mapMaybe txSkelProposalWitness txSkelProposals) && Map.null spendingScripts + then return Nothing + else + Just <$> case txOptCollateralUtxos txSkelOpts of + CollateralUtxosFromBalancingWallet -> case balancingWallet of + Nothing -> fail "Can't select collateral utxos from a balancing wallet because it does not exist." + Just bWallet -> (,bWallet) . Set.fromList . map fst <$> runUtxoSearch (onlyValueOutputsAtSearch bWallet) + CollateralUtxosFromWallet cWallet -> (,cWallet) . Set.fromList . map fst <$> runUtxoSearch (onlyValueOutputsAtSearch cWallet) + CollateralUtxosFromSet utxos rWallet -> return (utxos, rWallet) -- At this point, the presence (or absence) of balancing wallet dictates -- whether the transaction should be automatically balanced or not. - (txSkelBal, fee, adjustedCollateralIns) <- case balancingWallet of + (txSkelBal, fee, adjustedMCollaterals) <- case balancingWallet of Nothing -> -- The balancing should not be performed. We still adjust the collaterals -- though around a provided fee, or the maximum fee. let fee = case txOptFeePolicy txSkelOpts of AutoFeeComputation -> maxFee ManualFee fee' -> fee' - in (skelUnbal,fee,) <$> collateralInsFromFees requireCollaterals fee collateralIns returnCollateralWallet + in (skelUnbal,fee,) <$> collateralsFromFees fee mCollaterals Just bWallet -> do -- The balancing should be performed. We collect the candidates balancing -- utxos based on the associated policy @@ -111,15 +119,15 @@ balanceTxSkel skelUnbal@TxSkel {..} = do -- If fees are left for us to compute, we run a dichotomic search. This -- is full auto mode, the most powerful but time-consuming. AutoFeeComputation -> - computeFeeAndBalance requireCollaterals bWallet minFee maxFee collateralIns balancingUtxos returnCollateralWallet skelUnbal + computeFeeAndBalance bWallet minFee maxFee balancingUtxos mCollaterals skelUnbal -- If fee are provided manually, we adjust the collaterals and the -- skeleton around them directly. ManualFee fee -> do - adjustedCollateralIns <- collateralInsFromFees requireCollaterals fee collateralIns returnCollateralWallet + adjustedMCollaterals <- collateralsFromFees fee mCollaterals attemptedSkel <- computeBalancedTxSkel bWallet balancingUtxos skelUnbal fee - return (attemptedSkel, fee, adjustedCollateralIns) + return (attemptedSkel, fee, adjustedMCollaterals) - return (txSkelBal, fee, adjustedCollateralIns, returnCollateralWallet) + return (txSkelBal, fee, adjustedMCollaterals) where filterAndWarn f s l | (ok, toInteger . length -> koLength) <- partition f l = @@ -152,22 +160,22 @@ getMinAndMaxFee = do -- | 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 :: (MonadBlockChainBalancing m) => Bool -> Wallet -> Fee -> Fee -> Collaterals -> BalancingOutputs -> Wallet -> TxSkel -> m (TxSkel, Fee, Collaterals) -computeFeeAndBalance _ _ minFee maxFee _ _ _ _ +computeFeeAndBalance :: (MonadBlockChainBalancing m) => Wallet -> Fee -> Fee -> BalancingOutputs -> MCollaterals -> TxSkel -> m (TxSkel, Fee, MCollaterals) +computeFeeAndBalance _ minFee maxFee _ _ _ | minFee > maxFee = throwError $ FailWith "Unreachable case, please report a bug at https://github.com/tweag/cooked-validators/issues" -computeFeeAndBalance requireCollaterals balancingWallet minFee maxFee collateralIns balancingUtxos returnCollateralWallet skel +computeFeeAndBalance balancingWallet minFee maxFee balancingUtxos mCollaterals skel | minFee == maxFee = do -- The fee interval is reduced to a single element, we balance around it - (adjustedCollateralIns, attemptedSkel) <- attemptBalancingAndCollaterals requireCollaterals balancingWallet collateralIns balancingUtxos returnCollateralWallet minFee skel - return (attemptedSkel, minFee, adjustedCollateralIns) -computeFeeAndBalance requireCollaterals balancingWallet minFee maxFee collateralIns balancingUtxos returnCollateralWallet skel + (adjustedMCollaterals, attemptedSkel) <- attemptBalancingAndCollaterals balancingWallet balancingUtxos minFee mCollaterals skel + return (attemptedSkel, minFee, adjustedMCollaterals) +computeFeeAndBalance balancingWallet minFee maxFee balancingUtxos mCollaterals skel | fee <- (minFee + maxFee) `div` 2 = do -- The fee interval is larger than a single element. We attempt to balance -- around its central point, which can fail due to missing value in -- balancing utxos or collateral utxos. attemptedBalancing <- catchError - (Just <$> attemptBalancingAndCollaterals requireCollaterals balancingWallet collateralIns balancingUtxos returnCollateralWallet fee skel) + (Just <$> attemptBalancingAndCollaterals balancingWallet balancingUtxos fee mCollaterals skel) $ \case -- If it fails, and the remaining fee interval is not reduced to the -- current fee attempt, we return `Nothing` which signifies that we @@ -182,8 +190,8 @@ computeFeeAndBalance requireCollaterals balancingWallet minFee maxFee collateral Nothing -> return (minFee, fee - 1) -- The skeleton was balanceable, we compute and analyse the resulting -- fee to seach upwards or downwards for an optimal solution - Just (adjustedCollateralIns, attemptedSkel) -> do - newFee <- estimateTxSkelFee attemptedSkel fee adjustedCollateralIns returnCollateralWallet + Just (adjustedMCollaterals, attemptedSkel) -> do + newFee <- estimateTxSkelFee attemptedSkel fee adjustedMCollaterals return $ case fee - newFee of -- Current fee is insufficient, we look on the right (strictly) n | n < 0 -> (fee + 1, maxFee) @@ -202,13 +210,13 @@ computeFeeAndBalance requireCollaterals balancingWallet minFee maxFee collateral -- fee of the input skeleton. _ -> (minFee, newFee) - computeFeeAndBalance requireCollaterals balancingWallet newMinFee newMaxFee collateralIns balancingUtxos returnCollateralWallet skel + computeFeeAndBalance balancingWallet newMinFee newMaxFee balancingUtxos mCollaterals skel -- | Helper function to group the two real steps of the balancing: balance a -- skeleton around a given fee, and compute the associated collateral inputs -attemptBalancingAndCollaterals :: (MonadBlockChainBalancing m) => Bool -> Wallet -> Collaterals -> BalancingOutputs -> Wallet -> Fee -> TxSkel -> m (Collaterals, TxSkel) -attemptBalancingAndCollaterals requireCollaterals balancingWallet collateralIns balancingUtxos returnCollateralWallet fee skel = do - adjustedCollateralIns <- collateralInsFromFees requireCollaterals fee collateralIns returnCollateralWallet +attemptBalancingAndCollaterals :: (MonadBlockChainBalancing m) => Wallet -> BalancingOutputs -> Fee -> MCollaterals -> TxSkel -> m (MCollaterals, TxSkel) +attemptBalancingAndCollaterals balancingWallet balancingUtxos fee mCollaterals skel = do + adjustedCollateralIns <- collateralsFromFees fee mCollaterals attemptedSkel <- computeBalancedTxSkel balancingWallet balancingUtxos skel fee return (adjustedCollateralIns, attemptedSkel) @@ -216,8 +224,8 @@ attemptBalancingAndCollaterals requireCollaterals balancingWallet collateralIns -- accounting for the ratio to respect between fees and total collaterals, the -- min ada requirements in the associated return collateral and the maximum -- number of collateral inputs authorized by protocol parameters. -collateralInsFromFees :: (MonadBlockChainBalancing m) => Bool -> Fee -> Collaterals -> Wallet -> m Collaterals -collateralInsFromFees True fee collateralIns returnCollateralWallet = do +collateralInsFromFees :: (MonadBlockChainBalancing m) => Fee -> Collaterals -> Wallet -> m Collaterals +collateralInsFromFees fee collateralIns returnCollateralWallet = do -- We retrieve the max number of collateral inputs, with a default of 10. In -- practice this will be around 3. nbMax <- toInteger . fromMaybe 10 . Cardano.protocolParamMaxCollateralInputs . Emulator.pProtocolParams <$> getParams @@ -235,7 +243,12 @@ collateralInsFromFees True fee collateralIns returnCollateralWallet = do let noSuitableCollateralError = MCENoSuitableCollateral fee percentage totalCollateral -- Retrieving and returning the best candidate as a utxo set Set.fromList . fst <$> getOptimalCandidate candidatesRaw returnCollateralWallet noSuitableCollateralError -collateralInsFromFees False _ _ _ = return Set.empty + +-- | This adjusts collateral inputs when necessary +collateralsFromFees :: (MonadBlockChainBalancing m) => Fee -> MCollaterals -> m MCollaterals +collateralsFromFees _ Nothing = return Nothing +collateralsFromFees fee (Just (collateralIns, returnCollateralWallet)) = + Just . (,returnCollateralWallet) <$> collateralInsFromFees fee collateralIns returnCollateralWallet -- | The main computing function for optimal balancing and collaterals. It -- computes the subsets of a set of UTxOs that sum up to a certain target. It @@ -281,15 +294,18 @@ getOptimalCandidate candidates paymentTarget mceError = do -- | This function is essentially a copy of -- https://github.com/input-output-hk/plutus-apps/blob/d4255f05477fd8477ee9673e850ebb9ebb8c9657/plutus-ledger/src/Ledger/Fee.hs#L19 -estimateTxSkelFee :: (MonadBlockChainBalancing m) => TxSkel -> Fee -> Collaterals -> Wallet -> m Fee -estimateTxSkelFee skel fee collateralIns returnCollateralWallet = do +estimateTxSkelFee :: (MonadBlockChainBalancing m) => TxSkel -> Fee -> MCollaterals -> m Fee +estimateTxSkelFee skel fee mCollaterals = do -- We retrieve the necessary data to generate the transaction body params <- getParams managedData <- txSkelInputData skel - managedTxOuts <- lookupUtxosPl $ txSkelKnownTxOutRefs skel <> Set.toList collateralIns + let collateralIns = case mCollaterals of + Nothing -> [] + Just (s, _) -> Set.toList s + managedTxOuts <- lookupUtxosPl $ txSkelKnownTxOutRefs skel <> collateralIns managedValidators <- txSkelInputValidators skel -- We generate the transaction body content, handling errors in the meantime - txBodyContent <- case generateBodyContent fee returnCollateralWallet collateralIns params managedData managedTxOuts managedValidators skel of + txBodyContent <- case generateBodyContent fee params managedData managedTxOuts managedValidators mCollaterals skel of Left err -> throwError $ MCEGenerationError err Right txBodyContent -> return txBodyContent -- We create the actual body and send if for validation diff --git a/src/Cooked/MockChain/BlockChain.hs b/src/Cooked/MockChain/BlockChain.hs index 01828985d..ee7b5f0a0 100644 --- a/src/Cooked/MockChain/BlockChain.hs +++ b/src/Cooked/MockChain/BlockChain.hs @@ -121,7 +121,7 @@ data MockChainLogEntry where MCLogSubmittedTxSkel :: SkelContext -> TxSkel -> MockChainLogEntry -- | Logging a Skeleton as it has been adjusted by the balancing mechanism, -- alongside fee, collateral utxos and return collateral wallet. - MCLogAdjustedTxSkel :: SkelContext -> TxSkel -> Integer -> Set Api.TxOutRef -> Wallet -> MockChainLogEntry + MCLogAdjustedTxSkel :: SkelContext -> TxSkel -> Integer -> Maybe (Set Api.TxOutRef, Wallet) -> MockChainLogEntry -- | Logging the appearance of a new transaction, after a skeleton has been -- successfully sent for validation. MCLogNewTx :: Api.TxId -> MockChainLogEntry diff --git a/src/Cooked/MockChain/Direct.hs b/src/Cooked/MockChain/Direct.hs index 01c08fef6..122cdb784 100644 --- a/src/Cooked/MockChain/Direct.hs +++ b/src/Cooked/MockChain/Direct.hs @@ -348,9 +348,9 @@ instance (Monad m) => MonadBlockChain (MockChainT m) where minAdaSkelUnbal <- if txOptEnsureMinAda . txSkelOpts $ skelUnbal then toTxSkelWithMinAda skelUnbal else return skelUnbal -- We balance the skeleton when requested in the skeleton option, and get -- the associated fee, collateral inputs and return collateral wallet - (skel, fee, collateralIns, returnCollateralWallet) <- balanceTxSkel minAdaSkelUnbal + (skel, fee, mCollaterals) <- balanceTxSkel minAdaSkelUnbal -- We log the adjusted skeleton - gets mcstToSkelContext >>= \ctx -> publish $ MCLogAdjustedTxSkel ctx skel fee collateralIns returnCollateralWallet + gets mcstToSkelContext >>= \ctx -> publish $ MCLogAdjustedTxSkel ctx skel fee mCollaterals -- We retrieve data that will be used in the transaction generation process: -- datums, validators and various kinds of inputs. This idea is to provide a -- rich-enough context for the transaction generation to succeed. @@ -358,11 +358,13 @@ instance (Monad m) => MonadBlockChain (MockChainT m) where insValidators <- txSkelInputValidators skel insMap <- txSkelInputUtxosPl skel refInsMap <- txSkelReferenceInputUtxosPl skel - collateralInsMap <- lookupUtxosPl $ Set.toList collateralIns + collateralInsMap <- case mCollaterals of + Nothing -> return Map.empty + Just (collateralIns, _) -> lookupUtxosPl $ Set.toList collateralIns -- We attempt to generate the transaction associated with the balanced -- skeleton and the retrieved data. This is an internal generation, there is -- no validation involved yet. - cardanoTx <- case generateTx fee returnCollateralWallet collateralIns newParams insData (insMap <> refInsMap <> collateralInsMap) insValidators skel of + cardanoTx <- case generateTx fee newParams insData (insMap <> refInsMap <> collateralInsMap) insValidators mCollaterals skel of Left err -> throwError . MCEGenerationError $ err -- We apply post-generation modification when applicable Right tx -> return $ Ledger.CardanoEmulatorEraTx $ applyRawModOnBalancedTx (txOptUnsafeModTx . txSkelOpts $ skelUnbal) tx diff --git a/src/Cooked/MockChain/GenerateTx.hs b/src/Cooked/MockChain/GenerateTx.hs index 2220f486d..f4af14830 100644 --- a/src/Cooked/MockChain/GenerateTx.hs +++ b/src/Cooked/MockChain/GenerateTx.hs @@ -36,10 +36,6 @@ generateTxOut networkId txSkelOut = runReaderT (toCardanoTxOut txSkelOut) networ generateBodyContent :: -- | fee to apply to body generation Integer -> - -- | wallet to return collaterals to - Wallet -> - -- | collaterals to add to body generation - Set Api.TxOutRef -> -- | parameters of the emulator Emulator.Params -> -- | datums present in our environment @@ -48,10 +44,12 @@ generateBodyContent :: Map Api.TxOutRef Api.TxOut -> -- | validators present in our environment Map Script.ValidatorHash (Script.Versioned Script.Validator) -> + -- | Possible collaterals to use + Maybe (Set Api.TxOutRef, Wallet) -> -- | The skeleton to translate TxSkel -> Either GenerateTxError (Cardano.TxBodyContent Cardano.BuildTx Cardano.ConwayEra) -generateBodyContent fee returnCollateralWallet collateralIns params managedData managedTxOuts managedValidators = +generateBodyContent fee params managedData managedTxOuts managedValidators mCollaterals = flip runReaderT TxContext {..} . txSkelToBodyContent -- | Generates a transaction from a skeleton. Shares the same parameters as @@ -59,10 +57,6 @@ generateBodyContent fee returnCollateralWallet collateralIns params managedData generateTx :: -- | fee to apply to body generation Integer -> - -- | wallet to return collaterals to - Wallet -> - -- | collaterals to add to body generation - Set Api.TxOutRef -> -- | parameters of the emulator Emulator.Params -> -- | datums present in our environment @@ -71,8 +65,10 @@ generateTx :: Map Api.TxOutRef Api.TxOut -> -- | validators present in our environment Map Script.ValidatorHash (Script.Versioned Script.Validator) -> + -- | The collateral inputs and associated collateral wallet + Maybe (Set Api.TxOutRef, Wallet) -> -- | The skeleton to translate TxSkel -> Either GenerateTxError (Cardano.Tx Cardano.ConwayEra) -generateTx fee returnCollateralWallet collateralIns params managedData managedTxOuts managedValidators = +generateTx fee params managedData managedTxOuts managedValidators mCollaterals = flip runReaderT TxContext {..} . txSkelToCardanoTx diff --git a/src/Cooked/MockChain/GenerateTx/Body.hs b/src/Cooked/MockChain/GenerateTx/Body.hs index 3ac292d96..5d628c5f6 100644 --- a/src/Cooked/MockChain/GenerateTx/Body.hs +++ b/src/Cooked/MockChain/GenerateTx/Body.hs @@ -10,13 +10,11 @@ import Cooked.MockChain.GenerateTx.Input qualified as Input import Cooked.MockChain.GenerateTx.Mint qualified as Mint import Cooked.MockChain.GenerateTx.Output qualified as Output import Cooked.MockChain.GenerateTx.Proposal qualified as Proposal -import Cooked.Output import Cooked.Skeleton import Cooked.Wallet import Data.Bifunctor import Data.Map (Map) import Data.Map qualified as Map -import Data.Maybe import Data.Set (Set) import Ledger.Address qualified as Ledger import Ledger.Tx qualified as Ledger @@ -27,8 +25,7 @@ import PlutusLedgerApi.V3 qualified as Api data TxContext where TxContext :: { fee :: Integer, - collateralIns :: Set Api.TxOutRef, - returnCollateralWallet :: Wallet, + mCollaterals :: Maybe (Set Api.TxOutRef, Wallet), params :: Emulator.Params, managedData :: Map Api.DatumHash Api.Datum, managedTxOuts :: Map Api.TxOutRef Api.TxOut, @@ -65,15 +62,7 @@ txSkelToBodyContent skel@TxSkel {..} | txSkelReferenceInputs <- txSkelReferenceT "txSkelToBodyContent: Unable to translate reference inputs." (Cardano.TxInsReference Cardano.BabbageEraOnwardsConway) $ mapM Ledger.toCardanoTxIn txSkelReferenceInputs - (txInsCollateral, txTotalCollateral, txReturnCollateral) <- do - refs <- asks managedTxOuts - txOuts <- forM (Map.keys txSkelIns) $ flip (throwOnLookup "txSkelToBodyContent: Unable to resolve input utxo.") refs - if not $ - null (mapMaybe isScriptOutput txOuts) - && Map.null txSkelMints - && null (mapMaybe txSkelProposalWitness txSkelProposals) - then liftTxGen Collateral.toCollateralTriplet - else return (Cardano.TxInsCollateralNone, Cardano.TxTotalCollateralNone, Cardano.TxReturnCollateralNone) + (txInsCollateral, txTotalCollateral, txReturnCollateral) <- liftTxGen Collateral.toCollateralTriplet txOuts <- mapM (liftTxGen . Output.toCardanoTxOut) txSkelOuts (txValidityLowerBound, txValidityUpperBound) <- throwOnToCardanoError diff --git a/src/Cooked/MockChain/GenerateTx/Collateral.hs b/src/Cooked/MockChain/GenerateTx/Collateral.hs index b7df81534..28a854922 100644 --- a/src/Cooked/MockChain/GenerateTx/Collateral.hs +++ b/src/Cooked/MockChain/GenerateTx/Collateral.hs @@ -21,9 +21,8 @@ import PlutusTx.Numeric qualified as PlutusTx data CollateralContext where CollateralContext :: { managedTxOuts :: Map Api.TxOutRef Api.TxOut, - collateralIns :: Set Api.TxOutRef, fee :: Integer, - returnCollateralWallet :: Wallet, + mCollaterals :: Maybe (Set Api.TxOutRef, Wallet), params :: Emulator.Params } -> CollateralContext @@ -44,59 +43,62 @@ toCollateralTriplet :: Cardano.TxReturnCollateral Cardano.CtxTx Cardano.ConwayEra ) toCollateralTriplet = do - -- Retrieving know outputs - knownTxOuts <- asks managedTxOuts - -- Retrieving the outputs to be used as collateral inputs - collateralInsList <- asks (Set.toList . collateralIns) - -- We build the collateral inputs from this list - txInsCollateral <- - case collateralInsList of - [] -> return Cardano.TxInsCollateralNone - l -> throwOnToCardanoError "txOutRefsToTxInCollateral" (Cardano.TxInsCollateral Cardano.AlonzoEraOnwardsConway <$> mapM Ledger.toCardanoTxIn l) - -- Retrieving the total value in collateral inputs. This fails if one of the - -- collaterals has been been successfully resolved. - collateralInsValue <- do - let collateralInsResolved = mapMaybe (`Map.lookup` knownTxOuts) collateralInsList - when (length collateralInsResolved /= length collateralInsList) $ throwOnString "toCollateralTriplet: unresolved txOutRefs" - return $ mconcat (Api.txOutValue <$> collateralInsResolved) - -- We retrieve the collateral percentage compared to fees. By default, we use - -- 150% which is the current value in the parameters, although the default - -- value should never be used here, as the call is supposed to always succeed. - collateralPercentage <- asks (toInteger . fromMaybe 150 . Cardano.protocolParamCollateralPercent . Emulator.pProtocolParams . params) - -- The total collateral corresponds to the fees multiplied by the collateral - -- percentage. We add 1 because the ledger apparently rounds up this value. - coinTotalCollateral <- asks (Emulator.Coin . (+ 1) . (`div` 100) . (* collateralPercentage) . fee) - -- We create the total collateral based on the computed value - let txTotalCollateral = Cardano.TxTotalCollateral Cardano.BabbageEraOnwardsConway coinTotalCollateral - -- We compute a return collateral value by subtracting the total collateral to - -- the value in collateral inputs - let returnCollateralValue = collateralInsValue <> PlutusTx.negate (toValue coinTotalCollateral) - -- This should never happen, as we always compute the collaterals for the - -- user, but we guard against having some negative elements in the value in - -- case we give more freedom to the users in the future - when (fst (Api.split returnCollateralValue) /= mempty) $ throwOnString "toCollateralTriplet: negative parts in return collateral value" - -- The return collateral is then computed - txReturnCollateral <- - -- If the total collateral equal what the inputs provide, we return `None` - if returnCollateralValue == mempty - then return Cardano.TxReturnCollateralNone - else -- Otherwise, we compute the elements of a new output - do - -- The value is a translation of the remaining value - txReturnCollateralValue <- - Ledger.toCardanoTxOutValue - <$> throwOnToCardanoError - "toCollateralTriplet: cannot build return collateral value" - (Ledger.toCardanoValue returnCollateralValue) - -- The address is the one from the return collateral wallet, which is - -- required to exist here. - address <- do - returnCollateralWallet <- asks returnCollateralWallet - networkId <- asks (Emulator.pNetworkId . params) - throwOnToCardanoError "toCollateralTriplet: cannot build return collateral address" $ - Ledger.toCardanoAddressInEra networkId (walletAddress returnCollateralWallet) - -- The return collateral is built up from those elements - return $ - Cardano.TxReturnCollateral Cardano.BabbageEraOnwardsConway $ - Cardano.TxOut address txReturnCollateralValue Cardano.TxOutDatumNone Cardano.ReferenceScriptNone - return (txInsCollateral, txTotalCollateral, txReturnCollateral) + -- Retrieving the optional collaterals and associated wallet + mCollaterals <- asks mCollaterals + case mCollaterals of + -- If this is nothing, it means no collateral is needed (no script involved) + Nothing -> return (Cardano.TxInsCollateralNone, Cardano.TxTotalCollateralNone, Cardano.TxReturnCollateralNone) + Just (Set.toList -> collateralInsList, returnCollateralWallet) -> do + -- Retrieving know outputs + knownTxOuts <- asks managedTxOuts + -- We build the collateral inputs from this list + txInsCollateral <- + case collateralInsList of + [] -> return Cardano.TxInsCollateralNone + l -> throwOnToCardanoError "txOutRefsToTxInCollateral" (Cardano.TxInsCollateral Cardano.AlonzoEraOnwardsConway <$> mapM Ledger.toCardanoTxIn l) + -- Retrieving the total value in collateral inputs. This fails if one of the + -- collaterals has been been successfully resolved. + collateralInsValue <- do + let collateralInsResolved = mapMaybe (`Map.lookup` knownTxOuts) collateralInsList + when (length collateralInsResolved /= length collateralInsList) $ throwOnString "toCollateralTriplet: unresolved txOutRefs" + return $ mconcat (Api.txOutValue <$> collateralInsResolved) + -- We retrieve the collateral percentage compared to fees. By default, we use + -- 150% which is the current value in the parameters, although the default + -- value should never be used here, as the call is supposed to always succeed. + collateralPercentage <- asks (toInteger . fromMaybe 150 . Cardano.protocolParamCollateralPercent . Emulator.pProtocolParams . params) + -- The total collateral corresponds to the fees multiplied by the collateral + -- percentage. We add 1 because the ledger apparently rounds up this value. + coinTotalCollateral <- asks (Emulator.Coin . (+ 1) . (`div` 100) . (* collateralPercentage) . fee) + -- We create the total collateral based on the computed value + let txTotalCollateral = Cardano.TxTotalCollateral Cardano.BabbageEraOnwardsConway coinTotalCollateral + -- We compute a return collateral value by subtracting the total collateral to + -- the value in collateral inputs + let returnCollateralValue = collateralInsValue <> PlutusTx.negate (toValue coinTotalCollateral) + -- This should never happen, as we always compute the collaterals for the + -- user, but we guard against having some negative elements in the value in + -- case we give more freedom to the users in the future + when (fst (Api.split returnCollateralValue) /= mempty) $ throwOnString "toCollateralTriplet: negative parts in return collateral value" + -- The return collateral is then computed + txReturnCollateral <- + -- If the total collateral equal what the inputs provide, we return + -- `TxReturnCollateralNone`, otherwise, we compute the new output + if returnCollateralValue == mempty + then return Cardano.TxReturnCollateralNone + else do + -- The value is a translation of the remaining value + txReturnCollateralValue <- + Ledger.toCardanoTxOutValue + <$> throwOnToCardanoError + "toCollateralTriplet: cannot build return collateral value" + (Ledger.toCardanoValue returnCollateralValue) + -- The address is the one from the return collateral wallet, which is + -- required to exist here. + address <- do + networkId <- asks (Emulator.pNetworkId . params) + throwOnToCardanoError "toCollateralTriplet: cannot build return collateral address" $ + Ledger.toCardanoAddressInEra networkId (walletAddress returnCollateralWallet) + -- The return collateral is built up from those elements + return $ + Cardano.TxReturnCollateral Cardano.BabbageEraOnwardsConway $ + Cardano.TxOut address txReturnCollateralValue Cardano.TxOutDatumNone Cardano.ReferenceScriptNone + return (txInsCollateral, txTotalCollateral, txReturnCollateral) diff --git a/tests/Cooked/BalancingSpec.hs b/tests/Cooked/BalancingSpec.hs index 536628aca..878361fb0 100644 --- a/tests/Cooked/BalancingSpec.hs +++ b/tests/Cooked/BalancingSpec.hs @@ -45,7 +45,7 @@ initialDistributionBalancing = paysPK alice (Script.ada 105 <> banana 2) `withDatumHash` () ] -type TestBalancingOutcome = (TxSkel, TxSkel, Integer, Set Api.TxOutRef, [Api.TxOutRef]) +type TestBalancingOutcome = (TxSkel, TxSkel, Integer, Maybe (Set Api.TxOutRef, Wallet), [Api.TxOutRef]) spendsScriptUtxo :: (MonadBlockChain m) => Bool -> m (Map Api.TxOutRef TxSkelRedeemer) spendsScriptUtxo False = return Map.empty @@ -93,10 +93,10 @@ testingBalancingTemplate toBobValue toAliceValue spendSearch balanceSearch colla }, txSkelSigners = [alice] } - (skel', fee, cols, _) <- balanceTxSkel skel + (skel', fee, mCols) <- balanceTxSkel skel void $ validateTxSkel skel nonOnlyValueUtxos <- runUtxoSearch $ utxosAtSearch alice `filterWithPred` \o -> isJust (Api.txOutReferenceScript o) || (Api.txOutDatum o /= Api.NoOutputDatum) - return (skel, skel', fee, cols, fst <$> nonOnlyValueUtxos) + return (skel, skel', fee, mCols, fst <$> nonOnlyValueUtxos) aliceNonOnlyValueUtxos :: (MonadBlockChain m) => UtxoSearch m Api.TxOut aliceNonOnlyValueUtxos = utxosAtSearch alice `filterWithPred` \o -> isJust (Api.txOutReferenceScript o) || (Api.txOutDatum o /= Api.NoOutputDatum) @@ -153,8 +153,8 @@ balanceReduceFee = do { txSkelOuts = [paysPK bob (Script.ada 50)], txSkelSigners = [alice] } - (skelBalanced, feeBalanced, cols, rColWal) <- balanceTxSkel skelAutoFee - feeBalanced' <- estimateTxSkelFee skelBalanced feeBalanced cols rColWal + (skelBalanced, feeBalanced, mCols) <- balanceTxSkel skelAutoFee + feeBalanced' <- estimateTxSkelFee skelBalanced feeBalanced mCols let skelManualFee = skelAutoFee { txSkelOpts = @@ -162,8 +162,8 @@ balanceReduceFee = do { txOptFeePolicy = ManualFee (feeBalanced - 1) } } - (skelBalancedManual, feeBalancedManual, colsManual, rColWalManual) <- balanceTxSkel skelManualFee - feeBalancedManual' <- estimateTxSkelFee skelBalancedManual feeBalancedManual colsManual rColWalManual + (skelBalancedManual, feeBalancedManual, mColsManual) <- balanceTxSkel skelManualFee + feeBalancedManual' <- estimateTxSkelFee skelBalancedManual feeBalancedManual mColsManual return (feeBalanced, feeBalanced', feeBalancedManual, feeBalancedManual') reachingMagic :: (MonadBlockChain m) => m () @@ -192,7 +192,8 @@ insNb :: (IsProp prop) => Int -> ResProp prop insNb is (_, TxSkel {..}, _, _, _) = testBool $ length txSkelIns == is colInsNb :: (IsProp prop) => Int -> ResProp prop -colInsNb cis (_, _, _, refs, _) = testBool $ cis == length refs +colInsNb cis (_, _, _, Nothing, _) = testBool $ cis == 0 +colInsNb cis (_, _, _, Just (refs, _), _) = testBool $ cis == length refs retOutsNb :: (IsProp prop) => Int -> ResProp prop retOutsNb ros (_, _, _, _, refs) = testBool $ ros == length refs @@ -249,27 +250,74 @@ tests = [ testBalancingFailsWith "Balancing does not occur when not requested, fails with empty inputs" failsWithEmptyTxIns - (simplePaymentToBob 20_000_000 0 0 0 False (setCollateralWallet alice . setDontBalance . setFixedFee 1_000_000)), + ( simplePaymentToBob + 20_000_000 + 0 + 0 + 0 + False + (setCollateralWallet alice . setDontBalance . setFixedFee 1_000_000) + ), testBalancingFailsWith "Balancing does not occur when not requested, fails with too small inputs" failsWithValueNotConserved - (testingBalancingTemplate (Script.ada 50) mempty (aliceNAdaUtxos 8) emptySearch emptySearch False (setCollateralWallet alice . setDontBalance . setFixedFee 1_000_000)), + ( testingBalancingTemplate + (Script.ada 50) + mempty + (aliceNAdaUtxos 8) + emptySearch + emptySearch + False + (setCollateralWallet alice . setDontBalance . setFixedFee 1_000_000) + ), testBalancingSucceedsWith "It is possible to balance the transaction by hand without collaterals" [hasFee 1_000_000, insNb 1, additionalOutsNb 0, colInsNb 0, retOutsNb 3] - (testingBalancingTemplate (Script.ada 7) mempty (aliceNAdaUtxos 8) emptySearch emptySearch False (setCollateralWallet alice . setDontBalance . setFixedFee 1_000_000)), + ( testingBalancingTemplate + (Script.ada 7) + mempty + (aliceNAdaUtxos 8) + emptySearch + emptySearch + False + (setCollateralWallet alice . setDontBalance . setFixedFee 1_000_000) + ), testBalancingSucceedsWith "It is also possible to balance the transaction by hand with collaterals" [hasFee 1_000_000, insNb 2, additionalOutsNb 0, colInsNb 1, retOutsNb 3] - (testingBalancingTemplate (Script.ada 49) mempty (aliceNAdaUtxos 8) emptySearch emptySearch True (setCollateralWallet alice . setDontBalance . setFixedFee 1_000_000)), + ( testingBalancingTemplate + (Script.ada 49) + mempty + (aliceNAdaUtxos 8) + emptySearch + emptySearch + True + (setCollateralWallet alice . setDontBalance . setFixedFee 1_000_000) + ), testBalancingFailsWith "A collateral wallet needs to be provided when auto balancing is enabled" failsLackOfCollateralWallet - (testingBalancingTemplate (Script.ada 7) mempty (aliceNAdaUtxos 8) emptySearch emptySearch False (setDontBalance . setFixedFee 1_000_000)), + ( testingBalancingTemplate + (Script.ada 7) + mempty + (aliceNAdaUtxos 8) + emptySearch + emptySearch + False + (setDontBalance . setFixedFee 1_000_000) + ), testBalancingSucceedsWith "We can also directly give a set of collateral utxos" [hasFee 1_000_000, insNb 1, additionalOutsNb 0, colInsNb 1, retOutsNb 3] - (testingBalancingTemplate (Script.ada 7) mempty (aliceNAdaUtxos 8) emptySearch (aliceNAdaUtxos 8) False (setDontBalance . setFixedFee 1_000_000)) + ( testingBalancingTemplate + (Script.ada 7) + mempty + (aliceNAdaUtxos 8) + emptySearch + (aliceNAdaUtxos 8) + False + (setDontBalance . setFixedFee 1_000_000) + ) ], testGroup "Manual balancing with auto fee" @@ -281,28 +329,75 @@ tests = [ testBalancingSucceedsWith "We can auto balance a transaction with auto fee" [insNb 1, additionalOutsNb 1, colInsNb 1, retOutsNb 3] - (simplePaymentToBob 20_000_000 0 0 0 False id), + ( simplePaymentToBob + 20_000_000 + 0 + 0 + 0 + False + id + ), testCase "Auto fee are minimal: less fee will lead to strictly smaller fee than Cardano's estimate" $ testSucceedsFrom' def - (\(feeBalanced, feeBalanced', feeBalancedManual, feeBalancedManual') _ -> testBool $ feeBalanced' <= feeBalanced && feeBalancedManual' > feeBalancedManual) + ( \(feeBalanced, feeBalanced', feeBalancedManual, feeBalancedManual') _ -> + testBool $ + feeBalanced' <= feeBalanced && feeBalancedManual' > feeBalancedManual + ) initialDistributionBalancing balanceReduceFee, testCase "The auto-fee process can sometimes recover from a temporary balancing error..." $ - testSucceedsFrom def initialDistributionBalancing (simplePaymentToBob 103_500_000 0 0 0 False id), + testSucceedsFrom + def + initialDistributionBalancing + ( simplePaymentToBob + 103_500_000 + 0 + 0 + 0 + False + id + ), testCase "... but not always" $ - testFailsFrom def failsAtBalancing initialDistributionBalancing (simplePaymentToBob 104_000_000 0 0 0 False id), + testFailsFrom + def + failsAtBalancing + initialDistributionBalancing + ( simplePaymentToBob + 104_000_000 + 0 + 0 + 0 + False + id + ), testCase "The auto-fee process can recover from a temporary collateral error..." $ testSucceedsFrom def initialDistributionBalancing - (testingBalancingTemplate (Script.ada 100) mempty emptySearch emptySearch (aliceNAdaUtxos 2) False id), + ( testingBalancingTemplate + (Script.ada 100) + mempty + emptySearch + emptySearch + (aliceNAdaUtxos 2) + False + id + ), testCase "... but not always" $ testFailsFrom def failsAtCollaterals initialDistributionBalancing - (testingBalancingTemplate (Script.ada 100) mempty (utxosAtSearch alice) emptySearch (aliceNAdaUtxos 1) False id), + ( testingBalancingTemplate + (Script.ada 100) + mempty + (utxosAtSearch alice) + emptySearch + (aliceNAdaUtxos 1) + False + id + ), testCase "Reaching magical spot with the exact balance during auto fee computation" $ testSucceedsFrom def @@ -314,66 +409,174 @@ tests = [ testBalancingSucceedsWith "We can use a single utxo for balancing purpose" [hasFee 1_000_000, insNb 1, additionalOutsNb 1, colInsNb 1, retOutsNb 3] - (simplePaymentToBob 20_000_000 0 0 0 False (setFixedFee 1_000_000)), + ( simplePaymentToBob + 20_000_000 + 0 + 0 + 0 + False + (setFixedFee 1_000_000) + ), testBalancingSucceedsWith "We can use several utxos for balancing with ridiculously high fee" [hasFee 40_000_000, insNb 3, additionalOutsNb 1, colInsNb 3, retOutsNb 3] - (simplePaymentToBob 20_000_000 0 0 0 False (setFixedFee 40_000_000)), + ( simplePaymentToBob + 20_000_000 + 0 + 0 + 0 + False + (setFixedFee 40_000_000) + ), testBalancingFailsWith "We cannot balance with too little fee" failsWithTooLittleFee - (simplePaymentToBob 20_000_000 0 0 0 False (setFixedFee 150_000)), + ( simplePaymentToBob + 20_000_000 + 0 + 0 + 0 + False + (setFixedFee 150_000) + ), testBalancingFailsWith "Fee are rightfully included in the balancing process, which fails when they are too high" (failsAtBalancingWith (Script.ada 1) alice) - (simplePaymentToBob 100_000_000 0 0 0 False (setFixedFee 6_000_000)), + ( simplePaymentToBob + 100_000_000 + 0 + 0 + 0 + False + (setFixedFee 6_000_000) + ), testBalancingFailsWith "Collaterals are rightfully included in the balancing process, which fails when they are too high" (failsAtCollateralsWith 80_000_000) - (simplePaymentToBob 6_000_000 0 0 0 False (setFixedFee 80_000_000)), + ( simplePaymentToBob + 6_000_000 + 0 + 0 + 0 + False + (setFixedFee 80_000_000) + ), testBalancingSucceedsWith "Exactly the right amount leads to no output change" [hasFee 2_000_000, insNb 3, additionalOutsNb 0, colInsNb 1, retOutsNb 3] - (simplePaymentToBob 65_000_000 3 6 0 False (setFixedFee 2_000_000)), + ( simplePaymentToBob + 65_000_000 + 3 + 6 + 0 + False + (setFixedFee 2_000_000) + ), testBalancingSucceedsWith "It still leads to no output change when requesting a new output" [hasFee 2_000_000, insNb 3, additionalOutsNb 0, colInsNb 1, retOutsNb 3] - (simplePaymentToBob 65_000_000 3 6 0 False (setDontAdjustOutput . setFixedFee 2_000_000)), + ( simplePaymentToBob + 65_000_000 + 3 + 6 + 0 + False + (setDontAdjustOutput . setFixedFee 2_000_000) + ), testBalancingSucceedsWith "1 lovelace more than the exact right amount leads to an additional output" [hasFee 2_000_000, insNb 3, additionalOutsNb 1, colInsNb 1, retOutsNb 3] - (simplePaymentToBob 65_000_001 3 6 0 False (setFixedFee 2_000_000)), + ( simplePaymentToBob + 65_000_001 + 3 + 6 + 0 + False + (setFixedFee 2_000_000) + ), testBalancingSucceedsWith "1 lovelace less than the exact right amount leads to an additional output to account for minAda" [hasFee 2_000_000, insNb 3, additionalOutsNb 1, colInsNb 1, retOutsNb 3] - (simplePaymentToBob 65_000_001 3 6 0 False (setFixedFee 2_000_000)), + ( simplePaymentToBob + 65_000_001 + 3 + 6 + 0 + False + (setFixedFee 2_000_000) + ), testBalancingSucceedsWith "We can merge assets to an existing outputs at the balancing wallet address" [hasFee 2_000_000, insNb 1, additionalOutsNb 0, colInsNb 1, retOutsNb 3] - (bothPaymentsToBobAndAlice 6_000_000 False (setFixedFee 2_000_000)), + ( bothPaymentsToBobAndAlice + 6_000_000 + False + (setFixedFee 2_000_000) + ), testBalancingSucceedsWith "We can create a new output at the balancing wallet address even if one already exists" [hasFee 2_000_000, insNb 1, additionalOutsNb 1, colInsNb 1, retOutsNb 3] - (bothPaymentsToBobAndAlice 6_000_000 False (setFixedFee 2_000_000 . setDontAdjustOutput)), + ( bothPaymentsToBobAndAlice + 6_000_000 + False + (setFixedFee 2_000_000 . setDontAdjustOutput) + ), testBalancingSucceedsWith "We can balance transactions with non-Script.ada assets" [hasFee 2_000_000, insNb 1, additionalOutsNb 1, colInsNb 1, retOutsNb 3] - (simplePaymentToBob 0 0 5 0 False (setFixedFee 2_000_000 . setEnsureMinAda)), + ( simplePaymentToBob + 0 + 0 + 5 + 0 + False + (setFixedFee 2_000_000 . setEnsureMinAda) + ), testBalancingSucceedsWith "Successful balancing with multiple assets" [hasFee 1_000_000, insNb 2, additionalOutsNb 1, colInsNb 1, retOutsNb 3] - (simplePaymentToBob 0 2 5 0 False (setEnsureMinAda . setFixedFee 1_000_000)), + ( simplePaymentToBob + 0 + 2 + 5 + 0 + False + (setEnsureMinAda . setFixedFee 1_000_000) + ), testBalancingFailsWith "Unsuccessful balancing with multiple assets in non value only utxos" (failsAtBalancingWith (banana 4) alice) - (simplePaymentToBob 0 2 5 4 False (setEnsureMinAda . setFixedFee 1_000_000)), + ( simplePaymentToBob + 0 + 2 + 5 + 4 + False + (setEnsureMinAda . setFixedFee 1_000_000) + ), testBalancingSucceedsWith "Successful balancing with multiple assets and explicit utxo set, reference script is lost" [hasFee 1_000_000, insNb 3, additionalOutsNb 1, colInsNb 1, retOutsNb 2] - (testingBalancingTemplate (apple 2 <> orange 5 <> banana 4) mempty emptySearch (utxosAtSearch alice) emptySearch False (setEnsureMinAda . setFixedFee 1_000_000)), + ( testingBalancingTemplate + (apple 2 <> orange 5 <> banana 4) + mempty + emptySearch + (utxosAtSearch alice) + emptySearch + False + (setEnsureMinAda . setFixedFee 1_000_000) + ), testBalancingSucceedsWith "Successful balancing with excess initial consumption" [hasFee 1_000_000, insNb 5, additionalOutsNb 1, colInsNb 1, retOutsNb 3] - (testingBalancingTemplate mempty mempty (onlyValueOutputsAtSearch alice) emptySearch emptySearch False (setFixedFee 1_000_000)) + ( testingBalancingTemplate + mempty + mempty + (onlyValueOutputsAtSearch alice) + emptySearch + emptySearch + False + (setFixedFee 1_000_000) + ) ] ] diff --git a/tests/Cooked/ShowBSSpec.hs b/tests/Cooked/ShowBSSpec.hs index e786bbdd5..806ab4a29 100644 --- a/tests/Cooked/ShowBSSpec.hs +++ b/tests/Cooked/ShowBSSpec.hs @@ -82,7 +82,7 @@ tests = ) ], testCase "printing the 'TxInfo' from a validator produces the expected string" $ - let isExpectedString = (==) "(Script context:Script Tx info:(inputs:[(TxInInfo (TxOutRef (TxId \"ad1f1a5a545dbe830383711cc3302f8fb5eb41c5154a7bc5336921abd453f001\") 0) (TxOut (Address (ScriptCredential (ScriptHash \"d02455c9a6cc9296707d031d8c668ea75612663d111d54a4155e4371\")) Nothing) (Value (fromList [((CurrencySymbol \"\"),(fromList [((TokenName \"\"),30000000)]))])) (OutputDatum (Datum (BuiltinData (Constr 0 [])))) Nothing))]reference inputs:[]outputs:[(TxOut (Address (PubKeyCredential (PubKeyHash \"a2c20c77887ace1cd986193e4e75babd8993cfd56995cd5cfce609c2\")) Nothing) (Value (fromList [((CurrencySymbol \"\"),(fromList [((TokenName \"\"),29515587)]))])) NoOutputDatum Nothing)]fees:(Lovelace 484413)minted value:(Value (fromList []))certificates:[]wdrl:(fromList [])valid range:(Interval (LowerBound NegInf True) (UpperBound PosInf True))signatories:[(PubKeyHash \"a2c20c77887ace1cd986193e4e75babd8993cfd56995cd5cfce609c2\")]redeemers:(fromList [((Spending (TxOutRef (TxId \"ad1f1a5a545dbe830383711cc3302f8fb5eb41c5154a7bc5336921abd453f001\") 0)),(Redeemer (BuiltinData (Constr 1 []))))])datums:(fromList [])transaction id:(TxId \"6fb3cb19b99bd2a5e880f64a80555ea096e583c269597329d03adb3a69ca53dd\")votes:(fromList [])proposals:[]treasury amount:Nothingtreasury donation:Nothing)Script purpose:(Spending (TxOutRef (TxId \"ad1f1a5a545dbe830383711cc3302f8fb5eb41c5154a7bc5336921abd453f001\") 0)))" + let isExpectedString = (==) "(Script context:Script Tx info:(inputs:[(TxInInfo (TxOutRef (TxId \"41892ee014fdfc05ace214461e61ba074679c711c761fdf5eb655d08acfb5450\") 0) (TxOut (Address (ScriptCredential (ScriptHash \"d02455c9a6cc9296707d031d8c668ea75612663d111d54a4155e4371\")) Nothing) (Value (fromList [((CurrencySymbol \"\"),(fromList [((TokenName \"\"),30000000)]))])) (OutputDatum (Datum (BuiltinData (Constr 0 [])))) Nothing))]reference inputs:[]outputs:[(TxOut (Address (PubKeyCredential (PubKeyHash \"a2c20c77887ace1cd986193e4e75babd8993cfd56995cd5cfce609c2\")) Nothing) (Value (fromList [((CurrencySymbol \"\"),(fromList [((TokenName \"\"),29515587)]))])) NoOutputDatum Nothing)]fees:(Lovelace 484413)minted value:(Value (fromList []))certificates:[]wdrl:(fromList [])valid range:(Interval (LowerBound NegInf True) (UpperBound PosInf True))signatories:[(PubKeyHash \"a2c20c77887ace1cd986193e4e75babd8993cfd56995cd5cfce609c2\")]redeemers:(fromList [((Spending (TxOutRef (TxId \"41892ee014fdfc05ace214461e61ba074679c711c761fdf5eb655d08acfb5450\") 0)),(Redeemer (BuiltinData (Constr 1 []))))])datums:(fromList [])transaction id:(TxId \"0c2e3a3357b837273758c9d15647b039763a1d326341087dacc830afa324accb\")votes:(fromList [])proposals:[]treasury amount:Nothingtreasury donation:Nothing)Script purpose:(Spending (TxOutRef (TxId \"41892ee014fdfc05ace214461e61ba074679c711c761fdf5eb655d08acfb5450\") 0)))" in testFails (def @PrettyCookedOpts) ( isCekEvaluationFailureWithMsg From 34aa2af7149ee6969c6a7339cc275d37b93184b2 Mon Sep 17 00:00:00 2001 From: mmontin Date: Thu, 11 Jul 2024 14:36:12 +0200 Subject: [PATCH 17/44] =?UTF-8?q?2=20first=20test=20groups=20pass=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/Cooked/BalancingSpec.hs | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/tests/Cooked/BalancingSpec.hs b/tests/Cooked/BalancingSpec.hs index 878361fb0..57ff89e3f 100644 --- a/tests/Cooked/BalancingSpec.hs +++ b/tests/Cooked/BalancingSpec.hs @@ -140,8 +140,7 @@ noBalanceMaxFee = do txSkelOpts = def { txOptBalancingPolicy = DoNotBalance, - txOptFeePolicy = AutoFeeComputation, - txOptCollateralUtxos = CollateralUtxosFromSet (Set.singleton txOutRef) alice + txOptFeePolicy = AutoFeeComputation }, txSkelSigners = [alice] } @@ -295,8 +294,20 @@ tests = (setCollateralWallet alice . setDontBalance . setFixedFee 1_000_000) ), testBalancingFailsWith - "A collateral wallet needs to be provided when auto balancing is enabled" + "A collateral wallet needs to be provided when auto balancing is enabled and script are involved..." failsLackOfCollateralWallet + ( testingBalancingTemplate + (Script.ada 49) + mempty + (aliceNAdaUtxos 8) + emptySearch + emptySearch + True + (setDontBalance . setFixedFee 1_000_000) + ), + testBalancingSucceedsWith + "... but is not necessary otherwise." + [hasFee 1_000_000, insNb 1, additionalOutsNb 0, colInsNb 0, retOutsNb 3] ( testingBalancingTemplate (Script.ada 7) mempty @@ -307,8 +318,20 @@ tests = (setDontBalance . setFixedFee 1_000_000) ), testBalancingSucceedsWith - "We can also directly give a set of collateral utxos" - [hasFee 1_000_000, insNb 1, additionalOutsNb 0, colInsNb 1, retOutsNb 3] + "We can also directly give a set of collateral utxos..." + [hasFee 1_000_000, insNb 2, additionalOutsNb 0, colInsNb 1, retOutsNb 3] + ( testingBalancingTemplate + (ada 49) + mempty + (aliceNAdaUtxos 8) + emptySearch + (aliceNAdaUtxos 8) + True + (setDontBalance . setFixedFee 1_000_000) + ), + testBalancingSucceedsWith + "... which will be ignored when no script is involved." + [hasFee 1_000_000, insNb 1, additionalOutsNb 0, colInsNb 0, retOutsNb 3] ( testingBalancingTemplate (Script.ada 7) mempty From 291df890b61aabaa5260e7fb38628119afd0df62 Mon Sep 17 00:00:00 2001 From: mmontin Date: Thu, 11 Jul 2024 16:33:41 +0200 Subject: [PATCH 18/44] all tests fixed --- tests/Cooked/BalancingSpec.hs | 68 +++++++++++++++++++++++------------ 1 file changed, 45 insertions(+), 23 deletions(-) diff --git a/tests/Cooked/BalancingSpec.hs b/tests/Cooked/BalancingSpec.hs index 57ff89e3f..210d0b5c0 100644 --- a/tests/Cooked/BalancingSpec.hs +++ b/tests/Cooked/BalancingSpec.hs @@ -330,7 +330,7 @@ tests = (setDontBalance . setFixedFee 1_000_000) ), testBalancingSucceedsWith - "... which will be ignored when no script is involved." + "... which will be ignored when no script is involved" [hasFee 1_000_000, insNb 1, additionalOutsNb 0, colInsNb 0, retOutsNb 3] ( testingBalancingTemplate (Script.ada 7) @@ -350,8 +350,19 @@ tests = testGroup "Auto balancing with auto fee" [ testBalancingSucceedsWith - "We can auto balance a transaction with auto fee" + "We can auto balance a transaction with auto fee, collaterals and no pk inputs" [insNb 1, additionalOutsNb 1, colInsNb 1, retOutsNb 3] + ( simplePaymentToBob + 20_000_000 + 0 + 0 + 0 + True + id + ), + testBalancingSucceedsWith + "We can auto balance a transaction with auto fee, no collateral and no script inputs" + [insNb 1, additionalOutsNb 1, colInsNb 0, retOutsNb 3] ( simplePaymentToBob 20_000_000 0 @@ -360,6 +371,17 @@ tests = False id ), + testBalancingSucceedsWith + "We can auto balance a transaction with auto fee, collaterals, script and pk inputs" + [insNb 2, additionalOutsNb 1, colInsNb 1, retOutsNb 3] + ( simplePaymentToBob + 60_000_000 + 0 + 0 + 0 + True + id + ), testCase "Auto fee are minimal: less fee will lead to strictly smaller fee than Cardano's estimate" $ testSucceedsFrom' def @@ -374,7 +396,7 @@ tests = def initialDistributionBalancing ( simplePaymentToBob - 103_500_000 + 103_650_000 0 0 0 @@ -399,12 +421,12 @@ tests = def initialDistributionBalancing ( testingBalancingTemplate - (Script.ada 100) + (Script.ada 142) mempty emptySearch emptySearch (aliceNAdaUtxos 2) - False + True id ), testCase "... but not always" $ @@ -413,12 +435,12 @@ tests = failsAtCollaterals initialDistributionBalancing ( testingBalancingTemplate - (Script.ada 100) + (Script.ada 142) mempty (utxosAtSearch alice) emptySearch (aliceNAdaUtxos 1) - False + True id ), testCase "Reaching magical spot with the exact balance during auto fee computation" $ @@ -431,7 +453,7 @@ tests = "Auto balancing with manual fee" [ testBalancingSucceedsWith "We can use a single utxo for balancing purpose" - [hasFee 1_000_000, insNb 1, additionalOutsNb 1, colInsNb 1, retOutsNb 3] + [hasFee 1_000_000, insNb 1, additionalOutsNb 1, colInsNb 0, retOutsNb 3] ( simplePaymentToBob 20_000_000 0 @@ -442,7 +464,7 @@ tests = ), testBalancingSucceedsWith "We can use several utxos for balancing with ridiculously high fee" - [hasFee 40_000_000, insNb 3, additionalOutsNb 1, colInsNb 3, retOutsNb 3] + [hasFee 40_000_000, insNb 3, additionalOutsNb 1, colInsNb 0, retOutsNb 3] ( simplePaymentToBob 20_000_000 0 @@ -477,16 +499,16 @@ tests = "Collaterals are rightfully included in the balancing process, which fails when they are too high" (failsAtCollateralsWith 80_000_000) ( simplePaymentToBob - 6_000_000 + 48_000_000 0 0 0 - False + True (setFixedFee 80_000_000) ), testBalancingSucceedsWith "Exactly the right amount leads to no output change" - [hasFee 2_000_000, insNb 3, additionalOutsNb 0, colInsNb 1, retOutsNb 3] + [hasFee 2_000_000, insNb 3, additionalOutsNb 0, colInsNb 0, retOutsNb 3] ( simplePaymentToBob 65_000_000 3 @@ -497,7 +519,7 @@ tests = ), testBalancingSucceedsWith "It still leads to no output change when requesting a new output" - [hasFee 2_000_000, insNb 3, additionalOutsNb 0, colInsNb 1, retOutsNb 3] + [hasFee 2_000_000, insNb 3, additionalOutsNb 0, colInsNb 0, retOutsNb 3] ( simplePaymentToBob 65_000_000 3 @@ -508,7 +530,7 @@ tests = ), testBalancingSucceedsWith "1 lovelace more than the exact right amount leads to an additional output" - [hasFee 2_000_000, insNb 3, additionalOutsNb 1, colInsNb 1, retOutsNb 3] + [hasFee 2_000_000, insNb 3, additionalOutsNb 1, colInsNb 0, retOutsNb 3] ( simplePaymentToBob 65_000_001 3 @@ -519,9 +541,9 @@ tests = ), testBalancingSucceedsWith "1 lovelace less than the exact right amount leads to an additional output to account for minAda" - [hasFee 2_000_000, insNb 3, additionalOutsNb 1, colInsNb 1, retOutsNb 3] + [hasFee 2_000_000, insNb 3, additionalOutsNb 1, colInsNb 0, retOutsNb 3] ( simplePaymentToBob - 65_000_001 + 64_999_999 3 6 0 @@ -530,7 +552,7 @@ tests = ), testBalancingSucceedsWith "We can merge assets to an existing outputs at the balancing wallet address" - [hasFee 2_000_000, insNb 1, additionalOutsNb 0, colInsNb 1, retOutsNb 3] + [hasFee 2_000_000, insNb 1, additionalOutsNb 0, colInsNb 0, retOutsNb 3] ( bothPaymentsToBobAndAlice 6_000_000 False @@ -538,15 +560,15 @@ tests = ), testBalancingSucceedsWith "We can create a new output at the balancing wallet address even if one already exists" - [hasFee 2_000_000, insNb 1, additionalOutsNb 1, colInsNb 1, retOutsNb 3] + [hasFee 2_000_000, insNb 1, additionalOutsNb 1, colInsNb 0, retOutsNb 3] ( bothPaymentsToBobAndAlice 6_000_000 False (setFixedFee 2_000_000 . setDontAdjustOutput) ), testBalancingSucceedsWith - "We can balance transactions with non-Script.ada assets" - [hasFee 2_000_000, insNb 1, additionalOutsNb 1, colInsNb 1, retOutsNb 3] + "We can balance transactions with non-ada assets" + [hasFee 2_000_000, insNb 1, additionalOutsNb 1, colInsNb 0, retOutsNb 3] ( simplePaymentToBob 0 0 @@ -557,7 +579,7 @@ tests = ), testBalancingSucceedsWith "Successful balancing with multiple assets" - [hasFee 1_000_000, insNb 2, additionalOutsNb 1, colInsNb 1, retOutsNb 3] + [hasFee 1_000_000, insNb 2, additionalOutsNb 1, colInsNb 0, retOutsNb 3] ( simplePaymentToBob 0 2 @@ -579,7 +601,7 @@ tests = ), testBalancingSucceedsWith "Successful balancing with multiple assets and explicit utxo set, reference script is lost" - [hasFee 1_000_000, insNb 3, additionalOutsNb 1, colInsNb 1, retOutsNb 2] + [hasFee 1_000_000, insNb 3, additionalOutsNb 1, colInsNb 0, retOutsNb 2] ( testingBalancingTemplate (apple 2 <> orange 5 <> banana 4) mempty @@ -591,7 +613,7 @@ tests = ), testBalancingSucceedsWith "Successful balancing with excess initial consumption" - [hasFee 1_000_000, insNb 5, additionalOutsNb 1, colInsNb 1, retOutsNb 3] + [hasFee 1_000_000, insNb 5, additionalOutsNb 1, colInsNb 0, retOutsNb 3] ( testingBalancingTemplate mempty mempty From 5f696fdce161f7f79c258ccc057483a3432f121b Mon Sep 17 00:00:00 2001 From: mmontin Date: Thu, 11 Jul 2024 16:58:53 +0200 Subject: [PATCH 19/44] doc --- doc/BALANCING.md | 41 ++++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/doc/BALANCING.md b/doc/BALANCING.md index 5bb6f8984..14183e5e6 100644 --- a/doc/BALANCING.md +++ b/doc/BALANCING.md @@ -11,16 +11,18 @@ is currently implemented, and which options affect this mechanism. ## Balancing: what this is about. ### Balancing requirements + In Cardano, transactions must be balanced before they can be submitted for validation. This means the equation `input value + minted value = output value + burned value + fee` must be satisfied to proceed to phase 2 of the validation -process. Additionally, collaterals must be provided to account for transaction -failures in phase 2. These collaterals are related to the fee by the following -inequation: `totalCollateral >= fee * feeToCollateralRatio`, and they must -satisfy their own preservation equation: `collateralInputs = totalCollaterals + -returnCollaterals`. Lastly, the actual required fee for a given transaction -depends on the size of the transaction and the (not yet executed) resources used -by the scripts during validation. +process. Additionally, when a transaction involves scripts, and thus its +validation can fail in phase 2, collaterals must be provided to account for such +possible failures. These collaterals are related to the fee through the protocol +parameter `feeToCollateralRatio` by the inequation `totalCollateral >= fee * +feeToCollateralRatio`, and they must satisfy their own preservation equation: +`collateralInputs = totalCollaterals + returnCollaterals`. Lastly, the actual +required fee for a given transaction depends on the size of the transaction and +the (not yet executed) resources used by the scripts during validation. ### Balancing mechanism @@ -213,17 +215,19 @@ data CollateralUtxos | CollateralUtxosFromSet (Set Api.TxOutRef) Wallet ``` -In addition to the regular UTXOs consumed in a transaction, additional UTXOs -must be provided to cover potential phase 2 validation failures. These UTXOs -need to be sufficient to meet a specified total collateral requirement, -typically 1.5 times the transaction fee based on protocol parameters. Any -surplus can be returned to a designated wallet through an output known as return -collateral. This setting determines which UTXOs the balancing mechanism should -consider for inclusion in the transaction. Similar to -[`BalancingUtxos`](#balancing-utxos), the final set of UTXOs included may not -necessarily match the considered set, especially for collaterals, as protocol -parameters also impose limits on the number of allowable collateral UTXOs -(typically 3). +When a transaction involves executing scripts, UTXOs must be provided as +collaterals to cover potential phase 2 validation failures. These UTXOs need to +be sufficient to meet a specified total collateral requirement, typically 1.5 +times the transaction fee based on protocol parameters. Any surplus can be +returned to a designated wallet through an output known as return +collateral. + +This setting is ignored when the transaction does not involve any +scripts, but otherwise determines which UTXOs the balancing mechanism should +consider for inclusion. Similar to [`BalancingUtxos`](#balancing-utxos), the +final set of UTXOs included may not necessarily match the considered set, +especially for collaterals, as protocol parameters also impose a limit on the +number of allowable collateral UTXOs (typically 3). Here are the options available: @@ -387,4 +391,3 @@ the estimated fee resulting from this balancing, we adjust our search interval: The recursion continues until an error is propagated or the interval is reduced to a single point. - From c8cf3733ee40c799730e6a2f1445b1dfd84fbbac Mon Sep 17 00:00:00 2001 From: mmontin Date: Thu, 11 Jul 2024 17:06:57 +0200 Subject: [PATCH 20/44] updating doc --- doc/BALANCING.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/doc/BALANCING.md b/doc/BALANCING.md index 14183e5e6..4d394b640 100644 --- a/doc/BALANCING.md +++ b/doc/BALANCING.md @@ -14,15 +14,16 @@ is currently implemented, and which options affect this mechanism. In Cardano, transactions must be balanced before they can be submitted for validation. This means the equation `input value + minted value = output value + -burned value + fee` must be satisfied to proceed to phase 2 of the validation -process. Additionally, when a transaction involves scripts, and thus its -validation can fail in phase 2, collaterals must be provided to account for such -possible failures. These collaterals are related to the fee through the protocol -parameter `feeToCollateralRatio` by the inequation `totalCollateral >= fee * -feeToCollateralRatio`, and they must satisfy their own preservation equation: -`collateralInputs = totalCollaterals + returnCollaterals`. Lastly, the actual -required fee for a given transaction depends on the size of the transaction and -the (not yet executed) resources used by the scripts during validation. +burned value + deposited value + fee` must be satisfied to proceed to phase 2 of +the validation process. Additionally, when a transaction involves scripts, and +thus its validation can fail in phase 2, collaterals must be provided to account +for such possible failures. These collaterals are related to the fee through the +protocol parameter `collateralPercentage` by the inequation `totalCollateral >= +fee * collateralPercentage`, and they must satisfy their own preservation +equation: `collateralInputs = totalCollaterals + returnCollaterals`. Lastly, the +actual required fee for a given transaction depends on the size of the +transaction and the (not yet executed) resources used by the scripts during +validation. ### Balancing mechanism From 73c209f47f57d5b4f718682b77df6d7481c407f0 Mon Sep 17 00:00:00 2001 From: mmontin Date: Thu, 11 Jul 2024 17:51:46 +0200 Subject: [PATCH 21/44] logging of unused collateral option --- CHANGELOG.md | 3 +++ src/Cooked/MockChain/Balancing.hs | 19 ++++++++++--------- src/Cooked/MockChain/BlockChain.hs | 5 ++++- src/Cooked/Pretty/Cooked.hs | 8 ++++++++ 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4189d309..adffbe2bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,11 +30,14 @@ * it is now a component of `MonadBlockChainBalancing` * it can be turned on/off in pretty-printing options * it now displays the discarding of utxos during balancing. + * it now displays when the users specifies useless collateral utxos. * it is not visible from outside of `cooked-validators` ### Fixed - All kinds of scripts can now be used as reference scripts. +- Transactions that do not involve script are now properly generated without any + collateral. ## [[4.0.0]](https://github.com/tweag/cooked-validators/releases/tag/v4.0.0) - 2024-06-28 diff --git a/src/Cooked/MockChain/Balancing.hs b/src/Cooked/MockChain/Balancing.hs index 8f2f649e4..c66f71701 100644 --- a/src/Cooked/MockChain/Balancing.hs +++ b/src/Cooked/MockChain/Balancing.hs @@ -79,15 +79,16 @@ balanceTxSkel skelUnbal@TxSkel {..} = do -- We retrieve the various kinds of scripts spendingScripts <- txSkelInputValidators skelUnbal -- The transaction will only require collaterals when involving scripts - if Map.null txSkelMints && null (mapMaybe txSkelProposalWitness txSkelProposals) && Map.null spendingScripts - then return Nothing - else - Just <$> case txOptCollateralUtxos txSkelOpts of - CollateralUtxosFromBalancingWallet -> case balancingWallet of - Nothing -> fail "Can't select collateral utxos from a balancing wallet because it does not exist." - Just bWallet -> (,bWallet) . Set.fromList . map fst <$> runUtxoSearch (onlyValueOutputsAtSearch bWallet) - CollateralUtxosFromWallet cWallet -> (,cWallet) . Set.fromList . map fst <$> runUtxoSearch (onlyValueOutputsAtSearch cWallet) - CollateralUtxosFromSet utxos rWallet -> return (utxos, rWallet) + let noScriptInvolved = Map.null txSkelMints && null (mapMaybe txSkelProposalWitness txSkelProposals) && Map.null spendingScripts + case (noScriptInvolved, txOptCollateralUtxos txSkelOpts) of + (True, CollateralUtxosFromSet utxos _) -> publish (MCLogUnusedCollaterals $ Right utxos) >> return Nothing + (True, CollateralUtxosFromWallet cWallet) -> publish (MCLogUnusedCollaterals $ Left cWallet) >> return Nothing + (True, CollateralUtxosFromBalancingWallet) -> return Nothing + (False, CollateralUtxosFromSet utxos rWallet) -> return $ Just (utxos, rWallet) + (False, CollateralUtxosFromWallet cWallet) -> Just . (,cWallet) . Set.fromList . map fst <$> runUtxoSearch (onlyValueOutputsAtSearch cWallet) + (False, CollateralUtxosFromBalancingWallet) -> case balancingWallet of + Nothing -> fail "Can't select collateral utxos from a balancing wallet because it does not exist." + Just bWallet -> Just . (,bWallet) . Set.fromList . map fst <$> runUtxoSearch (onlyValueOutputsAtSearch bWallet) -- At this point, the presence (or absence) of balancing wallet dictates -- whether the transaction should be automatically balanced or not. diff --git a/src/Cooked/MockChain/BlockChain.hs b/src/Cooked/MockChain/BlockChain.hs index ee7b5f0a0..7e8a24202 100644 --- a/src/Cooked/MockChain/BlockChain.hs +++ b/src/Cooked/MockChain/BlockChain.hs @@ -126,8 +126,11 @@ data MockChainLogEntry where -- successfully sent for validation. MCLogNewTx :: Api.TxId -> MockChainLogEntry -- | Logging the fact that utxos provided by the user for balancing have to be - -- discarded for a specific reason. + -- discarded for a given reason. MCLogDiscardedUtxos :: Integer -> String -> MockChainLogEntry + -- | Logging the fact that utxos provided as collaterals will not be used + -- because the transaction does not need involve scripts. + MCLogUnusedCollaterals :: Either Wallet (Set Api.TxOutRef) -> MockChainLogEntry -- | Contains methods needed for balancing. class (MonadFail m, MonadError MockChainError m) => MonadBlockChainBalancing m where diff --git a/src/Cooked/Pretty/Cooked.hs b/src/Cooked/Pretty/Cooked.hs index 36ec31017..bdf8a8043 100644 --- a/src/Cooked/Pretty/Cooked.hs +++ b/src/Cooked/Pretty/Cooked.hs @@ -148,6 +148,14 @@ instance PrettyCooked MockChainLogEntry where ] prettyCookedOpt opts (MCLogNewTx txId) = "New transaction:" <+> prettyCookedOpt opts txId prettyCookedOpt opts (MCLogDiscardedUtxos n s) = prettyCookedOpt opts n <+> "balancing utxos were discarded:" <+> PP.pretty s + prettyCookedOpt opts (MCLogUnusedCollaterals (Left cWallet)) = + "Specific request to fetch collateral utxos from" + <+> prettyCookedOpt opts (walletPKHash cWallet) + <+> "has been disregarded because the transaction does not require collaterals" + prettyCookedOpt opts (MCLogUnusedCollaterals (Right (length -> n))) = + "Specific request to fetch collateral utxos from the given set of" + <+> prettyCookedOpt opts n + <+> "elements has been disregarded because the transaction does not require collaterals" prettyTxSkel :: PrettyCookedOpts -> SkelContext -> TxSkel -> DocCooked prettyTxSkel opts skelContext (TxSkel lbl txopts mints signers validityRange ins insReference outs proposals) = From 2275916482b4dc2a529dd11806287abc6b2f1c67 Mon Sep 17 00:00:00 2001 From: mmontin Date: Sun, 28 Jul 2024 13:24:07 +0200 Subject: [PATCH 22/44] post-rebase small fixes --- src/Cooked/Pretty/Cooked.hs | 26 ++++++++++++++++---------- tests/Cooked/BalancingSpec.hs | 2 +- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/Cooked/Pretty/Cooked.hs b/src/Cooked/Pretty/Cooked.hs index bdf8a8043..64d0f61ee 100644 --- a/src/Cooked/Pretty/Cooked.hs +++ b/src/Cooked/Pretty/Cooked.hs @@ -44,7 +44,7 @@ import Data.Default import Data.Function (on) import Data.List qualified as List import Data.Map qualified as Map -import Data.Maybe (catMaybes, mapMaybe) +import Data.Maybe (catMaybes, fromMaybe, mapMaybe) import Data.Set qualified as Set import Optics.Core import Plutus.Script.Utils.Ada qualified as Script @@ -137,15 +137,21 @@ instance (Show a) => PrettyCooked (MockChainReturn a UtxoState) where -- been validated if the 'MCLogSubmittedTxSkel' is followed by a 'MCLogNewTx'. instance PrettyCooked MockChainLogEntry where prettyCookedOpt opts (MCLogSubmittedTxSkel skelContext skel) = prettyItemize "Submitted:" "-" [prettyTxSkel opts skelContext skel] - prettyCookedOpt opts (MCLogAdjustedTxSkel skelContext skel fee collaterals returnWallet) = - prettyItemize - "Adjusted:" - "-" - [ prettyTxSkel opts skelContext skel, - "Fee:" <+> prettyCookedOpt opts (Script.lovelace fee), - prettyItemize "Collateral inputs:" "-" (prettyCollateralIn opts skelContext <$> Set.toList collaterals), - "Return collateral target:" <+> prettyCookedOpt opts (walletPKHash returnWallet) - ] + prettyCookedOpt opts (MCLogAdjustedTxSkel skelContext skel fee mCollaterals) = + let mCollateralsDoc = + ( \(collaterals, returnWallet) -> + [ prettyItemize "Collateral inputs:" "-" (prettyCollateralIn opts skelContext <$> Set.toList collaterals), + "Return collateral target:" <+> prettyCookedOpt opts (walletPKHash returnWallet) + ] + ) + <$> mCollaterals + in prettyItemize + "Adjusted:" + "-" + $ [ prettyTxSkel opts skelContext skel, + "Fee:" <+> prettyCookedOpt opts (Script.lovelace fee) + ] + ++ fromMaybe [] mCollateralsDoc prettyCookedOpt opts (MCLogNewTx txId) = "New transaction:" <+> prettyCookedOpt opts txId prettyCookedOpt opts (MCLogDiscardedUtxos n s) = prettyCookedOpt opts n <+> "balancing utxos were discarded:" <+> PP.pretty s prettyCookedOpt opts (MCLogUnusedCollaterals (Left cWallet)) = diff --git a/tests/Cooked/BalancingSpec.hs b/tests/Cooked/BalancingSpec.hs index 210d0b5c0..df25e251f 100644 --- a/tests/Cooked/BalancingSpec.hs +++ b/tests/Cooked/BalancingSpec.hs @@ -321,7 +321,7 @@ tests = "We can also directly give a set of collateral utxos..." [hasFee 1_000_000, insNb 2, additionalOutsNb 0, colInsNb 1, retOutsNb 3] ( testingBalancingTemplate - (ada 49) + (Script.ada 49) mempty (aliceNAdaUtxos 8) emptySearch From f44b98e1786d6a559b7596c746bc64785c65c300 Mon Sep 17 00:00:00 2001 From: mmontin Date: Sun, 28 Jul 2024 23:26:40 +0200 Subject: [PATCH 23/44] bye bye Ledger.TxOut --- src/Cooked/MockChain/Balancing.hs | 2 +- src/Cooked/MockChain/BlockChain.hs | 69 ++++++++++-------------------- src/Cooked/MockChain/Direct.hs | 18 ++++---- src/Cooked/MockChain/Staged.hs | 18 ++++---- src/Cooked/MockChain/UtxoSearch.hs | 12 ------ src/Cooked/Skeleton.hs | 2 +- 6 files changed, 43 insertions(+), 78 deletions(-) diff --git a/src/Cooked/MockChain/Balancing.hs b/src/Cooked/MockChain/Balancing.hs index c66f71701..1d9577429 100644 --- a/src/Cooked/MockChain/Balancing.hs +++ b/src/Cooked/MockChain/Balancing.hs @@ -303,7 +303,7 @@ estimateTxSkelFee skel fee mCollaterals = do let collateralIns = case mCollaterals of Nothing -> [] Just (s, _) -> Set.toList s - managedTxOuts <- lookupUtxosPl $ txSkelKnownTxOutRefs skel <> collateralIns + managedTxOuts <- lookupUtxos $ txSkelKnownTxOutRefs skel <> collateralIns managedValidators <- txSkelInputValidators skel -- We generate the transaction body content, handling errors in the meantime txBodyContent <- case generateBodyContent fee params managedData managedTxOuts managedValidators mCollaterals skel of diff --git a/src/Cooked/MockChain/BlockChain.hs b/src/Cooked/MockChain/BlockChain.hs index 7e8a24202..1e2ea52af 100644 --- a/src/Cooked/MockChain/BlockChain.hs +++ b/src/Cooked/MockChain/BlockChain.hs @@ -20,13 +20,9 @@ module Cooked.MockChain.BlockChain MonadBlockChainWithoutValidation (..), MonadBlockChain (..), AsTrans (..), - allUtxos, currentTime, waitNSlots, - utxosAt, - txOutByRef, utxosFromCardanoTx, - txOutV2FromLedger, typedDatumFromTxOutRef, valueFromTxOutRef, outputDatumFromTxOutRef, @@ -40,15 +36,12 @@ module Cooked.MockChain.BlockChain slotRangeBefore, slotRangeAfter, slotToTimeInterval, - txSkelInputUtxosPl, txSkelInputUtxos, - txSkelReferenceInputUtxosPl, txSkelReferenceInputUtxos, txSkelInputValidators, txSkelInputValue, txSkelInputData, lookupUtxos, - lookupUtxosPl, validateTxSkel', txSkelProposalsDeposit, govActionDeposit, @@ -59,7 +52,6 @@ import Cardano.Api.Ledger qualified as Cardano import Cardano.Ledger.Conway.PParams qualified as Conway import Cardano.Node.Emulator qualified as Emulator import Cardano.Node.Emulator.Internal.Node qualified as Emulator -import Control.Arrow import Control.Lens qualified as Lens import Control.Monad import Control.Monad.Except @@ -138,7 +130,7 @@ class (MonadFail m, MonadError MockChainError m) => MonadBlockChainBalancing m w getParams :: m Emulator.Params -- | Returns a list of all UTxOs at a certain address. - utxosAtLedger :: Api.Address -> m [(Api.TxOutRef, Ledger.TxOut)] + utxosAt :: Api.Address -> m [(Api.TxOutRef, Api.TxOut)] -- | Returns the datum with the given hash if present. datumFromHash :: Api.DatumHash -> m (Maybe Api.Datum) @@ -148,14 +140,14 @@ class (MonadFail m, MonadError MockChainError m) => MonadBlockChainBalancing m w validatorFromHash :: Script.ValidatorHash -> m (Maybe (Script.Versioned Script.Validator)) -- | Returns an output given a reference to it - txOutByRefLedger :: Api.TxOutRef -> m (Maybe Ledger.TxOut) + txOutByRef :: Api.TxOutRef -> m (Maybe Api.TxOut) -- | Logs an event that occured during a BlockChain run publish :: MockChainLogEntry -> m () class (MonadBlockChainBalancing m) => MonadBlockChainWithoutValidation m where -- | Returns a list of all currently known outputs. - allUtxosLedger :: m [(Api.TxOutRef, Ledger.TxOut)] + allUtxos :: m [(Api.TxOutRef, Api.TxOut)] -- | Updates parameters setParams :: Emulator.Params -> m () @@ -188,15 +180,6 @@ class (MonadBlockChainWithoutValidation m) => MonadBlockChain m where validateTxSkel' :: (MonadBlockChain m) => TxSkel -> m [Api.TxOutRef] validateTxSkel' = (map fst . utxosFromCardanoTx <$>) . validateTxSkel -allUtxos :: (MonadBlockChainWithoutValidation m) => m [(Api.TxOutRef, Api.TxOut)] -allUtxos = fmap (second txOutV2FromLedger) <$> allUtxosLedger - -utxosAt :: (MonadBlockChainBalancing m) => Api.Address -> m [(Api.TxOutRef, Api.TxOut)] -utxosAt address = fmap (second txOutV2FromLedger) <$> utxosAtLedger address - -txOutByRef :: (MonadBlockChainBalancing m) => Api.TxOutRef -> m (Maybe Api.TxOut) -txOutByRef oref = fmap txOutV2FromLedger <$> txOutByRefLedger oref - -- | Retrieve the ordered list of outputs of the given "CardanoTx". -- -- This is useful when writing endpoints and/or traces to fetch utxos of @@ -204,10 +187,13 @@ txOutByRef oref = fmap txOutV2FromLedger <$> txOutByRefLedger oref -- afterwards using 'allUtxos' or similar functions. utxosFromCardanoTx :: Ledger.CardanoTx -> [(Api.TxOutRef, Api.TxOut)] utxosFromCardanoTx = - map (\(txOut, txOutRef) -> (Ledger.fromCardanoTxIn txOutRef, txOutV2FromLedger txOut)) . Ledger.getCardanoTxOutRefs - -txOutV2FromLedger :: Ledger.TxOut -> Api.TxOut -txOutV2FromLedger = Ledger.fromCardanoTxOutToPV2TxInfoTxOut . Ledger.getTxOut + map + ( \(txOut, txOutRef) -> + ( Ledger.fromCardanoTxIn txOutRef, + Ledger.fromCardanoTxOutToPV2TxInfoTxOut $ Ledger.getTxOut txOut + ) + ) + . Ledger.getCardanoTxOutRefs -- | Try to resolve the datum on the output: If there's an inline datum, take -- that; if there's a datum hash, look the corresponding datum up (with @@ -333,16 +319,10 @@ typedDatumFromTxOutRef = ((>>= (\(Api.Datum datum) -> Api.fromBuiltinData datum) valueFromTxOutRef :: (MonadBlockChainWithoutValidation m) => Api.TxOutRef -> m (Maybe Api.Value) valueFromTxOutRef = ((outputValue <$>) <$>) . txOutByRef -txSkelInputUtxosPl :: (MonadBlockChainBalancing m) => TxSkel -> m (Map Api.TxOutRef Api.TxOut) -txSkelInputUtxosPl = lookupUtxosPl . Map.keys . txSkelIns - -txSkelInputUtxos :: (MonadBlockChainBalancing m) => TxSkel -> m (Map Api.TxOutRef Ledger.TxOut) +txSkelInputUtxos :: (MonadBlockChainBalancing m) => TxSkel -> m (Map Api.TxOutRef Api.TxOut) txSkelInputUtxos = lookupUtxos . Map.keys . txSkelIns -txSkelReferenceInputUtxosPl :: (MonadBlockChainBalancing m) => TxSkel -> m (Map Api.TxOutRef Api.TxOut) -txSkelReferenceInputUtxosPl = (Map.map txOutV2FromLedger <$>) . txSkelReferenceInputUtxos - -txSkelReferenceInputUtxos :: (MonadBlockChainBalancing m) => TxSkel -> m (Map Api.TxOutRef Ledger.TxOut) +txSkelReferenceInputUtxos :: (MonadBlockChainBalancing m) => TxSkel -> m (Map Api.TxOutRef Api.TxOut) txSkelReferenceInputUtxos = lookupUtxos . txSkelReferenceTxOutRefs -- | Retrieves the required deposit amount for issuing governance actions. @@ -361,7 +341,7 @@ maybeErrM err f = (maybe (throwError err) (return . f) =<<) -- | All validators which protect transaction inputs txSkelInputValidators :: (MonadBlockChainBalancing m) => TxSkel -> m (Map Script.ValidatorHash (Script.Versioned Script.Validator)) txSkelInputValidators skel = do - utxos <- Map.toList <$> lookupUtxosPl (Map.keys . txSkelIns $ skel) + utxos <- Map.toList <$> lookupUtxos (Map.keys . txSkelIns $ skel) Map.fromList . catMaybes <$> mapM ( \(_oref, out) -> case outputAddress out of @@ -380,17 +360,14 @@ txSkelInputValidators skel = do -- | 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. -lookupUtxosPl :: (MonadBlockChainBalancing m) => [Api.TxOutRef] -> m (Map Api.TxOutRef Api.TxOut) -lookupUtxosPl outRefs = Map.map txOutV2FromLedger <$> lookupUtxos outRefs - -lookupUtxos :: (MonadBlockChainBalancing m) => [Api.TxOutRef] -> m (Map Api.TxOutRef Ledger.TxOut) +lookupUtxos :: (MonadBlockChainBalancing m) => [Api.TxOutRef] -> m (Map Api.TxOutRef Api.TxOut) lookupUtxos = (Map.fromList <$>) - . mapM (\oRef -> (oRef,) <$> maybeErrM (MCEUnknownOutRefError "lookupUtxos: unknown TxOutRef" oRef) id (txOutByRefLedger oRef)) + . mapM (\oRef -> (oRef,) <$> maybeErrM (MCEUnknownOutRefError "lookupUtxos: unknown TxOutRef" oRef) id (txOutByRef oRef)) -- | look up the UTxOs the transaction consumes, and sum their values. txSkelInputValue :: (MonadBlockChainBalancing m) => TxSkel -> m Api.Value -txSkelInputValue = (foldMap (Api.txOutValue . txOutV2FromLedger) <$>) . txSkelInputUtxos +txSkelInputValue = (foldMap Api.txOutValue <$>) . txSkelInputUtxos -- | Look up the data on UTxOs the transaction consumes. txSkelInputData :: (MonadBlockChainBalancing m) => TxSkel -> m (Map Api.DatumHash Api.Datum) @@ -404,7 +381,7 @@ txSkelInputData skel = do Api.OutputDatumHash dHash -> Just dHash ) . Map.elems - <$> txSkelInputUtxosPl skel + <$> txSkelInputUtxos skel Map.fromList <$> mapM ( \dHash -> @@ -515,13 +492,13 @@ instance (MonadTransControl t, MonadError MockChainError m, Monad (t m)) => Mona instance (MonadTrans t, MonadBlockChainBalancing m, Monad (t m), MonadError MockChainError (AsTrans t m)) => MonadBlockChainBalancing (AsTrans t m) where getParams = lift getParams validatorFromHash = lift . validatorFromHash - utxosAtLedger = lift . utxosAtLedger - txOutByRefLedger = lift . txOutByRefLedger + utxosAt = lift . utxosAt + txOutByRef = lift . txOutByRef datumFromHash = lift . datumFromHash publish = lift . publish instance (MonadTrans t, MonadBlockChainWithoutValidation m, Monad (t m), MonadError MockChainError (AsTrans t m)) => MonadBlockChainWithoutValidation (AsTrans t m) where - allUtxosLedger = lift allUtxosLedger + allUtxos = lift allUtxos setParams = lift . setParams currentSlot = lift currentSlot awaitSlot = lift . awaitSlot @@ -559,13 +536,13 @@ deriving via (AsTrans (StateT s) m) instance (MonadBlockChain m) => MonadBlockCh instance (MonadBlockChainBalancing m) => MonadBlockChainBalancing (ListT m) where getParams = lift getParams validatorFromHash = lift . validatorFromHash - utxosAtLedger = lift . utxosAtLedger - txOutByRefLedger = lift . txOutByRefLedger + utxosAt = lift . utxosAt + txOutByRef = lift . txOutByRef datumFromHash = lift . datumFromHash publish = lift . publish instance (MonadBlockChainWithoutValidation m) => MonadBlockChainWithoutValidation (ListT m) where - allUtxosLedger = lift allUtxosLedger + allUtxos = lift allUtxos setParams = lift . setParams currentSlot = lift currentSlot awaitSlot = lift . awaitSlot diff --git a/src/Cooked/MockChain/Direct.hs b/src/Cooked/MockChain/Direct.hs index 122cdb784..5337d90ef 100644 --- a/src/Cooked/MockChain/Direct.hs +++ b/src/Cooked/MockChain/Direct.hs @@ -101,7 +101,7 @@ data MockChainSt = MockChainSt mcstToSkelContext :: MockChainSt -> SkelContext mcstToSkelContext MockChainSt {..} = SkelContext - (txOutV2FromLedger <$> getIndex mcstIndex) + (getIndex mcstIndex) (Map.map fst mcstDatums) -- | Generating an emulated state for the emulator from a mockchain state and @@ -302,10 +302,10 @@ utxoIndex0 = utxoIndex0From def -- * Direct Interpretation of Operations -getIndex :: Ledger.UtxoIndex -> Map Api.TxOutRef Ledger.TxOut +getIndex :: Ledger.UtxoIndex -> Map Api.TxOutRef Api.TxOut getIndex = Map.fromList - . map (bimap Ledger.fromCardanoTxIn (Ledger.TxOut . toCtxTxTxOut)) + . map (bimap Ledger.fromCardanoTxIn (Ledger.fromCardanoTxOutToPV2TxInfoTxOut . toCtxTxTxOut)) . Map.toList . Cardano.unUTxO where @@ -322,13 +322,13 @@ getIndex = instance (Monad m) => MonadBlockChainBalancing (MockChainT m) where getParams = gets mcstParams validatorFromHash valHash = gets $ Map.lookup valHash . mcstValidators - txOutByRefLedger outref = gets $ Map.lookup outref . getIndex . mcstIndex + txOutByRef outref = gets $ Map.lookup outref . getIndex . mcstIndex datumFromHash datumHash = (txSkelOutUntypedDatum <=< Just . fst <=< Map.lookup datumHash) <$> gets mcstDatums - utxosAtLedger addr = filter ((addr ==) . outputAddress . txOutV2FromLedger . snd) <$> allUtxosLedger + utxosAt addr = filter ((addr ==) . outputAddress . snd) <$> allUtxos publish l = tell [l] instance (Monad m) => MonadBlockChainWithoutValidation (MockChainT m) where - allUtxosLedger = gets $ Map.toList . getIndex . mcstIndex + allUtxos = gets $ Map.toList . getIndex . mcstIndex setParams newParams = modify (\st -> st {mcstParams = newParams}) currentSlot = gets mcstCurrentSlot awaitSlot s = modify' (\st -> st {mcstCurrentSlot = max s (mcstCurrentSlot st)}) >> currentSlot @@ -356,11 +356,11 @@ instance (Monad m) => MonadBlockChain (MockChainT m) where -- rich-enough context for the transaction generation to succeed. insData <- txSkelInputData skel insValidators <- txSkelInputValidators skel - insMap <- txSkelInputUtxosPl skel - refInsMap <- txSkelReferenceInputUtxosPl skel + insMap <- txSkelInputUtxos skel + refInsMap <- txSkelReferenceInputUtxos skel collateralInsMap <- case mCollaterals of Nothing -> return Map.empty - Just (collateralIns, _) -> lookupUtxosPl $ Set.toList collateralIns + Just (collateralIns, _) -> lookupUtxos $ Set.toList collateralIns -- We attempt to generate the transaction associated with the balanced -- skeleton and the retrieved data. This is an internal generation, there is -- no validation involved yet. diff --git a/src/Cooked/MockChain/Staged.hs b/src/Cooked/MockChain/Staged.hs index 17016929f..d06709697 100644 --- a/src/Cooked/MockChain/Staged.hs +++ b/src/Cooked/MockChain/Staged.hs @@ -72,12 +72,12 @@ data MockChainBuiltin a where GetParams :: MockChainBuiltin Emulator.Params SetParams :: Emulator.Params -> MockChainBuiltin () ValidateTxSkel :: TxSkel -> MockChainBuiltin Ledger.CardanoTx - TxOutByRefLedger :: Api.TxOutRef -> MockChainBuiltin (Maybe Ledger.TxOut) + TxOutByRef :: Api.TxOutRef -> MockChainBuiltin (Maybe Api.TxOut) GetCurrentSlot :: MockChainBuiltin Ledger.Slot AwaitSlot :: Ledger.Slot -> MockChainBuiltin Ledger.Slot DatumFromHash :: Api.DatumHash -> MockChainBuiltin (Maybe Api.Datum) - AllUtxosLedger :: MockChainBuiltin [(Api.TxOutRef, Ledger.TxOut)] - UtxosAtLedger :: Api.Address -> MockChainBuiltin [(Api.TxOutRef, Ledger.TxOut)] + AllUtxos :: MockChainBuiltin [(Api.TxOutRef, Api.TxOut)] + UtxosAt :: Api.Address -> MockChainBuiltin [(Api.TxOutRef, Api.TxOut)] ValidatorFromHash :: Script.ValidatorHash -> MockChainBuiltin (Maybe (Script.Versioned Script.Validator)) Publish :: MockChainLogEntry -> MockChainBuiltin () -- | The empty set of traces @@ -124,13 +124,13 @@ instance InterpLtl (UntypedTweak InterpMockChain) MockChainBuiltin InterpMockCha (_, skel') <- lift $ runTweakInChain now skel put later validateTxSkel skel' - interpBuiltin (TxOutByRefLedger o) = txOutByRefLedger o + interpBuiltin (TxOutByRef o) = txOutByRef o interpBuiltin GetCurrentSlot = currentSlot interpBuiltin (AwaitSlot s) = awaitSlot s interpBuiltin (DatumFromHash h) = datumFromHash h interpBuiltin (ValidatorFromHash h) = validatorFromHash h - interpBuiltin AllUtxosLedger = allUtxosLedger - interpBuiltin (UtxosAtLedger address) = utxosAtLedger address + interpBuiltin AllUtxos = allUtxos + interpBuiltin (UtxosAt address) = utxosAt address interpBuiltin Empty = mzero interpBuiltin (Alt l r) = interpLtl l `mplus` interpLtl r interpBuiltin (Fail msg) = fail msg @@ -198,13 +198,13 @@ instance MonadError MockChainError StagedMockChain where instance MonadBlockChainBalancing StagedMockChain where getParams = singletonBuiltin GetParams datumFromHash = singletonBuiltin . DatumFromHash - txOutByRefLedger = singletonBuiltin . TxOutByRefLedger - utxosAtLedger = singletonBuiltin . UtxosAtLedger + txOutByRef = singletonBuiltin . TxOutByRef + utxosAt = singletonBuiltin . UtxosAt validatorFromHash = singletonBuiltin . ValidatorFromHash publish = singletonBuiltin . Publish instance MonadBlockChainWithoutValidation StagedMockChain where - allUtxosLedger = singletonBuiltin AllUtxosLedger + allUtxos = singletonBuiltin AllUtxos setParams = singletonBuiltin . SetParams currentSlot = singletonBuiltin GetCurrentSlot awaitSlot = singletonBuiltin . AwaitSlot diff --git a/src/Cooked/MockChain/UtxoSearch.hs b/src/Cooked/MockChain/UtxoSearch.hs index 3ed50d709..d517f5873 100644 --- a/src/Cooked/MockChain/UtxoSearch.hs +++ b/src/Cooked/MockChain/UtxoSearch.hs @@ -5,9 +5,7 @@ module Cooked.MockChain.UtxoSearch ( UtxoSearch, runUtxoSearch, allUtxosSearch, - allUtxosLedgerSearch, utxosAtSearch, - utxosAtLedgerSearch, utxosFromCardanoTxSearch, txOutByRefSearch, filterWith, @@ -54,21 +52,11 @@ runUtxoSearch = ListT.toList allUtxosSearch :: (MonadBlockChain m) => UtxoSearch m Api.TxOut allUtxosSearch = allUtxos >>= ListT.fromFoldable --- | Like 'allUtxosSearch', but returns a Ledger-level representation of the --- transaction outputs, which might contain more information. -allUtxosLedgerSearch :: (MonadBlockChain m) => UtxoSearch m Ledger.TxOut -allUtxosLedgerSearch = allUtxosLedger >>= ListT.fromFoldable - -- | Search all 'TxOutRef's at a certain address, together with their -- 'TxInfo'-'TxOut'. utxosAtSearch :: (MonadBlockChainBalancing m, ToAddress addr) => addr -> UtxoSearch m Api.TxOut utxosAtSearch = utxosAt . toAddress >=> ListT.fromFoldable --- | Like 'utxosAtSearch', but returns a Ledger-level representation of the --- transaction outputs, which might contain more information. -utxosAtLedgerSearch :: (MonadBlockChainBalancing m, ToAddress addr) => addr -> UtxoSearch m Ledger.TxOut -utxosAtLedgerSearch = utxosAtLedger . toAddress >=> ListT.fromFoldable - -- | Search all 'TxOutRef's of a transaction, together with their -- 'TxInfo'-'TxOut'. utxosFromCardanoTxSearch :: (Monad m) => Ledger.CardanoTx -> UtxoSearch m Api.TxOut diff --git a/src/Cooked/Skeleton.hs b/src/Cooked/Skeleton.hs index 23e415a40..db2401858 100644 --- a/src/Cooked/Skeleton.hs +++ b/src/Cooked/Skeleton.hs @@ -1100,7 +1100,7 @@ txSkelReferenceTxOutRefs TxSkel {..} = Set.toList txSkelInsReference -- reference inputs in inputs redeemers <> mapMaybe txSkelReferenceScript (Map.elems txSkelIns) - -- reference inputs in porposals redeemers + -- reference inputs in proposals redeemers <> mapMaybe (txSkelReferenceScript . snd) (mapMaybe txSkelProposalWitness txSkelProposals) -- reference inputs in mints redeemers <> mapMaybe (txSkelReferenceScript . fst . snd) (Map.toList txSkelMints) From 14da58f7db2f57e31f2b8d0ec59334f6361859a1 Mon Sep 17 00:00:00 2001 From: mmontin Date: Wed, 31 Jul 2024 14:29:38 +0200 Subject: [PATCH 24/44] update capi --- cabal.project | 9 +++---- flake.lock | 6 ++--- src/Cooked/MockChain/Balancing.hs | 30 ++++++++++++++++++++-- src/Cooked/MockChain/Direct.hs | 18 ++++++++++++- src/Cooked/MockChain/GenerateTx/Witness.hs | 6 ++--- tests/Cooked/ProposingScriptSpec.hs | 2 +- 6 files changed, 55 insertions(+), 16 deletions(-) diff --git a/cabal.project b/cabal.project index 4721e608c..7e81c2dea 100644 --- a/cabal.project +++ b/cabal.project @@ -10,8 +10,8 @@ package cardano-crypto-praos source-repository-package type: git - location: https://github.com/IntersectMBO/cardano-node-emulator - tag: 645c1d04ba1c90aa76ad720e66215e754568b81e + location: https://github.com/tweag/cardano-node-emulator-forked/ + tag: 3087d61fc26929dc29b97bf4d25a519907174324 subdir: cardano-node-emulator plutus-ledger @@ -69,13 +69,12 @@ package plutus-ledger haddock-options: "--optghc=-fplugin-opt PlutusTx.Plugin:defer-errors" constraints: - cardano-api ^>= 8.42 + cardano-api ^>= 8.46 source-repository-package type: git location: https://github.com/input-output-hk/quickcheck-contractmodel - tag: fdfbbfed09b48dada31bcb4e5b0e0e6d2cffba7f - --sha256: sha256-25BrEBetu35rAlNM+rZijjgS4cIG/Q4gcd5YYNqn2Vk= + tag: b19a7689a0d40ba3c7f91da87ef5fbcf20f3926c subdir: quickcheck-contractmodel quickcheck-threatmodel diff --git a/flake.lock b/flake.lock index 7706bb0e8..b7fa84f75 100644 --- a/flake.lock +++ b/flake.lock @@ -57,11 +57,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1721051935, - "narHash": "sha256-Cb9yT0wk1+ExeAPCqSImSM1AaB7cmKUbW+7oqIEDYcE=", + "lastModified": 1722428703, + "narHash": "sha256-8oQgiehHJymg0l2Cd+97zd7+qeXwpYKnKY7lAB5Om5I=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "87d7e80daaa1c8c8d9cf399c2b49ea3e0b0d2b44", + "rev": "e26d97050286eb781956720c1e868a9d587e48c0", "type": "github" }, "original": { diff --git a/src/Cooked/MockChain/Balancing.hs b/src/Cooked/MockChain/Balancing.hs index ee9814630..3b53e0645 100644 --- a/src/Cooked/MockChain/Balancing.hs +++ b/src/Cooked/MockChain/Balancing.hs @@ -7,6 +7,7 @@ import Cardano.Api.Ledger qualified as Cardano import Cardano.Api.Shelley qualified as Cardano import Cardano.Node.Emulator.Internal.Node.Params qualified as Emulator import Cardano.Node.Emulator.Internal.Node.Validation qualified as Emulator +import Control.Monad import Control.Monad.Except import Cooked.Conversion import Cooked.MockChain.BlockChain @@ -17,13 +18,18 @@ import Cooked.Output import Cooked.Skeleton import Cooked.Wallet import Data.Bifunctor +import Data.Either.Combinators import Data.Function import Data.List +import Data.Map (Map) import Data.Map qualified as Map import Data.Maybe import Data.Ratio qualified as Rat import Data.Set (Set) import Data.Set qualified as Set +import Ledger.Index qualified as Ledger +import Ledger.Tx qualified as Ledger +import Ledger.Tx.CardanoAPI qualified as Ledger import Optics.Core import Plutus.Script.Utils.Ada qualified as Script import Plutus.Script.Utils.Value qualified as Script @@ -273,8 +279,28 @@ estimateTxSkelFee skel fee collateralIns returnCollateralWallet = do Right txBody -> return txBody -- We retrieve the estimate number of required witness in the transaction let nkeys = Cardano.estimateTransactionKeyWitnessCount txBodyContent - -- We return an accurate estimate of the resulting transaction fee - return $ Emulator.unCoin $ Cardano.evaluateTransactionFee Cardano.ShelleyBasedEraConway (Emulator.pEmulatorPParams params) txBody nkeys 0 + index <- toIndex <$> lookupUtxos (txSkelKnownTxOutRefs skel <> Set.toList collateralIns) + case index of + Left err -> throwError $ MCEGenerationError err + -- We return an accurate estimate of the resulting transaction fee + Right index' -> + return . Emulator.unCoin $ + Cardano.calculateMinTxFee Cardano.ShelleyBasedEraConway (Emulator.pEmulatorPParams params) index' txBody nkeys + where + toIndex :: Map Api.TxOutRef Ledger.TxOut -> Either GenerateTxError Ledger.UtxoIndex + toIndex innerMap = do + let (txOutRefL, txOutL) = unzip $ Map.toList innerMap + txInL <- mapLeft (ToCardanoError "toIndex: unable to generate TxOut") $ forM txOutRefL Ledger.toCardanoTxIn + txOutL' <- forM (Ledger.getTxOut <$> txOutL) toCtxUTxOTxOut + return $ Cardano.UTxO $ Map.fromList $ zip txInL txOutL' + toCtxUTxOTxOut :: Cardano.TxOut Cardano.CtxTx era -> Either GenerateTxError (Cardano.TxOut Cardano.CtxUTxO era) + toCtxUTxOTxOut (Cardano.TxOut addr val d refS) = do + dat <- case d of + Cardano.TxOutDatumNone -> return Cardano.TxOutDatumNone + Cardano.TxOutDatumInTx _ _ -> Left $ GenerateTxErrorGeneral "Wrong datum kind" + Cardano.TxOutDatumHash s h -> return $ Cardano.TxOutDatumHash s h + Cardano.TxOutDatumInline s sd -> return $ Cardano.TxOutDatumInline s sd + return $ Cardano.TxOut addr val dat refS -- | This creates a balanced skeleton from a given skeleton and fee. In other -- words, this ensures that the following equation holds: input value + minted diff --git a/src/Cooked/MockChain/Direct.hs b/src/Cooked/MockChain/Direct.hs index c2a977eca..ec98c56b2 100644 --- a/src/Cooked/MockChain/Direct.hs +++ b/src/Cooked/MockChain/Direct.hs @@ -52,7 +52,7 @@ import PlutusLedgerApi.V3 qualified as Api -- complexity the 'Contract' monad introduces. -- -- Running a 'MockChain' produces a 'UtxoState', a simplified view on --- 'Api.UtxoIndex', which we also keep in our state. +-- 'Ledger.UtxoIndex', which we also keep in our state. mcstToUtxoState :: MockChainSt -> UtxoState mcstToUtxoState MockChainSt {mcstIndex, mcstDatums} = @@ -310,6 +310,22 @@ getIndex = Cardano.TxOutDatumInline s sd -> Cardano.TxOutDatumInline s sd in Cardano.TxOut addr val dat refS +toIndex :: Map Api.TxOutRef Ledger.TxOut -> Either Ledger.ToCardanoError Ledger.UtxoIndex +toIndex innerMap = do + let (txOutRefL, txOutL) = unzip $ Map.toList innerMap + txInL <- forM txOutRefL Ledger.toCardanoTxIn + txOutL' <- forM (Ledger.getTxOut <$> txOutL) toCtxUTxOTxOut + return $ Cardano.UTxO $ Map.fromList $ zip txInL txOutL' + where + toCtxUTxOTxOut :: Cardano.TxOut Cardano.CtxTx era -> Either Ledger.ToCardanoError (Cardano.TxOut Cardano.CtxUTxO era) + toCtxUTxOTxOut (Cardano.TxOut addr val d refS) = do + dat <- case d of + Cardano.TxOutDatumNone -> return Cardano.TxOutDatumNone + Cardano.TxOutDatumInTx _ _ -> Left $ Ledger.TxBodyError "Wrong datum kind" + Cardano.TxOutDatumHash s h -> return $ Cardano.TxOutDatumHash s h + Cardano.TxOutDatumInline s sd -> return $ Cardano.TxOutDatumInline s sd + return $ Cardano.TxOut addr val dat refS + instance (Monad m) => MonadBlockChainBalancing (MockChainT m) where getParams = gets mcstParams validatorFromHash valHash = gets $ Map.lookup valHash . mcstValidators diff --git a/src/Cooked/MockChain/GenerateTx/Witness.hs b/src/Cooked/MockChain/GenerateTx/Witness.hs index a52f9ac3e..276269660 100644 --- a/src/Cooked/MockChain/GenerateTx/Witness.hs +++ b/src/Cooked/MockChain/GenerateTx/Witness.hs @@ -25,9 +25,9 @@ import PlutusLedgerApi.V3 qualified as Api type WitnessGen a = TxGen (Map Api.TxOutRef Api.TxOut) a -- | Translates a given credential to a reward account. -toRewardAccount :: Api.Credential -> WitnessGen (Cardano.RewardAcnt Crypto.StandardCrypto) +toRewardAccount :: Api.Credential -> WitnessGen (Cardano.RewardAccount Crypto.StandardCrypto) toRewardAccount cred = - Cardano.RewardAcnt Cardano.Testnet <$> case cred of + Cardano.RewardAccount Cardano.Testnet <$> case cred of Api.ScriptCredential scriptHash -> do Cardano.ScriptHash cHash <- throwOnToCardanoError @@ -38,8 +38,6 @@ toRewardAccount cred = Cardano.StakeKeyHash pkHash <- throwOnToCardanoError "toRewardAccount: Unable to convert private key hash." - -- TODO: we take the pubkeyHash, maybe we should take the stakehash if - -- any exists. The nature of the stake address can be confusing. (Ledger.toCardanoStakeKeyHash pubkeyHash) return $ Cardano.KeyHashObj pkHash diff --git a/tests/Cooked/ProposingScriptSpec.hs b/tests/Cooked/ProposingScriptSpec.hs index fa0c67936..95c945ad5 100644 --- a/tests/Cooked/ProposingScriptSpec.hs +++ b/tests/Cooked/ProposingScriptSpec.hs @@ -25,7 +25,7 @@ checkParameterChangeScript _ ctx = in case Api.ppGovernanceAction proposalProcedure of Api.ParameterChange _ (Api.ChangedParameters dat) _ -> let innerMap = PlutusTx.unsafeFromBuiltinData @(PlutusTx.Map PlutusTx.Integer PlutusTx.Integer) dat - in if innerMap PlutusTx.== PlutusTx.fromList [(0, 100)] then () else PlutusTx.traceError "wrong map" + in if innerMap PlutusTx.== PlutusTx.safeFromList [(0, 100)] then () else PlutusTx.traceError "wrong map" _ -> PlutusTx.traceError "Wrong proposal procedure" checkProposingScript :: Script.Versioned Script.Script From 1ccb4b1a2473ae7f31541074b3be56830205b60f Mon Sep 17 00:00:00 2001 From: mmontin Date: Wed, 31 Jul 2024 18:42:14 +0200 Subject: [PATCH 25/44] Proper script hash computation for all plutus versions --- CHANGELOG.md | 4 +++- src/Cooked/Conversion/ToScriptHash.hs | 16 ++++++---------- src/Cooked/MockChain/GenerateTx/Witness.hs | 6 +++--- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97ad33523..ef7ee6e7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - `toInitDistWithMinAda` and `unsafeToInitDistWithMinAda` to ensure the initial distribution only provides outputs with the required minimal ada based on default parameters. +- All kinds of scripts can now be used as reference scripts. ### Removed @@ -25,7 +26,8 @@ ### Fixed -- All kinds of scripts can now be used as reference scripts. +- A bug where the script hashes would not be computed properly for early plutus + version (V1 and V2). ## [[4.0.0]](https://github.com/tweag/cooked-validators/releases/tag/v4.0.0) - 2024-06-28 diff --git a/src/Cooked/Conversion/ToScriptHash.hs b/src/Cooked/Conversion/ToScriptHash.hs index 22407a92c..bc8165f61 100644 --- a/src/Cooked/Conversion/ToScriptHash.hs +++ b/src/Cooked/Conversion/ToScriptHash.hs @@ -2,9 +2,8 @@ module Cooked.Conversion.ToScriptHash where import Cooked.Conversion.ToScript -import Plutus.Script.Utils.Scripts qualified as Script hiding (scriptHash) +import Plutus.Script.Utils.Scripts qualified as Script import Plutus.Script.Utils.Typed qualified as Script -import Plutus.Script.Utils.V3.Scripts qualified as Script (scriptHash) import PlutusLedgerApi.V3 qualified as Api class ToScriptHash a where @@ -13,17 +12,14 @@ class ToScriptHash a where instance ToScriptHash Api.ScriptHash where toScriptHash = id -instance ToScriptHash Script.Script where - toScriptHash = Script.scriptHash - -instance ToScriptHash Api.SerialisedScript where - toScriptHash = toScriptHash . Script.Script +instance ToScriptHash Script.MintingPolicyHash where + toScriptHash (Script.MintingPolicyHash hash) = Script.ScriptHash hash instance ToScriptHash Script.ValidatorHash where - toScriptHash (Script.ValidatorHash h) = Script.ScriptHash h + toScriptHash (Script.ValidatorHash hash) = Script.ScriptHash hash instance ToScriptHash (Script.Versioned Script.Script) where - toScriptHash (Script.Versioned s _) = toScriptHash s + toScriptHash = Script.scriptHash instance ToScriptHash (Script.Versioned Script.Validator) where toScriptHash = toScriptHash . toScript @@ -32,4 +28,4 @@ instance ToScriptHash (Script.TypedValidator a) where toScriptHash = toScriptHash . toScript instance ToScriptHash (Script.Versioned Script.MintingPolicy) where - toScriptHash = toScriptHash . toScript + toScriptHash = toScriptHash . Script.mintingPolicyHash diff --git a/src/Cooked/MockChain/GenerateTx/Witness.hs b/src/Cooked/MockChain/GenerateTx/Witness.hs index a52f9ac3e..5e31129fd 100644 --- a/src/Cooked/MockChain/GenerateTx/Witness.hs +++ b/src/Cooked/MockChain/GenerateTx/Witness.hs @@ -45,8 +45,8 @@ toRewardAccount cred = -- | Translate a script and a reference script utxo into into either a plutus -- script or a reference input containing the right script -toPlutusScriptOrReferenceInput :: Api.SerialisedScript -> Maybe Api.TxOutRef -> WitnessGen (Cardano.PlutusScriptOrReferenceInput lang) -toPlutusScriptOrReferenceInput script Nothing = return $ Cardano.PScript $ Cardano.PlutusScriptSerialised script +toPlutusScriptOrReferenceInput :: Script.Versioned Script.Script -> Maybe Api.TxOutRef -> WitnessGen (Cardano.PlutusScriptOrReferenceInput lang) +toPlutusScriptOrReferenceInput (Script.Versioned (Script.Script script) _) Nothing = return $ Cardano.PScript $ Cardano.PlutusScriptSerialised script toPlutusScriptOrReferenceInput script (Just scriptOutRef) = do referenceScriptsMap <- asks $ Map.mapMaybe (^. outputReferenceScriptL) refScriptHash <- @@ -69,7 +69,7 @@ toPlutusScriptOrReferenceInput script (Just scriptOutRef) = do -- | Translates a script with its associated redeemer and datum to a script -- witness. toScriptWitness :: (ToScript a) => a -> TxSkelRedeemer -> Cardano.ScriptDatum b -> WitnessGen (Cardano.ScriptWitness b Cardano.ConwayEra) -toScriptWitness (toScript -> (Script.Versioned (Script.Script script) version)) (TxSkelRedeemer {..}) datum = +toScriptWitness (toScript -> script@(Script.Versioned _ version)) (TxSkelRedeemer {..}) datum = let scriptData = case txSkelRedeemer of EmptyRedeemer -> Ledger.toCardanoScriptData $ Api.toBuiltinData () SomeRedeemer s -> Ledger.toCardanoScriptData $ Api.toBuiltinData s From 5114f606c039920ec5074e99ecf7c2ae08c1fed9 Mon Sep 17 00:00:00 2001 From: mmontin Date: Thu, 1 Aug 2024 00:13:46 +0200 Subject: [PATCH 26/44] helpers and qol changes --- CHANGELOG.md | 6 +++ src/Cooked/MockChain/BlockChain.hs | 71 +++++++++-------------------- src/Cooked/Skeleton.hs | 14 +++--- src/Cooked/Tweak/TamperDatum.hs | 2 +- src/Cooked/Validators.hs | 30 +++++++++--- src/Cooked/Wallet.hs | 5 ++ tests/Cooked/ProposingScriptSpec.hs | 2 +- 7 files changed, 66 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ef7ee6e7f..c77dcd6c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,11 @@ distribution only provides outputs with the required minimal ada based on default parameters. - All kinds of scripts can now be used as reference scripts. +- `validateTxSkel_` which validates a skeleton and ignores the output. +- `txSkelMintsFromList'` which only allows one redeemer per minting policy. +- `validatorToTypedValidatorV2` +- `walletPKHashToWallet` that retrives a wallet from a pkh. It used to be + present but somehow disapeared. ### Removed @@ -23,6 +28,7 @@ constructors: `txSkelSomeRedeemer`, `txSkelEmptyRedeemer`, `txSkelSomeRedeemerAndReferenceScript`, `txSkelEmptyRedeemerAndReferenceScript`. +- `mkProposingScript` changed to `mkScript` ### Fixed diff --git a/src/Cooked/MockChain/BlockChain.hs b/src/Cooked/MockChain/BlockChain.hs index fcdf38e79..ed4f9c2ff 100644 --- a/src/Cooked/MockChain/BlockChain.hs +++ b/src/Cooked/MockChain/BlockChain.hs @@ -49,6 +49,7 @@ module Cooked.MockChain.BlockChain lookupUtxos, lookupUtxosPl, validateTxSkel', + validateTxSkel_, txSkelProposalsDeposit, govActionDeposit, ) @@ -163,6 +164,10 @@ class (MonadBlockChainWithoutValidation m) => MonadBlockChain m where validateTxSkel' :: (MonadBlockChain m) => TxSkel -> m [Api.TxOutRef] validateTxSkel' = (map fst . utxosFromCardanoTx <$>) . validateTxSkel +-- | Validates a skeleton, and erases the outputs +validateTxSkel_ :: (MonadBlockChain m) => TxSkel -> m () +validateTxSkel_ = void . validateTxSkel + allUtxos :: (MonadBlockChainWithoutValidation m) => m [(Api.TxOutRef, Api.TxOut)] allUtxos = fmap (second txOutV2FromLedger) <$> allUtxosLedger @@ -200,16 +205,9 @@ resolveDatum out = do Api.OutputDatumHash datumHash -> datumFromHash datumHash Api.OutputDatum datum -> return $ Just datum Api.NoOutputDatum -> return Nothing - return $ - ( \mDat -> - ConcreteOutput - (out ^. outputOwnerL) - (out ^. outputStakingCredentialL) - mDat - (out ^. outputValueL) - (out ^. outputReferenceScriptL) - ) - <$> mDatum + return $ do + mDat <- mDatum + return $ (fromAbstractOutput out) {concreteOutputDatum = mDat} -- | Like 'resolveDatum', but also tries to use 'fromBuiltinData' to extract a -- datum of the suitable type. @@ -223,19 +221,11 @@ resolveTypedDatum :: m (Maybe (ConcreteOutput (OwnerType out) a (ValueType out) (ReferenceScriptType out))) resolveTypedDatum out = do mOut <- resolveDatum out - return $ - ( \out' -> do - let Api.Datum datum = out' ^. outputDatumL - dat <- Api.fromBuiltinData datum - return $ - ConcreteOutput - (out' ^. outputOwnerL) - (out' ^. outputStakingCredentialL) - dat - (out' ^. outputValueL) - (out' ^. outputReferenceScriptL) - ) - =<< mOut + return $ do + out' <- mOut + let Api.Datum datum = out' ^. outputDatumL + dat <- Api.fromBuiltinData datum + return $ (fromAbstractOutput out) {concreteOutputDatum = dat} -- | Try to resolve the validator that owns an output: If the output is owned by -- a public key, or if the validator's hash is not known (i.e. if @@ -252,16 +242,9 @@ resolveValidator out = Api.PubKeyCredential _ -> return Nothing Api.ScriptCredential (Api.ScriptHash hash) -> do mVal <- validatorFromHash (Script.ValidatorHash hash) - return $ - ( \val -> - ConcreteOutput - val - (out ^. outputStakingCredentialL) - (out ^. outputDatumL) - (out ^. outputValueL) - (out ^. outputReferenceScriptL) - ) - <$> mVal + return $ do + val <- mVal + return $ (fromAbstractOutput out) {concreteOutputOwner = val} -- | Try to resolve the reference script on an output: If the output has no -- reference script, or if the reference script's hash is not known (i.e. if @@ -273,22 +256,12 @@ resolveReferenceScript :: ) => out -> m (Maybe (ConcreteOutput (OwnerType out) (DatumType out) (ValueType out) (Script.Versioned Script.Validator))) -resolveReferenceScript out = - maybe - (return Nothing) - ( \(Api.ScriptHash hash) -> - ( \mVal -> - ConcreteOutput - (out ^. outputOwnerL) - (out ^. outputStakingCredentialL) - (out ^. outputDatumL) - (out ^. outputValueL) - . Just - <$> mVal - ) - <$> validatorFromHash (Script.ValidatorHash hash) - ) - $ outputReferenceScriptHash out +resolveReferenceScript out | Just (Api.ScriptHash hash) <- outputReferenceScriptHash out = do + mVal <- validatorFromHash (Script.ValidatorHash hash) + return $ do + val <- mVal + return $ (fromAbstractOutput out) {concreteOutputReferenceScript = Just val} +resolveReferenceScript _ = return Nothing outputDatumFromTxOutRef :: (MonadBlockChainWithoutValidation m) => Api.TxOutRef -> m (Maybe Api.OutputDatum) outputDatumFromTxOutRef = ((outputOutputDatum <$>) <$>) . txOutByRef diff --git a/src/Cooked/Skeleton.hs b/src/Cooked/Skeleton.hs index 23e415a40..f5aa339db 100644 --- a/src/Cooked/Skeleton.hs +++ b/src/Cooked/Skeleton.hs @@ -35,6 +35,7 @@ module Cooked.Skeleton addToTxSkelMints, txSkelMintsToList, txSkelMintsFromList, + txSkelMintsFromList', txSkelMintsValue, txSkelOutValueL, txSkelOutDatumL, @@ -608,11 +609,7 @@ type TxSkelMints = -- In every case, if you add mints with a different redeemer for the same -- policy, the redeemer used in the right argument takes precedence. instance {-# OVERLAPPING #-} Semigroup TxSkelMints where - a <> b = - foldl - (flip addToTxSkelMints) - a - (txSkelMintsToList b) + a <> b = foldl (flip addToTxSkelMints) a (txSkelMintsToList b) instance {-# OVERLAPPING #-} Monoid TxSkelMints where mempty = Map.empty @@ -694,6 +691,11 @@ txSkelMintsToList = txSkelMintsFromList :: [(Script.Versioned Script.MintingPolicy, TxSkelRedeemer, Api.TokenName, Integer)] -> TxSkelMints txSkelMintsFromList = foldr addToTxSkelMints mempty +-- | Another smart constructor for 'TxSkelMints', where the redeemer and minting +-- policies are not duplicated. +txSkelMintsFromList' :: [(Script.Versioned Script.MintingPolicy, TxSkelRedeemer, [(Api.TokenName, Integer)])] -> TxSkelMints +txSkelMintsFromList' = txSkelMintsFromList . concatMap (\(mp, r, m) -> (\(tn, i) -> (mp, r, tn, i)) <$> m) + -- | The value described by a 'TxSkelMints' txSkelMintsValue :: TxSkelMints -> Api.Value txSkelMintsValue = @@ -1001,7 +1003,7 @@ data TxSkel where -- specifying how to spend it. You must make sure that -- -- - On 'TxOutRef's referencing UTxOs belonging to public keys, you use - -- the 'TxSkelEmptyRedeemer' constructor. + -- the 'txSkelEmptyRedeemer' smart constructor. -- -- - On 'TxOutRef's referencing UTxOs belonging to scripts, you must make -- sure that the type of the redeemer is appropriate for the script. diff --git a/src/Cooked/Tweak/TamperDatum.hs b/src/Cooked/Tweak/TamperDatum.hs index f00647bb2..f1d6fbafc 100644 --- a/src/Cooked/Tweak/TamperDatum.hs +++ b/src/Cooked/Tweak/TamperDatum.hs @@ -36,7 +36,7 @@ tamperDatumTweak :: ) => -- | Use this function to return 'Just' the changed datum, if you want to -- perform a change, and 'Nothing', if you want to leave it as-is. All datums - -- on outputs not paying to a validator of type @a@ are never touched. + -- on outputs that are not of type @a@ are never touched. (a -> Maybe a) -> m [a] tamperDatumTweak change = do diff --git a/src/Cooked/Validators.hs b/src/Cooked/Validators.hs index b99ad1423..aba8ff9c6 100644 --- a/src/Cooked/Validators.hs +++ b/src/Cooked/Validators.hs @@ -6,7 +6,9 @@ module Cooked.Validators alwaysFalseValidator, alwaysFalseProposingValidator, alwaysTrueProposingValidator, - mkProposingScript, + mkScript, + validatorToTypedValidator, + validatorToTypedValidatorV2, MockContract, ) where @@ -34,6 +36,20 @@ validatorToTypedValidator val = forwardingPolicy = Script.mkForwardingMintingPolicy vValidatorHash vMintingPolicy = Script.Versioned forwardingPolicy Script.PlutusV3 +validatorToTypedValidatorV2 :: Script.Validator -> Script.TypedValidator a +validatorToTypedValidatorV2 val = + Script.TypedValidator + { Script.tvValidator = vValidator, + Script.tvValidatorHash = vValidatorHash, + Script.tvForwardingMPS = vMintingPolicy, + Script.tvForwardingMPSHash = Script.mintingPolicyHash vMintingPolicy + } + where + vValidator = Script.Versioned val Script.PlutusV2 + vValidatorHash = Script.validatorHash vValidator + forwardingPolicy = Script.mkForwardingMintingPolicy vValidatorHash + vMintingPolicy = Script.Versioned forwardingPolicy Script.PlutusV2 + -- | The trivial validator that always succeds; this is in particular a -- sufficient target for the datum hijacking attack since we only want to show -- feasibility of the attack. @@ -54,14 +70,14 @@ instance Script.ValidatorTypes MockContract where -- | A dummy false proposing validator alwaysFalseProposingValidator :: Script.Versioned Script.Script alwaysFalseProposingValidator = - mkProposingScript $$(PlutusTx.compile [||PlutusTx.traceError "False proposing validator"||]) + mkScript $$(PlutusTx.compile [||PlutusTx.traceError "False proposing validator"||]) -- | A dummy true proposing validator alwaysTrueProposingValidator :: Script.Versioned Script.Script alwaysTrueProposingValidator = - mkProposingScript $$(PlutusTx.compile [||\_ _ -> ()||]) + mkScript $$(PlutusTx.compile [||\_ _ -> ()||]) --- | Helper to build a proposing script. This should come from --- plutus-script-utils at some point. -mkProposingScript :: PlutusTx.CompiledCode (PlutusTx.BuiltinData -> PlutusTx.BuiltinData -> ()) -> Script.Versioned Script.Script -mkProposingScript code = Script.Versioned (Script.Script $ Api.serialiseCompiledCode code) Script.PlutusV3 +-- | Helper to build a script. This should come from plutus-script-utils at some +-- point. +mkScript :: PlutusTx.CompiledCode (PlutusTx.BuiltinData -> PlutusTx.BuiltinData -> ()) -> Script.Versioned Script.Script +mkScript code = Script.Versioned (Script.Script $ Api.serialiseCompiledCode code) Script.PlutusV3 diff --git a/src/Cooked/Wallet.hs b/src/Cooked/Wallet.hs index 4506c83d1..e4d14b66b 100644 --- a/src/Cooked/Wallet.hs +++ b/src/Cooked/Wallet.hs @@ -7,6 +7,7 @@ module Cooked.Wallet ( knownWallets, wallet, walletPKHashToId, + walletPKHashToWallet, walletPK, walletStakingPK, walletPKHash, @@ -66,6 +67,10 @@ wallet j walletPKHashToId :: Api.PubKeyHash -> Maybe Int walletPKHashToId = (succ <$>) . flip elemIndex (walletPKHash <$> knownWallets) +-- | Retrieves the known wallet that corresponds to a public key hash +walletPKHashToWallet :: Api.PubKeyHash -> Maybe Wallet +walletPKHashToWallet pkh = wallet . fromIntegral <$> walletPKHashToId pkh + -- | Retrieves a wallet public key (PK) walletPK :: Wallet -> Ledger.PubKey walletPK = Ledger.unPaymentPubKey . Ledger.paymentPubKey diff --git a/tests/Cooked/ProposingScriptSpec.hs b/tests/Cooked/ProposingScriptSpec.hs index fa0c67936..e152c0046 100644 --- a/tests/Cooked/ProposingScriptSpec.hs +++ b/tests/Cooked/ProposingScriptSpec.hs @@ -29,7 +29,7 @@ checkParameterChangeScript _ ctx = _ -> PlutusTx.traceError "Wrong proposal procedure" checkProposingScript :: Script.Versioned Script.Script -checkProposingScript = mkProposingScript $$(PlutusTx.compile [||checkParameterChangeScript||]) +checkProposingScript = mkScript $$(PlutusTx.compile [||checkParameterChangeScript||]) testProposingScript :: (MonadBlockChain m) => Script.Versioned Script.Script -> TxGovAction -> m () testProposingScript script govAction = From 5c6adbe427c7cd6bb1b4cec81907a3e0131b30f7 Mon Sep 17 00:00:00 2001 From: mmontin Date: Thu, 1 Aug 2024 00:26:44 +0200 Subject: [PATCH 27/44] MockChainSt has its own module now --- cooked-validators.cabal | 1 + src/Cooked/MockChain.hs | 1 + src/Cooked/MockChain/Direct.hs | 186 +------------------------ src/Cooked/MockChain/MockChainSt.hs | 209 ++++++++++++++++++++++++++++ src/Cooked/MockChain/Staged.hs | 1 + 5 files changed, 219 insertions(+), 179 deletions(-) create mode 100644 src/Cooked/MockChain/MockChainSt.hs diff --git a/cooked-validators.cabal b/cooked-validators.cabal index f6839dc13..71f6167e9 100644 --- a/cooked-validators.cabal +++ b/cooked-validators.cabal @@ -44,6 +44,7 @@ library Cooked.MockChain.GenerateTx.Proposal Cooked.MockChain.GenerateTx.Witness Cooked.MockChain.MinAda + Cooked.MockChain.MockChainSt Cooked.MockChain.Staged Cooked.MockChain.Testing Cooked.MockChain.UtxoSearch diff --git a/src/Cooked/MockChain.hs b/src/Cooked/MockChain.hs index 78ba59c79..4c9a59411 100644 --- a/src/Cooked/MockChain.hs +++ b/src/Cooked/MockChain.hs @@ -6,6 +6,7 @@ import Cooked.MockChain.Balancing as X import Cooked.MockChain.BlockChain as X import Cooked.MockChain.Direct as X import Cooked.MockChain.MinAda as X +import Cooked.MockChain.MockChainSt as X (MockChainSt (..), mockChainSt0From) import Cooked.MockChain.Staged as X hiding ( MockChainLog, MockChainLogEntry, diff --git a/src/Cooked/MockChain/Direct.hs b/src/Cooked/MockChain/Direct.hs index c2a977eca..f0dc703c8 100644 --- a/src/Cooked/MockChain/Direct.hs +++ b/src/Cooked/MockChain/Direct.hs @@ -5,9 +5,6 @@ module Cooked.MockChain.Direct where import Cardano.Api qualified as Cardano -import Cardano.Api.Shelley qualified as Cardano -import Cardano.Ledger.Shelley.API qualified as Shelley -import Cardano.Ledger.Shelley.LedgerState qualified as Shelley import Cardano.Node.Emulator.Internal.Node qualified as Emulator import Control.Applicative import Control.Arrow @@ -16,31 +13,24 @@ import Control.Monad.Except import Control.Monad.Identity import Control.Monad.Reader import Control.Monad.State.Strict -import Cooked.Conversion.ToScript -import Cooked.Conversion.ToScriptHash import Cooked.InitialDistribution import Cooked.MockChain.Balancing import Cooked.MockChain.BlockChain import Cooked.MockChain.GenerateTx import Cooked.MockChain.MinAda +import Cooked.MockChain.MockChainSt import Cooked.MockChain.UtxoState import Cooked.Output import Cooked.Skeleton import Data.Bifunctor (bimap) import Data.Default -import Data.Either.Combinators (mapLeft) -import Data.List (foldl') import Data.Map.Strict (Map) import Data.Map.Strict qualified as Map -import Data.Maybe (mapMaybe) import Data.Set qualified as Set import Ledger.Index qualified as Ledger import Ledger.Orphans () -import Ledger.Slot qualified as Ledger import Ledger.Tx qualified as Ledger import Ledger.Tx.CardanoAPI qualified as Ledger -import Optics.Core (view) -import Plutus.Script.Utils.Scripts qualified as Script import PlutusLedgerApi.V3 qualified as Api -- * Direct Emulation @@ -54,80 +44,6 @@ import PlutusLedgerApi.V3 qualified as Api -- Running a 'MockChain' produces a 'UtxoState', a simplified view on -- 'Api.UtxoIndex', which we also keep in our state. -mcstToUtxoState :: MockChainSt -> UtxoState -mcstToUtxoState MockChainSt {mcstIndex, mcstDatums} = - UtxoState - . foldr (\(address, utxoValueSet) acc -> Map.insertWith (<>) address utxoValueSet acc) Map.empty - . mapMaybe - ( extractPayload - . bimap - Ledger.fromCardanoTxIn - Ledger.fromCardanoTxOutToPV2TxInfoTxOut' - ) - . Map.toList - . Cardano.unUTxO - $ mcstIndex - where - extractPayload :: (Api.TxOutRef, Api.TxOut) -> Maybe (Api.Address, UtxoPayloadSet) - extractPayload (txOutRef, out@Api.TxOut {Api.txOutAddress, Api.txOutValue, Api.txOutDatum}) = - do - let mRefScript = outputReferenceScriptHash out - txSkelOutDatum <- - case txOutDatum of - Api.NoOutputDatum -> Just TxSkelOutNoDatum - Api.OutputDatum datum -> fst <$> Map.lookup (Script.datumHash datum) mcstDatums - Api.OutputDatumHash hash -> fst <$> Map.lookup hash mcstDatums - return - ( txOutAddress, - UtxoPayloadSet [UtxoPayload txOutRef txOutValue txSkelOutDatum mRefScript] - ) - --- | Slightly more concrete version of 'UtxoState', used to actually run the --- simulation. -data MockChainSt = MockChainSt - { mcstParams :: Emulator.Params, - mcstIndex :: Ledger.UtxoIndex, - -- map from datum hash to (datum, count), where count is the number of UTxOs - -- that currently have the datum. This map is used to display the contents - -- of the state to the user, and to recover datums for transaction - -- generation. - mcstDatums :: Map Api.DatumHash (TxSkelOutDatum, Integer), - mcstValidators :: Map Script.ValidatorHash (Script.Versioned Script.Validator), - mcstCurrentSlot :: Ledger.Slot - } - deriving (Show) - --- | Generating an emulated state for the emulator from a mockchain state and --- some parameters, based on a standard initial state -mcstToEmulatedLedgerState :: MockChainSt -> Emulator.EmulatedLedgerState -mcstToEmulatedLedgerState MockChainSt {..} = - let els@(Emulator.EmulatedLedgerState le mps) = Emulator.initialState mcstParams - in els - { Emulator._ledgerEnv = le {Shelley.ledgerSlotNo = fromIntegral mcstCurrentSlot}, - Emulator._memPoolState = - mps - { Shelley.lsUTxOState = - Shelley.smartUTxOState - (Emulator.emulatorPParams mcstParams) - (Ledger.fromPlutusIndex mcstIndex) - (Emulator.Coin 0) - (Emulator.Coin 0) - def - (Emulator.Coin 0) - } - } - -instance Eq MockChainSt where - (MockChainSt params1 index1 datums1 validators1 currentSlot1) - == (MockChainSt params2 index2 datums2 validators2 currentSlot2) = - and - [ params1 == params2, - index1 == index2, - datums1 == datums2, - validators1 == validators2, - currentSlot1 == currentSlot2 - ] - newtype MockChainT m a = MockChainT {unMockChain :: StateT MockChainSt (ExceptT MockChainError m) a} deriving newtype (Functor, Applicative, MonadState MockChainSt, MonadError MockChainError) @@ -205,92 +121,6 @@ runMockChainFrom i0 = runIdentity . runMockChainTFrom i0 runMockChain :: MockChain a -> Either MockChainError (a, UtxoState) runMockChain = runIdentity . runMockChainT --- * Canonical initial values - -utxoState0 :: UtxoState -utxoState0 = mcstToUtxoState mockChainSt0 - -mockChainSt0 :: MockChainSt -mockChainSt0 = MockChainSt def utxoIndex0 Map.empty Map.empty 0 - --- * Initial `MockChainSt` from an initial distribution - -mockChainSt0From :: InitialDistribution -> MockChainSt -mockChainSt0From i0 = MockChainSt def (utxoIndex0From i0) (datumMap0From i0) (referenceScriptMap0From i0) 0 - -instance Default MockChainSt where - def = mockChainSt0 - --- | Reference scripts from initial distributions should be accounted for in the --- `MockChainSt` which is done using this function. -referenceScriptMap0From :: InitialDistribution -> Map Script.ValidatorHash (Script.Versioned Script.Validator) -referenceScriptMap0From (InitialDistribution initDist) = - -- This builds a map of entries from the reference scripts contained in the - -- initial distribution - Map.fromList $ mapMaybe unitMaybeFrom initDist - where - -- This takes a single output and returns a possible map entry when it - -- contains a reference script - unitMaybeFrom :: TxSkelOut -> Maybe (Script.ValidatorHash, Script.Versioned Script.Validator) - unitMaybeFrom (Pays output) = do - refScript <- view outputReferenceScriptL output - let vScript@(Script.Versioned script version) = toScript refScript - Api.ScriptHash scriptHash = toScriptHash vScript - return (Script.ValidatorHash scriptHash, Script.Versioned (Script.Validator script) version) - --- | Datums from initial distributions should be accounted for in the --- `MockChainSt` which is done using this function. -datumMap0From :: InitialDistribution -> Map Api.DatumHash (TxSkelOutDatum, Integer) -datumMap0From (InitialDistribution initDist) = - -- This concatenates singleton maps from inputs and accounts for the number of - -- occurrences of similar datums - foldl' (\m -> Map.unionWith (\(d, n1) (_, n2) -> (d, n1 + n2)) m . unitMapFrom) Map.empty initDist - where - -- This takes a single output and creates an empty map if it contains no - -- datum, or a singleton map if it contains one - unitMapFrom :: TxSkelOut -> Map Api.DatumHash (TxSkelOutDatum, Integer) - unitMapFrom txSkelOut = - let datum = view txSkelOutDatumL txSkelOut - in maybe Map.empty (flip Map.singleton (datum, 1) . Script.datumHash) $ txSkelOutUntypedDatum datum - --- | This creates the initial UtxoIndex from an initial distribution by --- submitting an initial transaction with the appropriate content: --- --- - inputs consist of a single dummy pseudo input --- --- - all non-ada assets in outputs are considered minted --- --- - outputs are translated from the `TxSkelOut` list in the initial --- distribution --- --- Two things to note: --- --- - We don't know what "Magic" means for the network ID (TODO) --- --- - The genesis key hash has been taken from --- https://github.com/input-output-hk/cardano-node/blob/543b267d75d3d448e1940f9ec04b42bd01bbb16b/cardano-api/test/Test/Cardano/Api/Genesis.hs#L60 -utxoIndex0From :: InitialDistribution -> Ledger.UtxoIndex -utxoIndex0From (InitialDistribution initDist) = case mkBody of - Left err -> error $ show err - -- TODO: There may be better ways to generate this initial state, see - -- createGenesisTransaction for instance - Right body -> Ledger.initialise [[Emulator.unsafeMakeValid $ Ledger.CardanoEmulatorEraTx $ Cardano.Tx body []]] - where - mkBody :: Either GenerateTxError (Cardano.TxBody Cardano.ConwayEra) - mkBody = do - value <- mapLeft (ToCardanoError "Value error") $ Ledger.toCardanoValue (foldl' (\v -> (v <>) . view txSkelOutValueL) mempty initDist) - let mintValue = flip (Cardano.TxMintValue Cardano.MaryEraOnwardsConway) (Cardano.BuildTxWith mempty) . Cardano.filterValue (/= Cardano.AdaAssetId) $ value - theNetworkId = Cardano.Testnet $ Cardano.NetworkMagic 42 - genesisKeyHash = Cardano.GenesisUTxOKeyHash $ Shelley.KeyHash "23d51e91ae5adc7ae801e9de4cd54175fb7464ec2680b25686bbb194" - inputs = [(Cardano.genesisUTxOPseudoTxIn theNetworkId genesisKeyHash, Cardano.BuildTxWith $ Cardano.KeyWitness Cardano.KeyWitnessForSpending)] - outputs <- mapM (generateTxOut theNetworkId) initDist - left (TxBodyError "Body error") $ - Cardano.createAndValidateTransactionBody Cardano.ShelleyBasedEraConway $ - Ledger.emptyTxBodyContent {Cardano.txMintValue = mintValue, Cardano.txOuts = outputs, Cardano.txIns = inputs} - -utxoIndex0 :: Ledger.UtxoIndex -utxoIndex0 = utxoIndex0From def - -- * Direct Interpretation of Operations getIndex :: Ledger.UtxoIndex -> Map Api.TxOutRef Ledger.TxOut @@ -378,9 +208,12 @@ instance (Monad m) => MonadBlockChain (MockChainT m) where -- behavior could be subject to change in the future. Just err -> throwError (uncurry MCEValidationError err) -- Otherwise, we update known validators and datums. - Nothing -> do - modify' (\st -> st {mcstDatums = (mcstDatums st `removeMcstDatums` insData) `addMcstDatums` txSkelDataInOutputs skel}) - modify' (\st -> st {mcstValidators = mcstValidators st `Map.union` (txSkelValidatorsInOutputs skel <> txSkelReferenceScripts skel)}) + Nothing -> + modify' + ( removeDatums (Map.keys insData) + . addDatums (Map.toList $ txSkelDataInOutputs skel) + . addValidators (txSkelValidatorsInOutputs skel <> txSkelReferenceScripts skel) + ) -- We apply a change of slot when requested in the options when (txOptAutoSlotIncrease $ txSkelOpts skel) $ modify' (\st -> st {mcstCurrentSlot = mcstCurrentSlot st + 1}) @@ -388,8 +221,3 @@ instance (Monad m) => MonadBlockChain (MockChainT m) where setParams oldParams -- We return the validated transaction return cardanoTx - where - addMcstDatums stored new = Map.unionWith (\(d, n1) (_, n2) -> (d, n1 + n2)) stored (Map.map (,1) new) - -- FIXME: is this correct? What happens if we remove several similar - -- datums? - removeMcstDatums = Map.differenceWith $ \(d, n) _ -> if n == 1 then Nothing else Just (d, n - 1) diff --git a/src/Cooked/MockChain/MockChainSt.hs b/src/Cooked/MockChain/MockChainSt.hs new file mode 100644 index 000000000..45b53dbf2 --- /dev/null +++ b/src/Cooked/MockChain/MockChainSt.hs @@ -0,0 +1,209 @@ +module Cooked.MockChain.MockChainSt where + +import Cardano.Api qualified as Cardano +import Cardano.Api.Shelley qualified as Cardano +import Cardano.Ledger.Shelley.API qualified as Shelley +import Cardano.Ledger.Shelley.LedgerState qualified as Shelley +import Cardano.Node.Emulator.Internal.Node qualified as Emulator +import Control.Arrow +import Cooked.Conversion.ToScript +import Cooked.Conversion.ToScriptHash +import Cooked.InitialDistribution +import Cooked.MockChain.GenerateTx (GenerateTxError (..), generateTxOut) +import Cooked.MockChain.UtxoState +import Cooked.Output +import Cooked.Skeleton +import Data.Bifunctor (bimap) +import Data.Default +import Data.Either.Combinators (mapLeft) +import Data.List (foldl') +import Data.Map.Strict (Map) +import Data.Map.Strict qualified as Map +import Data.Maybe (mapMaybe) +import Ledger.Index qualified as Ledger +import Ledger.Orphans () +import Ledger.Slot qualified as Ledger +import Ledger.Tx qualified as Ledger +import Ledger.Tx.CardanoAPI qualified as Ledger +import Optics.Core (view) +import Plutus.Script.Utils.Scripts qualified as Script +import PlutusLedgerApi.V3 qualified as Api + +-- | Slightly more concrete version of 'UtxoState', used to actually run the +-- simulation. +data MockChainSt = MockChainSt + { mcstParams :: Emulator.Params, + mcstIndex :: Ledger.UtxoIndex, + -- map from datum hash to (datum, count), where count is the number of UTxOs + -- that currently have the datum. This map is used to display the contents + -- of the state to the user, and to recover datums for transaction + -- generation. + mcstDatums :: Map Api.DatumHash (TxSkelOutDatum, Integer), + mcstValidators :: Map Script.ValidatorHash (Script.Versioned Script.Validator), + mcstCurrentSlot :: Ledger.Slot + } + deriving (Show, Eq) + +instance Default MockChainSt where + def = mockChainSt0 + +mcstToUtxoState :: MockChainSt -> UtxoState +mcstToUtxoState MockChainSt {mcstIndex, mcstDatums} = + UtxoState + . foldr (\(address, utxoValueSet) acc -> Map.insertWith (<>) address utxoValueSet acc) Map.empty + . mapMaybe + ( extractPayload + . bimap + Ledger.fromCardanoTxIn + Ledger.fromCardanoTxOutToPV2TxInfoTxOut' + ) + . Map.toList + . Cardano.unUTxO + $ mcstIndex + where + extractPayload :: (Api.TxOutRef, Api.TxOut) -> Maybe (Api.Address, UtxoPayloadSet) + extractPayload (txOutRef, out@Api.TxOut {Api.txOutAddress, Api.txOutValue, Api.txOutDatum}) = + do + let mRefScript = outputReferenceScriptHash out + txSkelOutDatum <- + case txOutDatum of + Api.NoOutputDatum -> Just TxSkelOutNoDatum + Api.OutputDatum datum -> fst <$> Map.lookup (Script.datumHash datum) mcstDatums + Api.OutputDatumHash hash -> fst <$> Map.lookup hash mcstDatums + return + ( txOutAddress, + UtxoPayloadSet [UtxoPayload txOutRef txOutValue txSkelOutDatum mRefScript] + ) + +-- | Generating an emulated state for the emulator from a mockchain state and +-- some parameters, based on a standard initial state +mcstToEmulatedLedgerState :: MockChainSt -> Emulator.EmulatedLedgerState +mcstToEmulatedLedgerState MockChainSt {..} = + let els@(Emulator.EmulatedLedgerState le mps) = Emulator.initialState mcstParams + in els + { Emulator._ledgerEnv = le {Shelley.ledgerSlotNo = fromIntegral mcstCurrentSlot}, + Emulator._memPoolState = + mps + { Shelley.lsUTxOState = + Shelley.smartUTxOState + (Emulator.emulatorPParams mcstParams) + (Ledger.fromPlutusIndex mcstIndex) + (Emulator.Coin 0) + (Emulator.Coin 0) + def + (Emulator.Coin 0) + } + } + +addDatums :: [(Api.DatumHash, TxSkelOutDatum)] -> MockChainSt -> MockChainSt +addDatums toAdd st@(MockChainSt {mcstDatums}) = + st + { mcstDatums = + foldl + ( \datumMap (dHash, dat) -> + Map.insertWith (\(d, n) (_, n') -> (d, n + n')) dHash (dat, 1) datumMap + ) + mcstDatums + toAdd + } + +removeDatums :: [Api.DatumHash] -> MockChainSt -> MockChainSt +removeDatums toRemove st@(MockChainSt {mcstDatums}) = + st + { mcstDatums = + foldl + (flip (Map.update (\(dat, n) -> (dat,) <$> minusMaybe n))) + mcstDatums + toRemove + } + where + -- This is unsafe as this assumes n >= 1 + minusMaybe :: Integer -> Maybe Integer + minusMaybe n | n == 1 = Nothing + minusMaybe n = Just $ n - 1 + +addValidators :: Map Script.ValidatorHash (Script.Versioned Script.Validator) -> MockChainSt -> MockChainSt +addValidators valMap st@(MockChainSt {mcstValidators}) = st {mcstValidators = Map.union valMap mcstValidators} + +-- * Canonical initial values + +utxoState0 :: UtxoState +utxoState0 = mcstToUtxoState mockChainSt0 + +mockChainSt0 :: MockChainSt +mockChainSt0 = MockChainSt def utxoIndex0 Map.empty Map.empty 0 + +-- * Initial `MockChainSt` from an initial distribution + +mockChainSt0From :: InitialDistribution -> MockChainSt +mockChainSt0From i0 = MockChainSt def (utxoIndex0From i0) (datumMap0From i0) (referenceScriptMap0From i0) 0 + +-- | Reference scripts from initial distributions should be accounted for in the +-- `MockChainSt` which is done using this function. +referenceScriptMap0From :: InitialDistribution -> Map Script.ValidatorHash (Script.Versioned Script.Validator) +referenceScriptMap0From (InitialDistribution initDist) = + -- This builds a map of entries from the reference scripts contained in the + -- initial distribution + Map.fromList $ mapMaybe unitMaybeFrom initDist + where + -- This takes a single output and returns a possible map entry when it + -- contains a reference script + unitMaybeFrom :: TxSkelOut -> Maybe (Script.ValidatorHash, Script.Versioned Script.Validator) + unitMaybeFrom (Pays output) = do + refScript <- view outputReferenceScriptL output + let vScript@(Script.Versioned script version) = toScript refScript + Api.ScriptHash scriptHash = toScriptHash vScript + return (Script.ValidatorHash scriptHash, Script.Versioned (Script.Validator script) version) + +-- | Datums from initial distributions should be accounted for in the +-- `MockChainSt` which is done using this function. +datumMap0From :: InitialDistribution -> Map Api.DatumHash (TxSkelOutDatum, Integer) +datumMap0From (InitialDistribution initDist) = + -- This concatenates singleton maps from inputs and accounts for the number of + -- occurrences of similar datums + foldl' (\m -> Map.unionWith (\(d, n1) (_, n2) -> (d, n1 + n2)) m . unitMapFrom) Map.empty initDist + where + -- This takes a single output and creates an empty map if it contains no + -- datum, or a singleton map if it contains one + unitMapFrom :: TxSkelOut -> Map Api.DatumHash (TxSkelOutDatum, Integer) + unitMapFrom txSkelOut = + let datum = view txSkelOutDatumL txSkelOut + in maybe Map.empty (flip Map.singleton (datum, 1) . Script.datumHash) $ txSkelOutUntypedDatum datum + +-- | This creates the initial UtxoIndex from an initial distribution by +-- submitting an initial transaction with the appropriate content: +-- +-- - inputs consist of a single dummy pseudo input +-- +-- - all non-ada assets in outputs are considered minted +-- +-- - outputs are translated from the `TxSkelOut` list in the initial +-- distribution +-- +-- Two things to note: +-- +-- - We don't know what "Magic" means for the network ID (TODO) +-- +-- - The genesis key hash has been taken from +-- https://github.com/input-output-hk/cardano-node/blob/543b267d75d3d448e1940f9ec04b42bd01bbb16b/cardano-api/test/Test/Cardano/Api/Genesis.hs#L60 +utxoIndex0From :: InitialDistribution -> Ledger.UtxoIndex +utxoIndex0From (InitialDistribution initDist) = case mkBody of + Left err -> error $ show err + -- TODO: There may be better ways to generate this initial state, see + -- createGenesisTransaction for instance + Right body -> Ledger.initialise [[Emulator.unsafeMakeValid $ Ledger.CardanoEmulatorEraTx $ Cardano.Tx body []]] + where + mkBody :: Either GenerateTxError (Cardano.TxBody Cardano.ConwayEra) + mkBody = do + value <- mapLeft (ToCardanoError "Value error") $ Ledger.toCardanoValue (foldl' (\v -> (v <>) . view txSkelOutValueL) mempty initDist) + let mintValue = flip (Cardano.TxMintValue Cardano.MaryEraOnwardsConway) (Cardano.BuildTxWith mempty) . Cardano.filterValue (/= Cardano.AdaAssetId) $ value + theNetworkId = Cardano.Testnet $ Cardano.NetworkMagic 42 + genesisKeyHash = Cardano.GenesisUTxOKeyHash $ Shelley.KeyHash "23d51e91ae5adc7ae801e9de4cd54175fb7464ec2680b25686bbb194" + inputs = [(Cardano.genesisUTxOPseudoTxIn theNetworkId genesisKeyHash, Cardano.BuildTxWith $ Cardano.KeyWitness Cardano.KeyWitnessForSpending)] + outputs <- mapM (generateTxOut theNetworkId) initDist + left (TxBodyError "Body error") $ + Cardano.createAndValidateTransactionBody Cardano.ShelleyBasedEraConway $ + Ledger.emptyTxBodyContent {Cardano.txMintValue = mintValue, Cardano.txOuts = outputs, Cardano.txIns = inputs} + +utxoIndex0 :: Ledger.UtxoIndex +utxoIndex0 = utxoIndex0From def diff --git a/src/Cooked/MockChain/Staged.hs b/src/Cooked/MockChain/Staged.hs index a61a6cff9..7bb1bdc7f 100644 --- a/src/Cooked/MockChain/Staged.hs +++ b/src/Cooked/MockChain/Staged.hs @@ -30,6 +30,7 @@ import Control.Monad.Trans.Writer (WriterT (runWriterT), tell) import Cooked.Ltl import Cooked.MockChain.BlockChain import Cooked.MockChain.Direct +import Cooked.MockChain.MockChainSt import Cooked.MockChain.UtxoState import Cooked.Skeleton import Cooked.Tweak.Common From db8439404a7a82ce54f4ee381e644b65f73d9b21 Mon Sep 17 00:00:00 2001 From: mmontin Date: Thu, 1 Aug 2024 00:48:06 +0200 Subject: [PATCH 28/44] Support for hashed datums in reference inputs --- CHANGELOG.md | 7 +++ cooked-validators.cabal | 2 + package.yaml | 1 + src/Cooked/MockChain/Balancing.hs | 2 +- src/Cooked/MockChain/BlockChain.hs | 55 +++++++++++------- src/Cooked/MockChain/Direct.hs | 9 +-- src/Cooked/MockChain/GenerateTx/Body.hs | 73 ++++++++++++++++++------ src/Cooked/Pretty/Cooked.hs | 21 +++++-- src/Cooked/Skeleton.hs | 32 ++++++----- tests/Cooked/BalancingSpec.hs | 2 +- tests/Cooked/InitialDistributionSpec.hs | 6 +- tests/Cooked/InlineDatumsSpec.hs | 2 +- tests/Cooked/ReferenceInputsSpec.hs | 75 +++++++++++++++++++------ 13 files changed, 204 insertions(+), 83 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c77dcd6c8..5a1891413 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ - `validatorToTypedValidatorV2` - `walletPKHashToWallet` that retrives a wallet from a pkh. It used to be present but somehow disapeared. +- It is now possible to reference an output which has a hashed datum. +- `txSkelHashedData` the gives all the datum hashes in inputs and reference inputs. ### Removed @@ -29,6 +31,11 @@ `txSkelSomeRedeemerAndReferenceScript`, `txSkelEmptyRedeemerAndReferenceScript`. - `mkProposingScript` changed to `mkScript` +- `withDatumHashed` changed to `withUnresolvedDatumHash` +- `paysScriptDatumHashed` changed to `paysScriptUnresolvedDatumHash` +- `txSkelInputData` changed to `txSkelConsumedData` +- Pretty printing of hashed datum now includes the hash (and not only the + resolved datum). ### Fixed diff --git a/cooked-validators.cabal b/cooked-validators.cabal index 71f6167e9..03cd53ba9 100644 --- a/cooked-validators.cabal +++ b/cooked-validators.cabal @@ -110,6 +110,7 @@ library , cardano-api , cardano-crypto , cardano-data + , cardano-ledger-alonzo , cardano-ledger-conway , cardano-ledger-core , cardano-ledger-shelley @@ -203,6 +204,7 @@ test-suite spec , cardano-api , cardano-crypto , cardano-data + , cardano-ledger-alonzo , cardano-ledger-conway , cardano-ledger-core , cardano-ledger-shelley diff --git a/package.yaml b/package.yaml index e0fc963f0..6bd307a0a 100644 --- a/package.yaml +++ b/package.yaml @@ -11,6 +11,7 @@ dependencies: - cardano-api - cardano-crypto - cardano-data + - cardano-ledger-alonzo - cardano-ledger-core - cardano-ledger-shelley - cardano-ledger-conway diff --git a/src/Cooked/MockChain/Balancing.hs b/src/Cooked/MockChain/Balancing.hs index ee9814630..d58c75481 100644 --- a/src/Cooked/MockChain/Balancing.hs +++ b/src/Cooked/MockChain/Balancing.hs @@ -260,7 +260,7 @@ estimateTxSkelFee :: (MonadBlockChainBalancing m) => TxSkel -> Fee -> Collateral estimateTxSkelFee skel fee collateralIns returnCollateralWallet = do -- We retrieve the necessary data to generate the transaction body params <- getParams - managedData <- txSkelInputData skel + managedData <- txSkelHashedData skel managedTxOuts <- lookupUtxosPl $ txSkelKnownTxOutRefs skel <> Set.toList collateralIns managedValidators <- txSkelInputValidators skel -- We generate the transaction body content, handling errors in the meantime diff --git a/src/Cooked/MockChain/BlockChain.hs b/src/Cooked/MockChain/BlockChain.hs index ed4f9c2ff..038d26c99 100644 --- a/src/Cooked/MockChain/BlockChain.hs +++ b/src/Cooked/MockChain/BlockChain.hs @@ -45,7 +45,8 @@ module Cooked.MockChain.BlockChain txSkelReferenceInputUtxos, txSkelInputValidators, txSkelInputValue, - txSkelInputData, + txSkelHashedData, + txSkelConsumedData, lookupUtxos, lookupUtxosPl, validateTxSkel', @@ -340,28 +341,40 @@ lookupUtxos = txSkelInputValue :: (MonadBlockChainBalancing m) => TxSkel -> m Api.Value txSkelInputValue = (foldMap (Api.txOutValue . txOutV2FromLedger) <$>) . txSkelInputUtxos --- | Look up the data on UTxOs the transaction consumes. -txSkelInputData :: (MonadBlockChainBalancing m) => TxSkel -> m (Map Api.DatumHash Api.Datum) -txSkelInputData skel = do - mDatumHashes <- - mapMaybe - ( \output -> - case output ^. outputDatumL of - Api.NoOutputDatum -> Nothing - Api.OutputDatum datum -> Just $ Script.datumHash datum - Api.OutputDatumHash dHash -> Just dHash - ) - . Map.elems - <$> txSkelInputUtxosPl skel - Map.fromList - <$> mapM - ( \dHash -> +-- | Looks up and resolves the hashed datums on UTxOs the transaction consumes +-- or references, which will be needed by the transaction body. +txSkelHashedData :: (MonadBlockChainBalancing m) => TxSkel -> m (Map Api.DatumHash Api.Datum) +txSkelHashedData skel = do + let outputToDatumHashM output = case output ^. outputDatumL of + Api.OutputDatumHash dHash -> Just dHash + _ -> Nothing + (Map.elems -> inputTxOuts) <- txSkelInputUtxosPl skel + (Map.elems -> refInputTxOuts) <- txSkelReferenceInputUtxosPl skel + foldM + ( \dat dHash -> + maybeErrM + (MCEUnknownDatum "txSkelHashedData: Transaction input with unknown datum hash" dHash) + (\rDat -> Map.insert dHash rDat dat) + (datumFromHash dHash) + ) + Map.empty + (mapMaybe outputToDatumHashM $ inputTxOuts <> refInputTxOuts) + +-- | Looks us the data on UTxOs the transaction consumes. This corresponds to +-- the keys of what should be removed from the stored datums in our mockchain. +-- There can be duplicates, which is expected. +txSkelConsumedData :: (MonadBlockChainBalancing m) => TxSkel -> m [Api.DatumHash] +txSkelConsumedData skel = do + let outputToDatumHashM output = case output ^. outputDatumL of + Api.OutputDatumHash dHash -> maybeErrM - (MCEUnknownDatum "txSkelInputData: Transaction input with un-resolvable datum hash" dHash) - (dHash,) + (MCEUnknownDatum "txSkelConsumedData: Transaction input with unknown datum hash" dHash) + (Just . const dHash) (datumFromHash dHash) - ) - mDatumHashes + Api.OutputDatum datum -> return $ Just $ Script.datumHash datum + Api.NoOutputDatum -> return Nothing + (Map.elems -> inputTxOuts) <- txSkelInputUtxosPl skel + catMaybes <$> mapM outputToDatumHashM inputTxOuts -- ** Slot and Time Management diff --git a/src/Cooked/MockChain/Direct.hs b/src/Cooked/MockChain/Direct.hs index f0dc703c8..f69cf444e 100644 --- a/src/Cooked/MockChain/Direct.hs +++ b/src/Cooked/MockChain/Direct.hs @@ -170,7 +170,8 @@ instance (Monad m) => MonadBlockChain (MockChainT m) where -- We retrieve data that will be used in the transaction generation process: -- datums, validators and various kinds of inputs. This idea is to provide a -- rich-enough context for the transaction generation to succeed. - insData <- txSkelInputData skel + hashedData <- txSkelHashedData skel + insData <- txSkelConsumedData skel insValidators <- txSkelInputValidators skel insMap <- txSkelInputUtxosPl skel refInsMap <- txSkelReferenceInputUtxosPl skel @@ -178,7 +179,7 @@ instance (Monad m) => MonadBlockChain (MockChainT m) where -- We attempt to generate the transaction associated with the balanced -- skeleton and the retrieved data. This is an internal generation, there is -- no validation involved yet. - cardanoTx <- case generateTx fee returnCollateralWallet collateralIns newParams insData (insMap <> refInsMap <> collateralInsMap) insValidators skel of + cardanoTx <- case generateTx fee returnCollateralWallet collateralIns newParams hashedData (insMap <> refInsMap <> collateralInsMap) insValidators skel of Left err -> throwError . MCEGenerationError $ err -- We apply post-generation modification when applicable Right tx -> return $ Ledger.CardanoEmulatorEraTx $ applyRawModOnBalancedTx (txOptUnsafeModTx . txSkelOpts $ skelUnbal) tx @@ -210,8 +211,8 @@ instance (Monad m) => MonadBlockChain (MockChainT m) where -- Otherwise, we update known validators and datums. Nothing -> modify' - ( removeDatums (Map.keys insData) - . addDatums (Map.toList $ txSkelDataInOutputs skel) + ( removeDatums insData + . addDatums (txSkelDataInOutputs skel) . addValidators (txSkelValidatorsInOutputs skel <> txSkelReferenceScripts skel) ) -- We apply a change of slot when requested in the options diff --git a/src/Cooked/MockChain/GenerateTx/Body.hs b/src/Cooked/MockChain/GenerateTx/Body.hs index bbfb958be..2c816617f 100644 --- a/src/Cooked/MockChain/GenerateTx/Body.hs +++ b/src/Cooked/MockChain/GenerateTx/Body.hs @@ -1,7 +1,14 @@ module Cooked.MockChain.GenerateTx.Body where import Cardano.Api qualified as Cardano +import Cardano.Api.Shelley qualified as Cardano +import Cardano.Ledger.Alonzo.Tx qualified as Alonzo +import Cardano.Ledger.Alonzo.TxBody qualified as Alonzo +import Cardano.Ledger.Alonzo.TxWits qualified as Alonzo +import Cardano.Ledger.Conway.PParams qualified as Conway +import Cardano.Ledger.Plutus qualified as Cardano import Cardano.Node.Emulator.Internal.Node qualified as Emulator +import Control.Lens qualified as Lens import Control.Monad import Control.Monad.Reader import Cooked.MockChain.GenerateTx.Collateral qualified as Collateral @@ -12,10 +19,11 @@ import Cooked.MockChain.GenerateTx.Output qualified as Output import Cooked.MockChain.GenerateTx.Proposal qualified as Proposal import Cooked.Skeleton import Cooked.Wallet -import Data.Bifunctor +import Data.Either.Combinators import Data.Map (Map) import Data.Map qualified as Map import Data.Set (Set) +import Data.Set qualified as Set import Ledger.Address qualified as Ledger import Ledger.Tx qualified as Ledger import Ledger.Tx.CardanoAPI qualified as Ledger @@ -86,8 +94,8 @@ txSkelToBodyContent skel@TxSkel {..} | txSkelReferenceInputs <- txSkelReferenceT let txMetadata = Cardano.TxMetadataNone -- That's what plutus-apps does as well txAuxScripts = Cardano.TxAuxScriptsNone -- That's what plutus-apps does as well txWithdrawals = Cardano.TxWithdrawalsNone -- That's what plutus-apps does as well - txCertificates = Cardano.TxCertificatesNone -- That's what plutus-apps does as well txUpdateProposal = Cardano.TxUpdateProposalNone -- That's what plutus-apps does as well + txCertificates = Cardano.TxCertificatesNone -- That's what plutus-apps does as well txScriptValidity = Cardano.TxScriptValidityNone -- That's what plutus-apps does as well txVotingProcedures = Nothing -- TODO, same as above return Cardano.TxBodyContent {..} @@ -97,17 +105,50 @@ txSkelToBodyContent skel@TxSkel {..} | txSkelReferenceInputs <- txSkelReferenceT txSkelToCardanoTx :: TxSkel -> BodyGen (Cardano.Tx Cardano.ConwayEra) txSkelToCardanoTx txSkel = do txBodyContent <- txSkelToBodyContent txSkel - cardanoTxUnsigned <- - lift $ - bimap - (TxBodyError "generateTx: ") - (`Cardano.Tx` []) - (Cardano.createAndValidateTransactionBody Cardano.ShelleyBasedEraConway txBodyContent) - foldM - ( \tx wal -> - case Ledger.addCardanoTxWitness (Ledger.toWitness $ Ledger.PaymentPrivateKey $ walletSK wal) (Ledger.CardanoTx tx Cardano.ShelleyBasedEraConway) of - Ledger.CardanoTx tx' Cardano.ShelleyBasedEraConway -> return tx' - _ -> throwOnString "txSkelToCardanoTx: Wrong output era" - ) - cardanoTxUnsigned - (txSkelSigners txSkel) + + -- We create the associated Shelley TxBody + txBody@(Cardano.ShelleyTxBody a body c dats e f) <- + lift $ mapLeft (TxBodyError "generateTx :") $ Cardano.createAndValidateTransactionBody Cardano.ShelleyBasedEraConway txBodyContent + + -- There is a chance that the body is in need of additional data. This happens + -- when the set of reference inputs contains hashed datums that will need to + -- be resolved during phase 2 validation. All that follows until the + -- definition of "txBody'" aims at doing just that. In the process, we have to + -- reconstruct the body with the new data and the associated hash. Hopefully, + -- in the future, cardano-api provides a way to add those data in the body + -- directly without requiring this methods, which somewhat feels like a hack. + mData <- asks managedData + mTxOut <- asks managedTxOuts + refIns <- forM (txSkelReferenceTxOutRefs txSkel) $ \oRef -> + throwOnLookup ("txSkelToCardanoTx: Unable to resolve TxOutRef " <> show oRef) oRef mTxOut + let datumHashes = [hash | (Api.TxOut _ _ (Api.OutputDatumHash hash) _) <- refIns] + additionalData <- forM datumHashes $ \dHash -> + throwOnLookup ("txSkelToCardanoTx: Unable to resolve datum hash " <> show dHash) dHash mData + let additionalDataMap = Map.fromList [(Cardano.hashData dat, dat) | Api.Datum (Cardano.Data . Api.toData -> dat) <- additionalData] + toLangDepViewParam <- asks (Conway.getLanguageView . Cardano.unLedgerProtocolParameters . Emulator.ledgerProtocolParameters . params) + let txDats' = Alonzo.TxDats additionalDataMap + (era, datums, redeemers) = case dats of + Cardano.TxBodyNoScriptData -> (Cardano.AlonzoEraOnwardsConway, txDats', Alonzo.Redeemers Map.empty) + Cardano.TxBodyScriptData era' txDats reds -> (era', txDats <> txDats', reds) + witnesses = Cardano.collectTxBodyScriptWitnesses Cardano.ShelleyBasedEraConway txBodyContent + languages = [toCardanoLanguage v | (_, Cardano.AnyScriptWitness (Cardano.PlutusScriptWitness _ v _ _ _ _)) <- witnesses] + scriptIntegrityHash = + Cardano.alonzoEraOnwardsConstraints era $ + Alonzo.hashScriptIntegrity (Set.fromList $ toLangDepViewParam <$> languages) redeemers datums + body' = body Lens.& Alonzo.scriptIntegrityHashTxBodyL Lens..~ scriptIntegrityHash + txBody' = Cardano.ShelleyTxBody a body' c (Cardano.TxBodyScriptData era datums redeemers) e f + + -- We return the transaction signed by all the required signers. The body is + -- chosen based on whether or not it required additional data. + return $ + Ledger.getEmulatorEraTx $ + foldl + (flip Ledger.addCardanoTxWitness) + (Ledger.CardanoEmulatorEraTx $ Cardano.Tx (if null additionalDataMap then txBody else txBody') []) + (Ledger.toWitness . Ledger.PaymentPrivateKey . walletSK <$> txSkelSigners txSkel) + where + toCardanoLanguage :: Cardano.PlutusScriptVersion lang -> Cardano.Language + toCardanoLanguage = \case + Cardano.PlutusScriptV1 -> Cardano.PlutusV1 + Cardano.PlutusScriptV2 -> Cardano.PlutusV2 + Cardano.PlutusScriptV3 -> Cardano.PlutusV3 diff --git a/src/Cooked/Pretty/Cooked.hs b/src/Cooked/Pretty/Cooked.hs index aa1846f08..b4431d8cb 100644 --- a/src/Cooked/Pretty/Cooked.hs +++ b/src/Cooked/Pretty/Cooked.hs @@ -355,9 +355,12 @@ prettyTxSkelOut opts (Pays output) = "Datum (inlined):" <+> (PP.align . prettyCookedOpt opts) (output ^. outputDatumL) - Api.OutputDatumHash _datum -> + Api.OutputDatumHash dHash -> Just $ - "Datum (hashed):" + "Datum (hashed)" + <+> "(" + <> prettyHash (pcOptHashes opts) (toHash dHash) + <> "):" <+> (PP.align . prettyCookedOpt opts) (output ^. outputDatumL) Api.NoOutputDatum -> Nothing, @@ -371,9 +374,19 @@ prettyTxSkelOutDatumMaybe opts txSkelOutDatum@(TxSkelOutInlineDatum _) = Just $ "Datum (inlined):" <+> PP.align (prettyCookedOpt opts txSkelOutDatum) -prettyTxSkelOutDatumMaybe opts txSkelOutDatum = +prettyTxSkelOutDatumMaybe opts txSkelOutDatum@(TxSkelOutDatumHash dat) = Just $ - "Datum (hashed):" + "Datum (hashed)" + <+> "(" + <> prettyHash (pcOptHashes opts) (toHash $ Script.datumHash $ Api.Datum $ Api.toBuiltinData dat) + <> "):" + <+> PP.align (prettyCookedOpt opts txSkelOutDatum) +prettyTxSkelOutDatumMaybe opts txSkelOutDatum@(TxSkelOutDatum dat) = + Just $ + "Datum (hashed)" + <+> "(" + <> prettyHash (pcOptHashes opts) (toHash $ Script.datumHash $ Api.Datum $ Api.toBuiltinData dat) + <> "):" <+> PP.align (prettyCookedOpt opts txSkelOutDatum) prettyTxSkelIn :: PrettyCookedOpts -> SkelContext -> (Api.TxOutRef, TxSkelRedeemer) -> DocCooked diff --git a/src/Cooked/Skeleton.hs b/src/Cooked/Skeleton.hs index f5aa339db..4c9d1943e 100644 --- a/src/Cooked/Skeleton.hs +++ b/src/Cooked/Skeleton.hs @@ -49,11 +49,11 @@ module Cooked.Skeleton paysPK, paysScript, paysScriptInlineDatum, - paysScriptDatumHash, + paysScriptUnresolvedDatumHash, paysScriptNoDatum, withDatum, withInlineDatum, - withDatumHash, + withUnresolvedDatumHash, withReferenceScript, withStakingCredential, TxSkelRedeemer (..), @@ -870,8 +870,9 @@ paysPK pkh value = (Nothing @(Script.Versioned Script.Script)) ) --- | Pays a script a certain value with a certain datum, using the +-- | Pays a script a certain value with a certain datum hash, using the -- 'TxSkelOutDatum' constructor. (See the documentation of 'TxSkelOutDatum'.) +-- The datum is resolved in the transaction. paysScript :: ( Api.ToData (Script.DatumType a), Show (Script.DatumType a), @@ -917,9 +918,9 @@ paysScriptInlineDatum validator datum value = (Nothing @(Script.Versioned Script.Script)) ) --- | Pays a script a certain value with a certain hashed (not resolved in --- transaction) datum. -paysScriptDatumHash :: +-- | Pays a script a certain value with a certain hashed datum which is not +-- resolved in the transaction (as opposed to "paysScript"). +paysScriptUnresolvedDatumHash :: ( Api.ToData (Script.DatumType a), Show (Script.DatumType a), Typeable (Script.DatumType a), @@ -931,7 +932,7 @@ paysScriptDatumHash :: Script.DatumType a -> Api.Value -> TxSkelOut -paysScriptDatumHash validator datum value = +paysScriptUnresolvedDatumHash validator datum value = Pays ( ConcreteOutput validator @@ -942,7 +943,7 @@ paysScriptDatumHash validator datum value = ) -- | Pays a script a certain value without any datum. Intended to be used with --- 'withDatum', 'withDatumHash', or 'withInlineDatum' to try a datum whose type +-- 'withDatum', 'withUnresolvedDatumHash', or 'withInlineDatum' to try a datum whose type -- does not match the validator's. paysScriptNoDatum :: (Typeable a) => Script.TypedValidator a -> Api.Value -> TxSkelOut paysScriptNoDatum validator value = @@ -968,8 +969,8 @@ withInlineDatum (Pays output) datum = Pays $ (fromAbstractOutput output) {concre -- | Set the datum in a payment to the given hashed (not resolved in the -- transaction) datum (whose type may not fit the typed validator in case of a -- script). -withDatumHash :: (Api.ToData a, Show a, Typeable a, PlutusTx.Eq a, PrettyCooked a) => TxSkelOut -> a -> TxSkelOut -withDatumHash (Pays output) datum = Pays $ (fromAbstractOutput output) {concreteOutputDatum = TxSkelOutDatumHash datum} +withUnresolvedDatumHash :: (Api.ToData a, Show a, Typeable a, PlutusTx.Eq a, PrettyCooked a) => TxSkelOut -> a -> TxSkelOut +withUnresolvedDatumHash (Pays output) datum = Pays $ (fromAbstractOutput output) {concreteOutputDatum = TxSkelOutDatumHash datum} -- | Add a reference script to a transaction output (or replace it if there is -- already one) @@ -1058,18 +1059,19 @@ data SkelContext = SkelContext txSkelValueInOutputs :: TxSkel -> Api.Value txSkelValueInOutputs = foldOf (txSkelOutsL % folded % txSkelOutValueL) --- | Return all data on transaction outputs. -txSkelDataInOutputs :: TxSkel -> Map Api.DatumHash TxSkelOutDatum +-- | Return all data on transaction outputs. This can contain duplicates, which +-- is intended. +txSkelDataInOutputs :: TxSkel -> [(Api.DatumHash, TxSkelOutDatum)] txSkelDataInOutputs = foldMapOf ( txSkelOutsL % folded % txSkelOutDatumL ) - ( \txSkelOutDatum -> do + ( \txSkelOutDatum -> maybe - Map.empty - (\datum -> Map.singleton (Script.datumHash datum) txSkelOutDatum) + [] + (\datum -> [(Script.datumHash datum, txSkelOutDatum)]) (txSkelOutUntypedDatum txSkelOutDatum) ) diff --git a/tests/Cooked/BalancingSpec.hs b/tests/Cooked/BalancingSpec.hs index dcf08c95b..660af156c 100644 --- a/tests/Cooked/BalancingSpec.hs +++ b/tests/Cooked/BalancingSpec.hs @@ -40,7 +40,7 @@ initialDistributionBalancing = paysPK alice (Script.ada 30), paysPK alice (Script.lovelace 1280229 <> banana 3) `withDatum` (10 :: Integer), paysPK alice (Script.ada 1 <> banana 7) `withReferenceScript` (alwaysTrueValidator @MockContract), - paysPK alice (Script.ada 105 <> banana 2) `withDatumHash` () + paysPK alice (Script.ada 105 <> banana 2) `withUnresolvedDatumHash` () ] type TestBalancingOutcome = (TxSkel, TxSkel, Integer, Set Api.TxOutRef, [Api.TxOutRef]) diff --git a/tests/Cooked/InitialDistributionSpec.hs b/tests/Cooked/InitialDistributionSpec.hs index 2ae856e61..d586035a2 100644 --- a/tests/Cooked/InitialDistributionSpec.hs +++ b/tests/Cooked/InitialDistributionSpec.hs @@ -16,7 +16,7 @@ alice, bob :: Wallet -- type Int and value 10 for each datum kind initialDistributionWithDatum :: InitialDistribution initialDistributionWithDatum = - InitialDistribution $ [withDatum, withInlineDatum, withDatumHash] <*> [paysPK alice (Script.ada 2)] <*> [10 :: Integer] + InitialDistribution $ [withDatum, withInlineDatum] <*> [paysPK alice (Script.ada 2)] <*> [10 :: Integer] -- | An initial distribution where alice owns a UTxO with a reference -- script corresponding to the always succeed validators and bob owns @@ -51,8 +51,8 @@ tests :: TestTree tests = testGroup "Initial distributions" - [ testCase "Reading datums placed in the initial distribution, inlined, hashed or vanilla" $ - testSucceedsFrom' def (\results _ -> testBool $ results == [10, 10, 10]) initialDistributionWithDatum getValueFromInitialDatum, + [ testCase "Reading datums placed in the initial distribution, inlined or hashed" $ + testSucceedsFrom' def (\results _ -> testBool $ results == [10, 10]) initialDistributionWithDatum getValueFromInitialDatum, testCase "Spending a script placed as a reference script in the initial distribution" $ testSucceedsFrom def initialDistributionWithReferenceScript spendReferenceAlwaysTrueValidator ] diff --git a/tests/Cooked/InlineDatumsSpec.hs b/tests/Cooked/InlineDatumsSpec.hs index 160d21f75..d09271d7b 100644 --- a/tests/Cooked/InlineDatumsSpec.hs +++ b/tests/Cooked/InlineDatumsSpec.hs @@ -155,7 +155,7 @@ continuingOutputTestTrace datumKindOnSecondPayment validator = do txSkelIns = Map.singleton theTxOutRef $ txSkelSomeRedeemer (), txSkelOuts = [ ( case datumKindOnSecondPayment of - OnlyHash -> paysScriptDatumHash validator SecondPaymentDatum + OnlyHash -> paysScriptUnresolvedDatumHash validator SecondPaymentDatum Datum -> paysScript validator SecondPaymentDatum Inline -> paysScriptInlineDatum validator SecondPaymentDatum ) diff --git a/tests/Cooked/ReferenceInputsSpec.hs b/tests/Cooked/ReferenceInputsSpec.hs index ddde28b71..36b49d06f 100644 --- a/tests/Cooked/ReferenceInputsSpec.hs +++ b/tests/Cooked/ReferenceInputsSpec.hs @@ -10,21 +10,24 @@ import Plutus.Script.Utils.V3.Typed.Scripts qualified as Script import Plutus.Script.Utils.Value qualified as Script import PlutusLedgerApi.V3 qualified as Api import PlutusTx qualified +import PlutusTx.AssocMap qualified as PlutusTx (lookup) +import PlutusTx.Eq qualified as PlutusTx import PlutusTx.Prelude qualified as PlutusTx import Prettyprinter qualified as PP import Test.Tasty qualified as Tasty import Test.Tasty.HUnit qualified as Tasty --- Foo and Bar are two dummy scripts to test reference inputs. They --- serve no purpose and make no real sense. +-- Foo, Bar and Baz are dummy scripts to test reference inputs. They serve no +-- purpose and make no real sense. -- --- Foo contains a pkh in its datum. It can only be spent by ANOTHER --- public key. +-- Foo contains a pkh in its datum. It can only be spent by ANOTHER public key. -- --- Bar has no datum nor redeemer. Its outputs can only be spent by a --- public key who can provide a Foo UTxO containing its pkh as --- reference input (that is a UTxO they could not actually spend, --- according the the design of Foo). +-- Bar has no datum nor redeemer. Its outputs can only be spent by a public key +-- who can provide a Foo UTxO containing its pkh as reference input (that is a +-- UTxO they could not actually spend, according the the design of Foo). +-- +-- Baz has no datum nor redeemer. Its outputs can only be spent when a reference +-- input is provided with a hashed datum contain the integer 10. -- -- The datum in Foo outputs in expected to be inlined. @@ -58,12 +61,6 @@ fooTypedValidator = $$(PlutusTx.compile [||fooValidator||]) $$(PlutusTx.compile [||wrap||]) -data Bar - -instance Script.ValidatorTypes Bar where - type RedeemerType Bar = () - type DatumType Bar = () - -- | Outputs can only be spent by pks who provide a reference input to -- a Foo in which they are mentioned (in an inlined datum). barValidator :: () -> () -> Api.ScriptContext -> Bool @@ -77,13 +74,35 @@ barValidator _ _ (Api.ScriptContext txInfo _) = Just (FooDatum pkh) -> PlutusTx.elem pkh (Api.txInfoSignatories txInfo) f _ = False -barTypedValidator :: Script.TypedValidator Bar +barTypedValidator :: Script.TypedValidator MockContract barTypedValidator = let wrap = Script.mkUntypedValidator - in Script.mkTypedValidator @Bar + in Script.mkTypedValidator @MockContract $$(PlutusTx.compile [||barValidator||]) $$(PlutusTx.compile [||wrap||]) +bazValidator :: () -> () -> Api.ScriptContext -> Bool +bazValidator _ _ context = + let info = Api.scriptContextTxInfo context + refInputs = Api.txInfoReferenceInputs info + txData = Api.txInfoData info + in case refInputs of + [myRefInput] -> + let Api.TxOut _ _ dat _ = Api.txInInfoResolved myRefInput + in case dat of + (Api.OutputDatumHash hash) -> case PlutusTx.lookup hash txData of + Nothing -> False + Just (Api.Datum a) -> PlutusTx.unsafeFromBuiltinData @Integer a PlutusTx.== 10 + _ -> False + _ -> False + +bazTypedValidator :: Script.TypedValidator MockContract +bazTypedValidator = + let wrap = Script.mkUntypedValidator + in Script.mkTypedValidator @MockContract + $$(PlutusTx.compile [||bazValidator||]) + $$(PlutusTx.compile [||wrap||]) + trace1 :: (MonadBlockChain m) => m () trace1 = do txOutRefFoo : txOutRefBar : _ <- @@ -104,8 +123,30 @@ trace1 = do txSkelSigners = [wallet 3] } +trace2 :: (MonadBlockChain m) => m () +trace2 = do + refORef : scriptORef : _ <- + validateTxSkel' + ( txSkelTemplate + { txSkelOuts = + [ paysPK (wallet 1) (ada 2) `withDatum` (10 :: Integer), + paysScript bazTypedValidator () (ada 10) + ], + txSkelSigners = [wallet 2] + } + ) + void $ + validateTxSkel $ + txSkelTemplate + { txSkelSigners = [wallet 1], + txSkelIns = Map.singleton scriptORef (txSkelSomeRedeemer ()), + txSkelInsReference = Set.singleton refORef + } + tests :: Tasty.TestTree tests = Tasty.testGroup "Reference inputs" - [Tasty.testCase "Can reference an input that can't be spent" (testSucceeds def trace1)] + [ Tasty.testCase "We can reference an input that can't be spent" (testSucceeds def trace1), + Tasty.testCase "We can decode the datum hash from a reference input" (testSucceeds def trace2) + ] From 72e28206d8e04ade2975ca0e34047a32f0d49899 Mon Sep 17 00:00:00 2001 From: mmontin Date: Thu, 1 Aug 2024 01:02:41 +0200 Subject: [PATCH 29/44] withdrawal support --- CHANGELOG.md | 3 + cooked-validators.cabal | 2 + src/Cooked/MockChain/Balancing.hs | 5 +- src/Cooked/MockChain/GenerateTx/Body.hs | 9 ++- .../MockChain/GenerateTx/Withdrawals.hs | 58 ++++++++++++++++++ src/Cooked/Pretty/Cooked.hs | 18 +++++- src/Cooked/ShowBS.hs | 2 +- src/Cooked/Skeleton.hs | 27 +++++++-- tests/Cooked/WithdrawalsSpec.hs | 59 +++++++++++++++++++ tests/Spec.hs | 4 +- 10 files changed, 176 insertions(+), 11 deletions(-) create mode 100644 src/Cooked/MockChain/GenerateTx/Withdrawals.hs create mode 100644 tests/Cooked/WithdrawalsSpec.hs diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a1891413..9114efb7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ present but somehow disapeared. - It is now possible to reference an output which has a hashed datum. - `txSkelHashedData` the gives all the datum hashes in inputs and reference inputs. +- Partial support for withdrawals in txSkels. The rewarding scripts will be ran + and assets will be transferred. However, these withdrawals are not properly + constrainted yet. ### Removed diff --git a/cooked-validators.cabal b/cooked-validators.cabal index 03cd53ba9..e18ed7503 100644 --- a/cooked-validators.cabal +++ b/cooked-validators.cabal @@ -42,6 +42,7 @@ library Cooked.MockChain.GenerateTx.Mint Cooked.MockChain.GenerateTx.Output Cooked.MockChain.GenerateTx.Proposal + Cooked.MockChain.GenerateTx.Withdrawals Cooked.MockChain.GenerateTx.Witness Cooked.MockChain.MinAda Cooked.MockChain.MockChainSt @@ -172,6 +173,7 @@ test-suite spec Cooked.Tweak.TamperDatumSpec Cooked.Tweak.ValidityRangeSpec Cooked.TweakSpec + Cooked.WithdrawalsSpec Paths_cooked_validators autogen-modules: Paths_cooked_validators diff --git a/src/Cooked/MockChain/Balancing.hs b/src/Cooked/MockChain/Balancing.hs index d58c75481..6012b8d67 100644 --- a/src/Cooked/MockChain/Balancing.hs +++ b/src/Cooked/MockChain/Balancing.hs @@ -278,17 +278,18 @@ estimateTxSkelFee skel fee collateralIns returnCollateralWallet = do -- | 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 = output value + burned value + fee + deposits +-- value + withdrawn value = output value + burned value + fee + deposits computeBalancedTxSkel :: (MonadBlockChainBalancing m) => Wallet -> BalancingOutputs -> TxSkel -> Fee -> m TxSkel computeBalancedTxSkel balancingWallet balancingUtxos txSkel@TxSkel {..} (Script.lovelace -> feeValue) = do -- We compute the necessary values from the skeleton that are part of the -- equation, except for the `feeValue` which we already have. let (burnedValue, mintedValue) = Api.split $ txSkelMintsValue txSkelMints outValue = txSkelValueInOutputs txSkel + withdrawnValue = txSkelWithdrawnValue txSkel inValue <- txSkelInputValue txSkel depositedValue <- toValue <$> txSkelProposalsDeposit txSkel -- We compute the values missing in the left and right side of the equation - let (missingRight, missingLeft) = Api.split $ outValue <> burnedValue <> feeValue <> depositedValue <> PlutusTx.negate (inValue <> mintedValue) + let (missingRight, missingLeft) = Api.split $ outValue <> burnedValue <> feeValue <> depositedValue <> PlutusTx.negate (inValue <> mintedValue <> withdrawnValue) -- This gives us what we need to run our `reachValue` algorithm and append to -- the resulting values whatever payment was missing in the initial skeleton let candidatesRaw = second (<> missingRight) <$> reachValue balancingUtxos missingLeft (toInteger $ length balancingUtxos) diff --git a/src/Cooked/MockChain/GenerateTx/Body.hs b/src/Cooked/MockChain/GenerateTx/Body.hs index 2c816617f..04c5b1f70 100644 --- a/src/Cooked/MockChain/GenerateTx/Body.hs +++ b/src/Cooked/MockChain/GenerateTx/Body.hs @@ -17,6 +17,7 @@ import Cooked.MockChain.GenerateTx.Input qualified as Input import Cooked.MockChain.GenerateTx.Mint qualified as Mint import Cooked.MockChain.GenerateTx.Output qualified as Output import Cooked.MockChain.GenerateTx.Proposal qualified as Proposal +import Cooked.MockChain.GenerateTx.Withdrawals qualified as Withdrawals import Cooked.Skeleton import Cooked.Wallet import Data.Either.Combinators @@ -59,6 +60,11 @@ instance Transform TxContext Input.InputContext where instance Transform TxContext Collateral.CollateralContext where transform TxContext {..} = Collateral.CollateralContext {..} +instance Transform TxContext Withdrawals.WithdrawalsContext where + transform TxContext {..} = + let networkId = Emulator.pNetworkId params + in Withdrawals.WithdrawalsContext {..} + -- | Generates a body content from a skeleton txSkelToBodyContent :: TxSkel -> BodyGen (Cardano.TxBodyContent Cardano.BuildTx Cardano.ConwayEra) txSkelToBodyContent skel@TxSkel {..} | txSkelReferenceInputs <- txSkelReferenceTxOutRefs skel = do @@ -91,9 +97,9 @@ txSkelToBodyContent skel@TxSkel {..} | txSkelReferenceInputs <- txSkelReferenceT txProposalProcedures <- Just . Cardano.Featured Cardano.ConwayEraOnwardsConway <$> liftTxGen (Proposal.toProposalProcedures txSkelProposals (txOptAnchorResolution txSkelOpts)) + txWithdrawals <- liftTxGen (Withdrawals.toWithdrawals txSkelWithdrawals) let txMetadata = Cardano.TxMetadataNone -- That's what plutus-apps does as well txAuxScripts = Cardano.TxAuxScriptsNone -- That's what plutus-apps does as well - txWithdrawals = Cardano.TxWithdrawalsNone -- That's what plutus-apps does as well txUpdateProposal = Cardano.TxUpdateProposalNone -- That's what plutus-apps does as well txCertificates = Cardano.TxCertificatesNone -- That's what plutus-apps does as well txScriptValidity = Cardano.TxScriptValidityNone -- That's what plutus-apps does as well @@ -104,6 +110,7 @@ txSkelToBodyContent skel@TxSkel {..} | txSkelReferenceInputs <- txSkelReferenceT -- sign it with the required signers. txSkelToCardanoTx :: TxSkel -> BodyGen (Cardano.Tx Cardano.ConwayEra) txSkelToCardanoTx txSkel = do + -- We begin by creating the body content of the transaction txBodyContent <- txSkelToBodyContent txSkel -- We create the associated Shelley TxBody diff --git a/src/Cooked/MockChain/GenerateTx/Withdrawals.hs b/src/Cooked/MockChain/GenerateTx/Withdrawals.hs new file mode 100644 index 000000000..eb9d147b0 --- /dev/null +++ b/src/Cooked/MockChain/GenerateTx/Withdrawals.hs @@ -0,0 +1,58 @@ +module Cooked.MockChain.GenerateTx.Withdrawals + ( WithdrawalsContext (..), + toWithdrawals, + ) +where + +import Cardano.Api qualified as Cardano +import Cardano.Api.Ledger qualified as Cardano +import Cardano.Api.Shelley qualified as Cardano +import Control.Monad +import Control.Monad.Reader +import Cooked.Conversion +import Cooked.MockChain.GenerateTx.Common +import Cooked.MockChain.GenerateTx.Witness +import Cooked.Skeleton +import Data.Map (Map) +import Data.Map qualified as Map +import Ledger.Tx.CardanoAPI qualified as Ledger +import Plutus.Script.Utils.Ada qualified as Script +import PlutusLedgerApi.V3 qualified as Api + +data WithdrawalsContext where + WithdrawalsContext :: + { managedTxOuts :: Map Api.TxOutRef Api.TxOut, + networkId :: Cardano.NetworkId + } -> + WithdrawalsContext + +instance Transform WithdrawalsContext (Map Api.TxOutRef Api.TxOut) where + transform = managedTxOuts + +type WithdrawalsGen a = TxGen WithdrawalsContext a + +toWithdrawals :: TxSkelWithdrawals -> WithdrawalsGen (Cardano.TxWithdrawals Cardano.BuildTx Cardano.ConwayEra) +toWithdrawals (Map.toList -> []) = return Cardano.TxWithdrawalsNone +toWithdrawals (Map.toList -> withdrawals) = + fmap + (Cardano.TxWithdrawals Cardano.ShelleyBasedEraConway) + $ forM withdrawals + $ \(staker, Script.Lovelace n) -> + do + (witness, sCred) <- + case staker of + Right pkh -> do + sCred <- + throwOnToCardanoError "toWithdrawals: unable to translate pkh stake credential" $ + Cardano.StakeCredentialByKey <$> Ledger.toCardanoStakeKeyHash pkh + return (Cardano.KeyWitness Cardano.KeyWitnessForStakeAddr, sCred) + Left (script, red) -> do + witness <- + Cardano.ScriptWitness Cardano.ScriptWitnessForStakeAddr + <$> liftTxGen (toScriptWitness script red Cardano.NoScriptDatumForStake) + sCred <- + throwOnToCardanoError "toWithdrawals: unable to translate script stake credential" $ + Cardano.StakeCredentialByScript <$> Ledger.toCardanoScriptHash (toScriptHash script) + return (witness, sCred) + networkId <- asks networkId + return (Cardano.makeStakeAddress networkId sCred, Cardano.Coin n, Cardano.BuildTxWith witness) diff --git a/src/Cooked/Pretty/Cooked.hs b/src/Cooked/Pretty/Cooked.hs index b4431d8cb..974005735 100644 --- a/src/Cooked/Pretty/Cooked.hs +++ b/src/Cooked/Pretty/Cooked.hs @@ -170,7 +170,7 @@ instance PrettyCooked MockChainLog where go acc [] = reverse acc prettyTxSkel :: PrettyCookedOpts -> SkelContext -> TxSkel -> DocCooked -prettyTxSkel opts skelContext (TxSkel lbl txopts mints signers validityRange ins insReference outs proposals) = +prettyTxSkel opts skelContext (TxSkel lbl txopts mints signers validityRange ins insReference outs proposals withdrawals) = prettyItemize "transaction skeleton:" "-" @@ -183,10 +183,24 @@ prettyTxSkel opts skelContext (TxSkel lbl txopts mints signers validityRange ins prettyItemizeNonEmpty "Inputs:" "-" (prettyTxSkelIn opts skelContext <$> Map.toList ins), prettyItemizeNonEmpty "Reference inputs:" "-" (mapMaybe (prettyTxSkelInReference opts skelContext) $ Set.toList insReference), prettyItemizeNonEmpty "Outputs:" "-" (prettyTxSkelOut opts <$> outs), - prettyItemizeNonEmpty "Proposals:" "-" (prettyTxSkelProposal opts <$> proposals) + prettyItemizeNonEmpty "Proposals:" "-" (prettyTxSkelProposal opts <$> proposals), + prettyWithdrawals opts withdrawals ] ) +prettyWithdrawals :: PrettyCookedOpts -> TxSkelWithdrawals -> Maybe DocCooked +prettyWithdrawals pcOpts withdrawals = + prettyItemizeNonEmpty "Withdrawals:" "-" $ prettyWithdrawal <$> Map.toList withdrawals + where + prettyWithdrawal :: (Either (Script.Versioned Script.Script, TxSkelRedeemer) Api.PubKeyHash, Script.Ada) -> DocCooked + prettyWithdrawal (cred, ada) = + prettyItemizeNoTitle "-" $ + ( case cred of + Left (script, red) -> prettyCookedOpt pcOpts script : prettyTxSkelRedeemer pcOpts red + Right pkh -> [prettyCookedOpt pcOpts pkh] + ) + ++ [prettyCookedOpt pcOpts (toValue ada)] + prettyTxParameterChange :: PrettyCookedOpts -> TxParameterChange -> DocCooked prettyTxParameterChange opts (FeePerByte n) = "Fee per byte:" <+> prettyCookedOpt opts n prettyTxParameterChange opts (FeeFixed n) = "Fee fixed:" <+> prettyCookedOpt opts n diff --git a/src/Cooked/ShowBS.hs b/src/Cooked/ShowBS.hs index ba9b3ea9e..f6b09eb91 100644 --- a/src/Cooked/ShowBS.hs +++ b/src/Cooked/ShowBS.hs @@ -370,7 +370,7 @@ instance ShowBS Api.TxInfo where <> showBS txInfoMint <> "certificates:" <> showBS txInfoTxCerts - <> "wdrl:" -- TODO: what is wdrl? Explain better here + <> "wdrl:" <> showBS txInfoWdrl <> "valid range:" <> showBS txInfoValidRange diff --git a/src/Cooked/Skeleton.hs b/src/Cooked/Skeleton.hs index 4c9d1943e..d329ef10c 100644 --- a/src/Cooked/Skeleton.hs +++ b/src/Cooked/Skeleton.hs @@ -67,6 +67,8 @@ module Cooked.Skeleton txSkelProposalActionL, txSkelProposalWitnessL, txSkelProposalAnchorL, + TxSkelWithdrawals, + txSkelWithdrawnValue, TxSkel (..), txSkelLabelL, txSkelOptsL, @@ -76,6 +78,7 @@ module Cooked.Skeleton txSkelInsL, txSkelInsReferenceL, txSkelOutsL, + txSkelWithdrawalsL, txSkelTemplate, txSkelDataInOutputs, txSkelValidatorsInOutputs, @@ -119,6 +122,7 @@ import Data.Set qualified as Set import Ledger.Slot qualified as Ledger import Optics.Core import Optics.TH +import Plutus.Script.Utils.Ada qualified as Script import Plutus.Script.Utils.Scripts qualified as Script import Plutus.Script.Utils.Typed qualified as Script hiding (validatorHash) import Plutus.Script.Utils.Value qualified as Script hiding (adaSymbol, adaToken) @@ -582,6 +586,16 @@ withWitness prop (s, red) = prop {txSkelProposalWitness = Just (toScript s, red) withAnchor :: TxSkelProposal -> String -> TxSkelProposal withAnchor prop url = prop {txSkelProposalAnchor = Just url} +-- * Description of the Withdrawals + +type TxSkelWithdrawals = + Map + (Either (Script.Versioned Script.Script, TxSkelRedeemer) Api.PubKeyHash) + Script.Ada + +txSkelWithdrawnValue :: TxSkel -> Api.Value +txSkelWithdrawnValue = mconcat . (toValue . snd <$>) . Map.toList . txSkelWithdrawals + -- * Description of the Minting -- | A description of what a transaction mints. For every policy, there can only @@ -1014,8 +1028,11 @@ data TxSkel where -- | The outputs of the transaction. These will occur in exactly this -- order on the transaction. txSkelOuts :: [TxSkelOut], - -- | Possible proposals issued in this transaction to be voted on and possible enacted later on. - txSkelProposals :: [TxSkelProposal] + -- | Possible proposals issued in this transaction to be voted on and + -- possible enacted later on. + txSkelProposals :: [TxSkelProposal], + -- | Withdrawals performed by the transaction + txSkelWithdrawals :: TxSkelWithdrawals } -> TxSkel deriving (Show, Eq) @@ -1029,7 +1046,8 @@ makeLensesFor ("txSkelIns", "txSkelInsL"), ("txSkelInsReference", "txSkelInsReferenceL"), ("txSkelOuts", "txSkelOutsL"), - ("txSkelProposals", "txSkelProposalsL") + ("txSkelProposals", "txSkelProposalsL"), + ("txSkelWithdrawals", "txSkelWithdrawalsL") ] ''TxSkel @@ -1045,7 +1063,8 @@ txSkelTemplate = txSkelIns = Map.empty, txSkelInsReference = Set.empty, txSkelOuts = [], - txSkelProposals = [] + txSkelProposals = [], + txSkelWithdrawals = Map.empty } -- | The missing information on a 'TxSkel' that can only be resolved by querying diff --git a/tests/Cooked/WithdrawalsSpec.hs b/tests/Cooked/WithdrawalsSpec.hs new file mode 100644 index 000000000..a451eb375 --- /dev/null +++ b/tests/Cooked/WithdrawalsSpec.hs @@ -0,0 +1,59 @@ +module Cooked.WithdrawalsSpec where + +import Control.Monad +import Cooked +import Data.Default +import Data.Map qualified as Map +import Plutus.Script.Utils.Ada qualified as Script +import Plutus.Script.Utils.Scripts qualified as Script +import PlutusLedgerApi.V3 qualified as Api +import PlutusTx qualified +import PlutusTx.AssocMap qualified as PMap +import PlutusTx.Prelude qualified as PlutusTx +import Test.Tasty +import Test.Tasty.HUnit + +checkWithdrawalScript :: PlutusTx.BuiltinData -> PlutusTx.BuiltinData -> () +checkWithdrawalScript red ctx = + let scriptContext = PlutusTx.unsafeFromBuiltinData @Api.ScriptContext ctx + withdrawals = Api.txInfoWdrl PlutusTx.$ Api.scriptContextTxInfo scriptContext + quantity = PlutusTx.unsafeFromBuiltinData @Integer red + purpose = Api.scriptContextPurpose scriptContext + in case purpose of + Api.Rewarding cred -> case PMap.toList withdrawals of + [(cred', Api.Lovelace n)] -> + if cred PlutusTx.== cred' + then + if n PlutusTx.== quantity + then () + else PlutusTx.traceError "Wrong quantity." + else PlutusTx.traceError "Wrong credential." + _ -> PlutusTx.traceError "Wrong withdrawal." + _ -> PlutusTx.traceError "Wrong script purpose." + +checkWithdrawalVersionedScript :: Script.Versioned Script.Script +checkWithdrawalVersionedScript = mkScript $$(PlutusTx.compile [||checkWithdrawalScript||]) + +testWithdrawingScript :: (MonadBlockChain m) => Integer -> Integer -> m () +testWithdrawingScript n1 n2 = + void $ + validateTxSkel $ + txSkelTemplate + { txSkelSigners = [wallet 1], + txSkelWithdrawals = + Map.singleton + (Left (checkWithdrawalVersionedScript, txSkelSomeRedeemer (n1 * 1_000 :: Integer))) + (Script.Lovelace $ n2 * 1_000) + } + +tests :: TestTree +tests = + testGroup + "Withdrawing scripts" + [ testCase "We can use a withdrawing script" $ + testSucceeds def $ + testWithdrawingScript 2 2, + testCase "But the script might fail" $ + testFailsFrom def (isCekEvaluationFailure def) def $ + testWithdrawingScript 2 1 + ] diff --git a/tests/Spec.hs b/tests/Spec.hs index ec6566fc7..646fbc7ec 100644 --- a/tests/Spec.hs +++ b/tests/Spec.hs @@ -11,6 +11,7 @@ import Cooked.ReferenceInputsSpec qualified as ReferenceInputsSpec import Cooked.ReferenceScriptsSpec qualified as ReferenceScriptsSpec import Cooked.ShowBSSpec qualified as ShowBSSpec import Cooked.TweakSpec qualified as TweakSpec +import Cooked.WithdrawalsSpec qualified as WithdrawalsSpec import Test.Tasty main :: IO () @@ -30,5 +31,6 @@ main = MockChainSpec.tests, ShowBSSpec.tests, InitDistribSpec.tests, - ProposingSpec.tests + ProposingSpec.tests, + WithdrawalsSpec.tests ] From cc595b183b24b96f1a743889a2ed6304fb601e6d Mon Sep 17 00:00:00 2001 From: mmontin Date: Thu, 1 Aug 2024 01:23:03 +0200 Subject: [PATCH 30/44] fixing balancing bug --- CHANGELOG.md | 2 ++ src/Cooked/MockChain/Balancing.hs | 19 +++++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9114efb7c..97206fe92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,8 @@ - A bug where the script hashes would not be computed properly for early plutus version (V1 and V2). +- A bug where balancing would fail with excessive inputs and not enough min ada + in the excess. ## [[4.0.0]](https://github.com/tweag/cooked-validators/releases/tag/v4.0.0) - 2024-06-28 diff --git a/src/Cooked/MockChain/Balancing.hs b/src/Cooked/MockChain/Balancing.hs index 6012b8d67..3fa2cbf2b 100644 --- a/src/Cooked/MockChain/Balancing.hs +++ b/src/Cooked/MockChain/Balancing.hs @@ -281,6 +281,7 @@ estimateTxSkelFee skel fee collateralIns returnCollateralWallet = do -- value + withdrawn value = output value + burned value + fee + deposits computeBalancedTxSkel :: (MonadBlockChainBalancing m) => Wallet -> BalancingOutputs -> TxSkel -> Fee -> m TxSkel computeBalancedTxSkel balancingWallet balancingUtxos txSkel@TxSkel {..} (Script.lovelace -> feeValue) = do + params <- getParams -- We compute the necessary values from the skeleton that are part of the -- equation, except for the `feeValue` which we already have. let (burnedValue, mintedValue) = Api.split $ txSkelMintsValue txSkelMints @@ -290,13 +291,27 @@ computeBalancedTxSkel balancingWallet balancingUtxos txSkel@TxSkel {..} (Script. depositedValue <- toValue <$> txSkelProposalsDeposit txSkel -- We compute the values missing in the left and right side of the equation let (missingRight, missingLeft) = Api.split $ outValue <> burnedValue <> feeValue <> depositedValue <> PlutusTx.negate (inValue <> mintedValue <> withdrawnValue) + -- We compute the minimal ada requirement of the missing payment + rightMinAda <- case getTxSkelOutMinAda params $ paysPK balancingWallet missingRight of + Left err -> throwError $ MCEGenerationError err + Right a -> return a + -- We compute the current ada of the missing payment. If the missing payment + -- is not empty and the minimal ada is not present, some value is missing. + let Script.Lovelace rightAda = missingRight ^. adaL + missingAda = rightMinAda - rightAda + missingAdaValue = if missingRight /= mempty && missingAda > 0 then lovelace missingAda else mempty + -- The actual missing value on the left might needs to account for any missing + -- min ada on the missing payment of the transaction skeleton. This also has + -- to be repercuted on the missing value on the right. + let missingLeft' = missingLeft <> missingAdaValue + missingRight' = missingRight <> missingAdaValue -- This gives us what we need to run our `reachValue` algorithm and append to -- the resulting values whatever payment was missing in the initial skeleton - let candidatesRaw = second (<> missingRight) <$> reachValue balancingUtxos missingLeft (toInteger $ length balancingUtxos) + let candidatesRaw = second (<> missingRight') <$> reachValue balancingUtxos missingLeft' (toInteger $ length balancingUtxos) -- We prepare a possible balancing error with the difference between the -- requested amount and the maximum amount provided by the balancing wallet let totalValue = mconcat $ Api.txOutValue . snd <$> balancingUtxos - difference = snd $ Api.split $ missingLeft <> PlutusTx.negate totalValue + difference = snd $ Api.split $ missingLeft' <> PlutusTx.negate totalValue balancingError = MCEUnbalanceable balancingWallet difference txSkel -- Which one of our candidates should be picked depends on three factors -- - Whether there exists a perfect candidate set with empty surplus value From 63bb58763ebe2e9f7246e45597642d46bbc6b273 Mon Sep 17 00:00:00 2001 From: mmontin Date: Thu, 1 Aug 2024 01:54:52 +0200 Subject: [PATCH 31/44] small post-rebase changes --- src/Cooked/MockChain/Balancing.hs | 4 ++-- tests/Cooked/ReferenceInputsSpec.hs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Cooked/MockChain/Balancing.hs b/src/Cooked/MockChain/Balancing.hs index 3fa2cbf2b..a4bde19fd 100644 --- a/src/Cooked/MockChain/Balancing.hs +++ b/src/Cooked/MockChain/Balancing.hs @@ -297,9 +297,9 @@ computeBalancedTxSkel balancingWallet balancingUtxos txSkel@TxSkel {..} (Script. Right a -> return a -- We compute the current ada of the missing payment. If the missing payment -- is not empty and the minimal ada is not present, some value is missing. - let Script.Lovelace rightAda = missingRight ^. adaL + let Script.Lovelace rightAda = missingRight ^. Script.adaL missingAda = rightMinAda - rightAda - missingAdaValue = if missingRight /= mempty && missingAda > 0 then lovelace missingAda else mempty + missingAdaValue = if missingRight /= mempty && missingAda > 0 then Script.lovelace missingAda else mempty -- The actual missing value on the left might needs to account for any missing -- min ada on the missing payment of the transaction skeleton. This also has -- to be repercuted on the missing value on the right. diff --git a/tests/Cooked/ReferenceInputsSpec.hs b/tests/Cooked/ReferenceInputsSpec.hs index 36b49d06f..cad6e437e 100644 --- a/tests/Cooked/ReferenceInputsSpec.hs +++ b/tests/Cooked/ReferenceInputsSpec.hs @@ -129,8 +129,8 @@ trace2 = do validateTxSkel' ( txSkelTemplate { txSkelOuts = - [ paysPK (wallet 1) (ada 2) `withDatum` (10 :: Integer), - paysScript bazTypedValidator () (ada 10) + [ paysPK (wallet 1) (Script.ada 2) `withDatum` (10 :: Integer), + paysScript bazTypedValidator () (Script.ada 10) ], txSkelSigners = [wallet 2] } From 97d146026f578c0127fe6e0b617790b61bd29dbe Mon Sep 17 00:00:00 2001 From: mmontin Date: Wed, 31 Jul 2024 14:30:10 +0200 Subject: [PATCH 32/44] showbsspec finally gone --- cooked-validators.cabal | 1 - tests/Cooked/ShowBSSpec.hs | 93 -------------------------------------- tests/Spec.hs | 2 - 3 files changed, 96 deletions(-) delete mode 100644 tests/Cooked/ShowBSSpec.hs diff --git a/cooked-validators.cabal b/cooked-validators.cabal index f6839dc13..854fbf014 100644 --- a/cooked-validators.cabal +++ b/cooked-validators.cabal @@ -164,7 +164,6 @@ test-suite spec Cooked.ProposingScriptSpec Cooked.ReferenceInputsSpec Cooked.ReferenceScriptsSpec - Cooked.ShowBSSpec Cooked.Tweak.CommonSpec Cooked.Tweak.OutPermutationsSpec Cooked.Tweak.TamperDatumSpec diff --git a/tests/Cooked/ShowBSSpec.hs b/tests/Cooked/ShowBSSpec.hs deleted file mode 100644 index e786bbdd5..000000000 --- a/tests/Cooked/ShowBSSpec.hs +++ /dev/null @@ -1,93 +0,0 @@ -module Cooked.ShowBSSpec (tests) where - -import Cardano.Node.Emulator.Internal.Node qualified as Emulator -import Control.Monad -import Cooked -import Data.Default -import Data.Map qualified as Map -import Plutus.Script.Utils.Ada qualified as Script -import Plutus.Script.Utils.Typed qualified as Script -import Plutus.Script.Utils.V3.Typed.Scripts qualified as Script -import Plutus.Script.Utils.Value qualified as Script -import PlutusLedgerApi.V3 qualified as Api -import PlutusTx qualified -import PlutusTx.Builtins qualified as PlutusTx -import PlutusTx.Prelude qualified as PlutusTx -import Test.Tasty -import Test.Tasty.HUnit - -data UnitContract - -instance Script.ValidatorTypes UnitContract where - type RedeemerType UnitContract = Bool - type DatumType UnitContract = () - -{-# INLINEABLE traceValidator #-} -traceValidator :: () -> Bool -> Api.ScriptContext -> Bool -traceValidator _ _ ctx = PlutusTx.trace (showBS ctx) False - -printValidator :: Script.TypedValidator UnitContract -printValidator = - Script.mkTypedValidator @UnitContract - $$(PlutusTx.compile [||traceValidator||]) - $$(PlutusTx.compile [||wrap||]) - where - wrap = Script.mkUntypedValidator - -printTrace :: (MonadBlockChain m) => m () -printTrace = do - oref : _ <- - validateTxSkel' - txSkelTemplate - { txSkelSigners = [wallet 1], - txSkelOuts = [paysScriptInlineDatum printValidator () (Script.ada 30)] - } - void $ - validateTxSkel - txSkelTemplate - { txSkelOpts = def {txOptEmulatorParamsModification = Just $ EmulatorParamsModification Emulator.increaseTransactionLimits}, - txSkelSigners = [wallet 1], - txSkelIns = Map.singleton oref $ txSkelSomeRedeemer True - } - -tests :: TestTree -tests = - testGroup - "Printing transaction data as bytestrings" - [ testCase "a few simple examples" $ - testConjoin $ - map - (uncurry (@?=)) - [ (showBS @PlutusTx.Integer 123, "123"), - (showBS @PlutusTx.Integer (-123), "-123"), - (showBS @[PlutusTx.Integer] [1, 2, 3], "[1,2,3]"), - (showBS (PlutusTx.True, PlutusTx.False), "(True,False)"), - (showBS @PlutusTx.BuiltinByteString "abca", "\"61626361\""), - (showBS @Api.Value mempty, "(Value (fromList []))"), - ( showBS @Api.Value (Script.lovelaceValueOf 123), - "(Value (fromList [((CurrencySymbol \"\"),(fromList [((TokenName \"\"),123)]))]))" - ), - ( showBS @Api.Value (quickValue "banana" 4), - "(Value (fromList [((CurrencySymbol \"b9423defc80322887cd6461655989eb97bd9d706884fdd32ca613864\"),(fromList [((TokenName \"62616e616e61\"),4)]))]))" - ), - ( showBS (PlutusTx.mkConstr 0 [PlutusTx.mkMap [(PlutusTx.mkI 1, PlutusTx.mkList [PlutusTx.mkB "abc"])]]), - "(BuiltinData (Constr 0 [(Map [((I 1),(List [(B \"616263\")]))])]))" - ), - ( showBS - ( Api.Interval - (Api.LowerBound (Api.Finite $ Api.POSIXTime 123) True) - (Api.UpperBound (Api.Finite $ Api.POSIXTime 234) False) - ), - "(Interval (LowerBound (Finite (POSIXTime 123)) True) (UpperBound (Finite (POSIXTime 234)) False))" - ) - ], - testCase "printing the 'TxInfo' from a validator produces the expected string" $ - let isExpectedString = (==) "(Script context:Script Tx info:(inputs:[(TxInInfo (TxOutRef (TxId \"ad1f1a5a545dbe830383711cc3302f8fb5eb41c5154a7bc5336921abd453f001\") 0) (TxOut (Address (ScriptCredential (ScriptHash \"d02455c9a6cc9296707d031d8c668ea75612663d111d54a4155e4371\")) Nothing) (Value (fromList [((CurrencySymbol \"\"),(fromList [((TokenName \"\"),30000000)]))])) (OutputDatum (Datum (BuiltinData (Constr 0 [])))) Nothing))]reference inputs:[]outputs:[(TxOut (Address (PubKeyCredential (PubKeyHash \"a2c20c77887ace1cd986193e4e75babd8993cfd56995cd5cfce609c2\")) Nothing) (Value (fromList [((CurrencySymbol \"\"),(fromList [((TokenName \"\"),29515587)]))])) NoOutputDatum Nothing)]fees:(Lovelace 484413)minted value:(Value (fromList []))certificates:[]wdrl:(fromList [])valid range:(Interval (LowerBound NegInf True) (UpperBound PosInf True))signatories:[(PubKeyHash \"a2c20c77887ace1cd986193e4e75babd8993cfd56995cd5cfce609c2\")]redeemers:(fromList [((Spending (TxOutRef (TxId \"ad1f1a5a545dbe830383711cc3302f8fb5eb41c5154a7bc5336921abd453f001\") 0)),(Redeemer (BuiltinData (Constr 1 []))))])datums:(fromList [])transaction id:(TxId \"6fb3cb19b99bd2a5e880f64a80555ea096e583c269597329d03adb3a69ca53dd\")votes:(fromList [])proposals:[]treasury amount:Nothingtreasury donation:Nothing)Script purpose:(Spending (TxOutRef (TxId \"ad1f1a5a545dbe830383711cc3302f8fb5eb41c5154a7bc5336921abd453f001\") 0)))" - in testFails - (def @PrettyCookedOpts) - ( isCekEvaluationFailureWithMsg - (def @PrettyCookedOpts) - isExpectedString - ) - printTrace - ] diff --git a/tests/Spec.hs b/tests/Spec.hs index ec6566fc7..ae3ae1633 100644 --- a/tests/Spec.hs +++ b/tests/Spec.hs @@ -9,7 +9,6 @@ import Cooked.MockChainSpec qualified as MockChainSpec import Cooked.ProposingScriptSpec qualified as ProposingSpec import Cooked.ReferenceInputsSpec qualified as ReferenceInputsSpec import Cooked.ReferenceScriptsSpec qualified as ReferenceScriptsSpec -import Cooked.ShowBSSpec qualified as ShowBSSpec import Cooked.TweakSpec qualified as TweakSpec import Test.Tasty @@ -28,7 +27,6 @@ main = TweakSpec.tests, LtlSpec.tests, MockChainSpec.tests, - ShowBSSpec.tests, InitDistribSpec.tests, ProposingSpec.tests ] From bdf3676ce0dbcf81bea61c2aa1f498d4aa52aa81 Mon Sep 17 00:00:00 2001 From: mmontin Date: Wed, 31 Jul 2024 15:05:40 +0200 Subject: [PATCH 33/44] Recreating an index to pass to the new fee estimate function --- src/Cooked/MockChain/Balancing.hs | 32 +++++++------------------------ src/Cooked/MockChain/Direct.hs | 16 ---------------- 2 files changed, 7 insertions(+), 41 deletions(-) diff --git a/src/Cooked/MockChain/Balancing.hs b/src/Cooked/MockChain/Balancing.hs index 3b53e0645..16274dca2 100644 --- a/src/Cooked/MockChain/Balancing.hs +++ b/src/Cooked/MockChain/Balancing.hs @@ -18,16 +18,13 @@ import Cooked.Output import Cooked.Skeleton import Cooked.Wallet import Data.Bifunctor -import Data.Either.Combinators import Data.Function import Data.List -import Data.Map (Map) import Data.Map qualified as Map import Data.Maybe import Data.Ratio qualified as Rat import Data.Set (Set) import Data.Set qualified as Set -import Ledger.Index qualified as Ledger import Ledger.Tx qualified as Ledger import Ledger.Tx.CardanoAPI qualified as Ledger import Optics.Core @@ -279,28 +276,13 @@ estimateTxSkelFee skel fee collateralIns returnCollateralWallet = do Right txBody -> return txBody -- We retrieve the estimate number of required witness in the transaction let nkeys = Cardano.estimateTransactionKeyWitnessCount txBodyContent - index <- toIndex <$> lookupUtxos (txSkelKnownTxOutRefs skel <> Set.toList collateralIns) - case index of - Left err -> throwError $ MCEGenerationError err - -- We return an accurate estimate of the resulting transaction fee - Right index' -> - return . Emulator.unCoin $ - Cardano.calculateMinTxFee Cardano.ShelleyBasedEraConway (Emulator.pEmulatorPParams params) index' txBody nkeys - where - toIndex :: Map Api.TxOutRef Ledger.TxOut -> Either GenerateTxError Ledger.UtxoIndex - toIndex innerMap = do - let (txOutRefL, txOutL) = unzip $ Map.toList innerMap - txInL <- mapLeft (ToCardanoError "toIndex: unable to generate TxOut") $ forM txOutRefL Ledger.toCardanoTxIn - txOutL' <- forM (Ledger.getTxOut <$> txOutL) toCtxUTxOTxOut - return $ Cardano.UTxO $ Map.fromList $ zip txInL txOutL' - toCtxUTxOTxOut :: Cardano.TxOut Cardano.CtxTx era -> Either GenerateTxError (Cardano.TxOut Cardano.CtxUTxO era) - toCtxUTxOTxOut (Cardano.TxOut addr val d refS) = do - dat <- case d of - Cardano.TxOutDatumNone -> return Cardano.TxOutDatumNone - Cardano.TxOutDatumInTx _ _ -> Left $ GenerateTxErrorGeneral "Wrong datum kind" - Cardano.TxOutDatumHash s h -> return $ Cardano.TxOutDatumHash s h - Cardano.TxOutDatumInline s sd -> return $ Cardano.TxOutDatumInline s sd - return $ Cardano.TxOut addr val dat refS + -- We need to reconstruct an index to pass to the fee estimate function + (knownTxORefs, knownTxOuts) <- unzip . Map.toList <$> lookupUtxos (txSkelKnownTxOutRefs skel <> Set.toList collateralIns) + index <- case forM knownTxORefs Ledger.toCardanoTxIn of + Left err -> throwError $ MCEGenerationError $ ToCardanoError "estimateTxSkelFee: unable to generate TxIn" err + Right txInL -> return $ Cardano.UTxO $ Map.fromList $ zip txInL $ Cardano.toCtxUTxOTxOut . Ledger.getTxOut <$> knownTxOuts + -- We finally can the fee estimate function + return . Emulator.unCoin $ Cardano.calculateMinTxFee Cardano.ShelleyBasedEraConway (Emulator.pEmulatorPParams params) index txBody nkeys -- | This creates a balanced skeleton from a given skeleton and fee. In other -- words, this ensures that the following equation holds: input value + minted diff --git a/src/Cooked/MockChain/Direct.hs b/src/Cooked/MockChain/Direct.hs index ec98c56b2..fb1a1f5f3 100644 --- a/src/Cooked/MockChain/Direct.hs +++ b/src/Cooked/MockChain/Direct.hs @@ -310,22 +310,6 @@ getIndex = Cardano.TxOutDatumInline s sd -> Cardano.TxOutDatumInline s sd in Cardano.TxOut addr val dat refS -toIndex :: Map Api.TxOutRef Ledger.TxOut -> Either Ledger.ToCardanoError Ledger.UtxoIndex -toIndex innerMap = do - let (txOutRefL, txOutL) = unzip $ Map.toList innerMap - txInL <- forM txOutRefL Ledger.toCardanoTxIn - txOutL' <- forM (Ledger.getTxOut <$> txOutL) toCtxUTxOTxOut - return $ Cardano.UTxO $ Map.fromList $ zip txInL txOutL' - where - toCtxUTxOTxOut :: Cardano.TxOut Cardano.CtxTx era -> Either Ledger.ToCardanoError (Cardano.TxOut Cardano.CtxUTxO era) - toCtxUTxOTxOut (Cardano.TxOut addr val d refS) = do - dat <- case d of - Cardano.TxOutDatumNone -> return Cardano.TxOutDatumNone - Cardano.TxOutDatumInTx _ _ -> Left $ Ledger.TxBodyError "Wrong datum kind" - Cardano.TxOutDatumHash s h -> return $ Cardano.TxOutDatumHash s h - Cardano.TxOutDatumInline s sd -> return $ Cardano.TxOutDatumInline s sd - return $ Cardano.TxOut addr val dat refS - instance (Monad m) => MonadBlockChainBalancing (MockChainT m) where getParams = gets mcstParams validatorFromHash valHash = gets $ Map.lookup valHash . mcstValidators From 13271b9af4633cf0adb09a4216160b721b2abf50 Mon Sep 17 00:00:00 2001 From: mmontin Date: Wed, 7 Aug 2024 00:08:24 +0200 Subject: [PATCH 34/44] post merge fix --- src/Cooked/MockChain/Balancing.hs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Cooked/MockChain/Balancing.hs b/src/Cooked/MockChain/Balancing.hs index 4fadd706a..2e3e79017 100644 --- a/src/Cooked/MockChain/Balancing.hs +++ b/src/Cooked/MockChain/Balancing.hs @@ -30,7 +30,6 @@ import Data.Maybe import Data.Ratio qualified as Rat import Data.Set (Set) import Data.Set qualified as Set -import Ledger.Tx qualified as Ledger import Ledger.Tx.CardanoAPI qualified as Ledger import Optics.Core import Plutus.Script.Utils.Ada qualified as Script @@ -315,15 +314,22 @@ estimateTxSkelFee skel fee mCollaterals = do Right txBodyContent -> return txBodyContent -- We create the actual body and send if for validation txBody <- case Cardano.createAndValidateTransactionBody Cardano.ShelleyBasedEraConway txBodyContent of - Left err -> throwError $ MCEGenerationError (TxBodyError "Error creating body when estimating fees" err) + Left err -> throwError $ MCEGenerationError $ TxBodyError "Error creating body when estimating fees" err Right txBody -> return txBody -- We retrieve the estimate number of required witness in the transaction let nkeys = Cardano.estimateTransactionKeyWitnessCount txBodyContent -- We need to reconstruct an index to pass to the fee estimate function - (knownTxORefs, knownTxOuts) <- unzip . Map.toList <$> lookupUtxos (txSkelKnownTxOutRefs skel <> Set.toList collateralIns) - index <- case forM knownTxORefs Ledger.toCardanoTxIn of - Left err -> throwError $ MCEGenerationError $ ToCardanoError "estimateTxSkelFee: unable to generate TxIn" err - Right txInL -> return $ Cardano.UTxO $ Map.fromList $ zip txInL $ Cardano.toCtxUTxOTxOut . Ledger.getTxOut <$> knownTxOuts + -- We begin by retrieving the relevant utxos used in the skeleton + (knownTxORefs, knownTxOuts) <- unzip . Map.toList <$> lookupUtxos (txSkelKnownTxOutRefs skel <> collateralIns) + -- We then compute their Cardano counterparts + let indexOrError = do + txInL <- forM knownTxORefs Ledger.toCardanoTxIn + txOutL <- forM knownTxOuts $ Ledger.toCardanoTxOut $ Emulator.pNetworkId params + return $ Cardano.UTxO $ Map.fromList $ zip txInL $ Cardano.toCtxUTxOTxOut <$> txOutL + -- We retrieve the index when it was successfully created + index <- case indexOrError of + Left err -> throwError $ MCEGenerationError $ ToCardanoError "estimateTxSkelFee: toCardanoError" err + Right index' -> return index' -- We finally can the fee estimate function return . Emulator.unCoin $ Cardano.calculateMinTxFee Cardano.ShelleyBasedEraConway (Emulator.pEmulatorPParams params) index txBody nkeys From 395cc532c378b6157743a69c52cd4f869f061912 Mon Sep 17 00:00:00 2001 From: mmontin Date: Thu, 8 Aug 2024 17:45:04 +0200 Subject: [PATCH 35/44] no tests built for dependencies, relying on cne directly --- cabal.project | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/cabal.project b/cabal.project index 7e81c2dea..3eb18d3f2 100644 --- a/cabal.project +++ b/cabal.project @@ -10,8 +10,8 @@ package cardano-crypto-praos source-repository-package type: git - location: https://github.com/tweag/cardano-node-emulator-forked/ - tag: 3087d61fc26929dc29b97bf4d25a519907174324 + location: https://github.com/IntersectMBO/cardano-node-emulator/ + tag: 3bdb1f2a578226c1aa39fe09b8fb13e3a0be6d6a subdir: cardano-node-emulator plutus-ledger @@ -41,10 +41,6 @@ index-state: -- We never, ever, want this. write-ghc-environment-files: never --- Always build tests and benchmarks. -tests: true -benchmarks: true - -- The only sensible test display option, since it allows us to have colourized -- 'tasty' output. test-show-details: direct @@ -61,13 +57,7 @@ package cardano-api optimization: False package cardano-crypto-praos flags: -external-libsodium-vrf - --- Haddock needs this to be able to build the documentation -package plutus-script-utils - haddock-options: "--optghc=-fplugin-opt PlutusTx.Plugin:defer-errors" -package plutus-ledger - haddock-options: "--optghc=-fplugin-opt PlutusTx.Plugin:defer-errors" - + constraints: cardano-api ^>= 8.46 From 9d4be2d7213b2bd41d47f6cfe8811ab6a0bf2bcd Mon Sep 17 00:00:00 2001 From: mmontin Date: Thu, 8 Aug 2024 18:02:21 +0200 Subject: [PATCH 36/44] CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97ad33523..7610163ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ constructors: `txSkelSomeRedeemer`, `txSkelEmptyRedeemer`, `txSkelSomeRedeemerAndReferenceScript`, `txSkelEmptyRedeemerAndReferenceScript`. +- Dependency to cardano-api bumped to 8.46. ### Fixed From 48545870a382519679cead3c11141678d83a0ba0 Mon Sep 17 00:00:00 2001 From: mmontin Date: Thu, 8 Aug 2024 18:33:44 +0200 Subject: [PATCH 37/44] relying on the fork for translation functions --- cabal.project | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cabal.project b/cabal.project index 3eb18d3f2..7e1c1b17e 100644 --- a/cabal.project +++ b/cabal.project @@ -10,8 +10,8 @@ package cardano-crypto-praos source-repository-package type: git - location: https://github.com/IntersectMBO/cardano-node-emulator/ - tag: 3bdb1f2a578226c1aa39fe09b8fb13e3a0be6d6a + location: https://github.com/tweag/cardano-node-emulator-forked/ + tag: 6155b35bad5a2a2ea3e2791e6bf6bda6127ac535 subdir: cardano-node-emulator plutus-ledger From 55c00e2fe109390e4698f30d1c566d072cc63131 Mon Sep 17 00:00:00 2001 From: mmontin Date: Thu, 29 Aug 2024 02:19:04 +0200 Subject: [PATCH 38/44] credential and staking credential of a wallet --- src/Cooked/Wallet.hs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Cooked/Wallet.hs b/src/Cooked/Wallet.hs index e4d14b66b..47daf586c 100644 --- a/src/Cooked/Wallet.hs +++ b/src/Cooked/Wallet.hs @@ -15,6 +15,8 @@ module Cooked.Wallet walletAddress, walletSK, walletStakingSK, + walletStakingCredential, + walletCredential, Wallet, PrivateKey, ) @@ -87,6 +89,13 @@ walletPKHash = Ledger.pubKeyHash . walletPK walletStakingPKHash :: Wallet -> Maybe Api.PubKeyHash walletStakingPKHash = fmap Ledger.pubKeyHash . walletStakingPK +-- | Retrieves a wallet credential +walletCredential :: Wallet -> Api.Credential +walletCredential = Api.PubKeyCredential . walletPKHash + +walletStakingCredential :: Wallet -> Maybe Api.StakingCredential +walletStakingCredential = (Api.StakingHash . Api.PubKeyCredential <$>) . walletStakingPKHash + -- | Retrieves a wallet's address walletAddress :: Wallet -> Api.Address walletAddress w = From 785be72880310b7d765640ef1db6c7c6396487f3 Mon Sep 17 00:00:00 2001 From: mmontin Date: Thu, 29 Aug 2024 02:20:53 +0200 Subject: [PATCH 39/44] moving time from either the lower or upper bound of current slot --- src/Cooked/MockChain/BlockChain.hs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Cooked/MockChain/BlockChain.hs b/src/Cooked/MockChain/BlockChain.hs index 1db6eeddb..13a7914be 100644 --- a/src/Cooked/MockChain/BlockChain.hs +++ b/src/Cooked/MockChain/BlockChain.hs @@ -33,6 +33,8 @@ module Cooked.MockChain.BlockChain resolveReferenceScript, getEnclosingSlot, awaitEnclosingSlot, + awaitDurationFromLowerBound, + awaitDurationFromUpperBound, slotRangeBefore, slotRangeAfter, slotToTimeInterval, @@ -437,6 +439,16 @@ getEnclosingSlot t = (`Emulator.posixTimeToEnclosingSlot` t) . Emulator.pSlotCon awaitEnclosingSlot :: (MonadBlockChainWithoutValidation m) => Api.POSIXTime -> m Ledger.Slot awaitEnclosingSlot = awaitSlot <=< getEnclosingSlot +-- | Wait a given number of ms from the lower bound of the current slot and +-- returns the current slot after waiting. +awaitDurationFromLowerBound :: (MonadBlockChainWithoutValidation m) => Integer -> m Ledger.Slot +awaitDurationFromLowerBound duration = currentTime >>= 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. +awaitDurationFromUpperBound :: (MonadBlockChainWithoutValidation m) => Integer -> m Ledger.Slot +awaitDurationFromUpperBound duration = currentTime >>= awaitEnclosingSlot . (+ fromIntegral duration) . fst + -- | The infinite range of slots ending before or at the given time slotRangeBefore :: (MonadBlockChainWithoutValidation m) => Api.POSIXTime -> m Ledger.SlotRange slotRangeBefore t = do From cdc788b57465121a46d485826a64ff58e6fdbe92 Mon Sep 17 00:00:00 2001 From: mmontin Date: Thu, 29 Aug 2024 11:24:40 +0200 Subject: [PATCH 40/44] depending on cne --- cabal.project | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cabal.project b/cabal.project index 7e1c1b17e..3eb18d3f2 100644 --- a/cabal.project +++ b/cabal.project @@ -10,8 +10,8 @@ package cardano-crypto-praos source-repository-package type: git - location: https://github.com/tweag/cardano-node-emulator-forked/ - tag: 6155b35bad5a2a2ea3e2791e6bf6bda6127ac535 + location: https://github.com/IntersectMBO/cardano-node-emulator/ + tag: 3bdb1f2a578226c1aa39fe09b8fb13e3a0be6d6a subdir: cardano-node-emulator plutus-ledger From ee333f6ca57e2b566a2557d66aff27b131422891 Mon Sep 17 00:00:00 2001 From: mmontin Date: Thu, 29 Aug 2024 13:51:42 +0200 Subject: [PATCH 41/44] post merge mini fix --- src/Cooked/MockChain/Balancing.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Cooked/MockChain/Balancing.hs b/src/Cooked/MockChain/Balancing.hs index 720493473..54659fbbb 100644 --- a/src/Cooked/MockChain/Balancing.hs +++ b/src/Cooked/MockChain/Balancing.hs @@ -318,7 +318,7 @@ estimateTxSkelFee skel fee mCollaterals = do -- We retrieve the estimate number of required witness in the transaction let nkeys = Cardano.estimateTransactionKeyWitnessCount txBodyContent -- We need to reconstruct an index to pass to the fee estimate function - (knownTxORefs, knownTxOuts) <- unzip . Map.toList <$> lookupUtxos (txSkelKnownTxOutRefs skel <> Set.toList collateralIns) + (knownTxORefs, knownTxOuts) <- unzip . Map.toList <$> lookupUtxos (txSkelKnownTxOutRefs skel <> collateralIns) index <- case forM knownTxORefs Ledger.toCardanoTxIn of Left err -> throwError $ MCEGenerationError $ ToCardanoError "estimateTxSkelFee: unable to generate TxIn" err Right txInL -> return $ Cardano.UTxO $ Map.fromList $ zip txInL $ Cardano.toCtxUTxOTxOut . Ledger.getTxOut <$> knownTxOuts From f08e5041ff12cf13b49be4586b8acfffc4b61390 Mon Sep 17 00:00:00 2001 From: mmontin Date: Fri, 30 Aug 2024 17:02:28 +0200 Subject: [PATCH 42/44] reworking withdrawals for proper maps --- cooked-validators.cabal | 2 +- .../MockChain/GenerateTx/Withdrawals.hs | 4 ++-- src/Cooked/Pretty/Cooked.hs | 6 +++--- src/Cooked/Skeleton.hs | 19 +++++++++++++++---- tests/Cooked/WithdrawalsSpec.hs | 6 +----- 5 files changed, 22 insertions(+), 15 deletions(-) diff --git a/cooked-validators.cabal b/cooked-validators.cabal index db5ce1f23..beb2a6158 100644 --- a/cooked-validators.cabal +++ b/cooked-validators.cabal @@ -1,6 +1,6 @@ cabal-version: 3.4 --- This file has been generated from package.yaml by hpack version 0.36.1. +-- This file has been generated from package.yaml by hpack version 0.36.0. -- -- see: https://github.com/sol/hpack diff --git a/src/Cooked/MockChain/GenerateTx/Withdrawals.hs b/src/Cooked/MockChain/GenerateTx/Withdrawals.hs index eb9d147b0..ef8eb99c6 100644 --- a/src/Cooked/MockChain/GenerateTx/Withdrawals.hs +++ b/src/Cooked/MockChain/GenerateTx/Withdrawals.hs @@ -37,7 +37,7 @@ toWithdrawals (Map.toList -> withdrawals) = fmap (Cardano.TxWithdrawals Cardano.ShelleyBasedEraConway) $ forM withdrawals - $ \(staker, Script.Lovelace n) -> + $ \(staker, (red, Script.Lovelace n)) -> do (witness, sCred) <- case staker of @@ -46,7 +46,7 @@ toWithdrawals (Map.toList -> withdrawals) = throwOnToCardanoError "toWithdrawals: unable to translate pkh stake credential" $ Cardano.StakeCredentialByKey <$> Ledger.toCardanoStakeKeyHash pkh return (Cardano.KeyWitness Cardano.KeyWitnessForStakeAddr, sCred) - Left (script, red) -> do + Left script -> do witness <- Cardano.ScriptWitness Cardano.ScriptWitnessForStakeAddr <$> liftTxGen (toScriptWitness script red Cardano.NoScriptDatumForStake) diff --git a/src/Cooked/Pretty/Cooked.hs b/src/Cooked/Pretty/Cooked.hs index 1b44b41b1..c239d82cc 100644 --- a/src/Cooked/Pretty/Cooked.hs +++ b/src/Cooked/Pretty/Cooked.hs @@ -186,11 +186,11 @@ prettyWithdrawals :: PrettyCookedOpts -> TxSkelWithdrawals -> Maybe DocCooked prettyWithdrawals pcOpts withdrawals = prettyItemizeNonEmpty "Withdrawals:" "-" $ prettyWithdrawal <$> Map.toList withdrawals where - prettyWithdrawal :: (Either (Script.Versioned Script.Script, TxSkelRedeemer) Api.PubKeyHash, Script.Ada) -> DocCooked - prettyWithdrawal (cred, ada) = + prettyWithdrawal :: (Either (Script.Versioned Script.Script) Api.PubKeyHash, (TxSkelRedeemer, Script.Ada)) -> DocCooked + prettyWithdrawal (cred, (red, ada)) = prettyItemizeNoTitle "-" $ ( case cred of - Left (script, red) -> prettyCookedOpt pcOpts script : prettyTxSkelRedeemer pcOpts red + Left script -> prettyCookedOpt pcOpts script : prettyTxSkelRedeemer pcOpts red Right pkh -> [prettyCookedOpt pcOpts pkh] ) ++ [prettyCookedOpt pcOpts (toValue ada)] diff --git a/src/Cooked/Skeleton.hs b/src/Cooked/Skeleton.hs index f320a9c0e..754c8dd86 100644 --- a/src/Cooked/Skeleton.hs +++ b/src/Cooked/Skeleton.hs @@ -70,6 +70,8 @@ module Cooked.Skeleton TxSkelWithdrawals, txSkelWithdrawnValue, txSkelWithdrawalsScripts, + pkWithdrawal, + scriptWithdrawal, TxSkel (..), txSkelLabelL, txSkelOptsL, @@ -590,16 +592,25 @@ withAnchor prop url = prop {txSkelProposalAnchor = Just url} -- * Description of the Withdrawals +-- | Withdrawals associate either a script or a private key with a redeemer and +-- a certain amount of ada. Note that the redeemer will be ignored in the case +-- of a private key. type TxSkelWithdrawals = Map - (Either (Script.Versioned Script.Script, TxSkelRedeemer) Api.PubKeyHash) - Script.Ada + (Either (Script.Versioned Script.Script) Api.PubKeyHash) + (TxSkelRedeemer, Script.Ada) txSkelWithdrawnValue :: TxSkel -> Api.Value -txSkelWithdrawnValue = mconcat . (toValue . snd <$>) . Map.toList . txSkelWithdrawals +txSkelWithdrawnValue = mconcat . (toValue . snd . snd <$>) . Map.toList . txSkelWithdrawals txSkelWithdrawalsScripts :: TxSkel -> [Script.Versioned Script.Script] -txSkelWithdrawalsScripts = (fst <$>) . fst . partitionEithers . (fst <$>) . Map.toList . txSkelWithdrawals +txSkelWithdrawalsScripts = fst . partitionEithers . (fst <$>) . Map.toList . txSkelWithdrawals + +pkWithdrawal :: (ToPubKeyHash pkh) => pkh -> Script.Ada -> TxSkelWithdrawals +pkWithdrawal pkh amount = Map.singleton (Right $ toPubKeyHash pkh) (txSkelEmptyRedeemer, amount) + +scriptWithdrawal :: (ToScript script) => script -> TxSkelRedeemer -> Script.Ada -> TxSkelWithdrawals +scriptWithdrawal script red amount = Map.singleton (Left $ toScript script) (red, amount) -- * Description of the Minting diff --git a/tests/Cooked/WithdrawalsSpec.hs b/tests/Cooked/WithdrawalsSpec.hs index a451eb375..def3e0e81 100644 --- a/tests/Cooked/WithdrawalsSpec.hs +++ b/tests/Cooked/WithdrawalsSpec.hs @@ -3,7 +3,6 @@ module Cooked.WithdrawalsSpec where import Control.Monad import Cooked import Data.Default -import Data.Map qualified as Map import Plutus.Script.Utils.Ada qualified as Script import Plutus.Script.Utils.Scripts qualified as Script import PlutusLedgerApi.V3 qualified as Api @@ -40,10 +39,7 @@ testWithdrawingScript n1 n2 = validateTxSkel $ txSkelTemplate { txSkelSigners = [wallet 1], - txSkelWithdrawals = - Map.singleton - (Left (checkWithdrawalVersionedScript, txSkelSomeRedeemer (n1 * 1_000 :: Integer))) - (Script.Lovelace $ n2 * 1_000) + txSkelWithdrawals = scriptWithdrawal checkWithdrawalVersionedScript (txSkelSomeRedeemer (n1 * 1_000 :: Integer)) $ Script.Lovelace $ n2 * 1_000 } tests :: TestTree From b15fc6967867a0389e96ec514878a24dacaeebe4 Mon Sep 17 00:00:00 2001 From: mmontin Date: Fri, 30 Aug 2024 17:07:36 +0200 Subject: [PATCH 43/44] hpack --- cooked-validators.cabal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cooked-validators.cabal b/cooked-validators.cabal index beb2a6158..db5ce1f23 100644 --- a/cooked-validators.cabal +++ b/cooked-validators.cabal @@ -1,6 +1,6 @@ cabal-version: 3.4 --- This file has been generated from package.yaml by hpack version 0.36.0. +-- This file has been generated from package.yaml by hpack version 0.36.1. -- -- see: https://github.com/sol/hpack From 943d937c415c0b7ac3e224debf8aa432e13b279b Mon Sep 17 00:00:00 2001 From: mmontin Date: Tue, 10 Sep 2024 14:14:56 +0200 Subject: [PATCH 44/44] post review changes --- CHANGELOG.md | 6 +++--- src/Cooked/MockChain/BlockChain.hs | 19 ++++++++----------- src/Cooked/MockChain/Direct.hs | 2 +- src/Cooked/MockChain/GenerateTx/Body.hs | 17 ++++++++++++++++- src/Cooked/Skeleton.hs | 9 +++++---- 5 files changed, 33 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ed8d6b73..d45887598 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,9 +17,9 @@ present but somehow disapeared. - It is now possible to reference an output which has a hashed datum. - `txSkelHashedData` the gives all the datum hashes in inputs and reference inputs. -- Partial support for withdrawals in txSkels. The rewarding scripts will be ran +- Partial support for withdrawals in txSkels. The rewarding scripts will be run and assets will be transferred. However, these withdrawals are not properly - constrainted yet. + constrained yet. - PrettyCooked option `pcOptPrintLog`, which is a boolean, to turn on or off the log display in the pretty printer. The default value is `True`. @@ -40,7 +40,7 @@ - `mkProposingScript` changed to `mkScript` - `withDatumHashed` changed to `withUnresolvedDatumHash` - `paysScriptDatumHashed` changed to `paysScriptUnresolvedDatumHash` -- `txSkelInputData` changed to `txSkelConsumedData` +- `txSkelInputData` changed to `txSkelInputDataAsHashes` - Pretty printing of hashed datum now includes the hash (and not only the resolved datum). - Dependency to cardano-api bumped to 8.46. diff --git a/src/Cooked/MockChain/BlockChain.hs b/src/Cooked/MockChain/BlockChain.hs index 2f291b901..5c0b4324e 100644 --- a/src/Cooked/MockChain/BlockChain.hs +++ b/src/Cooked/MockChain/BlockChain.hs @@ -43,7 +43,7 @@ module Cooked.MockChain.BlockChain txSkelInputValidators, txSkelInputValue, txSkelHashedData, - txSkelConsumedData, + txSkelInputDataAsHashes, lookupUtxos, validateTxSkel', validateTxSkel_, @@ -351,9 +351,6 @@ txSkelInputValue = (foldMap Api.txOutValue <$>) . txSkelInputUtxos -- or references, which will be needed by the transaction body. txSkelHashedData :: (MonadBlockChainBalancing m) => TxSkel -> m (Map Api.DatumHash Api.Datum) txSkelHashedData skel = do - let outputToDatumHashM output = case output ^. outputDatumL of - Api.OutputDatumHash dHash -> Just dHash - _ -> Nothing (Map.elems -> inputTxOuts) <- txSkelInputUtxos skel (Map.elems -> refInputTxOuts) <- txSkelReferenceInputUtxos skel foldM @@ -364,17 +361,17 @@ txSkelHashedData skel = do (datumFromHash dHash) ) Map.empty - (mapMaybe outputToDatumHashM $ inputTxOuts <> refInputTxOuts) + (mapMaybe (fmap (^. outputDatumL) . isOutputWithDatumHash) $ inputTxOuts <> refInputTxOuts) --- | Looks us the data on UTxOs the transaction consumes. This corresponds to --- the keys of what should be removed from the stored datums in our mockchain. --- There can be duplicates, which is expected. -txSkelConsumedData :: (MonadBlockChainBalancing m) => TxSkel -> m [Api.DatumHash] -txSkelConsumedData skel = do +-- | Looks up the data on UTxOs the transaction consumes and returns their +-- hashes. This corresponds to the keys of what should be removed from the +-- stored datums in our mockchain. There can be duplicates, which is expected. +txSkelInputDataAsHashes :: (MonadBlockChainBalancing m) => TxSkel -> m [Api.DatumHash] +txSkelInputDataAsHashes skel = do let outputToDatumHashM output = case output ^. outputDatumL of Api.OutputDatumHash dHash -> maybeErrM - (MCEUnknownDatum "txSkelConsumedData: Transaction input with unknown datum hash" dHash) + (MCEUnknownDatum "txSkelInputDataAsHashes: Transaction input with unknown datum hash" dHash) (Just . const dHash) (datumFromHash dHash) Api.OutputDatum datum -> return $ Just $ Script.datumHash datum diff --git a/src/Cooked/MockChain/Direct.hs b/src/Cooked/MockChain/Direct.hs index 3047a2396..43dcdf2b5 100644 --- a/src/Cooked/MockChain/Direct.hs +++ b/src/Cooked/MockChain/Direct.hs @@ -158,7 +158,7 @@ instance (Monad m) => MonadBlockChain (MockChainT m) where -- datums, validators and various kinds of inputs. This idea is to provide a -- rich-enough context for the transaction generation to succeed. hashedData <- txSkelHashedData skel - insData <- txSkelConsumedData skel + insData <- txSkelInputDataAsHashes skel insValidators <- txSkelInputValidators skel insMap <- txSkelInputUtxos skel refInsMap <- txSkelReferenceInputUtxos skel diff --git a/src/Cooked/MockChain/GenerateTx/Body.hs b/src/Cooked/MockChain/GenerateTx/Body.hs index 8e84ffe4b..38655246b 100644 --- a/src/Cooked/MockChain/GenerateTx/Body.hs +++ b/src/Cooked/MockChain/GenerateTx/Body.hs @@ -122,25 +122,40 @@ txSkelToCardanoTx txSkel = do -- definition of "txBody'" aims at doing just that. In the process, we have to -- reconstruct the body with the new data and the associated hash. Hopefully, -- in the future, cardano-api provides a way to add those data in the body - -- directly without requiring this methods, which somewhat feels like a hack. + -- directly without requiring this method, which somewhat feels like a hack. + + -- We retrieve the data available in the context mData <- asks managedData + -- We retrieve the outputs available in the context mTxOut <- asks managedTxOuts + -- We attempt to resolve the reference inputs used by the skeleton refIns <- forM (txSkelReferenceTxOutRefs txSkel) $ \oRef -> throwOnLookup ("txSkelToCardanoTx: Unable to resolve TxOutRef " <> show oRef) oRef mTxOut + -- We collect the datum hashes present at these outputs let datumHashes = [hash | (Api.TxOut _ _ (Api.OutputDatumHash hash) _) <- refIns] + -- We resolve those datum hashes from the context additionalData <- forM datumHashes $ \dHash -> throwOnLookup ("txSkelToCardanoTx: Unable to resolve datum hash " <> show dHash) dHash mData + -- We compute the map from datum hash to datum of these additional required data let additionalDataMap = Map.fromList [(Cardano.hashData dat, dat) | Api.Datum (Cardano.Data . Api.toData -> dat) <- additionalData] + -- We retrieve a needed parameter to process difference plutus languages toLangDepViewParam <- asks (Conway.getLanguageView . Cardano.unLedgerProtocolParameters . Emulator.ledgerProtocolParameters . params) + -- We convert our data map into a 'TxDats' let txDats' = Alonzo.TxDats additionalDataMap + -- We compute the new era, datums and redeemers based on the current dats + -- in the body and the additional data to include in the body. (era, datums, redeemers) = case dats of Cardano.TxBodyNoScriptData -> (Cardano.AlonzoEraOnwardsConway, txDats', Alonzo.Redeemers Map.empty) Cardano.TxBodyScriptData era' txDats reds -> (era', txDats <> txDats', reds) + -- We collect the various witnesses in the body witnesses = Cardano.collectTxBodyScriptWitnesses Cardano.ShelleyBasedEraConway txBodyContent + -- We collect their associated languages languages = [toCardanoLanguage v | (_, Cardano.AnyScriptWitness (Cardano.PlutusScriptWitness _ v _ _ _ _)) <- witnesses] + -- We compute the new script integrity hash with the added data scriptIntegrityHash = Cardano.alonzoEraOnwardsConstraints era $ Alonzo.hashScriptIntegrity (Set.fromList $ toLangDepViewParam <$> languages) redeemers datums + -- We wrap all of this in the new body body' = body Lens.& Alonzo.scriptIntegrityHashTxBodyL Lens..~ scriptIntegrityHash txBody' = Cardano.ShelleyTxBody a body' c (Cardano.TxBodyScriptData era datums redeemers) e f diff --git a/src/Cooked/Skeleton.hs b/src/Cooked/Skeleton.hs index 754c8dd86..bfa80c91a 100644 --- a/src/Cooked/Skeleton.hs +++ b/src/Cooked/Skeleton.hs @@ -901,8 +901,8 @@ paysPK pkh value = ) -- | Pays a script a certain value with a certain datum hash, using the --- 'TxSkelOutDatum' constructor. (See the documentation of 'TxSkelOutDatum'.) --- The datum is resolved in the transaction. +-- 'TxSkelOutDatum' constructor. The resolved datum is provided in the body of +-- the transaction that issues the payment. paysScript :: ( Api.ToData (Script.DatumType a), Show (Script.DatumType a), @@ -948,8 +948,9 @@ paysScriptInlineDatum validator datum value = (Nothing @(Script.Versioned Script.Script)) ) --- | Pays a script a certain value with a certain hashed datum which is not --- resolved in the transaction (as opposed to "paysScript"). +-- | Pays a script a certain value with a certain hashed datum, whose resolved +-- datum is not provided in the transaction body that issues the payment (as +-- opposed to "paysScript"). paysScriptUnresolvedDatumHash :: ( Api.ToData (Script.DatumType a), Show (Script.DatumType a),