Introduction: Understanding Plutus
Plutus is Cardano’s smart contract platform that enables developers to write applications that interact with the Cardano blockchain. Built on Haskell, Plutus offers a secure, functional programming approach to blockchain development.
Key Components:
- Plutus Core: The on-chain language that executes on the Cardano blockchain
- Plutus Tx: The compiler that translates Haskell code to Plutus Core
- Plutus Application Framework (PAF): Libraries for building DApps
- Extended UTXO Model (eUTXO): Cardano’s transaction model
Core Concepts
Extended UTXO Model
Unlike Ethereum’s account-based model, Cardano uses an extended UTXO model that combines Bitcoin’s UTXO approach with the ability to carry script state.
Key Differences:
- Deterministic transaction validation
- Increased parallelism
- Local state validation
- Explicit state management
Datum and Redeemer
Concept | Description | Use Case |
---|---|---|
Datum | Data attached to a UTXO | Script state, token metadata |
Redeemer | Input provided when spending a UTXO | Transaction parameters, action choice |
Context | Information about the transaction | Validation logic, constraints |
Validator | Script that controls spending conditions | Smart contract logic |
Script Address Types
- Minting Policies: Control token creation (Native assets)
- Spending Validators: Control UTXO spending
- Staking Validators: Control stake delegation and rewards
Plutus Programming Basics
Common Imports
import Plutus.V1.Ledger.Api
import Plutus.V1.Ledger.Contexts
import Plutus.V2.Ledger.Api
import Plutus.V2.Ledger.Contexts
import PlutusTx.Prelude
import qualified PlutusTx
import Ledger hiding (singleton)
import Ledger.Typed.Scripts as Scripts
import Ledger.Value
PlutusTx.Prelude Functions
Function | Description | Example |
---|---|---|
traceIfFalse | Log message if condition is false | traceIfFalse "Invalid amount" (amount > minAmount) |
traceError | Log error message and fail | traceError "Transaction must include deadline" |
emptyByteString | Create empty ByteString | emptyByteString |
appendByteString | Append ByteStrings | appendByteString bs1 bs2 |
Basic Validator Template
{-# INLINABLE mkValidator #-}
mkValidator :: Datum -> Redeemer -> ScriptContext -> Bool
mkValidator datum redeemer ctx =
traceIfFalse "Validation failed" condition
where
txInfo :: TxInfo
txInfo = scriptContextTxInfo ctx
condition :: Bool
condition = True -- Your validation logic here
validator :: Scripts.Validator
validator = mkValidatorScript $$(PlutusTx.compile [|| mkValidator ||])
valHash :: ValidatorHash
valHash = Scripts.validatorHash validator
scrAddress :: Address
scrAddress = scriptAddress validator
Datum and Redeemer Types
data MyDatum = MyDatum
{ beneficiary :: PubKeyHash
, amount :: Integer
, deadline :: POSIXTime
}
data MyRedeemer = Spend | Cancel
PlutusTx.makeIsDataIndexed ''MyDatum [('MyDatum, 0)]
PlutusTx.makeIsDataIndexed ''MyRedeemer [('Spend, 0), ('Cancel, 1)]
Common Validation Patterns
Check Transaction Signer
{-# INLINABLE signedByOwner #-}
signedByOwner :: PubKeyHash -> TxInfo -> Bool
signedByOwner pkh info = pkh `elem` txInfoSignatories info
Check Time Range
{-# INLINABLE checkDeadline #-}
checkDeadline :: POSIXTime -> ScriptContext -> Bool
checkDeadline deadline ctx =
traceIfFalse "Deadline not reached" $ from deadline `contains` txInfoValidRange (scriptContextTxInfo ctx)
Check Token Transfer
{-# INLINABLE checkTokenTransfer #-}
checkTokenTransfer :: CurrencySymbol -> TokenName -> Integer -> PubKeyHash -> TxInfo -> Bool
checkTokenTransfer cs tn amt receiver info =
traceIfFalse "Token transfer failed" $
valueOf (valuePaidTo info receiver) cs tn >= amt
Check Datum Update
{-# INLINABLE checkDatumUpdate #-}
checkDatumUpdate :: ScriptContext -> (Datum -> Datum -> Bool) -> Bool
checkDatumUpdate ctx checkFn = case findOwnInput ctx of
Nothing -> traceError "Input not found"
Just input ->
let
inputDatum = getDatum $ txOutDatumHash $ txInInfoResolved input
outputDatum = getOutputDatum $ getContinuingOutput ctx
in
traceIfFalse "Invalid datum update" $ checkFn inputDatum outputDatum
Plutus Contract Monad
Contract Endpoints
type MySchema =
Endpoint "give" GiveParams
.\/ Endpoint "grab" GrabParams
give :: AsContractError e => GiveParams -> Contract w s e ()
give params = do
pkh <- pubKeyHash <$> ownPubKey
let datum = MyDatum { ... }
tx = mustPayToTheScript datum $ Ada.lovelaceValueOf amount
ledgerTx <- submitTxConstraints typedValidator tx
void $ awaitTxConfirmed $ txId ledgerTx
logInfo @String $ "Created script output"
grab :: AsContractError e => GrabParams -> Contract w s e ()
grab params = do
utxos <- utxosAt scrAddress
-- Build and submit transaction to consume UTXOs
Error Handling
handleError :: Text -> Contract w s Text a -> Contract w s Text a
handleError msg action = catchError action $ \err -> do
logError $ msg <> ": " <> show err
throwError $ msg <> ": " <> show err
Minting Policies
Basic Minting Policy
{-# INLINABLE mkPolicy #-}
mkPolicy :: PubKeyHash -> () -> ScriptContext -> Bool
mkPolicy pkh _ ctx =
traceIfFalse "Not signed by token issuer" $
txSignedBy (scriptContextTxInfo ctx) pkh
policy :: PubKeyHash -> Scripts.MintingPolicy
policy pkh = mkMintingPolicyScript $
$$(PlutusTx.compile [|| Scripts.wrapMintingPolicy . mkPolicy ||])
`PlutusTx.applyCode`
PlutusTx.liftCode pkh
curSymbol :: PubKeyHash -> CurrencySymbol
curSymbol = scriptCurrencySymbol . policy
NFT Minting Policy
{-# INLINABLE mkNFTPolicy #-}
mkNFTPolicy :: TxOutRef -> TokenName -> () -> ScriptContext -> Bool
mkNFTPolicy oref tn () ctx =
traceIfFalse "UTxO not consumed" hasUTxO &&
traceIfFalse "Wrong amount minted" checkMintedAmount
where
info :: TxInfo
info = scriptContextTxInfo ctx
hasUTxO :: Bool
hasUTxO = any (\i -> txInInfoOutRef i == oref) $ txInfoInputs info
checkMintedAmount :: Bool
checkMintedAmount = case flattenValue (txInfoMint info) of
[(cs, tn', amt)] -> cs == ownCurrencySymbol ctx && tn' == tn && amt == 1
_ -> False
Plutus Application Backend (PAB) Integration
PAB Contract Setup
endpoints :: Contract () MySchema Text ()
endpoints = selectList [give, grab] >> endpoints
mkSchemaDefinitions ''MySchema
myContract :: AsContractDefinition (Last MyContractState)
myContract = ContractDefinition
{ contractBehavior = endpoints
, contractStartEnd = ISZ $ Last initialState
}
$(mkKnownCurrencies [])
Testing with EmulatorTrace
Basic Emulator Test
testScenario :: EmulatorTrace ()
testScenario = do
h1 <- activateContractWallet (Wallet 1) myContract
h2 <- activateContractWallet (Wallet 2) myContract
callEndpoint @"give" h1 GiveParams{...}
void $ Emulator.waitNSlots 1
callEndpoint @"grab" h2 GrabParams{...}
void $ Emulator.waitNSlots 1
test :: IO ()
test = runEmulatorTraceIO testScenario
Testing with Property Tests
prop_ValidDatum :: MyDatum -> Property
prop_ValidDatum datum =
let encoded = fromBuiltinData (toBuiltinData datum)
in case encoded of
Nothing -> property False
Just d' -> datum === d'
tests :: TestTree
tests = testGroup "MyContract"
[ testProperty "datum roundtrip" prop_ValidDatum
, testEmulator "successful give and grab" testScenario
]
Common Plutus Types
Type | Description | Example |
---|---|---|
PubKeyHash | Hash of a public key | "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" |
ValidatorHash | Hash of a validator script | Scripts.validatorHash validator |
Address | Script or pubkey address | scriptAddress validator |
Value | Tokens with amounts | Ada.lovelaceValueOf 5000000 <> assetClassValue token 1 |
AssetClass | Currency symbol + token name | AssetClass (curSymbol, tokenName) |
POSIXTime | Time in milliseconds | POSIXTime 1643067600000 |
Interval | Time range | interval (POSIXTime 1000) (POSIXTime 2000) |
Datum | Data attached to UTxO | Datum $ toBuiltinData myDatum |
Redeemer | Input to validator | Redeemer $ toBuiltinData myRedeemer |
Common Plutus V2 Features
Feature | Description | Usage |
---|---|---|
Reference Inputs | UTxOs that are read but not consumed | findReferenceInput |
Inline Datums | Datums stored directly in UTxOs | OutputDatum (OutputDatumHash dh) vs OutputDatum (OutputInlineDatum d) |
Reference Scripts | Reusable validator scripts | lookupReferenceScript |
Plutus V2 Contexts | Enhanced transaction context | import Plutus.V2.Ledger.Contexts |
Cardano CLI Commands for Plutus
Compile Plutus Script
cabal run plutus-example -- script --out-file my-validator.plutus
Create Datum/Redeemer Files
# Create datum JSON file
echo '{"constructor":0,"fields":[{"bytes":"deadbeef..."}]}' > datum.json
# Hash the datum
cardano-cli transaction hash-script-data --script-data-file datum.json
Transaction with Plutus Script
# Build transaction with Plutus script
cardano-cli transaction build \
--alonzo-era \
--testnet-magic 1 \
--tx-in "txhash#txix" \
--tx-in-script-file my-validator.plutus \
--tx-in-datum-file datum.json \
--tx-in-redeemer-file redeemer.json \
--tx-in-collateral "txhash#txix" \
--tx-out "addr+lovelace" \
--change-address addr \
--protocol-params-file pparams.json \
--out-file tx.body
# Sign transaction
cardano-cli transaction sign \
--tx-body-file tx.body \
--signing-key-file payment.skey \
--out-file tx.signed
# Submit transaction
cardano-cli transaction submit \
--testnet-magic 1 \
--tx-file tx.signed
Common Errors & Solutions
Error | Possible Cause | Solution |
---|---|---|
"ExUnitsError" | Execution units exceeded | Simplify validator logic or increase protocol parameters |
"InputsExhaustedError" | UTxO not found | Ensure UTxO exists and is unspent |
"ValidationError [... ] failed" | Validation failed | Check validator logic and ensure conditions are met |
"OutsideValidityIntervalError" | Transaction outside validity range | Set appropriate validity range or check POSIXTime calculations |
"MissingDatumHashError" | Missing datum | Ensure datum is provided with correct hash |
"NonOutputSupplimentaryDatums" | Invalid datum usage | Check datum references and usage in transaction |
Development Environment Setup
Project Template
# Clone Plutus Apps repository
git clone https://github.com/input-output-hk/plutus-apps.git
# Enter nix shell
cd plutus-apps
nix-shell
# Create new project using template
cookiecutter https://github.com/input-output-hk/plutus-starter
Build and Run
# Build project
cabal build
# Run tests
cabal test
# Run PAB
cabal run plutus-pab
Resources for Further Learning
Official Documentation
Community Resources
Tools
- Cardano Serialization Library
- Cardanoscan Explorer
- Blockfrost API
- Nami Wallet (for dApp integration)
This cheatsheet provides a comprehensive overview of Cardano Plutus development. For specific use cases and examples, refer to the official documentation and community resources listed above.