Skip to content

Commit

Permalink
Merge #2900
Browse files Browse the repository at this point in the history
2900: Direct CLI plutus testing r=newhoggy a=newhoggy

This introduces a test Plutus transaction test which does not use an intervening test.  Doing this directly from Haskell allows us more flexibility to make assertions.

Co-authored-by: John Ky <john.ky@iohk.io>
Co-authored-by: Jordan Millar <jordan.millar@iohk.io>
  • Loading branch information
3 people committed Jul 12, 2021
2 parents 0238f9d + 1e577e8 commit f3ef4ed
Show file tree
Hide file tree
Showing 9 changed files with 281 additions and 15 deletions.
7 changes: 5 additions & 2 deletions cabal.project
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ package cardano-node
package cardano-node-chairman
tests: True

package cardano-testnet
tests: True

-- The following is needed because Nix is doing something crazy.
package byron-spec-ledger
tests: False
Expand Down Expand Up @@ -107,8 +110,8 @@ package cardano-ledger-alonzo-test
source-repository-package
type: git
location: https://github.com/input-output-hk/hedgehog-extras
tag: 2f28e62f1508f07bb628963ee9bb23dc19ec0e03
--sha256: 0sjlk19v0dg63v8dawg844y3wzm1nmq0qxpvhip81x5yi091jjmm
tag: f7adf144bbd6da1aae9a668f40a1f8fdf69c9135
--sha256: 0f411zwlraqrp3f68vmif80kqdw18ddr0lqc9bfq83mv277gcb4k

source-repository-package
type: git
Expand Down
8 changes: 6 additions & 2 deletions cardano-testnet/cardano-testnet.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -95,15 +95,19 @@ test-suite cardano-testnet-tests

type: exitcode-stdio-1.0

build-depends: cardano-testnet
build-depends: aeson
, cardano-testnet
, directory
, hedgehog
, hedgehog-extras
, filepath
, tasty
, tasty-hedgehog
, text
, unordered-containers

other-modules: Spec.Plutus
other-modules: Spec.Plutus.Direct.TxInLockingPlutus
Spec.Plutus.Script.TxInLockingPlutus

ghc-options: -threaded -rtsopts -with-rtsopts=-N -with-rtsopts=-T

