1

I have written a script that basically keeps a counter and every time it is spent increases said counter, the way it guarantees that the counter was increased is by checking the current datum of the output and making sure the new datum is current datum + 1 (The script has some additional preconditions which are not important right now):

{-|
Module      : Horrocubes.Counter.
Description : Plutus script that keeps track of an internal counter.
License     : Apache-2.0
Maintainer  : angel.castillo@horrocubes.io
Stability   : experimental

This script keeps a counter and increases it everytime the eUTXO is spent.
-}

-- LANGUAGE EXTENSIONS --------------------------------------------------------

{-# LANGUAGE DataKinds                  #-}
{-# LANGUAGE DeriveAnyClass             #-}
{-# LANGUAGE DeriveGeneric              #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE MultiParamTypeClasses      #-}
{-# LANGUAGE NoImplicitPrelude          #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE ScopedTypeVariables        #-}
{-# LANGUAGE TemplateHaskell            #-}
{-# LANGUAGE TypeApplications           #-}
{-# LANGUAGE TypeFamilies               #-}
{-# LANGUAGE TypeOperators              #-}
{-# LANGUAGE DerivingStrategies         #-}
{-# LANGUAGE LambdaCase                 #-}
{-# LANGUAGE NamedFieldPuns             #-}
{-# LANGUAGE ViewPatterns               #-}

-- MODULE DEFINITION ----------------------------------------------------------

module Horrocubes.Counter
(
  counterScript,
  counterScriptShortBs,
  CounterParameter(..)
) where

-- IMPORTS --------------------------------------------------------------------

import           Cardano.Api.Shelley      (PlutusScript (..), PlutusScriptV1)
import           Codec.Serialise
import qualified Data.ByteString.Lazy     as LBS
import qualified Data.ByteString.Short    as SBS
import           Ledger                   hiding (singleton)
import qualified Ledger.Typed.Scripts     as Scripts
import           Ledger.Value             as Value
import qualified PlutusTx
import           PlutusTx.Prelude         as P hiding (Semigroup (..), unless)
import           Data.Aeson               (FromJSON, ToJSON)
import           GHC.Generics             (Generic)
import qualified Ledger.Contexts          as Validation
import           Text.Show

-- DATA TYPES -----------------------------------------------------------------

-- | The parameters for the counter contract.
data CounterParameter = CounterParameter {
        ownerPkh    :: !PubKeyHash, -- ^ The transaction that spends this output must be signed by the private key
        identityNft :: !AssetClass  -- ^ The NFT that identifies the correct eUTXO.
    } deriving (Show, Generic, FromJSON, ToJSON)

PlutusTx.makeLift ''CounterParameter

-- | This Datum represents the state of the counter.
data CounterDatum = CounterDatum {
        counter :: !Integer     -- ^ The current counter value.
    }
    deriving Show

PlutusTx.unstableMakeIsData ''CounterDatum

-- | The Counter script type. Sets the Redeemer and Datum types for this script.
data Counter 
instance Scripts.ValidatorTypes Counter where
    type instance DatumType Counter = CounterDatum
    type instance RedeemerType Counter = ()
    
-- DEFINITIONS ----------------------------------------------------------------

-- | Maybe gets the datum from the transatcion output.
{-# INLINABLE counterDatum #-}
counterDatum :: TxOut -> (DatumHash -> Maybe Datum) -> Maybe CounterDatum
counterDatum o f = do
    dh      <- txOutDatum o
    Datum d <- f dh
    PlutusTx.fromBuiltinData d

-- | Checks that the identity NFT is locked again in the contract.
{-# INLINABLE isIdentityNftRelocked #-}
isIdentityNftRelocked:: CounterParameter -> Value -> Bool
isIdentityNftRelocked params valueLockedByScript = assetClassValueOf valueLockedByScript (identityNft params) == 1

-- | Creates the validator script for the outputs on this contract.
{-# INLINABLE mkCounterValidator #-}
mkCounterValidator :: CounterParameter -> CounterDatum -> () -> ScriptContext -> Bool
mkCounterValidator parameters oldDatum _ ctx = 
    let oldCounterValue        = counter oldDatum
        isRightNexCounterValue = (newDatumValue == (oldCounterValue + 1))
        isIdentityLocked       = isIdentityNftRelocked parameters valueLockedByScript
    in traceIfFalse "Wrong counter value"           isRightNexCounterValue && 
       traceIfFalse "Wrong balance"                 isIdentityLocked && 
       traceIfFalse "Missing signature"             isTransactionSignedByOwner 
    where
        info :: TxInfo
        info = scriptContextTxInfo ctx

        ownOutput :: TxOut
        ownOutput = case getContinuingOutputs ctx of
            [o] -> o
            _   -> traceError "Expected exactly one output"

        newDatumValue :: Integer
        newDatumValue = case counterDatum ownOutput (`findDatum` info) of
            Nothing -> traceError "Counter output datum not found"
            Just datum  -> counter datum

        valueLockedByScript :: Value
        valueLockedByScript = Validation.valueLockedBy info (Validation.ownHash ctx)

        isTransactionSignedByOwner :: Bool
        isTransactionSignedByOwner = txSignedBy info (ownerPkh parameters)

-- | The script instance of the counter. It contains the mkCounterValidator function
--   compiled to a Plutus core validator script.
counterInstance :: CounterParameter -> Scripts.TypedValidator Counter
counterInstance counter = Scripts.mkTypedValidator @Counter
    ($$(PlutusTx.compile [|| mkCounterValidator ||]) `PlutusTx.applyCode` PlutusTx.liftCode counter) $$(PlutusTx.compile [|| wrap ||])
    where
        wrap = Scripts.wrapValidator @CounterDatum @()

-- | Gets the counter validator script that matches the given parameters.
counterValidator :: CounterParameter -> Validator
counterValidator params = Scripts.validatorScript . counterInstance $ params

-- | Generates the plutus script.
counterPlutusScript :: CounterParameter -> Script
counterPlutusScript params = unValidatorScript $ counterValidator params

-- | Serializes the contract in CBOR format.
counterScriptShortBs :: CounterParameter -> SBS.ShortByteString
counterScriptShortBs params = SBS.toShort . LBS.toStrict $ serialise $ counterPlutusScript params

-- | Gets a serizlized plutus script from the given parameters.
counterScript :: PubKeyHash -> AssetClass -> PlutusScript PlutusScriptV1
counterScript pkh ac = PlutusScriptSerialised $ counterScriptShortBs $ CounterParameter { ownerPkh = pkh,  identityNft = ac }

I already locked some funds in the script and I am trying to spent the eUTXO using the CLI and I am having a conceptual issue here, there is no way to specify the new output datum using the CLI, it seems I can only specify the datum hash of the new datum.

cardano-cli transaction build-raw --alonzo-era --fee 500000  --tx-in b2997baf426caa94762e4baeed051ac13bad7994f2f3a43f8c43299d2ba8f050#0 --tx-in b2997baf426caa94762e4baeed051ac13bad7994f2f3a43f8c43299d2ba8f050#1 --tx-in-script-file ./counter/out2.plutus --tx-in-execution-units "(491845099,1197950)" --tx-in-datum-value 0 --tx-in-redeemer-value 0 --tx-in-collateral b2997baf426caa94762e4baeed051ac13bad7994f2f3a43f8c43299d2ba8f050#0 --tx-out "addr_test1wqesjavxsh7g2q8lf92ptyt7rnrhh07ghnyjq65ra50uwwqsssy2q+2000000+1 a1c6cefca22b4527acdf17a1d44674b6d7cf17c3e7e35cbd1a57d8b5.Horrocube09997" --tx-out-datum-hash ee155ace9c40292074cb6aff8c9ccdd273c81648ff1149ef36bcea6ebb8a3e25 --protocol-params-file protocol.json --out-file tx-script2.build

Using the CLI to construct the transaction you need to:

  • Specify the actual datum value matching the datum hash in the current eUTXO you want to spend. (--tx-in-datum-value)

  • Specify the new datum hash for the new eUTXO we are creating
    (--tx-out-datum-hash)

However, the script needs both values in the validator as it verifies that the new datum value in the output is equal to the current datum value + 1 but it only has the current datum value and hash AND the new datum hash (not the value).

Using the plutus offchain code I can pass the new datum no problem:

-- | Tries to solve the puzzle at the given index.
solve ::  forall w s. HasBlockchainActions s => SolveParams -> Contract w s Text ()
solve solveParams = do
    let cube = CubeParameter { 
        cubeId          = spCubeId solveParams,
        stateMachineNft = spStateMachineNft solveParams
    }
    pkh <- pubKeyHash <$> Contract.ownPubKey
    utxos <- utxoAt $ cubeAddress cube
    addressUtxos <- utxoAt $ pubKeyHashAddress pkh

    let constriants = Constraints.unspentOutputs utxos  <>
                      Constraints.unspentOutputs addressUtxos <>
                      Constraints.otherScript (Scripts.validatorScript (cubeInstance cube))  <>
                      Constraints.scriptInstanceLookups (cubeInstance cube) <> 
                      Constraints.ownPubKeyHash pkh

    m <- findCubeOutput cube
    case m of
        Nothing -> logInfo @String "Cube output not found for solve parameters "
        Just (_, _, dat) -> do
            let datum = dat { currentPuzzleIndex = spPuzzleIndex solveParams + 1 }
                redeemmer = CubeRedeemer (spPuzzleIndex solveParams) (spAnswer solveParams)
                totalValue  = Prelude.foldMap (Tx.txOutValue . Tx.txOutTxOut) utxos
                orefs       = fst <$> Map.toList utxos
                payToSelf   = assetClassValue (cubeId cube) 1 -- We must pay to outselves the cube so we can prove ownership of the cube.
                payToScript = (buildValue cube datum (spPuzzleIndex solveParams) totalValue)
                tx = mconcat [Constraints.mustSpendScriptOutput oref (Redeemer (PlutusTx.toData redeemmer)) | oref <- orefs] <>
                              Constraints.mustPayToTheScript datum payToScript <> 
                              Constraints.mustPayToPubKey pkh payToSelf
            ledgerTx <- submitTxConstraintsWith @Cube constriants tx
            void $ awaitTxConfirmed $ txId ledgerTx

In fact, using the Plutus code I only pass the new datum value to "submitTxConstraintsWith" which now that I think I also don't understand, where is the Plutus off chain code getting the current datum value from? since the output only store the datum hash? Anyways the Plutus API works. But I am trying to use the CLI to use the contract and I don't see a way to pass the actual datum value (not just the hash) to the script so the validator can run.

1 Answer 1

1

I already found the answer and will leave it here in case anyone ran into the same problem.

You need to update the node-cli version to the lastest version (currently) 1.31.0 and then you have the following options available:

 [--tx-out ADDRESS VALUE
              [ --tx-out-datum-hash HASH
              | --tx-out-datum-hash-file FILE
              | --tx-out-datum-hash-value JSON VALUE
              | --tx-out-datum-embed-file FILE
              | --tx-out-datum-embed-value JSON VALUE
              ]]

And now you can specify the value of the new datum instead of the hash:

i.e

transaction build --alonzo-era --testnet-magic 1097911063  --change-address $(cat counter/payment.addr) --tx-in-collateral dc5a8bff48a9db6436559945ceb7a3f311045596407021312c133b1db70d7419#0 --tx-in 7d65ca46aca44532d94da57ec6b7297efdda2e523626f90bbdc780f3767202f2#1 --tx-in-script-file ./counter/out2.plutus --tx-in-datum-value 0 --tx-in-redeemer-value [] --tx-out "addr_test1wpxzyxrdt06f08v5le8h6tgwww9ud7qh4p7w3q8mhndtedg9fdzg2+1000000" --tx-out-datum-embed-value 1 --protocol-params-file protocol.json --out-file tx-script2.build

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.