Mary era NFTs, Alonzo era NFTs, which are better?

Thanks a lot, @bwbush, lets see if I can get it right, this is what I have layout so far:

{-# INLINABLE mkNFTPolicy #-}
mkNFTPolicy :: PubKeyHash -> AssetClass -> BuiltinData -> ScriptContext -> Bool
mkNFTPolicy pkh identityNft _ ctx  = 
    let currentDatumValue      = _
        tokenName              = "Horrocube" ++ show currentDatumValue
        isNexDatumValueCorrect = (newDatumValue == (currentDatumValue + 1))
    in  traceIfFalse "Identity NFT not found"         isIdentityNftSpended &&
        traceIfFalse "The new datum value is invalid" isNexDatumValueCorrect &&
        traceIfFalse "Wrong amount minted"            checkMintedAmount
        traceIfFalse "Missing signature"              isTransactionSignedByOwner
    where
      info :: TxInfo
      info = scriptContextTxInfo ctx

      isIdentityNftSpended :: Bool
      isIdentityNftSpended = assetClassValueOf valueSpentByScript identityNft == 1

      checkMintedAmount :: Bool
      checkMintedAmount = case flattenValue (txInfoMint info) of
          [(_, tn', amt)] -> tn' == tokenName && amt == 1
          _               -> False

      valueSpentByScript :: Value
      valueSpentByScript = Validation.valueSpent info

      isTransactionSignedByOwner :: Bool
      isTransactionSignedByOwner = txSignedBy info pkh

      stateDatum :: TxOut -> (DatumHash -> Maybe Datum) -> Maybe StateDatum
      stateDatum o f = do
          dh      <- txOutDatum o
          Datum d <- f dh
          PlutusTx.fromData d

      newDatumValue :: Integer
      newDatumValue = case stateDatum ownOutput (`findDatum` info) of
          Nothing -> traceError "Output datum not found"
          Just datum  -> currentIndex datum

So in the script, I am validating that:

a) The right NFT is being spent (Identity NFT for the eUTXO which will hold the counter)
b) The new datum is prev datum + 1
c) There is exactly one token with asset name “Horrocube” + appended counter being minted (The prefix maybe will be better to have it as another parameter of the validator).
d) The transaction is being signed by the proper key.

The script parameters are the Public Key allowed to mint and the AssetClass of the identity NFT for the eUTXO that will hold the counter.

However, I am not sure how to get the datum of the eUTXO from which the identity NFT is being spent and the datum on of the new eUTXO where the identity NFT is being locked.

1 Like

The validator that you posted looks great. I think you’ll need two validator scripts: a spending validator to increment the counter and the minting validator that you posted.

The spending validator would use findOwnInputs to read the old datum and getContinuingOutputs to read the new datum. (The spending script would check that the increment occurs, so that check wouldn’t actually be needed for in the minting script.)

The minting validator could use txInInfoResolved and txOutValue on each txInfoInputs to locate the input with the identity token holding the datum, and then findDatum and fromData on that to extract the sequence number.

1 Like

Thanks for the pointers @bwbush, I will post it once I have it working.

You mean by spending validator, to have the eUTOX that contains the identity NFT to also enforce the increment of the counter, correct?, out of curiosity, why this can not be done in the minting validator script?

I am really excited about this idea of having the counter included because now I can also enforce that no more tokens than promised (I.E 10.000) will ever be created, adding more guarantees to the process!

2 Likes

Yes. This approach requires spending the eUTxO with the identity token; because that eUTxO holds a datum, the spending must be validated by a script. However, a minting script cannot validate spending—minting validators don’t take a datum as input—, so a spending script is needed. In the end, you’ll have the identity token sitting at the address of the spending script, and each minting transaction will spend it and send it back to the spending script address.

It will be cool to see this running on testnet. Thanks for your great questions!

2 Likes

Hi @bwbush, I just finish the spending script and I have a few questions:

{-|
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 }
type or paste code here

I already created an instance of the script on the testnet with the appropriate identity NFT inside, you can check the script at the following address:

addr_test1wqesjavxsh7g2q8lf92ptyt7rnrhh07ghnyjq65ra50uwwqsssy2q

