diff --git a/CHANGELOG.md b/CHANGELOG.md index 1153c5907..d45887598 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,20 @@ - `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. +- `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. +- 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 run + and assets will be transferred. However, these withdrawals are not properly + 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`. - + ### Removed - `positivePart` and `negativePart` in `ValueUtils.hs`. Replaced by `Api.split`. @@ -26,6 +37,12 @@ constructors: `txSkelSomeRedeemer`, `txSkelEmptyRedeemer`, `txSkelSomeRedeemerAndReferenceScript`, `txSkelEmptyRedeemerAndReferenceScript`. +- `mkProposingScript` changed to `mkScript` +- `withDatumHashed` changed to `withUnresolvedDatumHash` +- `paysScriptDatumHashed` changed to `paysScriptUnresolvedDatumHash` +- `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. - Logging has been reworked: * it is no longer limited to `StagedMockChain` runs @@ -34,12 +51,16 @@ * it now displays the discarding of utxos during balancing. * it now displays when the user specifies useless collateral utxos. * it is not visible from outside of `cooked-validators` +- Dependency to cardano-api bumped to 8.46. ### Fixed +- 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. +- Transactions that do not involve script are now properly generated without any - All kinds of scripts can now be used as reference scripts. -- Transactions that do not involve scripts 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/cooked-validators.cabal b/cooked-validators.cabal index 8f5f9fd16..db5ce1f23 100644 --- a/cooked-validators.cabal +++ b/cooked-validators.cabal @@ -42,8 +42,10 @@ 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 Cooked.MockChain.Staged Cooked.MockChain.Testing Cooked.MockChain.UtxoSearch @@ -109,6 +111,7 @@ library , cardano-api , cardano-crypto , cardano-data + , cardano-ledger-alonzo , cardano-ledger-conway , cardano-ledger-core , cardano-ledger-shelley @@ -169,6 +172,7 @@ test-suite spec Cooked.Tweak.TamperDatumSpec Cooked.Tweak.ValidityRangeSpec Cooked.TweakSpec + Cooked.WithdrawalsSpec Paths_cooked_validators autogen-modules: Paths_cooked_validators @@ -201,6 +205,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/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.hs b/src/Cooked/MockChain.hs index 129b55d76..db41e70f8 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 hiding (MockChainLogEntry, logEvent) import Cooked.MockChain.Direct as X hiding (MockChainReturn) import Cooked.MockChain.MinAda as X +import Cooked.MockChain.MockChainSt as X (MockChainSt (..), mockChainSt0From) import Cooked.MockChain.Staged as X hiding (StagedMockChain) import Cooked.MockChain.Testing as X import Cooked.MockChain.UtxoSearch as X diff --git a/src/Cooked/MockChain/Balancing.hs b/src/Cooked/MockChain/Balancing.hs index a83f2cde9..417cf73b2 100644 --- a/src/Cooked/MockChain/Balancing.hs +++ b/src/Cooked/MockChain/Balancing.hs @@ -78,7 +78,11 @@ balanceTxSkel skelUnbal@TxSkel {..} = do -- We retrieve the various kinds of scripts spendingScripts <- txSkelInputValidators skelUnbal -- The transaction will only require collaterals when involving scripts - let noScriptInvolved = Map.null txSkelMints && null (mapMaybe txSkelProposalWitness txSkelProposals) && Map.null spendingScripts + let noScriptInvolved = + Map.null txSkelMints + && null (mapMaybe txSkelProposalWitness txSkelProposals) + && Map.null spendingScripts + && null (txSkelWithdrawalsScripts skelUnbal) case (noScriptInvolved, txOptCollateralUtxos txSkelOpts) of (True, CollateralUtxosFromSet utxos _) -> logEvent (MCLogUnusedCollaterals $ Right utxos) >> return Nothing (True, CollateralUtxosFromWallet cWallet) -> logEvent (MCLogUnusedCollaterals $ Left cWallet) >> return Nothing @@ -298,7 +302,7 @@ estimateTxSkelFee :: (MonadBlockChainBalancing m) => TxSkel -> Fee -> Maybe (Col estimateTxSkelFee skel fee mCollaterals = do -- We retrieve the necessary data to generate the transaction body params <- getParams - managedData <- txSkelInputData skel + managedData <- txSkelHashedData skel let collateralIns = case mCollaterals of Nothing -> [] Just (s, _) -> Set.toList s @@ -310,7 +314,7 @@ 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 @@ -331,24 +335,40 @@ estimateTxSkelFee skel fee mCollaterals = 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 + 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 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) + -- 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 ^. Script.adaL + missingAda = rightMinAda - rightAda + 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. + 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 diff --git a/src/Cooked/MockChain/BlockChain.hs b/src/Cooked/MockChain/BlockChain.hs index c31288ffb..5c0b4324e 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, @@ -40,9 +42,11 @@ module Cooked.MockChain.BlockChain txSkelReferenceInputUtxos, txSkelInputValidators, txSkelInputValue, - txSkelInputData, + txSkelHashedData, + txSkelInputDataAsHashes, lookupUtxos, validateTxSkel', + validateTxSkel_, txSkelProposalsDeposit, govActionDeposit, ) @@ -182,6 +186,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 + -- | Retrieve the ordered list of outputs of the given "CardanoTx". -- -- This is useful when writing endpoints and/or traces to fetch utxos of @@ -213,16 +221,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. @@ -236,19 +237,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 @@ -265,16 +258,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 @@ -286,22 +272,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 @@ -371,28 +347,37 @@ lookupUtxos = txSkelInputValue :: (MonadBlockChainBalancing m) => TxSkel -> m Api.Value txSkelInputValue = (foldMap Api.txOutValue <$>) . 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 - <$> txSkelInputUtxos 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 + (Map.elems -> inputTxOuts) <- txSkelInputUtxos skel + (Map.elems -> refInputTxOuts) <- txSkelReferenceInputUtxos 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 (fmap (^. outputDatumL) . isOutputWithDatumHash) $ inputTxOuts <> refInputTxOuts) + +-- | 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 "txSkelInputData: Transaction input with un-resolvable datum hash" dHash) - (dHash,) + (MCEUnknownDatum "txSkelInputDataAsHashes: 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) <- txSkelInputUtxos skel + catMaybes <$> mapM outputToDatumHashM inputTxOuts -- ** Slot and Time Management @@ -453,6 +438,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 diff --git a/src/Cooked/MockChain/Direct.hs b/src/Cooked/MockChain/Direct.hs index 9174758dc..43dcdf2b5 100644 --- a/src/Cooked/MockChain/Direct.hs +++ b/src/Cooked/MockChain/Direct.hs @@ -4,10 +4,6 @@ -- internal state. This choice might be revised in the future. 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 @@ -17,32 +13,22 @@ 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 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 @@ -55,86 +41,6 @@ import PlutusLedgerApi.V3 qualified as Api -- Running a 'MockChain' produces a 'UtxoState', a simplified view on -- 'Ledger.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) - -mcstToSkelContext :: MockChainSt -> SkelContext -mcstToSkelContext MockChainSt {..} = - SkelContext - (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 -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 (WriterT [MockChainLogEntry] m))) a} deriving newtype (Functor, Applicative, MonadState MockChainSt, MonadError MockChainError, MonadWriter [MockChainLogEntry]) @@ -214,111 +120,8 @@ runMockChainFrom i0 = runIdentity . runMockChainTFrom i0 runMockChain :: MockChain a -> MockChainReturn 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 Api.TxOut -getIndex = - Map.fromList - . map (bimap Ledger.fromCardanoTxIn (Ledger.fromCardanoTxOutToPV2TxInfoTxOut . toCtxTxTxOut)) - . Map.toList - . Cardano.unUTxO - where - -- We need to convert a UTxO context TxOut to a Transaction context Tx out. - -- It's complicated because the datum type is indexed by the context. - toCtxTxTxOut :: Cardano.TxOut Cardano.CtxUTxO era -> Cardano.TxOut Cardano.CtxTx era - toCtxTxTxOut (Cardano.TxOut addr val d refS) = - let dat = case d of - Cardano.TxOutDatumNone -> Cardano.TxOutDatumNone - Cardano.TxOutDatumHash s h -> Cardano.TxOutDatumHash s h - Cardano.TxOutDatumInline s sd -> Cardano.TxOutDatumInline s sd - in Cardano.TxOut addr val dat refS - instance (Monad m) => MonadBlockChainBalancing (MockChainT m) where getParams = gets mcstParams validatorFromHash valHash = gets $ Map.lookup valHash . mcstValidators @@ -354,17 +157,16 @@ 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 <- txSkelInputDataAsHashes skel insValidators <- txSkelInputValidators skel insMap <- txSkelInputUtxos skel refInsMap <- txSkelReferenceInputUtxos skel - collateralInsMap <- case mCollaterals of - Nothing -> return Map.empty - Just (collateralIns, _) -> lookupUtxos $ Set.toList collateralIns + collateralInsMap <- maybe (return Map.empty) (lookupUtxos . Set.toList . fst) mCollaterals -- 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 newParams insData (insMap <> refInsMap <> collateralInsMap) insValidators mCollaterals skel of + cardanoTx <- case generateTx fee newParams hashedData (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 @@ -394,9 +196,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 insData + . addDatums (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}) @@ -406,8 +211,3 @@ instance (Monad m) => MonadBlockChain (MockChainT m) where logEvent $ MCLogNewTx (Ledger.fromCardanoTxId $ Ledger.getCardanoTxId cardanoTx) -- 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/GenerateTx/Body.hs b/src/Cooked/MockChain/GenerateTx/Body.hs index 5d628c5f6..38655246b 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 @@ -10,12 +17,14 @@ 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.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 @@ -50,6 +59,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 @@ -82,11 +96,11 @@ 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 - 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 return Cardano.TxBodyContent {..} @@ -95,18 +109,67 @@ 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 - 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 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 + + -- 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/MockChain/GenerateTx/Withdrawals.hs b/src/Cooked/MockChain/GenerateTx/Withdrawals.hs new file mode 100644 index 000000000..ef8eb99c6 --- /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, (red, 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 -> 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/MockChain/GenerateTx/Witness.hs b/src/Cooked/MockChain/GenerateTx/Witness.hs index 276269660..eeac4a4cd 100644 --- a/src/Cooked/MockChain/GenerateTx/Witness.hs +++ b/src/Cooked/MockChain/GenerateTx/Witness.hs @@ -43,8 +43,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 <- @@ -67,7 +67,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 diff --git a/src/Cooked/MockChain/MockChainSt.hs b/src/Cooked/MockChain/MockChainSt.hs new file mode 100644 index 000000000..2dafcc03f --- /dev/null +++ b/src/Cooked/MockChain/MockChainSt.hs @@ -0,0 +1,236 @@ +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 + +-- | Converts a builtin UtxoIndex into our own usable map between utxos and +-- associated outputs. +getIndex :: Ledger.UtxoIndex -> Map Api.TxOutRef Api.TxOut +getIndex = + Map.fromList + . map (bimap Ledger.fromCardanoTxIn (Ledger.fromCardanoTxOutToPV2TxInfoTxOut . toCtxTxTxOut)) + . Map.toList + . Cardano.unUTxO + where + -- We need to convert a UTxO context TxOut to a Transaction context Tx out. + -- It's complicated because the datum type is indexed by the context. + toCtxTxTxOut :: Cardano.TxOut Cardano.CtxUTxO era -> Cardano.TxOut Cardano.CtxTx era + toCtxTxTxOut (Cardano.TxOut addr val d refS) = + let dat = case d of + Cardano.TxOutDatumNone -> Cardano.TxOutDatumNone + Cardano.TxOutDatumHash s h -> Cardano.TxOutDatumHash s h + Cardano.TxOutDatumInline s sd -> Cardano.TxOutDatumInline s sd + in Cardano.TxOut addr val dat refS + +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 a skeleton context from a mockchain state. This is dedicated to +-- allowing the pretty printer to resolve skeleton parts. +mcstToSkelContext :: MockChainSt -> SkelContext +mcstToSkelContext MockChainSt {..} = + SkelContext + (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 +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 5321937e6..92a2ae948 100644 --- a/src/Cooked/MockChain/Staged.hs +++ b/src/Cooked/MockChain/Staged.hs @@ -27,6 +27,7 @@ import Control.Monad.State 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 diff --git a/src/Cooked/Pretty/Cooked.hs b/src/Cooked/Pretty/Cooked.hs index 64d0f61ee..c239d82cc 100644 --- a/src/Cooked/Pretty/Cooked.hs +++ b/src/Cooked/Pretty/Cooked.hs @@ -164,7 +164,7 @@ instance PrettyCooked MockChainLogEntry where <+> "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) = +prettyTxSkel opts skelContext (TxSkel lbl txopts mints signers validityRange ins insReference outs proposals withdrawals) = prettyItemize "Transaction skeleton:" "-" @@ -177,10 +177,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) Api.PubKeyHash, (TxSkelRedeemer, Script.Ada)) -> DocCooked + prettyWithdrawal (cred, (red, ada)) = + prettyItemizeNoTitle "-" $ + ( case cred of + Left script -> 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 @@ -349,9 +363,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, @@ -362,9 +379,23 @@ prettyTxSkelOut opts (Pays output) = prettyTxSkelOutDatumMaybe :: PrettyCookedOpts -> TxSkelOutDatum -> Maybe DocCooked prettyTxSkelOutDatumMaybe _ TxSkelOutNoDatum = Nothing prettyTxSkelOutDatumMaybe opts txSkelOutDatum@(TxSkelOutInlineDatum _) = - Just $ "Datum (inlined):" <+> PP.align (prettyCookedOpt opts txSkelOutDatum) -prettyTxSkelOutDatumMaybe opts txSkelOutDatum = - Just $ "Datum (hashed):" <+> PP.align (prettyCookedOpt opts txSkelOutDatum) + Just $ + "Datum (inlined):" + <+> PP.align (prettyCookedOpt opts txSkelOutDatum) +prettyTxSkelOutDatumMaybe opts txSkelOutDatum@(TxSkelOutDatumHash dat) = + Just $ + "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) -- | 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 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 db2401858..bfa80c91a 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, @@ -48,11 +49,11 @@ module Cooked.Skeleton paysPK, paysScript, paysScriptInlineDatum, - paysScriptDatumHash, + paysScriptUnresolvedDatumHash, paysScriptNoDatum, withDatum, withInlineDatum, - withDatumHash, + withUnresolvedDatumHash, withReferenceScript, withStakingCredential, TxSkelRedeemer (..), @@ -66,6 +67,11 @@ module Cooked.Skeleton txSkelProposalActionL, txSkelProposalWitnessL, txSkelProposalAnchorL, + TxSkelWithdrawals, + txSkelWithdrawnValue, + txSkelWithdrawalsScripts, + pkWithdrawal, + scriptWithdrawal, TxSkel (..), txSkelLabelL, txSkelOptsL, @@ -75,6 +81,7 @@ module Cooked.Skeleton txSkelInsL, txSkelInsReferenceL, txSkelOutsL, + txSkelWithdrawalsL, txSkelTemplate, txSkelDataInOutputs, txSkelValidatorsInOutputs, @@ -104,6 +111,7 @@ import Cooked.Pretty.Class import Cooked.Wallet import Data.ByteString (ByteString) import Data.Default +import Data.Either import Data.Either.Combinators import Data.Function import Data.List (foldl') @@ -118,6 +126,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) @@ -581,6 +590,28 @@ 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 + +-- | 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) Api.PubKeyHash) + (TxSkelRedeemer, Script.Ada) + +txSkelWithdrawnValue :: TxSkel -> Api.Value +txSkelWithdrawnValue = mconcat . (toValue . snd . snd <$>) . Map.toList . txSkelWithdrawals + +txSkelWithdrawalsScripts :: TxSkel -> [Script.Versioned Script.Script] +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 -- | A description of what a transaction mints. For every policy, there can only @@ -608,11 +639,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 +721,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 = @@ -868,8 +900,9 @@ paysPK pkh value = (Nothing @(Script.Versioned Script.Script)) ) --- | Pays a script a certain value with a certain datum, using the --- 'TxSkelOutDatum' constructor. (See the documentation of 'TxSkelOutDatum'.) +-- | Pays a script a certain value with a certain datum hash, using the +-- '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), @@ -915,9 +948,10 @@ 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, 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), Typeable (Script.DatumType a), @@ -929,7 +963,7 @@ paysScriptDatumHash :: Script.DatumType a -> Api.Value -> TxSkelOut -paysScriptDatumHash validator datum value = +paysScriptUnresolvedDatumHash validator datum value = Pays ( ConcreteOutput validator @@ -940,7 +974,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 = @@ -966,8 +1000,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) @@ -1001,7 +1035,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. @@ -1011,8 +1045,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) @@ -1026,7 +1063,8 @@ makeLensesFor ("txSkelIns", "txSkelInsL"), ("txSkelInsReference", "txSkelInsReferenceL"), ("txSkelOuts", "txSkelOutsL"), - ("txSkelProposals", "txSkelProposalsL") + ("txSkelProposals", "txSkelProposalsL"), + ("txSkelWithdrawals", "txSkelWithdrawalsL") ] ''TxSkel @@ -1042,7 +1080,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 @@ -1056,18 +1095,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/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..47daf586c 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, @@ -14,6 +15,8 @@ module Cooked.Wallet walletAddress, walletSK, walletStakingSK, + walletStakingCredential, + walletCredential, Wallet, PrivateKey, ) @@ -66,6 +69,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 @@ -82,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 = diff --git a/tests/Cooked/BalancingSpec.hs b/tests/Cooked/BalancingSpec.hs index df25e251f..a3833248a 100644 --- a/tests/Cooked/BalancingSpec.hs +++ b/tests/Cooked/BalancingSpec.hs @@ -42,7 +42,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, Maybe (Set Api.TxOutRef, Wallet), [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 8a464c385..0a82d35dc 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/ProposingScriptSpec.hs b/tests/Cooked/ProposingScriptSpec.hs index 95c945ad5..63d118aa6 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 = diff --git a/tests/Cooked/ReferenceInputsSpec.hs b/tests/Cooked/ReferenceInputsSpec.hs index ddde28b71..cad6e437e 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) (Script.ada 2) `withDatum` (10 :: Integer), + paysScript bazTypedValidator () (Script.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) + ] diff --git a/tests/Cooked/WithdrawalsSpec.hs b/tests/Cooked/WithdrawalsSpec.hs new file mode 100644 index 000000000..def3e0e81 --- /dev/null +++ b/tests/Cooked/WithdrawalsSpec.hs @@ -0,0 +1,55 @@ +module Cooked.WithdrawalsSpec where + +import Control.Monad +import Cooked +import Data.Default +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 = scriptWithdrawal 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 ae3ae1633..b8f7c8aea 100644 --- a/tests/Spec.hs +++ b/tests/Spec.hs @@ -10,6 +10,7 @@ import Cooked.ProposingScriptSpec qualified as ProposingSpec import Cooked.ReferenceInputsSpec qualified as ReferenceInputsSpec import Cooked.ReferenceScriptsSpec qualified as ReferenceScriptsSpec import Cooked.TweakSpec qualified as TweakSpec +import Cooked.WithdrawalsSpec qualified as WithdrawalsSpec import Test.Tasty main :: IO () @@ -28,5 +29,6 @@ main = LtlSpec.tests, MockChainSpec.tests, InitDistribSpec.tests, - ProposingSpec.tests + ProposingSpec.tests, + WithdrawalsSpec.tests ]