diff --git a/demo/README.md b/demo/README.md index 10875ed9100..5195c9bcfc9 100644 --- a/demo/README.md +++ b/demo/README.md @@ -121,24 +121,18 @@ In the current stage of development, Hydra nodes need a specially crafted set of ## With Docker -The included script `seed-devnet.sh` uses the `cardano-cli` in the already running `cardano-node` container to give Alice, Bob and Carol some "fuel" UTXO while the change can be used to commit to the Head (this is why amounts vary in the script). +The included script `seed-devnet.sh` uses the `cardano-cli` in the already running `cardano-node` container to give Alice, Bob and Carol some UTXO to commit and some "fuel" UTXO. NOTE: There is nothing special about those transactions so one could any other Cardano client to create those transactions. This transaction must match the following characteristics: -* It must pay all its outputs to the key that's used by the Hydra Node's internal wallet, as defined by argument `--cardano-signing-key` of `hydra-node` executable, -* One of the outputs of the transaction must include datum hash `a654fb60d21c1fed48db2c320aa6df9737ec0204c0ba53b9b94a09fb40e757f3`. +* It must pay outputs to commit to the key that's used by the Hydra Node's internal wallet, as defined by argument `--cardano-signing-key` of `hydra-node` executable, +* One of the outputs must include datum hash `a654fb60d21c1fed48db2c320aa6df9737ec0204c0ba53b9b94a09fb40e757f3` as this is our "fuel" marker. ## Without Docker -To seed the network with those UTXO, posting a transaction, one can use the `seed-network` executable: +You can use the `seed-devnet.sh` script by passing it the path to a `cardano-cli` executable to use instead of having it using the docker container, e.g. -For example, to ensure Alice can commit some 1000 Ada and also that "her" node can pay for the Hydra transactions, run: - -``` -sudo chmod a+w devnet/ipc/node.socket -cabal run seed-network -- \ - --node-socket devnet/ipc/node.socket \ - --cardano-signing-key devnet/credentials/alice.sk \ - --commit-amount 1000000000 +``` sh +./seed-devnet.sh $(which cardano-cli) ``` # Running clients diff --git a/demo/seed-devnet.sh b/demo/seed-devnet.sh index 2342a4c5e9b..239722e6077 100755 --- a/demo/seed-devnet.sh +++ b/demo/seed-devnet.sh @@ -1,42 +1,64 @@ #!/usr/bin/env bash -# Seed a "devnet" by marking some ADA as "payment outputs" for the Hydra Head -# ("the fuel tank"). +# Seed a "devnet" by distributing some Ada to commit and also some marked as +# "fuel" for the Hydra Head. set -e -TESTNET_MAGIC=42 MARKER_DATUM_HASH="a654fb60d21c1fed48db2c320aa6df9737ec0204c0ba53b9b94a09fb40e757f3" -DEVNET_FUNDS=900000000000 -STANDARD_FEE=167393 +SCRIPT_DIR=$(realpath $(dirname $(realpath $0))) + +CCLI_PATH= DEVNET_DIR=/data +if [[ -n ${1} ]] && $(${1} version > /dev/null); then + CCLI_PATH=${1} + echo >&2 "Using provided cardano-cli" + DEVNET_DIR=${SCRIPT_DIR}/devnet +fi +# Invoke cardano-cli in running cardano-node container or via provided cardano-cli function ccli() { - # Invoke cardano-cli in running cardano-node container - docker-compose exec cardano-node cardano-cli ${@} --testnet-magic 42 + if [[ -x ${CCLI_PATH} ]]; then + cardano-cli ${@} --testnet-magic 42 + else + docker-compose exec cardano-node cardano-cli ${@} --testnet-magic 42 + fi } -function seedCommit() { +# Retrieve some lovelace from faucet, marked as "fuel" if requested +function seedFaucet() { ACTOR=${1} - COMMIT_AMOUNT=${2} - FUEL_AMOUNT=$((${DEVNET_FUNDS}-${COMMIT_AMOUNT}-${STANDARD_FEE})) - echo >&2 "Seeding a commit UTXO for ${ACTOR} with ${COMMIT_AMOUNT}Ł (${FUEL_AMOUNT}Ł of fuel)" + AMOUNT=${2} + MARKED=${3:-"normal"} + echo >&2 "Seeding a UTXO from faucet to ${ACTOR} with ${AMOUNT}Ł (${MARKED})" + + # Determine faucet address and just the **first** txin addressed to it + FAUCET_ADDR=$(ccli address build --payment-verification-key-file ${DEVNET_DIR}/credentials/faucet.vk) + FAUCET_TXIN=$(ccli query utxo --address ${FAUCET_ADDR} --out-file /dev/stdout | jq -r 'keys[0]') + + ACTOR_ADDR=$(ccli address build --payment-verification-key-file ${DEVNET_DIR}/credentials/${ACTOR}.vk) - ADDR=$(ccli address build --payment-verification-key-file ${DEVNET_DIR}/credentials/${ACTOR}.vk) - GENESIS_TXIN=$(ccli genesis initial-txin --verification-key-file ${DEVNET_DIR}/credentials/${ACTOR}-genesis.vk | tr -d '\n\r') + # Optionally mark output + MARKER="" + if [[ "${MARKED}" == "fuel" ]]; then + MARKER="--tx-out-datum-hash ${MARKER_DATUM_HASH}" + fi ccli transaction build --alonzo-era --cardano-mode \ - --change-address ${ADDR} \ - --tx-in ${GENESIS_TXIN} \ - --tx-out ${ADDR}+${FUEL_AMOUNT} \ - --tx-out-datum-hash ${MARKER_DATUM_HASH} \ - --out-file ${DEVNET_DIR}/${ACTOR}.draft + --change-address ${FAUCET_ADDR} \ + --tx-in ${FAUCET_TXIN} \ + --tx-out ${ACTOR_ADDR}+${AMOUNT} \ + ${MARKER} \ + --out-file ${DEVNET_DIR}/seed-${ACTOR}.draft ccli transaction sign \ - --tx-body-file ${DEVNET_DIR}/${ACTOR}.draft \ - --signing-key-file ${DEVNET_DIR}/credentials/${ACTOR}.sk \ - --out-file ${DEVNET_DIR}/${ACTOR}.signed - ccli transaction submit --tx-file ${DEVNET_DIR}/${ACTOR}.signed + --tx-body-file ${DEVNET_DIR}/seed-${ACTOR}.draft \ + --signing-key-file ${DEVNET_DIR}/credentials/faucet.sk \ + --out-file ${DEVNET_DIR}/seed-${ACTOR}.signed + ccli transaction submit --tx-file ${DEVNET_DIR}/seed-${ACTOR}.signed } -seedCommit "alice" 1000000000 -seedCommit "bob" 500000000 -seedCommit "carol" 250000000 +seedFaucet "alice" 1000000000 # 1000 Ada to commit +seedFaucet "alice" 100000000 "fuel" # 100 Ada marked as "fuel" +seedFaucet "bob" 500000000 # 500 Ada to commit +seedFaucet "bob" 100000000 "fuel" # 100 Ada marked as "fuel" +seedFaucet "carol" 250000000 # 250 Ada to commit +seedFaucet "carol" 100000000 "fuel" # 100 Ada marked as "fuel" diff --git a/hydra-cluster/bench/Bench/EndToEnd.hs b/hydra-cluster/bench/Bench/EndToEnd.hs index 5841d47867c..fa070acdd84 100644 --- a/hydra-cluster/bench/Bench/EndToEnd.hs +++ b/hydra-cluster/bench/Bench/EndToEnd.hs @@ -7,18 +7,14 @@ module Bench.EndToEnd where import Hydra.Prelude import Test.Hydra.Prelude -import qualified Cardano.Api.UTxO as UTxO import Cardano.Crypto.DSIGN ( DSIGNAlgorithm (deriveVerKeyDSIGN), MockDSIGN, SignKeyDSIGN, VerKeyDSIGN, ) -import CardanoClient ( - submit, - waitForTransaction, - ) -import CardanoCluster (defaultNetworkId, newNodeConfig, withBFTNode) +import CardanoClient (submit, waitForTransaction) +import CardanoCluster (Marked (Fuel), defaultNetworkId, newNodeConfig, seedFromFaucet, withBFTNode) import CardanoNode (RunningNode (..)) import Control.Lens (to, (^?)) import Control.Monad.Class.MonadAsync (mapConcurrently) @@ -41,7 +37,7 @@ import Data.Set ((\\)) import qualified Data.Set as Set import Data.Time (UTCTime (UTCTime), nominalDiffTimeToSeconds, utctDayTime) import Hydra.Cardano.Api (Tx, TxId, UTxO, getVerificationKey) -import Hydra.Generator (Dataset (..)) +import Hydra.Generator (ClientDataset (..), Dataset (..)) import Hydra.Ledger (txId) import Hydra.Logging (withTracerOutputTo) import Hydra.Party (deriveParty, generateKey) @@ -88,37 +84,42 @@ data Event = Event } deriving (Generic, Eq, Show, ToJSON) -bench :: DiffTime -> FilePath -> [Dataset] -> Word64 -> Spec -bench timeoutSeconds workDir dataset clusterSize = +bench :: DiffTime -> FilePath -> Dataset -> Word64 -> Spec +bench timeoutSeconds workDir dataset@Dataset{clientDatasets} clusterSize = specify ("Load test on " <> show clusterSize <> " local nodes in " <> workDir) $ do withFile (workDir "test.log") ReadWriteMode $ \hdl -> withTracerOutputTo hdl "Test" $ \tracer -> failAfter timeoutSeconds $ do - let cardanoKeys = map (\Dataset{signingKey} -> (getVerificationKey signingKey, signingKey)) dataset + putTextLn "Starting benchmark" + let cardanoKeys = map (\ClientDataset{signingKey} -> (getVerificationKey signingKey, signingKey)) clientDatasets let hydraKeys = generateKey <$> [1 .. toInteger (length cardanoKeys)] let parties = Set.fromList (deriveParty <$> hydraKeys) config <- newNodeConfig workDir withOSStats workDir $ - withBFTNode (contramap FromCluster tracer) config (fst <$> cardanoKeys) $ \(RunningNode _ nodeSocket) -> do - withHydraCluster tracer workDir nodeSocket 1 cardanoKeys hydraKeys $ \(leader :| followers) -> do - let nodes = leader : followers - waitForNodesConnected tracer nodes + withBFTNode (contramap FromCluster tracer) config $ \node@(RunningNode _ nodeSocket) -> do + withHydraCluster tracer workDir nodeSocket 0 cardanoKeys hydraKeys $ \(leader :| followers) -> do + let clients = leader : followers + waitForNodesConnected tracer clients - initialUTxOs <- createUTxOToCommit dataset nodeSocket + putTextLn "Seeding network" + seedNetwork node dataset + putTextLn "Initializing Head" let contestationPeriod = 10 :: Natural send leader $ input "Init" ["contestationPeriod" .= contestationPeriod] - waitFor tracer (fromIntegral $ 10 * clusterSize) nodes $ + waitFor tracer (fromIntegral $ 10 * clusterSize) clients $ output "ReadyToCommit" ["parties" .= parties] - expectedUTxO <- mconcat <$> forM (zip nodes initialUTxOs) (uncurry commit) + putTextLn "Comitting initialUTxO from dataset" + expectedUTxO <- commitUTxO clients dataset - waitFor tracer (fromIntegral $ 10 * clusterSize) nodes $ + waitFor tracer (fromIntegral $ 10 * clusterSize) clients $ output "HeadIsOpen" ["utxo" .= expectedUTxO] - processedTransactions <- processTransactions nodes dataset + putTextLn "HeadIsOpen" + processedTransactions <- processTransactions clients dataset - putTextLn "Closing the Head..." + putTextLn "Closing the Head" send leader $ input "Close" [] waitMatch (fromIntegral $ 60 * clusterSize) leader $ \v -> guard (v ^? key "tag" == Just "HeadIsFinalized") @@ -218,25 +219,42 @@ movingAverage confirmations = ) in map average fiveSeconds -createUTxOToCommit :: [Dataset] -> FilePath -> IO [UTxO] -createUTxOToCommit dataset nodeSocket = - forM dataset $ \Dataset{fundingTransaction} -> do +-- | Distribute 100 ADA fuel and starting funds from faucet for each client in +-- the dataset. +seedNetwork :: RunningNode -> Dataset -> IO () +seedNetwork node@(RunningNode _ nodeSocket) Dataset{fundingTransaction, clientDatasets} = do + fundClients + forM_ clientDatasets fuelWith100Ada + where + fundClients = do submit defaultNetworkId nodeSocket fundingTransaction - UTxO.min <$> waitForTransaction defaultNetworkId nodeSocket fundingTransaction + void $ waitForTransaction defaultNetworkId nodeSocket fundingTransaction + + fuelWith100Ada ClientDataset{signingKey} = do + let vk = getVerificationKey signingKey + seedFromFaucet defaultNetworkId node vk 100_000_000 Fuel + +-- | Commit all (expected to exit) 'initialUTxO' from the dataset using the +-- (asumed same sequence) of clients. +commitUTxO :: [HydraClient] -> Dataset -> IO UTxO +commitUTxO clients Dataset{clientDatasets} = + mconcat <$> forM (zip clients clientDatasets) doCommit + where + doCommit (client, ClientDataset{initialUTxO}) = commit client initialUTxO -processTransactions :: [HydraClient] -> [Dataset] -> IO (Map.Map TxId Event) -processTransactions clients dataset = do - let processors = zip (zip dataset (cycle clients)) [1 ..] - mconcat <$> mapConcurrently (uncurry clientProcessTransactionsSequence) processors +processTransactions :: [HydraClient] -> Dataset -> IO (Map.Map TxId Event) +processTransactions clients Dataset{clientDatasets} = do + let processors = zip (zip clientDatasets (cycle clients)) [1 ..] + mconcat <$> mapConcurrently (uncurry clientProcessDataset) processors where - clientProcessTransactionsSequence (Dataset{transactionsSequence}, client) clientId = do - let numberOfTxs = length transactionsSequence + clientProcessDataset (ClientDataset{txSequence}, client) clientId = do + let numberOfTxs = length txSequence submissionQ <- newTBQueueIO (fromIntegral numberOfTxs) registry <- newRegistry withNewClient client $ \client' -> do - atomically $ forM_ transactionsSequence $ writeTBQueue submissionQ + atomically $ forM_ txSequence $ writeTBQueue submissionQ submitTxs client' registry submissionQ - `concurrently_` waitForAllConfirmations client' registry submissionQ (Set.fromList $ map txId transactionsSequence) + `concurrently_` waitForAllConfirmations client' registry submissionQ (Set.fromList $ map txId txSequence) `concurrently_` progressReport (hydraNodeId client') clientId numberOfTxs submissionQ readTVarIO (processedTxs registry) diff --git a/hydra-cluster/bench/Main.hs b/hydra-cluster/bench/Main.hs index eac4c6fdffe..bac7428db6d 100644 --- a/hydra-cluster/bench/Main.hs +++ b/hydra-cluster/bench/Main.hs @@ -112,7 +112,7 @@ main = Left err -> fail $ show err Right shelleyGenesis -> pure $ fromLedgerPParams ShelleyBasedEraShelley (sgProtocolParams shelleyGenesis) - dataset <- replicateM (fromIntegral clusterSize) (generateConstantUTxODataset pparams numberOfTxs) + dataset <- generateConstantUTxODataset pparams (fromIntegral clusterSize) numberOfTxs saveDataset benchDir dataset run timeoutSeconds benchDir dataset clusterSize diff --git a/hydra-cluster/config/credentials/alice-genesis.vk b/hydra-cluster/config/credentials/alice-genesis.vk deleted file mode 100644 index 6017b8065a5..00000000000 --- a/hydra-cluster/config/credentials/alice-genesis.vk +++ /dev/null @@ -1,5 +0,0 @@ -{ - "type": "GenesisUTxOVerificationKey_ed25519", - "description": "", - "cborHex": "5820eb94e8236e2099357fa499bfbc415968691573f25ec77435b7949f5fdfaa5da0" -} diff --git a/hydra-cluster/config/credentials/bob-genesis.vk b/hydra-cluster/config/credentials/bob-genesis.vk deleted file mode 100644 index 2585aee0420..00000000000 --- a/hydra-cluster/config/credentials/bob-genesis.vk +++ /dev/null @@ -1,5 +0,0 @@ -{ - "type": "GenesisUTxOVerificationKey_ed25519", - "description": "", - "cborHex": "5820fb1e80f6b5c0ef33d1b68215389d0ac836412a99edfac8bb203eb1d782342ab3" -} diff --git a/hydra-cluster/config/credentials/carol-genesis.vk b/hydra-cluster/config/credentials/carol-genesis.vk deleted file mode 100644 index 4074f1e15df..00000000000 --- a/hydra-cluster/config/credentials/carol-genesis.vk +++ /dev/null @@ -1,5 +0,0 @@ -{ - "type": "GenesisUTxOVerificationKey_ed25519", - "description": "Payment Verification Key", - "cborHex": "5820e48471a0e6711b566ae3607582dfa1e79dacfadaa41682673c91cec014907904" -} diff --git a/hydra-cluster/config/genesis-shelley.json b/hydra-cluster/config/genesis-shelley.json index 1eefe41f974..cd7a81255c3 100644 --- a/hydra-cluster/config/genesis-shelley.json +++ b/hydra-cluster/config/genesis-shelley.json @@ -41,9 +41,7 @@ "updateQuorum": 5, "networkId": "Testnet", "initialFunds": { - "60f8a68cd18e59a6ace848155a0e967af64f4d00cf8acee8adc95a6b0d": 900000000000, - "601052386136b347f3bb7c67fe3f2ee4ef120e1836e5d2707bb068afa6": 900000000000, - "603aaa2e3de913b0f5aa7e7f076e122d737db5329df1aa905192284fea": 900000000000 + "609783be7d3c54f11377966dfabc9284cd6c32fca1cd42ef0a4f1cc45b": 900000000000 }, "maxLovelaceSupply": 300, "networkMagic": 42, diff --git a/hydra-cluster/exe/seed-network.hs b/hydra-cluster/exe/seed-network.hs deleted file mode 100644 index 42276270549..00000000000 --- a/hydra-cluster/exe/seed-network.hs +++ /dev/null @@ -1,88 +0,0 @@ -{-# LANGUAGE TypeApplications #-} - -import CardanoClient ( - buildAddress, - postSeedPayment, - queryProtocolParameters, - queryUTxO, - ) -import CardanoCluster (asSigningKey, availableInitialFunds, defaultNetworkId) -import Hydra.Cardano.Api (Lovelace, getVerificationKey) -import Hydra.Chain.Direct.Util (readFileTextEnvelopeThrow) -import Hydra.Ledger.Cardano () -import Hydra.Prelude -import Options.Applicative ( - Parser, - ParserInfo, - auto, - execParser, - fullDesc, - header, - help, - helper, - info, - long, - metavar, - option, - progDesc, - showDefault, - strOption, - value, - ) - -data Options = Options - { cardanoNodeSocket :: FilePath - , cardanoSigningKey :: FilePath - , amountLovelace :: Lovelace - } - -seedNetworkOptionsParser :: Parser Options -seedNetworkOptionsParser = - Options - <$> strOption - ( long "node-socket" - <> metavar "FILE" - <> help "The path to the Cardano node domain socket for client communication." - <> value "node.socket" - <> showDefault - ) - <*> strOption - ( long "cardano-signing-key" - <> metavar "FILE" - <> help "The path to the signing key file used for committing UTxO. This file used the same 'Envelope' format than cardano-cli." - <> value "me.sk" - <> showDefault - ) - <*> ( fromIntegral @Integer - <$> option - auto - ( long "commit-amount" - <> metavar "LOVELACE" - <> help "The amount of Lovelaces in the generated UTXO used for committing." - ) - ) - -seedNetworkOptions :: ParserInfo Options -seedNetworkOptions = - info - (seedNetworkOptionsParser <**> helper) - ( fullDesc - <> progDesc - "Post a transaction on a fresh testnet to create seed payment and some UTXO to commit." - <> header "seed-network - Seed UTXO on demo chain" - ) - -main :: IO () -main = do - Options{cardanoNodeSocket, cardanoSigningKey, amountLovelace} <- execParser seedNetworkOptions - putStrLn $ "Querying node for Protocol Parameters at " <> cardanoNodeSocket - pparams <- queryProtocolParameters networkId cardanoNodeSocket - signingKey <- readFileTextEnvelopeThrow asSigningKey cardanoSigningKey - putStrLn $ "Posting seed payment transaction at " <> cardanoNodeSocket <> ", amount: " <> show amountLovelace <> ", key: " <> cardanoSigningKey - postSeedPayment networkId pparams initialAmount cardanoNodeSocket signingKey amountLovelace - let address = buildAddress (getVerificationKey signingKey) networkId - putStrLn $ "UTXO for address " <> show address - queryUTxO networkId cardanoNodeSocket [address] >>= putTextLn . decodeUtf8 . encodePretty - where - networkId = defaultNetworkId - initialAmount = availableInitialFunds diff --git a/hydra-cluster/hydra-cluster.cabal b/hydra-cluster/hydra-cluster.cabal index 3037ae6bd8d..43c0fdc6859 100644 --- a/hydra-cluster/hydra-cluster.cabal +++ b/hydra-cluster/hydra-cluster.cabal @@ -179,25 +179,6 @@ executable log-filter , optparse-applicative , temporary -executable seed-network - import: project-config - hs-source-dirs: exe - main-is: seed-network.hs - ghc-options: -threaded -rtsopts - build-depends: - , aeson - , base >=4.7 && <5 - , bytestring - , contra-tracer - , hydra-cardano-api - , hydra-cluster - , hydra-node - , hydra-prelude - , lens - , lens-aeson - , optparse-applicative - , temporary - test-suite integration import: project-config hs-source-dirs: test diff --git a/hydra-cluster/src/CardanoClient.hs b/hydra-cluster/src/CardanoClient.hs index 4ea38fd0e6a..1f8c63e9f5a 100644 --- a/hydra-cluster/src/CardanoClient.hs +++ b/hydra-cluster/src/CardanoClient.hs @@ -1,5 +1,3 @@ -{-# OPTIONS_GHC -Wno-deferred-out-of-scope-variables #-} - -- | A basic cardano-node client that can talk to a local cardano-node. -- -- The idea of this module is to provide a Haskell interface on top of cardano-cli's API, @@ -12,10 +10,8 @@ import Hydra.Cardano.Api import qualified Cardano.Api.UTxO as UTxO import Cardano.Slotting.Time (SystemStart) -import CardanoNode (RunningNode (RunningNode)) import qualified Data.Map as Map import qualified Data.Set as Set -import qualified Hydra.Chain.Direct.Util as Hydra import Ouroboros.Consensus.HardFork.Combinator.AcrossEras (EraMismatch) import Ouroboros.Network.Protocol.LocalTxSubmission.Client (SubmitResult (..)) @@ -76,6 +72,19 @@ queryUTxOByTxIn networkId socket inputs = ) in UTxO.fromApi <$> runQuery networkId socket query +-- | Query the whole UTxO from node. Useful for debugging, but should obviously +-- not be used in production code. +queryUTxOWhole :: NetworkId -> FilePath -> IO UTxO +queryUTxOWhole networkId socket = + let query = + QueryInEra + AlonzoEraInCardanoMode + ( QueryInShelleyBasedEra + ShelleyBasedEraAlonzo + (QueryUTxO QueryUTxOWhole) + ) + in UTxO.fromApi <$> runQuery networkId socket query + -- | Query current protocol parameters. -- -- Throws 'CardanoClientException' if query fails. @@ -327,78 +336,40 @@ waitForTransaction networkId socket tx = mkGenesisTx :: NetworkId -> ProtocolParameters -> - -- | Amount of initialFunds - Lovelace -> -- | Owner of the 'initialFund'. SigningKey PaymentKey -> - -- | Recipient of this transaction. - VerificationKey PaymentKey -> - -- |Amount to pay + -- | Amount of initialFunds Lovelace -> + -- | Recipients and amounts to pay in this transaction. + [(VerificationKey PaymentKey, Lovelace)] -> Tx -mkGenesisTx networkId pparams initialAmount signingKey verificationKey amount = - let initialInput = - genesisUTxOPseudoTxIn - networkId - (unsafeCastHash $ verificationKeyHash $ getVerificationKey signingKey) - - rawTx = case buildRaw [initialInput] [] 0 of - Left err -> error $ "Fail to build genesis transactions: " <> show err - Right tx -> tx - fee = calculateMinFee networkId rawTx Sizes{inputs = 1, outputs = 2, witnesses = 1} pparams - - changeAddr = mkVkAddress networkId (getVerificationKey signingKey) - changeOutput = - TxOut - changeAddr - (lovelaceToValue $ initialAmount - amount - fee) - (TxOutDatumHash Hydra.markerDatumHash) - - recipientAddr = mkVkAddress networkId verificationKey - recipientOutput = - TxOut - recipientAddr - (lovelaceToValue amount) - TxOutDatumNone - in case buildRaw [initialInput] [recipientOutput, changeOutput] fee of - Left err -> error $ "Fail to build genesis transations: " <> show err - Right tx -> sign signingKey tx - --- TODO: replace with 'seedFromFaucet' -generatePaymentToCommit :: - HasCallStack => - NetworkId -> - RunningNode -> - SigningKey PaymentKey -> - VerificationKey PaymentKey -> - Lovelace -> - IO UTxO -generatePaymentToCommit networkId (RunningNode _ nodeSocket) spendingSigningKey receivingVerificationKey lovelace = do - UTxO availableUTxO <- queryUTxO networkId nodeSocket [spendingAddress] - let inputs = (,Nothing) <$> Map.keys (Map.filter (not . Hydra.isMarkedOutput) availableUTxO) - build networkId nodeSocket spendingAddress inputs [] [theOutput] >>= \case - Left e -> error (show e) - Right body -> do - let tx = sign spendingSigningKey body - submit networkId nodeSocket tx - waitForPayment networkId nodeSocket lovelace receivingAddress +mkGenesisTx networkId pparams signingKey initialAmount recipients = + case buildRaw [initialInput] (recipientOutputs <> [changeOutput]) fee of + Left err -> error $ "Fail to build genesis transations: " <> show err + Right tx -> sign signingKey tx where - spendingAddress = buildAddress (getVerificationKey spendingSigningKey) networkId + initialInput = + genesisUTxOPseudoTxIn + networkId + (unsafeCastHash $ verificationKeyHash $ getVerificationKey signingKey) - receivingAddress = buildAddress receivingVerificationKey networkId + fee = calculateMinFee networkId rawTx Sizes{inputs = 1, outputs = length recipients + 1, witnesses = 1} pparams + rawTx = case buildRaw [initialInput] [] 0 of + Left err -> error $ "Fail to build genesis transactions: " <> show err + Right tx -> tx - theOutput = + totalSent = foldMap snd recipients + + changeAddr = mkVkAddress networkId (getVerificationKey signingKey) + changeOutput = TxOut - (shelleyAddressInEra receivingAddress) - (lovelaceToValue lovelace) + changeAddr + (lovelaceToValue $ initialAmount - totalSent - fee) TxOutDatumNone --- TODO: replace usages with 'seedFromFaucet' -postSeedPayment :: NetworkId -> ProtocolParameters -> Lovelace -> FilePath -> SigningKey PaymentKey -> Lovelace -> IO () -postSeedPayment networkId pparams initialAmount nodeSocket signingKey amountLovelace = do - let genesisTx = mkGenesisTx networkId pparams initialAmount signingKey verificationKey amountLovelace - submit networkId nodeSocket genesisTx - void $ waitForPayment networkId nodeSocket amountLovelace addr - where - verificationKey = getVerificationKey signingKey - addr = buildAddress verificationKey networkId + recipientOutputs = + flip map recipients $ \(vk, ll) -> + TxOut + (mkVkAddress networkId vk) + (lovelaceToValue ll) + TxOutDatumNone diff --git a/hydra-cluster/src/CardanoCluster.hs b/hydra-cluster/src/CardanoCluster.hs index 0fc42f7d661..97837185759 100644 --- a/hydra-cluster/src/CardanoCluster.hs +++ b/hydra-cluster/src/CardanoCluster.hs @@ -1,9 +1,9 @@ {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DerivingStrategies #-} -{-# LANGUAGE TypeApplications #-} module CardanoCluster where +import Hydra.Cardano.Api import Hydra.Prelude import qualified Cardano.Api.UTxO as UTxO @@ -25,35 +25,12 @@ import CardanoNode ( Port, PortsConfig (..), RunningNode (..), - addField, defaultCardanoNodeArgs, withCardanoNode, ) -import Control.Lens ((.~)) import Control.Tracer (Tracer, traceWith) -import Data.Aeson (object) import qualified Data.Aeson as Aeson -import Data.Aeson.Lens (key) import qualified Data.ByteString as BS -import Data.ByteString.Base16 (encodeBase16) -import Hydra.Cardano.Api ( - AsType (..), - Lovelace, - NetworkId (Testnet), - NetworkMagic (NetworkMagic), - PaymentKey, - SigningKey, - TextEnvelopeError (TextEnvelopeAesonDecodeError), - UTxO, - VerificationKey (PaymentVerificationKey), - deserialiseFromTextEnvelope, - getVerificationKey, - lovelaceToValue, - serialiseToRawBytes, - shelleyAddressInEra, - txOutLovelace, - ) -import qualified Hydra.Cardano.Api as Api import Hydra.Chain.Direct.Util (markerDatumHash, retry) import qualified Hydra.Chain.Direct.Util as Cardano import qualified Paths_hydra_cluster as Pkg @@ -65,8 +42,6 @@ import System.Posix.Files ( ) import Test.Network.Ports (randomUnusedTCPPort, randomUnusedTCPPorts) -data RunningCluster = RunningCluster ClusterConfig [RunningNode] - -- | TODO: This is hard-coded and must match what's in the genesis file, so -- ideally, we want to either: -- @@ -75,35 +50,12 @@ data RunningCluster = RunningCluster ClusterConfig [RunningNode] defaultNetworkId :: NetworkId defaultNetworkId = Testnet (NetworkMagic 42) --- FIXME: This is hard-coded and should correspond to the initial funds set in --- the genesis file. +-- NOTE: This is hard-coded and needs to correspond to the initial funds set in +-- the genesis-shelley.json file. availableInitialFunds :: Num a => a availableInitialFunds = 900_000_000_000 --- | Configuration parameters for the cluster. -data ClusterConfig = ClusterConfig - { parentStateDirectory :: FilePath - , networkId :: NetworkId - , initialFunds :: [VerificationKey PaymentKey] - } - -asSigningKey :: AsType (SigningKey PaymentKey) -asSigningKey = AsSigningKey AsPaymentKey - -withCluster :: - Tracer IO ClusterLog -> ClusterConfig -> (RunningCluster -> IO ()) -> IO () -withCluster tr cfg@ClusterConfig{parentStateDirectory, initialFunds} action = do - systemStart <- initSystemStart - (cfgA, cfgB, cfgC) <- - makeNodesConfig parentStateDirectory systemStart - <$> randomUnusedTCPPorts 3 - - withBFTNode tr cfgA initialFunds $ \nodeA -> do - withBFTNode tr cfgB initialFunds $ \nodeB -> do - withBFTNode tr cfgC initialFunds $ \nodeC -> do - let nodes = [nodeA, nodeB, nodeC] - action (RunningCluster cfg nodes) - +-- | Enumeration of known actors for which we can get the 'keysFor' and 'writeKeysFor'. data Actor = Alice | Bob @@ -117,6 +69,7 @@ actorName = \case Carol -> "carol" Faucet -> "faucet" +-- | Get the "well-known" keys for given actor. keysFor :: Actor -> IO (VerificationKey PaymentKey, SigningKey PaymentKey) keysFor actor = do bs <- readConfigFile ("credentials" actorName actor <.> "sk") @@ -151,13 +104,43 @@ writeKeysFor targetDir actor = do vkName = actorName actor <.> ".vk" +-- * Starting a cluster or single nodes + +data RunningCluster = RunningCluster ClusterConfig [RunningNode] + +-- | Configuration parameters for the cluster. +data ClusterConfig = ClusterConfig + { parentStateDirectory :: FilePath + , networkId :: NetworkId + } + +asSigningKey :: AsType (SigningKey PaymentKey) +asSigningKey = AsSigningKey AsPaymentKey + +withCluster :: + Tracer IO ClusterLog -> ClusterConfig -> (RunningCluster -> IO ()) -> IO () +withCluster tr cfg@ClusterConfig{parentStateDirectory} action = do + systemStart <- initSystemStart + (cfgA, cfgB, cfgC) <- + makeNodesConfig parentStateDirectory systemStart + <$> randomUnusedTCPPorts 3 + + withBFTNode tr cfgA $ \nodeA -> do + withBFTNode tr cfgB $ \nodeB -> do + withBFTNode tr cfgC $ \nodeC -> do + let nodes = [nodeA, nodeB, nodeC] + action (RunningCluster cfg nodes) + +-- | Start a cardano-node in BFT mode using the config from config/ and +-- credentials from config/credentials/ using given 'nodeId'. NOTE: This means +-- that nodeId should only be 1,2 or 3 and that only the faucet receives +-- 'initialFunds'. Use 'seedFromFaucet' to distribute funds other wallets. withBFTNode :: Tracer IO ClusterLog -> CardanoNodeConfig -> - [VerificationKey PaymentKey] -> (RunningNode -> IO ()) -> IO () -withBFTNode clusterTracer cfg initialFunds action = do +withBFTNode clusterTracer cfg action = do createDirectoryIfMissing False (stateDirectory cfg) [dlgCert, signKey, vrfKey, kesKey, opCert] <- @@ -188,7 +171,9 @@ withBFTNode clusterTracer cfg initialFunds action = do >>= writeFileBS (stateDirectory cfg nodeByronGenesisFile args) - setInitialFundsInGenesisShelley (stateDirectory cfg nodeShelleyGenesisFile args) + readConfigFile "genesis-shelley.json" + >>= writeFileBS + (stateDirectory cfg nodeShelleyGenesisFile args) readConfigFile "genesis-alonzo.json" >>= writeFileBS @@ -205,28 +190,11 @@ withBFTNode clusterTracer cfg initialFunds action = do kesKeyFilename i = "delegate" <> show i <> ".kes.skey" opCertFilename i = "opcert" <> show i <> ".cert" - setInitialFundsInGenesisShelley file = do - bs <- readConfigFile "genesis-shelley.json" - genesisJson <- either fail pure $ Aeson.eitherDecodeStrict @Aeson.Value bs - let updatedJson = genesisJson & key "initialFunds" .~ initialFundsValue - Aeson.encodeFile file updatedJson - - initialFundsValue = - foldr - (uncurry addField) - (object []) - (mkInitialFundsEntry <$> initialFunds) - - mkInitialFundsEntry :: VerificationKey PaymentKey -> (Text, Word) - mkInitialFundsEntry vk = - let addr = buildAddress vk defaultNetworkId - bytes = serialiseToRawBytes addr - in (encodeBase16 bytes, availableInitialFunds) - copyCredential parentDir file = do bs <- readConfigFile ("credentials" file) let destination = parentDir file - writeFileBS destination bs + unlessM (doesFileExist destination) $ + writeFileBS destination bs setFileMode destination ownerReadMode pure destination @@ -240,7 +208,7 @@ withBFTNode clusterTracer cfg initialFunds action = do threadDelay 0.1 waitForSocket node -data Marked = Marked | Normal +data Marked = Fuel | Normal -- | Create a specially marked "seed" UTXO containing requested 'Lovelace' by -- redeeming funds available to the well-known faucet. @@ -254,7 +222,7 @@ seedFromFaucet :: VerificationKey PaymentKey -> -- | Amount to get from faucet Lovelace -> - -- | Marked or normal output? + -- | Marked as fuel or normal output? Marked -> IO UTxO seedFromFaucet networkId (RunningNode _ nodeSocket) receivingVerificationKey lovelace marked = do @@ -281,18 +249,32 @@ seedFromFaucet networkId (RunningNode _ nodeSocket) receivingVerificationKey lov receivingAddress = buildAddress receivingVerificationKey networkId theOutput = - Api.TxOut + TxOut (shelleyAddressInEra receivingAddress) (lovelaceToValue lovelace) theOutputDatum theOutputDatum = case marked of - Marked -> Api.TxOutDatumHash markerDatumHash - Normal -> Api.TxOutDatumNone + Fuel -> TxOutDatumHash markerDatumHash + Normal -> TxOutDatumNone isCardanoClientException :: CardanoClientException -> Bool isCardanoClientException = const True +-- | Like 'seedFromFaucet', but without returning the seeded 'UTxO'. +seedFromFaucet_ :: + NetworkId -> + RunningNode -> + -- | Recipient of the funds + VerificationKey PaymentKey -> + -- | Amount to get from faucet + Lovelace -> + -- | Marked as fuel or normal output? + Marked -> + IO () +seedFromFaucet_ nid node vk ll marked = + void $ seedFromFaucet nid node vk ll marked + -- | Initialize the system start time to now (modulo a small offset needed to -- give time to the system to bootstrap correctly). initSystemStart :: IO UTCTime diff --git a/hydra-cluster/src/CardanoNode.hs b/hydra-cluster/src/CardanoNode.hs index b41e8649acc..97071798f83 100644 --- a/hydra-cluster/src/CardanoNode.hs +++ b/hydra-cluster/src/CardanoNode.hs @@ -17,6 +17,7 @@ import qualified Data.Aeson as Aeson import qualified Data.HashMap.Strict as HM import Data.Time.Clock.POSIX (posixSecondsToUTCTime, utcTimeToPOSIXSeconds) import Hydra.Cardano.Api (AsType (AsPaymentKey), PaymentKey, SigningKey, VerificationKey, generateSigningKey, getVerificationKey) +import System.Directory (doesFileExist, removeFile) import System.Exit (ExitCode (..)) import System.FilePath ((<.>), ()) import System.Process ( @@ -111,12 +112,19 @@ withCardanoNode tr cfg@CardanoNodeConfig{stateDirectory, nodeId} args action = d race_ (checkProcessHasNotDied ("cardano-node-" <> show nodeId) processHandle) (action (RunningNode nodeId (stateDirectory nodeSocket args))) + `finally` cleanupSocketFile where generateEnvironment = do refreshSystemStart cfg args let topology = mkTopology $ peers $ ports cfg Aeson.encodeFile (stateDirectory nodeTopologyFile args) topology + cleanupSocketFile = + whenM (doesFileExist socketFile) $ + removeFile socketFile + + socketFile = stateDirectory nodeSocket args + -- | Generate command-line arguments for launching @cardano-node@. cardanoNodeProcess :: Maybe FilePath -> CardanoNodeArgs -> CreateProcess cardanoNodeProcess cwd args = (proc "cardano-node" strArgs){cwd} diff --git a/hydra-cluster/src/Hydra/Generator.hs b/hydra-cluster/src/Hydra/Generator.hs index 678a5da2c32..75a481741be 100644 --- a/hydra-cluster/src/Hydra/Generator.hs +++ b/hydra-cluster/src/Hydra/Generator.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE TypeApplications #-} module Hydra.Generator where @@ -7,12 +8,12 @@ import Hydra.Prelude hiding (size) import qualified Cardano.Api.UTxO as UTxO import CardanoClient (mkGenesisTx) -import CardanoCluster (availableInitialFunds) +import CardanoCluster (Actor (Faucet), availableInitialFunds, keysFor) import Control.Monad (foldM) import Data.Aeson (object, withObject, (.:), (.=)) import Data.Default (def) import Hydra.Ledger (IsTx (..)) -import Hydra.Ledger.Cardano (genKeyPair, mkSimpleTx) +import Hydra.Ledger.Cardano (genKeyPair, genSigningKey, mkSimpleTx) import Test.QuickCheck (choose, generate, sized) networkId :: NetworkId @@ -23,65 +24,104 @@ networkId = Testnet $ NetworkMagic 42 -- set. data Dataset = Dataset { fundingTransaction :: Tx - , transactionsSequence :: [Tx] - , signingKey :: SigningKey PaymentKey + , clientDatasets :: [ClientDataset] } - deriving (Show, Generic) - -defaultProtocolParameters :: ProtocolParameters -defaultProtocolParameters = fromLedgerPParams ShelleyBasedEraShelley def + deriving (Show, Generic, ToJSON, FromJSON) instance Arbitrary Dataset where - arbitrary = sized (genConstantUTxODataset defaultProtocolParameters) + arbitrary = sized $ \n -> do + sk <- genSigningKey + genDatasetConstantUTxO sk defaultProtocolParameters (n `div` 10) n + +data ClientDataset = ClientDataset + { signingKey :: SigningKey PaymentKey + , initialUTxO :: UTxO + , txSequence :: [Tx] + } + deriving (Show, Generic) -instance ToJSON Dataset where - toJSON Dataset{fundingTransaction, transactionsSequence, signingKey} = +instance ToJSON ClientDataset where + toJSON ClientDataset{initialUTxO, txSequence, signingKey} = object - [ "fundingTransaction" .= fundingTransaction - , "transactionsSequence" .= transactionsSequence - , "signingKey" .= serialiseToBech32 signingKey + [ "signingKey" .= serialiseToBech32 signingKey + , "initialUTxO" .= initialUTxO + , "txSequence" .= txSequence ] -instance FromJSON Dataset where +instance FromJSON ClientDataset where parseJSON = - withObject "Dataset" $ \o -> - Dataset <$> o .: "fundingTransaction" - <*> o .: "transactionsSequence" - <*> (decodeSigningKey =<< o .: "signingKey") + withObject "ClientDataset" $ \o -> + ClientDataset + <$> (decodeSigningKey =<< o .: "signingKey") + <*> o .: "initialUTxO" + <*> o .: "txSequence" where decodeSigningKey = either (fail . show) pure . deserialiseFromBech32 (AsSigningKey AsPaymentKey) --- | Generate a 'Dataset' which does not grow the UTXO set over time. --- The sequence of transactions generated consist only of simple payments from and to --- arbitrary keys controlled by the "client". -generateConstantUTxODataset :: ProtocolParameters -> Int -> IO Dataset -generateConstantUTxODataset pparams = generate . genConstantUTxODataset pparams +defaultProtocolParameters :: ProtocolParameters +defaultProtocolParameters = fromLedgerPParams ShelleyBasedEraShelley def + +-- | Generate 'Dataset' which does not grow the per-client UTXO set over time. +-- The sequence of transactions generated consist only of simple payments from +-- and to arbitrary keys controlled by the individual clients. +generateConstantUTxODataset :: + ProtocolParameters -> + -- | Number of clients + Int -> + -- | Number of transactions + Int -> + IO Dataset +generateConstantUTxODataset pparams nClients nTxs = do + (_, faucetSk) <- keysFor Faucet + generate $ genDatasetConstantUTxO faucetSk pparams nClients nTxs -genConstantUTxODataset :: ProtocolParameters -> Int -> Gen Dataset -genConstantUTxODataset pparams len = do - keyPair@(verificationKey, signingKey) <- genKeyPair - amount <- choose (1, availableInitialFunds `div` 2) +genDatasetConstantUTxO :: + -- | The faucet signing key + SigningKey PaymentKey -> + ProtocolParameters -> + -- | Number of clients + Int -> + -- | Number of transactions + Int -> + Gen Dataset +genDatasetConstantUTxO faucetSk pparams nClients nTxs = do + clientFunds <- replicateM nClients $ do + (_vk, sk) <- genKeyPair + amount <- Lovelace . fromIntegral <$> choose (1, availableInitialFunds `div` nClients) + pure (sk, amount) + -- Prepare funding transaction as it will be posted let fundingTransaction = mkGenesisTx networkId pparams + faucetSk (Lovelace availableInitialFunds) - signingKey - verificationKey - (Lovelace amount) - -- NOTE: The initialUTxO must contain only the UTXO we will later commit. We - -- know that by construction, the 'mkGenesisTx' will have exactly two outputs, - -- the last one being the change output. So, it suffices to lookup for the - -- minimum key in the utxo map to isolate the commit UTXO. - let initialUTxO = UTxO.min $ utxoFromTx fundingTransaction - transactionsSequence <- - reverse . thrd - <$> foldM generateOneTransfer (initialUTxO, keyPair, []) [1 .. len] - pure Dataset{fundingTransaction, transactionsSequence, signingKey} + (first getVerificationKey <$> clientFunds) + clientDatasets <- forM (zip clientFunds [0 ..]) (generateClientDataset fundingTransaction) + pure Dataset{fundingTransaction, clientDatasets} where thrd (_, _, c) = c + generateClientDataset fundingTransaction ((sk, amount), index) = do + let vk = getVerificationKey sk + keyPair = (vk, sk) + -- NOTE: The initialUTxO must contain only the UTXO we will later commit. We + -- know that by construction, the 'mkGenesisTx' will create outputs + -- addressed to recipient verification keys and only holding the requested + -- amount of lovelace (and a potential change output last). + let txIn = mkTxIn fundingTransaction index + txOut = + TxOut + (mkVkAddress networkId vk) + (lovelaceToValue amount) + TxOutDatumNone + initialUTxO = UTxO.singleton (txIn, txOut) + txSequence <- + reverse . thrd + <$> foldM generateOneTransfer (initialUTxO, keyPair, []) [1 .. nTxs] + pure ClientDataset{signingKey = sk, initialUTxO, txSequence} + generateOneTransfer :: (UTxO, (VerificationKey PaymentKey, SigningKey PaymentKey), [Tx]) -> Int -> @@ -98,6 +138,3 @@ genConstantUTxODataset pparams len = do pure (utxoFromTx tx, recipient, tx : txs) _ -> error "Couldn't generate transaction sequence: need exactly one UTXO." - -mkCredentials :: Int -> (VerificationKey PaymentKey, SigningKey PaymentKey) -mkCredentials = generateWith genKeyPair diff --git a/hydra-cluster/test/Test/DirectChainSpec.hs b/hydra-cluster/test/Test/DirectChainSpec.hs index 506b467af60..ad65caf1000 100644 --- a/hydra-cluster/test/Test/DirectChainSpec.hs +++ b/hydra-cluster/test/Test/DirectChainSpec.hs @@ -8,19 +8,18 @@ import Test.Hydra.Prelude import CardanoClient ( buildAddress, - generatePaymentToCommit, - postSeedPayment, - queryProtocolParameters, queryUTxO, waitForUTxO, ) import CardanoCluster ( Actor (Alice, Bob, Carol), ClusterLog, - availableInitialFunds, + Marked (Fuel, Normal), defaultNetworkId, keysFor, newNodeConfig, + seedFromFaucet, + seedFromFaucet_, withBFTNode, ) import CardanoNode (NodeLog, RunningNode (..)) @@ -57,15 +56,14 @@ spec = around showLogsOnFailure $ do bobsCallback <- newEmptyMVar withTempDir "hydra-cluster" $ \tmp -> do config <- newNodeConfig tmp - aliceKeys@(aliceCardanoVk, aliceCardanoSk) <- keysFor Alice - withBFTNode (contramap FromCluster tracer) config [aliceCardanoVk] $ \(RunningNode _ nodeSocket) -> do + aliceKeys@(aliceCardanoVk, _) <- keysFor Alice + withBFTNode (contramap FromCluster tracer) config $ \node@(RunningNode _ nodeSocket) -> do bobKeys <- keysFor Bob - pparams <- queryProtocolParameters defaultNetworkId nodeSocket cardanoKeys <- fmap fst <$> mapM keysFor [Alice, Bob, Carol] withIOManager $ \iocp -> do withDirectChain (contramap (FromDirectChain "alice") tracer) magic iocp nodeSocket aliceKeys alice cardanoKeys (putMVar alicesCallback) $ \Chain{postTx} -> do withDirectChain nullTracer magic iocp nodeSocket bobKeys bob cardanoKeys (putMVar bobsCallback) $ \_ -> do - postSeedPayment (Testnet magic) pparams availableInitialFunds nodeSocket aliceCardanoSk 100_000_000 + seedFromFaucet_ (Testnet magic) node aliceCardanoVk 100_000_000 Fuel postTx $ InitTx $ HeadParameters 100 [alice, bob, carol] alicesCallback `observesInTime` OnInitTx 100 [alice, bob, carol] @@ -81,22 +79,21 @@ spec = around showLogsOnFailure $ do bobsCallback <- newEmptyMVar withTempDir "hydra-cluster" $ \tmp -> do config <- newNodeConfig tmp - aliceKeys@(aliceCardanoVk, aliceCardanoSk) <- keysFor Alice - withBFTNode (contramap FromCluster tracer) config [aliceCardanoVk] $ \node@(RunningNode _ nodeSocket) -> do + aliceKeys@(aliceCardanoVk, _) <- keysFor Alice + withBFTNode (contramap FromCluster tracer) config $ \node@(RunningNode _ nodeSocket) -> do bobKeys <- keysFor Bob - pparams <- queryProtocolParameters defaultNetworkId nodeSocket cardanoKeys <- fmap fst <$> mapM keysFor [Alice, Bob, Carol] withIOManager $ \iocp -> do withDirectChain (contramap (FromDirectChain "alice") tracer) magic iocp nodeSocket aliceKeys alice cardanoKeys (putMVar alicesCallback) $ \Chain{postTx} -> do withDirectChain nullTracer magic iocp nodeSocket bobKeys bob cardanoKeys (putMVar bobsCallback) $ \_ -> do - postSeedPayment (Testnet magic) pparams availableInitialFunds nodeSocket aliceCardanoSk 100_000_000 + seedFromFaucet_ (Testnet magic) node aliceCardanoVk 100_000_000 Fuel postTx $ InitTx $ HeadParameters 100 [alice, bob, carol] alicesCallback `observesInTime` OnInitTx 100 [alice, bob, carol] bobsCallback `observesInTime` OnInitTx 100 [alice, bob, carol] let aliceCommitment = 66_000_000 - aliceUTxO <- generatePaymentToCommit defaultNetworkId node aliceCardanoSk aliceCardanoVk aliceCommitment + aliceUTxO <- seedFromFaucet defaultNetworkId node aliceCardanoVk aliceCommitment Normal postTx $ CommitTx alice aliceUTxO alicesCallback `observesInTime` OnCommitTx alice aliceUTxO @@ -120,15 +117,15 @@ spec = around showLogsOnFailure $ do bobsCallback <- newEmptyMVar withTempDir "hydra-cluster" $ \tmp -> do config <- newNodeConfig tmp - aliceKeys@(aliceCardanoVk, aliceCardanoSk) <- keysFor Alice - withBFTNode (contramap FromCluster tracer) config [aliceCardanoVk] $ \(RunningNode _ nodeSocket) -> do + aliceKeys@(aliceCardanoVk, _) <- keysFor Alice + withBFTNode (contramap FromCluster tracer) config $ \node@(RunningNode _ nodeSocket) -> do bobKeys <- keysFor Bob - pparams <- queryProtocolParameters defaultNetworkId nodeSocket let cardanoKeys = [] withIOManager $ \iocp -> do withDirectChain (contramap (FromDirectChain "alice") tracer) magic iocp nodeSocket aliceKeys alice cardanoKeys (putMVar alicesCallback) $ \Chain{postTx = alicePostTx} -> do withDirectChain nullTracer magic iocp nodeSocket bobKeys bob cardanoKeys (putMVar bobsCallback) $ \Chain{postTx = bobPostTx} -> do - postSeedPayment (Testnet magic) pparams availableInitialFunds nodeSocket aliceCardanoSk 100_000_000 + seedFromFaucet_ (Testnet magic) node aliceCardanoVk 100_000_000 Fuel + alicePostTx $ InitTx $ HeadParameters 100 [alice, carol] alicesCallback `observesInTime` OnInitTx 100 [alice, carol] @@ -139,13 +136,12 @@ spec = around showLogsOnFailure $ do alicesCallback <- newEmptyMVar withTempDir "hydra-cluster" $ \tmp -> do config <- newNodeConfig tmp - aliceKeys@(aliceCardanoVk, aliceCardanoSk) <- keysFor Alice - withBFTNode (contramap FromCluster tracer) config [aliceCardanoVk] $ \node@(RunningNode _ nodeSocket) -> do - pparams <- queryProtocolParameters defaultNetworkId nodeSocket + aliceKeys@(aliceCardanoVk, _) <- keysFor Alice + withBFTNode (contramap FromCluster tracer) config $ \node@(RunningNode _ nodeSocket) -> do let cardanoKeys = [aliceCardanoVk] withIOManager $ \iocp -> do withDirectChain (contramap (FromDirectChain "alice") tracer) magic iocp nodeSocket aliceKeys alice cardanoKeys (putMVar alicesCallback) $ \Chain{postTx} -> do - postSeedPayment (Testnet magic) pparams availableInitialFunds nodeSocket aliceCardanoSk 100_000_000 + seedFromFaucet_ (Testnet magic) node aliceCardanoVk 100_000_000 Fuel postTx $ InitTx $ HeadParameters 100 [alice] alicesCallback `observesInTime` OnInitTx 100 [alice] @@ -161,7 +157,7 @@ spec = around showLogsOnFailure $ do (CannotSpendInput{} :: PostTxError Tx) -> True _ -> False - aliceUTxO <- generatePaymentToCommit defaultNetworkId node aliceCardanoSk aliceCardanoVk 1_000_000 + aliceUTxO <- seedFromFaucet defaultNetworkId node aliceCardanoVk 1_000_000 Normal postTx $ CommitTx alice aliceUTxO alicesCallback `observesInTime` OnCommitTx alice aliceUTxO @@ -169,13 +165,12 @@ spec = around showLogsOnFailure $ do alicesCallback <- newEmptyMVar withTempDir "hydra-cluster" $ \tmp -> do config <- newNodeConfig tmp - aliceKeys@(aliceCardanoVk, aliceCardanoSk) <- keysFor Alice - withBFTNode (contramap FromCluster tracer) config [aliceCardanoVk] $ \(RunningNode _ nodeSocket) -> do - pparams <- queryProtocolParameters defaultNetworkId nodeSocket + aliceKeys@(aliceCardanoVk, _) <- keysFor Alice + withBFTNode (contramap FromCluster tracer) config $ \node@(RunningNode _ nodeSocket) -> do let cardanoKeys = [aliceCardanoVk] withIOManager $ \iocp -> do withDirectChain (contramap (FromDirectChain "alice") tracer) magic iocp nodeSocket aliceKeys alice cardanoKeys (putMVar alicesCallback) $ \Chain{postTx} -> do - postSeedPayment (Testnet magic) pparams availableInitialFunds nodeSocket aliceCardanoSk 100_000_000 + seedFromFaucet_ (Testnet magic) node aliceCardanoVk 100_000_000 Fuel postTx $ InitTx $ HeadParameters 100 [alice] alicesCallback `observesInTime` OnInitTx 100 [alice] @@ -187,18 +182,17 @@ spec = around showLogsOnFailure $ do alicesCallback <- newEmptyMVar withTempDir "hydra-cluster" $ \tmp -> do config <- newNodeConfig tmp - aliceKeys@(aliceCardanoVk, aliceCardanoSk) <- keysFor Alice - withBFTNode (contramap FromCluster tracer) config [aliceCardanoVk] $ \node@(RunningNode _ nodeSocket) -> do - pparams <- queryProtocolParameters defaultNetworkId nodeSocket + aliceKeys@(aliceCardanoVk, _) <- keysFor Alice + withBFTNode (contramap FromCluster tracer) config $ \node@(RunningNode _ nodeSocket) -> do let cardanoKeys = [aliceCardanoVk] withIOManager $ \iocp -> do withDirectChain (contramap (FromDirectChain "alice") tracer) magic iocp nodeSocket aliceKeys alice cardanoKeys (putMVar alicesCallback) $ \Chain{postTx} -> do - postSeedPayment (Testnet magic) pparams availableInitialFunds nodeSocket aliceCardanoSk 100_000_000 + seedFromFaucet_ (Testnet magic) node aliceCardanoVk 100_000_000 Fuel postTx $ InitTx $ HeadParameters 100 [alice] alicesCallback `observesInTime` OnInitTx 100 [alice] - someUTxO <- generatePaymentToCommit defaultNetworkId node aliceCardanoSk aliceCardanoVk 1_000_000 + someUTxO <- seedFromFaucet defaultNetworkId node aliceCardanoVk 1_000_000 Normal postTx $ CommitTx alice someUTxO alicesCallback `observesInTime` OnCommitTx alice someUTxO diff --git a/hydra-cluster/test/Test/EndToEndSpec.hs b/hydra-cluster/test/Test/EndToEndSpec.hs index 53a1b1e3171..117ed3ef478 100644 --- a/hydra-cluster/test/Test/EndToEndSpec.hs +++ b/hydra-cluster/test/Test/EndToEndSpec.hs @@ -12,19 +12,15 @@ import Cardano.Crypto.DSIGN ( SignKeyDSIGN, VerKeyDSIGN, ) -import CardanoClient ( - postSeedPayment, - queryProtocolParameters, - waitForUTxO, - ) +import CardanoClient (waitForUTxO) import CardanoCluster ( - Actor (Alice, Bob, Carol, Faucet), - Marked (Marked, Normal), - availableInitialFunds, + Actor (Alice, Bob, Carol), + Marked (Fuel, Normal), defaultNetworkId, keysFor, newNodeConfig, seedFromFaucet, + seedFromFaucet_, withBFTNode, writeKeysFor, ) @@ -82,8 +78,7 @@ spec = around showLogsOnFailure $ failAfter 60 $ withTempDir "end-to-end-cardano-node" $ \tmpDir -> do config <- newNodeConfig tmpDir - (faucetVk, _) <- keysFor Faucet - withBFTNode (contramap FromCluster tracer) config [faucetVk] $ \node -> do + withBFTNode (contramap FromCluster tracer) config $ \node -> do initAndClose tracer 1 node describe "two hydra heads scenario" $ do @@ -91,8 +86,7 @@ spec = around showLogsOnFailure $ failAfter 60 $ withTempDir "end-to-end-cardano-node" $ \tmpDir -> do config <- newNodeConfig tmpDir - (faucetVk, _) <- keysFor Faucet - withBFTNode (contramap FromCluster tracer) config [faucetVk] $ \node -> do + withBFTNode (contramap FromCluster tracer) config $ \node -> do concurrently_ (initAndClose tracer 0 node) (initAndClose tracer 1 node) @@ -112,16 +106,16 @@ spec = around showLogsOnFailure $ failAfter 60 $ withTempDir "end-to-end-two-heads" $ \tmpDir -> do config <- newNodeConfig tmpDir - (aliceCardanoVk, aliceCardanoSk) <- keysFor Alice - (bobCardanoVk, bobCardanoSk) <- keysFor Bob - withBFTNode (contramap FromCluster tracer) config [aliceCardanoVk, bobCardanoVk] $ \(RunningNode _ nodeSocket) -> do + withBFTNode (contramap FromCluster tracer) config $ \node@(RunningNode _ nodeSocket) -> do + (aliceCardanoVk, _aliceCardanoSk) <- keysFor Alice + (bobCardanoVk, _bobCardanoSk) <- keysFor Bob (aliceVkPath, aliceSkPath) <- writeKeysFor tmpDir Alice (_, bobSkPath) <- writeKeysFor tmpDir Bob - pparams <- queryProtocolParameters defaultNetworkId nodeSocket withHydraNode tracer aliceSkPath [] tmpDir nodeSocket 1 aliceSk [] allNodeIds $ \n1 -> withHydraNode tracer bobSkPath [aliceVkPath] tmpDir nodeSocket 2 bobSk [aliceVk] allNodeIds $ \n2 -> do - postSeedPayment defaultNetworkId pparams availableInitialFunds nodeSocket aliceCardanoSk 100_000_000 - postSeedPayment defaultNetworkId pparams availableInitialFunds nodeSocket bobCardanoSk 100_000_000 + -- Funds to be used as fuel by Hydra protocol transactions + seedFromFaucet_ defaultNetworkId node aliceCardanoVk 100_000_000 Fuel + seedFromFaucet_ defaultNetworkId node bobCardanoVk 100_000_000 Fuel let contestationPeriod = 10 :: Natural send n1 $ input "Init" ["contestationPeriod" .= contestationPeriod] @@ -162,18 +156,18 @@ spec = around showLogsOnFailure $ withTempDir "end-to-end-prometheus-metrics" $ \tmpDir -> do config <- newNodeConfig tmpDir - (aliceCardanoVk, aliceCardanoSk) <- keysFor Alice - withBFTNode (contramap FromCluster tracer) config [aliceCardanoVk] $ \(RunningNode _ nodeSocket) -> do + (aliceCardanoVk, _) <- keysFor Alice + withBFTNode (contramap FromCluster tracer) config $ \node@(RunningNode _ nodeSocket) -> do (aliceVkPath, aliceSkPath) <- writeKeysFor tmpDir Alice (bobVkPath, bobSkPath) <- writeKeysFor tmpDir Bob (carolVkPath, carolSkPath) <- writeKeysFor tmpDir Carol - pparams <- queryProtocolParameters defaultNetworkId nodeSocket failAfter 20 $ withHydraNode tracer aliceSkPath [bobVkPath, carolVkPath] tmpDir nodeSocket 1 aliceSk [bobVk, carolVk] allNodeIds $ \n1 -> - withHydraNode tracer bobSkPath [aliceVkPath, carolVkPath] tmpDir nodeSocket 2 bobSk [aliceVk, carolVk] allNodeIds $ \_ -> - withHydraNode tracer carolSkPath [aliceVkPath, bobVkPath] tmpDir nodeSocket 3 carolSk [aliceVk, bobVk] allNodeIds $ \_ -> do - postSeedPayment defaultNetworkId pparams availableInitialFunds nodeSocket aliceCardanoSk 100_000_000 - waitForNodesConnected tracer [n1] + withHydraNode tracer bobSkPath [aliceVkPath, carolVkPath] tmpDir nodeSocket 2 bobSk [aliceVk, carolVk] allNodeIds $ \n2 -> + withHydraNode tracer carolSkPath [aliceVkPath, bobVkPath] tmpDir nodeSocket 3 carolSk [aliceVk, bobVk] allNodeIds $ \n3 -> do + -- Funds to be used as fuel by Hydra protocol transactions + seedFromFaucet_ defaultNetworkId node aliceCardanoVk 100_000_000 Fuel + waitForNodesConnected tracer [n1, n2, n3] send n1 $ input "Init" ["contestationPeriod" .= int 10] waitFor tracer 3 [n1] $ output "ReadyToCommit" ["parties" .= Set.fromList [alice, bob, carol]] metrics <- getMetrics n1 @@ -210,9 +204,9 @@ initAndClose tracer clusterIx node@(RunningNode _ nodeSocket) = do waitForNodesConnected tracer [n1, n2, n3] -- Funds to be used as fuel by Hydra protocol transactions - void $ seedFromFaucet defaultNetworkId node aliceCardanoVk 100_000_000 Marked - void $ seedFromFaucet defaultNetworkId node bobCardanoVk 100_000_000 Marked - void $ seedFromFaucet defaultNetworkId node carolCardanoVk 100_000_000 Marked + seedFromFaucet_ defaultNetworkId node aliceCardanoVk 100_000_000 Fuel + seedFromFaucet_ defaultNetworkId node bobCardanoVk 100_000_000 Fuel + seedFromFaucet_ defaultNetworkId node carolCardanoVk 100_000_000 Fuel let contestationPeriod = 10 :: Natural send n1 $ input "Init" ["contestationPeriod" .= contestationPeriod] diff --git a/hydra-cluster/test/Test/GeneratorSpec.hs b/hydra-cluster/test/Test/GeneratorSpec.hs index f27803146fa..b71fdabf34e 100644 --- a/hydra-cluster/test/Test/GeneratorSpec.hs +++ b/hydra-cluster/test/Test/GeneratorSpec.hs @@ -1,18 +1,24 @@ {-# LANGUAGE TypeApplications #-} +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} module Test.GeneratorSpec where import Hydra.Prelude import Test.Hydra.Prelude -import Data.Aeson (encode) +import CardanoCluster (Actor (Faucet), keysFor) import Data.Text (unpack) -import Hydra.Cardano.Api (UTxO, utxoFromTx) -import Hydra.Generator (Dataset (..), defaultProtocolParameters, genConstantUTxODataset) +import Hydra.Cardano.Api (UTxO, prettyPrintJSON, utxoFromTx) +import Hydra.Generator ( + ClientDataset (..), + Dataset (..), + defaultProtocolParameters, + genDatasetConstantUTxO, + ) import Hydra.Ledger (applyTransactions, balance) import Hydra.Ledger.Cardano (Tx, cardanoLedger, genUTxO) import Test.Aeson.GenericSpecs (roundtripSpecs) -import Test.QuickCheck (Positive (Positive), Property, counterexample, forAll) +import Test.QuickCheck (Positive (Positive), Property, counterexample, forAll, idempotentIOProperty) spec :: Spec spec = parallel $ do @@ -27,13 +33,19 @@ prop_computeValueFromUTxO = prop_keepsUTxOConstant :: Property prop_keepsUTxOConstant = - forAll arbitrary $ \(Positive n) -> - forAll (genConstantUTxODataset defaultProtocolParameters n) $ \Dataset{fundingTransaction, transactionsSequence} -> - let initialUTxO = utxoFromTx fundingTransaction - finalUTxO = foldl' apply initialUTxO transactionsSequence - in length finalUTxO == length initialUTxO - & counterexample ("\ntransactions: " <> jsonString transactionsSequence) - & counterexample ("\nutxo: " <> jsonString initialUTxO) + forAll arbitrary $ \(Positive n) -> do + idempotentIOProperty $ do + faucetSk <- snd <$> keysFor Faucet + -- XXX: non-exhaustive pattern match + pure $ + forAll (genDatasetConstantUTxO faucetSk defaultProtocolParameters 1 n) $ + \Dataset{fundingTransaction, clientDatasets = [ClientDataset{txSequence}]} -> + let initialUTxO = utxoFromTx fundingTransaction + finalUTxO = foldl' apply initialUTxO txSequence + in length finalUTxO == length initialUTxO + & counterexample ("transactions: " <> prettyJSONString txSequence) + & counterexample ("utxo: " <> prettyJSONString initialUTxO) + & counterexample ("funding tx: " <> prettyJSONString fundingTransaction) apply :: UTxO -> Tx -> UTxO apply utxo tx = @@ -41,5 +53,5 @@ apply utxo tx = Left err -> error $ "invalid generated data set" <> show err Right finalUTxO -> finalUTxO -jsonString :: ToJSON a => a -> String -jsonString = unpack . decodeUtf8 . encode +prettyJSONString :: ToJSON a => a -> String +prettyJSONString = unpack . decodeUtf8 . prettyPrintJSON diff --git a/hydra-cluster/test/Test/LocalClusterSpec.hs b/hydra-cluster/test/Test/LocalClusterSpec.hs index 29f939a57dc..00594e93aca 100644 --- a/hydra-cluster/test/Test/LocalClusterSpec.hs +++ b/hydra-cluster/test/Test/LocalClusterSpec.hs @@ -5,20 +5,25 @@ import Hydra.Prelude import Test.Hydra.Prelude import CardanoClient ( - Sizes (..), build, buildAddress, - buildRaw, buildScriptAddress, - calculateMinFee, - defaultSizes, - queryProtocolParameters, queryUTxO, sign, submit, waitForPayment, ) -import CardanoCluster (Actor (Alice), ClusterConfig (..), ClusterLog (..), RunningCluster (..), defaultNetworkId, keysFor, withCluster) +import CardanoCluster ( + Actor (Alice), + ClusterConfig (..), + ClusterLog (..), + Marked (Normal), + RunningCluster (..), + defaultNetworkId, + keysFor, + seedFromFaucet_, + withCluster, + ) import CardanoNode (ChainTip (..), RunningNode (..), cliQueryTip) import qualified Data.Map as Map import Hydra.Chain.Direct.Tx (policyId) @@ -33,12 +38,10 @@ spec = it "should produce blocks, provide funds, and send Hydra OCV transactions" $ do showLogsOnFailure $ \tr -> withTempDir "hydra-cluster" $ \tmp -> do - (vk, _) <- keysFor Alice let config = ClusterConfig { parentStateDirectory = tmp , networkId = defaultNetworkId - , initialFunds = [vk] } withCluster tr config $ \cluster -> do failAfter 30 $ assertNetworkIsProducingBlock tr cluster @@ -61,29 +64,9 @@ assertNetworkIsProducingBlock tracer = go (-1) assertCanSpendInitialFunds :: HasCallStack => RunningCluster -> IO () assertCanSpendInitialFunds = \case - (RunningCluster ClusterConfig{networkId} (RunningNode _ socket : _)) -> do - (vk, sk) <- keysFor Alice - let addr = buildAddress vk networkId - UTxO utxo <- queryUTxO networkId socket [addr] - pparams <- queryProtocolParameters networkId socket - let (txIn, out) = case Map.toList utxo of - [] -> error "No UTxO found" - (tx : _) -> tx - initialAmount = selectLovelace (txOutValue out) - amountToPay = 100_000_001 - paymentOutput = TxOut (shelleyAddressInEra addr) (lovelaceToValue amountToPay) TxOutDatumNone - signedTx = do - rawTx <- buildRaw [txIn] [] 0 - let fee = calculateMinFee networkId rawTx defaultSizes{inputs = 1, outputs = 2, witnesses = 1} pparams - changeOutput = TxOut (shelleyAddressInEra addr) (lovelaceToValue $ initialAmount - amountToPay - fee) TxOutDatumNone - draftTx <- buildRaw [txIn] [paymentOutput, changeOutput] fee - pure $ sign sk draftTx - - case signedTx of - Left err -> failure ("transaction is malformed: " <> show err) - Right tx -> do - submit networkId socket tx - void $ waitForPayment networkId socket amountToPay addr + (RunningCluster ClusterConfig{networkId} (node : _)) -> do + (vk, _) <- keysFor Alice + seedFromFaucet_ networkId node vk 100_000_000 Normal _ -> error "empty cluster?" diff --git a/hydra-node/src/Hydra/Chain/Direct.hs b/hydra-node/src/Hydra/Chain/Direct.hs index b7b2bad5acd..0e532dcc66a 100644 --- a/hydra-node/src/Hydra/Chain/Direct.hs +++ b/hydra-node/src/Hydra/Chain/Direct.hs @@ -32,7 +32,6 @@ import Control.Monad.Class.MonadSTM (newTQueueIO, newTVarIO, readTQueue, retry, import Control.Monad.Class.MonadTimer (timeout) import Control.Tracer (nullTracer) import Data.Aeson (Value (String), object, (.=)) -import qualified Data.Map as Map import Data.Sequence.Strict (StrictSeq) import Hydra.Cardano.Api ( NetworkId (Testnet), @@ -80,6 +79,7 @@ import Hydra.Chain.Direct.Wallet ( ErrCoverFee (..), TinyWallet (..), TinyWalletLog, + getFuelUTxO, getTxId, withTinyWallet, ) @@ -417,13 +417,11 @@ fromPostChainTx :: TVar m SomeOnChainHeadState -> PostChainTx Tx -> STM m Tx -fromPostChainTx cardanoKeys TinyWallet{getUTxO} someHeadState tx = do +fromPostChainTx cardanoKeys wallet someHeadState tx = do SomeOnChainHeadState st <- readTVar someHeadState case (tx, reifyState st) of (InitTx params, TkIdle) -> do - u <- getUTxO - -- NOTE: 'lookupMax' to favor change outputs! - case Map.lookupMax u of + getFuelUTxO wallet >>= \case Just (fromLedgerTxIn -> seedInput, _) -> do pure $ initialize params cardanoKeys seedInput st Nothing -> diff --git a/hydra-node/src/Hydra/Chain/Direct/Wallet.hs b/hydra-node/src/Hydra/Chain/Direct/Wallet.hs index 753e1c7ada7..8fab8658ac2 100644 --- a/hydra-node/src/Hydra/Chain/Direct/Wallet.hs +++ b/hydra-node/src/Hydra/Chain/Direct/Wallet.hs @@ -16,7 +16,7 @@ import Cardano.Ledger.Alonzo.Data (Data (Data)) import Cardano.Ledger.Alonzo.Language (Language (PlutusV1)) import Cardano.Ledger.Alonzo.PParams (PParams' (..)) import Cardano.Ledger.Alonzo.PlutusScriptApi (language) -import Cardano.Ledger.Alonzo.Scripts (ExUnits (..), txscriptfee) +import Cardano.Ledger.Alonzo.Scripts (ExUnits (..), Tag (Spend), txscriptfee) import Cardano.Ledger.Alonzo.Tools ( BasicFailure (..), ScriptFailure (..), @@ -168,13 +168,19 @@ type AlonzoPoint = Point (ShelleyBlock Era) -- -- It can sign transactions and keeps track of its UTXO behind the scene. data TinyWallet m = TinyWallet - { getUTxO :: STM m (Map TxIn TxOut) + { -- | Return all known UTxO addressed to this wallet. + getUTxO :: STM m (Map TxIn TxOut) , getAddress :: Address , sign :: ValidatedTx Era -> ValidatedTx Era , coverFee :: Map TxIn TxOut -> ValidatedTx Era -> STM m (Either ErrCoverFee (ValidatedTx Era)) , verificationKey :: VerificationKey PaymentKey } +-- | Get a single, marked as "fuel" UTxO. +getFuelUTxO :: MonadSTM m => TinyWallet m -> STM m (Maybe (TxIn, TxOut)) +getFuelUTxO TinyWallet{getUTxO} = + findFuelUTxO <$> getUTxO + watchUTxOUntil :: (Map TxIn TxOut -> Bool) -> TinyWallet IO -> IO (Map TxIn TxOut) watchUTxOUntil predicate TinyWallet{getUTxO} = atomically $ do u <- getUTxO @@ -345,16 +351,12 @@ coverFee_ pparams systemStart epochInfo lookupUTxO walletUTxO partialTx@Validate } ) where - findUTxOToPayFees utxo = case Map.lookupMax (Map.filter hasMarkerDatum utxo) of + findUTxOToPayFees utxo = case findFuelUTxO utxo of Nothing -> Left ErrNoPaymentUTxOFound Just (i, o) -> Right (i, o) - hasMarkerDatum :: TxOut -> Bool - hasMarkerDatum (TxOut _ _ dh) = - dh == SJust (hashData $ Data @Era markerDatum) - -- TODO: Do a better fee estimation based on the transaction's content. calculateNeedlesslyHighFee (Redeemers redeemers) = let executionCost = txscriptfee (_prices pparams) $ foldMap snd redeemers @@ -398,11 +400,14 @@ coverFee_ pparams systemStart epochInfo lookupUTxO walletUTxO partialTx@Validate sortedInputs = sort $ toList initialInputs sortedFinalInputs = sort $ toList finalInputs differences = List.findIndices (not . uncurry (==)) $ zip sortedInputs sortedFinalInputs - adjustOne (ptr@(RdmrPtr t idx), (d, _exUnits)) - | fromIntegral idx `elem` differences = - (RdmrPtr t (idx + 1), (d, executionUnitsFor ptr)) - | otherwise = - (ptr, (d, executionUnitsFor ptr)) + + adjustOne (ptr, (d, _exUnits)) = + case ptr of + RdmrPtr Spend idx + | fromIntegral idx `elem` differences -> + (RdmrPtr Spend (idx + 1), (d, executionUnitsFor ptr)) + _ -> + (ptr, (d, executionUnitsFor ptr)) executionUnitsFor :: RdmrPtr -> ExUnits executionUnitsFor ptr = @@ -413,6 +418,14 @@ coverFee_ pparams systemStart epochInfo lookupUTxO walletUTxO partialTx@Validate (floor (maxMem * approxMem % totalMem)) (floor (maxCpu * approxCpu % totalCpu)) +findFuelUTxO :: Map TxIn TxOut -> Maybe (TxIn, TxOut) +findFuelUTxO utxo = + Map.lookupMax (Map.filter hasMarkerDatum utxo) + where + hasMarkerDatum :: TxOut -> Bool + hasMarkerDatum (TxOut _ _ dh) = + dh == SJust (hashData $ Data @Era markerDatum) + -- | Estimate cost of script executions on the transaction. This is only an -- estimates because the transaction isn't sealed at this point and adding new -- elements to it like change outputs or script integrity hash may increase that diff --git a/hydra-node/src/Hydra/Ledger/Cardano.hs b/hydra-node/src/Hydra/Ledger/Cardano.hs index 38d6eab91e6..4d8eb9f1a6c 100644 --- a/hydra-node/src/Hydra/Ledger/Cardano.hs +++ b/hydra-node/src/Hydra/Ledger/Cardano.hs @@ -28,7 +28,6 @@ import qualified Cardano.Ledger.BaseTypes as Ledger import qualified Cardano.Ledger.Core as Ledger import qualified Cardano.Ledger.Credential as Ledger import qualified Cardano.Ledger.Crypto as Ledger (StandardCrypto) -import qualified Cardano.Ledger.Keys as Ledger import qualified Cardano.Ledger.Mary as Ledger.Mary hiding (Value) import qualified Cardano.Ledger.Shelley.API.Mempool as Ledger import qualified Cardano.Ledger.Shelley.Genesis as Ledger @@ -210,16 +209,20 @@ instance Arbitrary UTxO where -- * Generators +genSigningKey :: Gen (SigningKey PaymentKey) +genSigningKey = do + -- NOTE: not using 'genKeyDSIGN' purposely here, it is not pure and does not + -- play well with pure generation from seed. + sk <- fromJust . CC.rawDeserialiseSignKeyDSIGN . fromList <$> vectorOf 64 arbitrary + pure (PaymentSigningKey sk) + genVerificationKey :: Gen (VerificationKey PaymentKey) -genVerificationKey = fst <$> genKeyPair +genVerificationKey = getVerificationKey <$> genSigningKey genKeyPair :: Gen (VerificationKey PaymentKey, SigningKey PaymentKey) genKeyPair = do - -- NOTE: not using 'genKeyDSIGN' purposely here, it is not pure and does not - -- play well with pure generation from seed. - sk <- fromJust . CC.rawDeserialiseSignKeyDSIGN . fromList <$> vectorOf 64 arbitrary - let vk = CC.deriveVerKeyDSIGN sk - pure (PaymentVerificationKey (Ledger.VKey vk), PaymentSigningKey sk) + sk <- genSigningKey + pure (getVerificationKey sk, sk) -- TODO: Generate non-genesis transactions for better coverage. -- TODO: Enable Alonzo-specific features. We started off in the Mary era, and diff --git a/hydra-tui/test/Hydra/TUISpec.hs b/hydra-tui/test/Hydra/TUISpec.hs index c250a22398d..41f1c891ac6 100644 --- a/hydra-tui/test/Hydra/TUISpec.hs +++ b/hydra-tui/test/Hydra/TUISpec.hs @@ -4,14 +4,14 @@ import Hydra.Prelude import Test.Hydra.Prelude import Blaze.ByteString.Builder.Char8 (writeChar) -import CardanoClient (postSeedPayment, queryProtocolParameters) import CardanoCluster ( Actor (Alice), ClusterLog, - availableInitialFunds, + Marked (Fuel, Normal), defaultNetworkId, keysFor, newNodeConfig, + seedFromFaucet_, withBFTNode, writeKeysFor, ) @@ -77,7 +77,7 @@ spec = shouldRender "Initializing" sendInputEvent $ EvKey (KChar 'c') [] threadDelay 1 - shouldRender "900000000 lovelace" + shouldRender "42000000 lovelace" sendInputEvent $ EvKey (KChar ' ') [] sendInputEvent $ EvKey KEnter [] threadDelay 1 @@ -87,7 +87,7 @@ spec = shouldRender "Closed" threadDelay 10 -- contestation period shouldRender "Final" - shouldRender "900000000 lovelace" + shouldRender "42000000 lovelace" sendInputEvent $ EvKey (KChar 'q') [] setupNodeAndTUI :: (TUITest -> IO ()) -> IO () @@ -95,14 +95,17 @@ setupNodeAndTUI action = showLogsOnFailure $ \tracer -> withTempDir "tui-end-to-end" $ \tmpDir -> do config <- newNodeConfig tmpDir - (aliceCardanoVk, aliceCardanoSk) <- keysFor Alice - withBFTNode (contramap FromCardano tracer) config [aliceCardanoVk] $ \(RunningNode _ nodeSocket) -> do + (aliceCardanoVk, _) <- keysFor Alice + withBFTNode (contramap FromCardano tracer) config $ \node@(RunningNode _ nodeSocket) -> do (_, aliceSkPath) <- writeKeysFor tmpDir Alice -- XXX(SN): API port id is inferred from nodeId, in this case 4001 let nodeId = 1 - pparams <- queryProtocolParameters defaultNetworkId nodeSocket withHydraNode (contramap FromHydra tracer) aliceSkPath [] tmpDir nodeSocket nodeId aliceSk [] [nodeId] $ \HydraClient{hydraNodeId} -> do - postSeedPayment defaultNetworkId pparams availableInitialFunds nodeSocket aliceCardanoSk 900_000_000 + -- Fuel to pay hydra transactions + seedFromFaucet_ defaultNetworkId node aliceCardanoVk 100_000_000 Fuel + -- Some ADA to commit + seedFromFaucet_ defaultNetworkId node aliceCardanoVk 42_000_000 Normal + withTUITest (150, 10) $ \brickTest@TUITest{buildVty} -> do race_ ( runWithVty