cardano-cli query utxo --address addr_test1wqesjavxsh7g2q8lf92ptyt7rnrhh07ghnyjq65ra50uwwqsssy2q --testnet-magic 1097911063
                           TxHash                                 TxIx        Amount
--------------------------------------------------------------------------------------
b2997baf426caa94762e4baeed051ac13bad7994f2f3a43f8c43299d2ba8f050     1        2000000 lovelace + 1 a1c6cefca22b4527acdf17a1d44674b6d7cf17c3e7e35cbd1a57d8b5.Horrocube09997 + TxOutDatumHash ScriptDataInAlonzoEra "03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c111314"

The parameters of the script are:

PubKeyHash: 52cdb93f3e798d9c73866450e6e2bfb4a3da911f702ea055e3042dab
AssetClass: a1c6cefca22b4527acdf17a1d44674b6d7cf17c3e7e35cbd1a57d8b5.Horrocube09997

And it was initialized with the datum value 0 (I know this is wrong, as my datum is not an integral type but of CounterDatum type instead).

This is how I am constructing the transaction:

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

And after I sign and try to send the transaction I get an error (of course).

I noticed this part in particular:

Datums: [ ( 03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c111314\n          , 0 )]

There is only reference to the input datum, but not to the new datum and now that I think about it carefully I am not giving it the new value of the datum, only the hash (which for now is also wrong as it is the datum hash of the integral type 1 and not CounterDatum):

--tx-out-datum-hash ee155ace9c40292074cb6aff8c9ccdd273c81648ff1149ef36bcea6ebb8a3e25

Sorry for the long preamble, now on to the actual questions:

1.- How do you specify the actual value of the new datum using the CLI? It seems it will only let me specify the datum hash of the output, however, if this is the case, how can the script get the actual value of the new datum? hash can not be reversed.

2.- How can we specify non-integral types to the CLI (this one I can probably find on my own).

I apologize for the lengthy post, but I wanted to share as much detail as possible to both share my progress and set the right context for the questions.

Thanks

When you create the eUTxO, you only specify the hash of the datum. When you spend the eUTxO, you supply the datum. So you just need to compute the datum and supply its hash when you create the eUTxO, but remember the datum off-chain so that you can supply it when you spend the eUTxO.

Just use rational numbers (numerator and denominator) or use an integer but with number of decimal places also specfied.

Hi @bwbush thanks for the quick reply:

1.- But the script needs both values, the old datum, and the new datum, If I only provide the value for the old datum, how will the script know the new value from the hash? (The script checks that the new value is equal to the old value +1) It will need to have both values to be able to compare.

The only way to enforce a controlled state transition in the datum within the validator is to have both the new and old values available while spending the script.

What makes me even more confused is that in our Plutus code that we run on the emulator for the other contracts, we actually pass the value for the new datum and not the old datum when we build the transaction:

-- | Tries to solve the ouzzle 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

2.- Sorry I expressed myself incorrectly (not a native speaker), I didn’t mean integral types, I guess the right word would be complex type? My datum is a “data” type instead of just an integer, so I am guessing I must somehow express that while calculating the datum hash.

For object type you will want ScriptData schema definition. Assuming you’re using JSON you may find this API documentation useful for how to convert between the off-chain and on-chain format:
https://input-output-hk.github.io/cardano-node/cardano-api/lib/Cardano-Api-ScriptData.html#g:4

1 Like

Thanks, @DinoDude I will check it out, for now, I am using BuiltinData type which should work with the CLI JSON format.

I already figure out how to send the Datum value instead of the hash (someone in this forum had already answered that question) while creating the new eUTXO, I had to update my CLI version to the latest, and now I have these 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
              ]]

However, I am getting an obscure error when I greater the transaction (not sure if it is because I am passing the value instead of the hash):