Expand Down
10 changes: 10 additions & 0 deletions cardano-testnet/src/Test/Process.hs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
module Test.Process
( execCli
, execCli'
, procCli
, procNode
, procChairman
Expand All @@ -11,6 +12,7 @@ import Data.Function
import Data.String
import GHC.Stack (HasCallStack)
import Hedgehog (MonadTest)
import Hedgehog.Extras.Test.Process (ExecConfig)
import System.Process (CreateProcess)

import qualified GHC.Stack as GHC
Expand All @@ -23,6 +25,14 @@ execCli
-> m String
execCli = GHC.withFrozenCallStack $ H.execFlex "cardano-cli" "CARDANO_CLI"

-- | Run cardano-cli, returning the stdout
execCli'
:: (MonadTest m, MonadCatch m, MonadIO m, HasCallStack)
=> ExecConfig
-> [String]
-> m String
execCli' execConfig = GHC.withFrozenCallStack $ H.execFlex' execConfig "cardano-cli" "CARDANO_CLI"

-- | Create a 'CreateProcess' describing how to start the cardano-cli process
-- and an argument list.
procCli
Expand Down
2 changes: 1 addition & 1 deletion cardano-testnet/src/Testnet/Conf.hs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,6 @@ mkConf tempAbsPath maybeMagic = do
tempRelPath <- H.noteShow $ FP.makeRelative tempBaseAbsPath tempAbsPath
base <- H.noteShowM H.getProjectBase
socketDir <- H.noteShow $ tempRelPath </> "socket"
logDir <- H.noteTempFile tempAbsPath "/logs"
logDir <- H.noteTempFile tempAbsPath "logs"

return $ Conf {..}
6 changes: 4 additions & 2 deletions cardano-testnet/test/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ import qualified Test.Tasty as T
import qualified Test.Tasty.Ingredients as T
import qualified Test.Tasty.Hedgehog as H

import qualified Spec.Plutus
import qualified Spec.Plutus.Direct.TxInLockingPlutus
import qualified Spec.Plutus.Script.TxInLockingPlutus

tests :: IO T.TestTree
tests = do
pure $ T.testGroup "test/Spec.hs"
[ T.testGroup "Spec"
[ H.testProperty "Plutus" Spec.Plutus.hprop_plutus
[ H.testProperty "Spec.Plutus.Direct.TxInLockingPlutus" Spec.Plutus.Direct.TxInLockingPlutus.hprop_plutus
, H.testProperty "Spec.Plutus.Script.TxInLockingPlutus" Spec.Plutus.Script.TxInLockingPlutus.hprop_plutus
]
]

Expand Down
234 changes: 234 additions & 0 deletions cardano-testnet/test/Spec/Plutus/Direct/TxInLockingPlutus.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
{-# LANGUAGE DisambiguateRecordFields #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

module Spec.Plutus.Direct.TxInLockingPlutus
( hprop_plutus
) where

import Control.Applicative
import Control.Monad
import Data.Aeson (FromJSON(..), Value, (.:))
import Data.Bool (not)
import Data.Eq
import Data.Function
import Data.Functor ((<&>))
import Data.HashMap.Lazy (HashMap)
import Data.Int
import Data.List ((!!))
import Data.Maybe
import Data.Monoid (Last(..), (<>))
import Data.Text (Text)
import GHC.Real
import GHC.Num
import Hedgehog (Property, (===))
import Prelude (head)
import System.FilePath ((</>))
import Text.Show (Show(..))

import qualified Data.Aeson as J
import qualified Data.List as L
import qualified Data.HashMap.Lazy as HM
import qualified Data.Text as T
import qualified Hedgehog.Extras.Stock.IO.Network.Sprocket as IO
import qualified Hedgehog.Extras.Test.Base as H
import qualified Hedgehog.Extras.Test.Concurrent as H
import qualified Hedgehog.Extras.Test.File as H
import qualified Hedgehog.Extras.Test.Process as H
import qualified System.Directory as IO
import qualified Test.Base as H
import qualified Test.Process as H
import qualified Testnet.Cardano as H
import qualified Testnet.Conf as H

{- HLINT ignore "Redundant <&>" -}
{- HLINT ignore "Redundant return" -}
{- HLINT ignore "Use let" -}

data Utxo = Utxo
{ address :: Text
, value :: HashMap Text Integer
} deriving (Eq, Show)

instance FromJSON Utxo where
parseJSON = J.withObject "Utxo" $ \v -> Utxo
<$> v .: "address"
<*> v .: "value"

hprop_plutus :: Property
hprop_plutus = H.integration . H.runFinallies . H.workspace "chairman" $ \tempAbsBasePath' -> do
projectBase <- H.note =<< H.noteIO . IO.canonicalizePath =<< H.getProjectBase
conf@H.Conf { H.tempBaseAbsPath, H.tempAbsPath } <- H.noteShowM $ H.mkConf tempAbsBasePath' Nothing

H.TestnetRuntime { H.bftSprockets, H.testnetMagic } <- H.testnet H.defaultTestnetOptions conf

execConfig <- H.noteShow H.ExecConfig
{ H.execConfigEnv = Last $ Just
[ ("CARDANO_NODE_SOCKET_PATH", IO.sprocketArgumentName (head bftSprockets))
]
, H.execConfigCwd = Last $ Just tempBaseAbsPath
}

base <- H.note projectBase
work <- H.note tempAbsPath
utxoVKeyFile <- H.note $ tempAbsPath </> "shelley/utxo-keys/utxo1.vkey"
utxoSKeyFile <- H.note $ tempAbsPath </> "shelley/utxo-keys/utxo1.skey"

plutusScriptFileInUse <- H.note $ base </> "scripts/plutus/scripts/always-succeeds-spending.plutus"

-- This datum hash is the hash of the untyped 42
let scriptDatumHash = "9e1199a988ba72ffd6e9c269cadb3b53b5f360ff99f112d9b2ee30c4d74ad88b"
let plutusRequiredSpace = id @Integer 70000000
let plutusRequiredTime = id @Integer 70000000

datumFile <- H.note $ base </> "scripts/plutus/data/42.datum"
redeemerFile <- H.note $ base </> "scripts/plutus/data/42.redeemer"

-- Always succeeds Plutus script in use. Any datum and redeemer combination will succeed.
-- Script at: $plutusscriptinuse

-- Step 1: Create a tx ouput with a datum hash at the script address. In order for a tx ouput to be locked
-- by a plutus script, it must have a datahash. We also need collateral tx inputs so we split the utxo
-- in order to accomodate this.

plutusScriptAddr <- H.execCli
[ "address", "build"
, "--payment-script-file", plutusScriptFileInUse
, "--testnet-magic", show @Int testnetMagic
]

utxoAddr <- H.execCli
[ "address", "build"
, "--testnet-magic", show @Int testnetMagic
, "--payment-verification-key-file", utxoVKeyFile
]

void $ H.execCli' execConfig
[ "query", "utxo"
, "--address", utxoAddr
, "--cardano-mode"
, "--testnet-magic", show @Int testnetMagic
, "--out-file", work </> "utxo-1.json"
]

H.cat $ work </> "utxo-1.json"

utxo1Json <- H.leftFailM . H.readJsonFile $ work </> "utxo-1.json"
utxo1 <- H.noteShowM $ H.jsonErrorFail $ J.fromJSON @(HashMap Text Utxo) utxo1Json
txin <- H.noteShow $ head $ HM.keys utxo1
lovelaceAtTxin <- H.nothingFailM . H.noteShow $ utxo1 & HM.lookup txin <&> value >>= HM.lookup "lovelace"
lovelaceAtTxinDiv2 <- H.noteShow $ lovelaceAtTxin `div` 2

void $ H.execCli
[ "transaction", "build-raw"
, "--alonzo-era"
, "--fee", "0"
, "--tx-in", T.unpack txin
, "--tx-out", plutusScriptAddr <> "+" <> show @Integer lovelaceAtTxinDiv2
, "--tx-out-datum-hash", scriptDatumHash
, "--tx-out", utxoAddr <> "+" <> show @Integer lovelaceAtTxinDiv2
, "--out-file", work </> "create-datum-output.body"
]

void $ H.execCli
[ "transaction", "sign"
, "--tx-body-file", work </> "create-datum-output.body"
, "--testnet-magic", show @Int testnetMagic
, "--signing-key-file", utxoSKeyFile
, "--out-file", work </> "create-datum-output.tx"
]

void $ H.execCli' execConfig
[ "transaction", "submit"
, "--tx-file", work </> "create-datum-output.tx"
, "--testnet-magic", show @Int testnetMagic
]

H.threadDelay 5000000

-- With the tx ouput at the script address we can now attempt to spend it.

void $ H.execCli' execConfig
[ "query", "utxo"
, "--address", plutusScriptAddr
, "--testnet-magic", show @Int testnetMagic
, "--out-file", work </> "plutusutxo.json"
]

H.cat $ work </> "plutusutxo.json"

plutusUtxoJson <- H.leftFailM . H.readJsonFile $ work </> "plutusutxo.json"
plutusUtxo <- H.noteShowM $ H.jsonErrorFail $ J.fromJSON @(HashMap Text Utxo) plutusUtxoJson
plutusUtxoTxIn <- H.noteShow $ head $ HM.keys plutusUtxo

void $ H.execCli' execConfig
[ "query", "utxo"
, "--address", utxoAddr
, "--cardano-mode"
, "--testnet-magic", show @Int testnetMagic
, "--out-file", work </> "utxo-2.json"
]

H.cat $ work </> "utxo-2.json"

utxo2Json :: Value <- H.leftFailM $ H.readJsonFile $ work </> "utxo-2.json"
utxo2 <- H.noteShowM $ H.jsonErrorFail $ J.fromJSON @(HashMap Text Utxo) utxo2Json
txinCollateral <- H.noteShow $ head $ HM.keys utxo2

void $ H.execCli' execConfig
[ "query", "protocol-parameters"
, "--testnet-magic", show @Int testnetMagic
, "--out-file", work </> "pparams.json"
]

let dummyaddress = "addr_test1vpqgspvmh6m2m5pwangvdg499srfzre2dd96qq57nlnw6yctpasy4"

lovelaceAtplutusScriptAddr <- H.nothingFailM . H.noteShow $ plutusUtxo & HM.lookup plutusUtxoTxIn <&> value >>= HM.lookup "lovelace"

txFee <- H.noteShow $ plutusRequiredTime + plutusRequiredSpace
spendable <- H.noteShow $ lovelaceAtplutusScriptAddr - plutusRequiredTime - plutusRequiredSpace

void $ H.execCli
[ "transaction", "build-raw"
, "--alonzo-era"
, "--fee", show @Integer txFee
, "--tx-in", T.unpack plutusUtxoTxIn
, "--tx-in-collateral", T.unpack txinCollateral
, "--tx-out", dummyaddress <> "+" <> show @Integer spendable
, "--tx-in-script-file", plutusScriptFileInUse
, "--tx-in-datum-file", datumFile
, "--protocol-params-file", work </> "pparams.json"
, "--tx-in-redeemer-file", redeemerFile
, "--tx-in-execution-units", show @(Integer, Integer) (plutusRequiredTime, plutusRequiredSpace)
, "--out-file", work </> "test-alonzo.body"
]

void $ H.execCli
[ "transaction", "sign"
, "--tx-body-file", work </> "test-alonzo.body"
, "--testnet-magic", show @Int testnetMagic
, "--signing-key-file", utxoSKeyFile
, "--out-file", work </> "alonzo.tx"
]

void $ H.execCli' execConfig
[ "transaction", "submit"
, "--tx-file", work </> "alonzo.tx"
, "--testnet-magic", show @Int testnetMagic
]

H.threadDelay 5000000

-- Querying UTxO at $dummyaddress. If there is ADA at the address the Plutus script successfully executed!

result <- T.pack <$> H.execCli' execConfig
[ "query", "utxo"
, "--address", dummyaddress
, "--testnet-magic", show @Int testnetMagic
]

H.noteShow_ result

L.filter (not . T.null) (T.splitOn " " (T.lines result !! 2)) !! 2 === "360000000"
Loading

0 comments on commit f3ef4ed

Please sign in to comment.