The Plutus script evaluation failed: An error has occurred:  User error:
The provided Plutus code called 'error'.
Caused by: [ (builtin unConstrData) (con data #80) ]

It seems it is failing while constructing the builtin data? (this is my datum type), but I am not sure why, this is the format of my json file for the datum:

{"constructor":0,"fields":[{"int":0}]}

And this is how I am building the transaction:

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

Man this is so much easier in the emulator, I hope the PAB is as smooth as well, the CLI is giving me a hard time (probably due to my own ignorance of the nuances)

I finally made it work, this contract in summary:

  • Force the counter on the datum to be incremented by exactly one every time the eUTXO is spent.
  • Verifies that the identity NFT specified in the parameters of the script is always present (you must pay it back to the script everytime you spend the eUTXO).
  • Verifies that the eUTXO was signed with the key that matches the PubKeyHash specified in the parameters of the contract.

There were several things that needed to be specified on the CLI for it to work properly:

1- You must specify the new output datum value instead of the hash

This is because the script actually uses the new datum value to enforce that it is incremented, if you only specified the hash then it can’t find the value in the datums list, this is pretty easy to do, but the ability to do this using the CLI was just recently added in the latest version (So you must update the CLI to the latest).

This is how the datum list looks on the transaction if you use –tx-out-datum-hash

Datums: [( b7afc0e9c25e4fa2933e0ed75b11024f44b271223ddeb5e919c9ed09489e29a1 , <0> ) ]

And this if you use –tx-out-datum-embed-file instead:

Datums: [ ( 58b85f4b6b8f3d8e62f406ee77f09afc99a9e1b959389367969bcce3c485c6ad, <1> )\n          , ( b7afc0e9c25e4fa2933e0ed75b11024f44b271223ddeb5e919c9ed09489e29a1, <0> ) ]

Notice the extra datum key-value pair on the list.

2.- You have to be careful with the encoding of the datums and the redeemer (even if you are not using the redeemer), this one took me a really long time, as I didn’t understand why I was getting some constructor error for a built-in type, I thought there was something wrong with the datums, it turned out to be the redeemer, I was using [] as a dummy value, but in reality, you have to pass a valid value as it will try to construct a value for the redeemer even if you only defined it as unit ().

3.- You have to add –required-signer to sign the new output. That way the eUTXO is signed.

Thanks for all the help and useful information @bwbush & @DinoDude. I apologize for extending this thread out of the initial essential question.

2 Likes

I created another script following one of the other strategies suggested here.

I took half the eUTXO id and encoded it as base58 (The minting script validates this), this yields a worst-case length of 22 characters, leaving 10 characters for a meaningful asset name.

Because I only took 128 bits of the UTXO hash, it seems like this is not very secure?:

You can see the script here:

The asset name would look like this: HCube097043XEdfDgC4VJUq2ut1X7Fb4

I minted one on the testnet:
https://explorer.cardano-testnet.iohkdev.io/en/transaction?id=27b147ae8875fa081b8704d6c0619a38eca2202821bedf5559f490a3bbc99437

Would love to hear your comments @DinoDude & @bwbush regarding the security of this approach. I know 128 bit hashes are not secure, but would brute-forcing a UTXO id in which half its bytes match another UTXO id be harder than brute-forcing a hash, or is roughly equal? I really wanted this approach to work as it is easier to implement and does not suffer from any concurrency problems/bottlenecks, however, it seems like is not good enough.

The probability of a hash collision scales inversely as the square root of the number of possibilities. Here is a brief discussion. Even for billions of NFTs, the probability of two identical ones would be incredibly small if you use 128 bits.

It’s roughly equal.

2 Likes

P.S. You can also use encoding strategies to store data in the token name. For instance, I used base-58 encoding to store in the asset name the “genome” of for NFT images of pigs: it encodes color, body size/shape, eye gaze, etc. One can “breed” new NFTs so that the child NFTs have characteristics of the parent NFTs. The code doesn’t need to lookup minting metadata because the information is encoded in the asset name.

3 Likes

That’s an awesome idea @bwbush :).

Every one of your ideas so far is truly creative and useful; thanks so much for sharing!

1 Like

Example meta data from a random pigly token as indicated above:

{
   cbf096ed812bdafc8b000886cf7b1ccd4e430e78dc579c7f25a155d3: {
      "PIG@2KRERCB6hHahWBvQ": {
         name: "PIG 2KRERCB6hHahWBvQ",
         image: "ipfs://QmRRMSephqt4d3sVUTK63jxH1mLKsmuhcwUQHR5A2JRVfj",
         ticker: "PIG@2KRERCB6hHahWBvQ",
         parents: [
            "PIG@2KQoATYSaEeSz1Gj",
            "PIG@2PSwsewVLn3RqYNC"
         ],
         url: "https://pigy.functionally.live"
      }
   }
}

@bwbush Nicely done, now I know why you were working on the that mixin :wink:

2 Likes

I just finish implementing the other approach we discussed here (with the two scripts), just tested it on the test net, and is working perfectly.

These are the two scripts:

Counter script

{-|
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 every time 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(..),
  CounterDatum(..)
) 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
import           PlutusTx.Builtins

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

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

PlutusTx.makeLift ''CounterParameter

-- | The counter datum datatype.
data CounterDatum = CounterDatum {
        cdValue :: !Integer, -- ^ The current counter value.
        cdLimit :: !Integer  -- ^ The value limit, after this limit is reached, this eUTXO can not be spent again.
    } deriving (Show, Generic, FromJSON, ToJSON)

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 (cpIdentityNft params) == 1

-- | Creates the validator script for the outputs on this contract.
{-# INLINABLE mkCounterValidator #-}
mkCounterValidator :: CounterParameter -> CounterDatum -> () -> ScriptContext -> Bool
mkCounterValidator parameters oldDatum _ ctx = 
    let isRightNexCounterValue = (newDatumIntegerValue == (oldDatumIntegerValue + 1))
        isIdentityLocked       = isIdentityNftRelocked parameters valueLockedByScript
        isLimitTheSame         = oldDatumLimitValue == newDatumLimitValue
        isLimitNotReached      = newDatumIntegerValue < newDatumLimitValue
    in traceIfFalse "Wrong counter value"           isRightNexCounterValue && 
       traceIfFalse "Identity NFT missing"          isIdentityLocked && 
       traceIfFalse "Missing signature"             isTransactionSignedByOwner &&
       traceIfFalse "Limit value changed"           isLimitTheSame &&
       traceIfFalse "Limit reached"                 isLimitNotReached
    where
        info :: TxInfo
        info = scriptContextTxInfo ctx

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

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

        oldDatumIntegerValue :: Integer
        oldDatumIntegerValue = cdValue oldDatum

        oldDatumLimitValue :: Integer
        oldDatumLimitValue = cdLimit oldDatum

        newDatumIntegerValue :: Integer
        newDatumIntegerValue = cdValue newDatum

        newDatumLimitValue :: Integer
        newDatumLimitValue = cdLimit newDatum

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

        isTransactionSignedByOwner :: Bool
        isTransactionSignedByOwner = txSignedBy info (cpOwnerPkh 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 { cpOwnerPkh = pkh,  cpIdentityNft = ac }

The counter scripts trap an identity NFT inside (the only way to spend the output is to pay the NFT back to the script) and force the increment of the internal counter until it reaches a limit, the starting value of the counter and the limit are first defined when the output is first created.

Pre-conditions:

  • The transaction spending this output must be signed with the proper key (passed as a script parameter)
  • The identity NFT must be present (this is indirectly validated by the fact that the script check that the identity NFT is being paid to itself)

Post-Conditions:

  • The field cdValue of the new datum must be equals to the field cdValue of the old datum plus 1 (value must be increased by exactly one)
  • The field cdLimit of the new datum must equal to the field cdLimit of the old datum (value cant not be change between datums)
  • The field cdValue of the new datum must be less than the value of the field cdLimit (once cdValue reach cdLimit the output can not be spent anymore)
  • The identity NFT must be paid back to the original script

Minter script

{-|
Module      : Horrocubes.MintingScriptWithCounter.
Description : Mint policy for NFTs.
License     : Apache-2.0
Maintainer  : angel.castillob@protonmail.com
Stability   : experimental

This policy creates an NFT and uses an eUTXO with an internal counter to make the NFT truly unique.
-}

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

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
 {-# LANGUAGE OverloadedStrings #-}

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

module Horrocubes.MintingScriptWithCounter
(
  mintScript,
  nftScriptShortBs
) where

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

import           Cardano.Api.Shelley      (PlutusScript (..), PlutusScriptV1)
import           Codec.Serialise
import qualified Data.ByteString.Lazy     as LB
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         hiding (Semigroup (..), unless)
import qualified Data.ByteString.Char8    as C
import           PlutusTx.Builtins 
import Horrocubes.Deserialisation
import           Data.Aeson               (FromJSON, ToJSON)
import           GHC.Generics             (Generic)
import qualified Ledger.Contexts          as Validation
import           Text.Show

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

-- | The counter datum datatype.
data CounterDatum = CounterDatum {
        cdValue :: !Integer, -- ^ The current counter value.
        cdLimit :: !Integer  -- ^ The value limit, after this limit is reached, this eUTXO can not be spent again.
    } deriving (Show, Generic, FromJSON, ToJSON)

PlutusTx.unstableMakeIsData ''CounterDatum

-- DEFINITIONS ----------------------------------------------------------------

-- | Zero pads a given hex value to 8 cahracters.
{-# INLINABLE padLeft #-}
padLeft :: BuiltinByteString -> BuiltinByteString  -> BuiltinByteString
padLeft charset bs = if lengthOfByteString bs < 8
  then  padLeft charset (consByteString (indexByteString charset 0) bs)
  else bs

-- | Gets the Hash of the given UTXO.
{-# INLINABLE utxoHash #-}
utxoHash:: TxOutRef -> BuiltinByteString
utxoHash utxo = getTxId $ txOutRefId utxo

-- | Encodes an Integer into a diffent base (ie base 16).
{-# INLINABLE encodeBase #-}
encodeBase :: BuiltinByteString -> Integer -> BuiltinByteString
encodeBase charset value = encoded where
  base     = lengthOfByteString charset
  encoded  = expand (value `divMod` base) emptyByteString
  lookup n = indexByteString charset n
  expand (dividend, rem) xs
    | (dividend >  0) = expand (dividend `divMod` base) result
    | (dividend == 0 && rem >  0) = result
    | (dividend == 0 && rem == 0) = xs
    where result = consByteString (lookup rem) xs

-- | Creates the minting script for the NFT.
{-# INLINABLE mkNFTPolicy #-}
mkNFTPolicy :: BuiltinByteString -> PubKeyHash -> AssetClass -> BuiltinData -> ScriptContext -> Bool
mkNFTPolicy charset pkh identityNft _ ctx  = 
        traceIfFalse "Identity NFT not found"          isIdentityNftSpent &&
        traceIfFalse "Invalid Postfix or wrong amount" checkMintedAmount &&
        traceIfFalse "Missing signature"               isTransactionSignedByOwner
    where
      info :: TxInfo
      info = scriptContextTxInfo ctx

      tokenNameToByteString :: TokenName -> BuiltinByteString
      tokenNameToByteString tn = unTokenName tn

      actuallPosfix :: BuiltinByteString -> BuiltinByteString
      actuallPosfix tn = sliceByteString ((lengthOfByteString tn) - 8) 8 $ tn

      expectedPosfix :: BuiltinByteString
      expectedPosfix = padLeft charset $ encodeBase charset $ datumIntegerValue

      isIdentityNftSpent :: Bool
      isIdentityNftSpent = assetClassValueOf valueSpentByScript identityNft == 1

      checkMintedAmount :: Bool
      checkMintedAmount = case flattenValue (txInfoMint info) of
        [(_, tn', amt)] -> (equalsByteString (actuallPosfix $ tokenNameToByteString tn') expectedPosfix) && amt == 1
        _               -> False

      valueSpentByScript :: Value
      valueSpentByScript = Validation.valueSpent info

      isTransactionSignedByOwner :: Bool
      isTransactionSignedByOwner = txSignedBy info pkh

      findUtxoWithIdentityNft :: TxOut
      findUtxoWithIdentityNft = case filter (\(TxOut{txOutValue}) -> assetClassValueOf txOutValue identityNft == 1) (txInfoOutputs info) of
        [o] -> o
        _   -> traceError "Expected exactly one output"

      stateDatum :: TxOut -> (DatumHash -> Maybe Datum) -> Maybe CounterDatum
      stateDatum o f = do
        dh      <- txOutDatum o
        Datum d <- f dh
        PlutusTx.fromBuiltinData d

      datumIntegerValue :: Integer
      datumIntegerValue = case stateDatum findUtxoWithIdentityNft (`findDatum` info) of
        Nothing -> traceError "Counter output datum not found"
        Just datum -> cdValue datum

 -- | Compiles the policy.
nftPolicy :: BuiltinByteString -> PubKeyHash -> AssetClass -> Scripts.MintingPolicy
nftPolicy charset pkh ac = mkMintingPolicyScript $
    $$(PlutusTx.compile [|| \charset' pkh' ac' -> Scripts.wrapMintingPolicy $ mkNFTPolicy charset' pkh' ac'||])
    `PlutusTx.applyCode`
     PlutusTx.liftCode charset
    `PlutusTx.applyCode`
     PlutusTx.liftCode pkh
    `PlutusTx.applyCode`
     PlutusTx.liftCode ac

-- | Generates the plutus script.
nftPlutusScript :: BuiltinByteString -> PubKeyHash -> AssetClass -> Script
nftPlutusScript charset pkh ac = unMintingPolicyScript $ nftPolicy charset pkh ac

-- | Generates the NFT validator.
nftValidator :: BuiltinByteString -> PubKeyHash  -> AssetClass -> Validator
nftValidator charset pkh ac = Validator $  nftPlutusScript charset pkh ac

-- | Serializes the contract in CBOR format.
nftScriptAsCbor :: BuiltinByteString -> PubKeyHash -> AssetClass -> LB.ByteString
nftScriptAsCbor charset pkh ac = serialise $ nftValidator charset pkh ac

-- | Serializes the contract in CBOR format.
nftScriptShortBs :: BuiltinByteString -> PubKeyHash -> AssetClass -> SBS.ShortByteString
nftScriptShortBs charset pkh ac = SBS.toShort . LB.toStrict $ nftScriptAsCbor charset pkh ac

-- | Gets a serizlize plutus script from the given UTXO and token name.
mintScript :: BuiltinByteString -> PubKeyHash -> AssetClass -> PlutusScript PlutusScriptV1
mintScript charset pkh ac = PlutusScriptSerialised . SBS.toShort . LB.toStrict $ nftScriptAsCbor charset pkh ac

This script only approves the minting transaction only if the identity NFT is present (the same one trapped in the counter eUTXO), the transaction is signed by the proper key (I think this could be removed as spending the counter eUTXO already requires this step), and then takes the datum from the output where the identity NFT is present, encodes it as a 32bit hex string and checks that the asset name of the new token being minted has this value as a postfix (last 8 characters must match this hex value).

Pre-conditions:

  • The transaction minting the asset must be signed with the proper key (passed as a script parameter)
  • The identity NFT must be present (the script checks that the identity NFT is being spent)
  • The last 8 characters of the asset name of the token matches the hexadecimal representation of the counter (in 32bits)

Post-Conditions:

  • Only one token with the right postfix value is being minted.

I already deployed this on the test net and it works, here is the address of the counter script:

addr_test1wz6g45e97dkqs3zxfptcmqt7lrssjjxg8aa389y5xyuwj3swtmgve

cardano-cli query utxo --address addr_test1wz6g45e97dkqs3zxfptcmqt7lrssjjxg8aa389y5xyuwj3swtmgve --testnet-magic 1097911063
                           TxHash                                 TxIx        Amount
--------------------------------------------------------------------------------------
4038e93e4cacbb98bfb8f7eaa5ca3cc0faf8630a432fbcad008b57065576f90c     1        2000000 lovelace + 1 06fa00b5ae593280e3d3a6693688523c85af4ff3033ddc8794ae311a.Horrocube00028 + TxOutDatumHash ScriptDataInAlonzoEra "a353f8598db438a37a4e9dd95587d93538b636e9f40b2b92e9537a4b37fd6731"

And this is the address of the wallet with the two minted tokens:

addr_test1vpfvmwfl8eucm8rnsej9pehzh7628k53raczagz4uvzzm2csx7sfl

cardano-cli query utxo --address addr_test1vpfvmwfl8eucm8rnsej9pehzh7628k53raczagz4uvzzm2csx7sfl --testnet-magic 1097911063
                           TxHash                                 TxIx        Amount
--------------------------------------------------------------------------------------
4038e93e4cacbb98bfb8f7eaa5ca3cc0faf8630a432fbcad008b57065576f90c     2        1413762 lovelace + 1 8bc230df616dedc8f35f61998a76c22bc516817d531c22d8b5025653.Horrocube00279c00000001 + TxOutDatumNo
ne
58407e484b6be51f6a4e7d5d8b3a792ece4345858ea90205789f1a9f5eac4c6a     0        4605885 lovelace + TxOutDatumNone
88391920ceb140791dc2da20d6b6aaed1ab78dd1c0e5cab255a48a9291aff558     0        207270848 lovelace + TxOutDatumNone
88391920ceb140791dc2da20d6b6aaed1ab78dd1c0e5cab255a48a9291aff558     2        1413762 lovelace + 1 8bc230df616dedc8f35f61998a76c22bc516817d531c22d8b5025653.Horrocube00279c00000002 + TxOutDatumNo
ne
b5aaf5fd5fa4da9ddac035620ae380ee0e5a6fa25eb6fb16685644eaa4d3f59e     0        9826887 lovelace + TxOutDatumNone
dc5a8bff48a9db6436559945ceb7a3f311045596407021312c133b1db70d7419     0        18659890 lovelace + TxOutDatumNone
f5d6b061ca29f9fcc8f0d7bac3b5eb8562568aeadb3cf797bfb8d33e64e1cc5a     0        110050044 lovelace + TxOutDatumNone

I think I would use this approach with a small change, right now the only inconvenient part of using this method is that it cant be used concurrently, as I must wait for the transaction to be added into a block so I can spend the new eUTXO, if I try to mint to fast I will end trying to double-spend the same output, to fix this what I was thinking was the following:

Instead of creating only one identity token, I create ten, using the same NFT factory but changing the value from 1 to 10, and lock all of them in different outputs with different starting values and using the limit to avoid overlapping, for example:

eUTXO 1: starting value 0, limit 1000
eUTXO 2: starting value 1000, limit 2000
eUTXO 3: starting value 2000, limit 3000

and so on (the validation on the script for the limit is not inclusive for the upper bound). this way I should always have available counters to use.

I would like to hear your opinion (@bwbush & @DinoDude) about the implementation, do you guys think it is safe enough?, I can’t see any flaw in the concept or the implementation.

If so, I will deploy it to the mainnet ASAP as my policy count keeps increasing rapidly.

PD: BTW @bwbush how did you manage to use ‘@’ in your token name? The CLI doesn’t allow me to use anything other than alphanumeric characters, I wanted to copy your convention xD.

3 Likes

Just wanted to give you guys an update, I already deployed this in the mainnet for a couple of weeks now :slight_smile: (200+ tokens minted).

You can see the tokens minted with this policy here:

This is one of the minting transactions, you can see here the interaction between the scripts:

This is the script address where the tokens for the counter script live:
addr1wye3sfyn0h2yyh65dyzcqkrjnxtl3lk6qgv9g2n6aa3pg0qfjmdan

I created 10 tokens for 10 eUTXO.

I also made a video explaining how everything works:

And this is the repository with the scripts:

cheers!

3 Likes

I was submitting from custom Haskell code, not using the CLI, so there wasn’t a restriction on using the @ character. Some token-explorer websites have trouble displaying tokens with non-alphanumeric characters, so you might want to test on those.

2 Likes

@AngelCastilloB you may be able to escape characters but the easiest option would be to base16 encode the asset name.

The @ character is 0x40 in ASCII …

So HC@0001 would be 48434030303031

1 Like

Hi there, it is a great thread to read through. One thought, is this implementation possible on StateMachine? Seem’s it is more intuitive to do it there. I am now trying this method, but still in troubleshooting phrase. Looking for any thought on this. Appreciate!