From 0232d7e76335ce7d06e0cfe6c1563b535a7a2bff Mon Sep 17 00:00:00 2001 From: Jordan Millar Date: Mon, 5 Oct 2026 16:53:04 -0400 Subject: [PATCH 1/6] cardano-testnet: add test that hard forks a cluster into the Dijkstra era Add `hprop_hardfork_to_dijkstra` ("Hard fork to Dijkstra"), which starts the default three-node cluster in Conway at protocol version 10 and hard forks it through governance, the way mainnet will: 1. a HardForkInitiation action to PV 11.0 (Conway intra-era hard fork), voted for by the three default DReps and the default SPO, ratified and enacted; 2. a chained HardForkInitiation action to PV 12.0, voted, ratified and enacted. It then asserts that `query tip` and the ledger state served over node-to-client both report the Dijkstra era, and reuses the existing block-production and clean-shutdown check. Set `ExperimentalHardForksEnabled: true` in the generated node configuration for every era. Without it the node declares PV11 as its maximum protocol version, so once the ledger enacts PV12 it rejects its own blocks with `ObsoleteNode` and the chain stalls. The golden default node configuration is updated accordingly. Needs ouroboros-consensus >= 5.0. With 4.2.1.0 the governance part succeeds (PV 10 -> 11 -> 12 is enacted and the node keeps forging at PV12) but the cluster stays in Conway: `protocolInfoCardano` builds the Conway ledger config with `TriggerHardForkNotDuringThisExecution`, so the Dijkstra trigger is ignored (IntersectMBO/ouroboros-consensus#2167). --- ...dan.millar_dijkstra_cluster_start_test.yml | 5 + cardano-testnet/cardano-testnet.cabal | 1 + cardano-testnet/src/Testnet/Defaults.hs | 4 + .../files/golden/node_default_config.json | 1 + .../Cardano/Testnet/Test/Node/DijkstraEra.hs | 286 ++++++++++++++++++ .../cardano-testnet-test.hs | 2 + 6 files changed, 299 insertions(+) create mode 100644 cardano-testnet/.changes/20260929_120000_jordan.millar_dijkstra_cluster_start_test.yml create mode 100644 cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Node/DijkstraEra.hs diff --git a/cardano-testnet/.changes/20260929_120000_jordan.millar_dijkstra_cluster_start_test.yml b/cardano-testnet/.changes/20260929_120000_jordan.millar_dijkstra_cluster_start_test.yml new file mode 100644 index 00000000000..46d84c98577 --- /dev/null +++ b/cardano-testnet/.changes/20260929_120000_jordan.millar_dijkstra_cluster_start_test.yml @@ -0,0 +1,5 @@ +pr: 6709 +kind: + - test +description: | + Added the `Hard fork to Dijkstra` integration test. It starts a cluster in Conway at protocol version 10, then hard forks it through governance the way mainnet will: a `HardForkInitiation` action to PV 11.0, voted for by the default DReps and SPO and enacted, followed by a chained `HardForkInitiation` action to PV 12.0. It then asserts that `query tip` and the ledger state served over node-to-client both report the Dijkstra era before checking block production and clean shutdown. The generated node configuration now sets `ExperimentalHardForksEnabled: true` for every era: without it the node declares PV11 as its maximum protocol version and rejects its own blocks with `ObsoleteNode` once the ledger reaches PV12. diff --git a/cardano-testnet/cardano-testnet.cabal b/cardano-testnet/cardano-testnet.cabal index b674f7a1f25..777c55ce0e8 100644 --- a/cardano-testnet/cardano-testnet.cabal +++ b/cardano-testnet/cardano-testnet.cabal @@ -253,6 +253,7 @@ test-suite cardano-testnet-test Cardano.Testnet.Test.Rpc.SearchUtxos Cardano.Testnet.Test.Rpc.Transaction Cardano.Testnet.Test.Misc + Cardano.Testnet.Test.Node.DijkstraEra Cardano.Testnet.Test.Node.Shutdown Cardano.Testnet.Test.Parser Cardano.Testnet.Test.Manifest diff --git a/cardano-testnet/src/Testnet/Defaults.hs b/cardano-testnet/src/Testnet/Defaults.hs index df07b687965..12269972953 100644 --- a/cardano-testnet/src/Testnet/Defaults.hs +++ b/cardano-testnet/src/Testnet/Defaults.hs @@ -275,6 +275,10 @@ defaultYamlHardforkViaConfig sbe = hardforkViaConfig sbe' = Aeson.fromList $ ("ExperimentalProtocolsEnabled", Aeson.Bool True) + -- Dijkstra is still experimental. Without this flag the node declares PV11 as its + -- maximum protocol version, so it cannot forge once the ledger is at PV12 (its own + -- blocks are rejected with ObsoleteNode), and it ignores the Dijkstra genesis file. + : ("ExperimentalHardForksEnabled", Aeson.Bool True) : (case sbe' of ShelleyBasedEraShelley -> [ ("TestShelleyHardForkAtEpoch", Aeson.Number 0) ] diff --git a/cardano-testnet/test/cardano-testnet-golden/files/golden/node_default_config.json b/cardano-testnet/test/cardano-testnet-golden/files/golden/node_default_config.json index 9528440b7bc..d9def979826 100644 --- a/cardano-testnet/test/cardano-testnet-golden/files/golden/node_default_config.json +++ b/cardano-testnet/test/cardano-testnet-golden/files/golden/node_default_config.json @@ -3,6 +3,7 @@ "ByronGenesisFile": "byron-genesis.json", "ConwayGenesisFile": "conway-genesis.json", "DijkstraGenesisFile": "dijkstra-genesis.json", + "ExperimentalHardForksEnabled": true, "ExperimentalProtocolsEnabled": true, "LastKnownBlockVersion-Alt": 0, "LastKnownBlockVersion-Major": 9, diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Node/DijkstraEra.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Node/DijkstraEra.hs new file mode 100644 index 00000000000..617707e3710 --- /dev/null +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Node/DijkstraEra.hs @@ -0,0 +1,286 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE GADTs #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} + +module Cardano.Testnet.Test.Node.DijkstraEra + ( hprop_hardfork_to_dijkstra + ) where + +import Cardano.Api +import Cardano.Api.Experimental (Some (..)) +import Cardano.Api.Ledger (EpochInterval (..)) + +import qualified Cardano.Ledger.BaseTypes as L +import qualified Cardano.Ledger.Core as L +import Cardano.Testnet +import Cardano.Testnet.Test.Utils (nodesProduceBlocks) + +import Prelude + +import Control.Monad +import Control.Monad.Catch (MonadCatch) +import qualified Data.Aeson as A +import qualified Data.Aeson.KeyMap as KM +import qualified Data.ByteString.Lazy.Char8 as B +import Data.Default.Class (def) +import qualified Data.List.NonEmpty as NEL +import qualified Data.Text as Text +import Data.Type.Equality ((:~:) (..)) +import Data.Word (Word16) +import GHC.Stack +import Lens.Micro +import Numeric.Natural (Natural) +import System.Directory (makeAbsolute) +import System.FilePath (()) + +import Test.Cardano.CLI.Hash (serveFilesWhile) +import Testnet.Components.Query +import Testnet.Defaults (defaultDRepKeyPair, defaultSpoColdKeyPair, defaultSpoKeys) +import Testnet.Process.Cli.DRep (generateVoteFiles) +import Testnet.Process.Cli.Keys (cliStakeAddressKeyGen) +import qualified Testnet.Process.Cli.SPO as SPO +import Testnet.Process.Cli.SPO (createStakeKeyRegistrationCertificate) +import Testnet.Process.Cli.Transaction (retrieveTransactionId, signTx, submitTx) +import Testnet.Process.Run (addEnvVarsToConfig, execCli', mkExecConfig) +import Testnet.Process.RunIO (liftIOAnnotated) +import Testnet.Property.Assert (assertErasEqual) +import Testnet.Property.Util (integrationRetryWorkspace) +import Testnet.Start.Types +import Testnet.Types + +import Hedgehog (Property) +import qualified Hedgehog as H +import qualified Hedgehog.Extras as H +import Hedgehog.Internal.Property (MonadTest) + +-- | Starts a cluster in Conway at protocol version 10 and hard forks it into the +-- Dijkstra era through governance, the same way mainnet will: +-- +-- 1. a @HardForkInitiation@ action to PV 11.0 (Conway intra-era hard fork), +-- voted for by the default DReps and SPO, ratified and enacted; +-- 2. a second @HardForkInitiation@ action to PV 12.0, chained to the first one, +-- voted, ratified and enacted; +-- +-- and then checks that: +-- +-- * @cardano-cli query tip@ reports the Dijkstra era over node-to-client, +-- * the ledger state served by the node is a Dijkstra 'NewEpochState', +-- * the SPO node keeps producing blocks and shuts down cleanly. +-- +-- Execute me with: +-- @DISABLE_RETRIES=1 cabal test cardano-testnet-test --test-options '-p "/Hard fork to Dijkstra/"'@ +hprop_hardfork_to_dijkstra :: Property +hprop_hardfork_to_dijkstra = integrationRetryWorkspace 2 "hardfork-to-dijkstra" $ \tempAbsBasePath' -> H.runWithDefaultWatchdog_ $ do + conf@Conf { tempAbsPath } <- mkConf tempAbsBasePath' + let tempAbsPath' = unTmpAbsPath tempAbsPath + tempBaseAbsPath = makeTmpBaseAbsPath tempAbsPath + + work <- H.createDirectoryIfMissing $ tempAbsPath' "work" + + let ceo = ConwayEraOnwardsConway + sbe = convert ceo + era = toCardanoEra sbe + eraName = eraToString era + creationOptions = def + { creationEra = AnyShelleyBasedEra sbe + , creationGenesisOptions = def + { genesisEpochLength = 300 + , genesisActiveSlotsCoeff = 0.3 + } + } + + runtime@TestnetRuntime + { testnetMagic + , testnetNodes + , wallets = wallet0:wallet1:_ + , configurationFile + } <- createAndRunTestnet creationOptions def conf + + let node@TestnetNode{nodeSprocket} = NEL.head testnetNodes + socketPath = nodeSocketPath node + execConfig <- mkExecConfig tempBaseAbsPath nodeSprocket testnetMagic + epochStateView <- getEpochStateView configurationFile socketPath + + H.note_ "the cluster starts in Conway at protocol version 10" + startPv <- getProtocolMajorVersion epochStateView ceo + startPv H.=== 10 + + gov <- H.createDirectoryIfMissing $ work "governance" + + -- Register a stake address to receive the governance action deposits back. + let stakeCertFp = gov "stake.regcert" + stakeKeys = KeyPair { verificationKey = File $ gov "stake.vkey" + , signingKey = File $ gov "stake.skey" + } + cliStakeAddressKeyGen stakeKeys + keyDeposit <- getKeyDeposit epochStateView ceo + createStakeKeyRegistrationCertificate + tempAbsPath (AnyShelleyBasedEra sbe) (verificationKey stakeKeys) keyDeposit stakeCertFp + + stakeCertTxBodyFp <- H.note $ gov "stake.registration.txbody" + stakeCertTxSignedFp <- H.note $ gov "stake.registration.tx" + txin1 <- findLargestUtxoForPaymentKey epochStateView sbe wallet1 + + void $ execCli' execConfig + [ eraName, "transaction", "build" + , "--change-address", Text.unpack $ paymentKeyInfoAddr wallet1 + , "--tx-in", Text.unpack $ renderTxIn txin1 + , "--tx-out", Text.unpack (paymentKeyInfoAddr wallet0) <> "+" <> show @Int 10_000_000 + , "--certificate-file", stakeCertFp + , "--witness-override", show @Int 2 + , "--out-file", stakeCertTxBodyFp + ] + void $ execCli' execConfig + [ eraName, "transaction", "sign" + , "--tx-body-file", stakeCertTxBodyFp + , "--signing-key-file", signingKeyFp $ paymentKeyInfoPair wallet1 + , "--signing-key-file", signingKeyFp stakeKeys + , "--out-file", stakeCertTxSignedFp + ] + void $ execCli' execConfig + [ eraName, "transaction", "submit" + , "--tx-file", stakeCertTxSignedFp + ] + _ <- waitForBlocks epochStateView 1 + + H.note_ "hard fork 10.0 -> 11.0 (Conway intra-era hard fork)" + hf11 <- proposeAndEnactHardFork execConfig epochStateView ceo (gov "hardfork-11") + Nothing 11 stakeKeys wallet0 + + H.note_ "hard fork 11.0 -> 12.0 (Conway -> Dijkstra)" + _ <- proposeAndEnactHardFork execConfig epochStateView ceo (gov "hardfork-12") + (Just hf11) 12 stakeKeys wallet0 + + H.note_ "query tip reports the Dijkstra era" + tipStr <- H.noteM $ execCli' execConfig [ "query", "tip", "--output-json" ] + tip :: A.Object <- H.nothingFail $ A.decode $ B.pack tipStr + KM.lookup "era" tip H.=== Just (A.String "Dijkstra") + + H.note_ "the ledger state served by the node is in the Dijkstra era" + AnyNewEpochState actualEra _ _ <- getEpochState epochStateView + Refl <- H.leftFail $ assertErasEqual ShelleyBasedEraDijkstra actualEra + + nodesProduceBlocks tempAbsBasePath' runtime + +-- | Submit a @HardForkInitiation@ governance action to the given major protocol +-- version (minor version 0), vote @yes@ on it with the three default DReps and +-- the default SPO, and wait until the ledger's protocol parameters report the +-- new major version. Returns the action's transaction id and index so that the +-- next hard fork action can be chained to it. +proposeAndEnactHardFork + :: (HasCallStack, MonadTest m, MonadIO m, MonadCatch m, H.MonadAssertion m, H.MonadBaseControl IO m) + => H.ExecConfig + -> EpochStateView + -> ConwayEraOnwards ConwayEra + -> FilePath -- ^ Working directory for this hard fork's files + -> Maybe (TxId, Word16) -- ^ Previous @HardForkInitiation@ action, if any + -> Natural -- ^ Target major protocol version + -> KeyPair StakeKey -- ^ Registered stake key that receives the deposit back + -> PaymentKeyInfo -- ^ Wallet paying for the transactions + -> m (TxId, Word16) +proposeAndEnactHardFork execConfig epochStateView ceo work prevAction targetMajor stakeKeys wallet = withFrozenCallStack $ do + let sbe = convert ceo + era = toCardanoEra sbe + cEra = AnyCardanoEra era + eraName = eraToString era + + baseDir <- H.createDirectoryIfMissing work + + let proposalAnchorDataIpfsHash = "QmexFJuEn5RtnHEqpxDcqrazdHPzAwe7zs2RxHLfMH5gBz" + proposalAnchorFile <- H.noteM $ liftIOAnnotated $ makeAbsolute $ + "test" "cardano-testnet-test" "files" "sample-proposal-anchor" + proposalAnchorDataHash <- execCli' execConfig + [ "hash", "anchor-data", "--file-binary", proposalAnchorFile ] + + govActionDeposit <- getMinGovActionDeposit epochStateView ceo + proposalFile <- H.note $ baseDir "hardfork.action" + proposalBody <- H.note $ baseDir "proposal.txbody" + + -- The CLI verifies the anchor against its URL, so serve the anchor file from a + -- temporary HTTP server standing in for an IPFS gateway. + serveFilesWhile + [(["ipfs", proposalAnchorDataIpfsHash], proposalAnchorFile)] + ( \port -> do + let execConfig' = addEnvVarsToConfig execConfig + [("IPFS_GATEWAY_URI", "http://localhost:" ++ show port ++ "/")] + void $ execCli' execConfig' $ + [ eraName, "governance", "action", "create-hardfork" + , "--testnet" + , "--governance-action-deposit", show govActionDeposit + , "--deposit-return-stake-verification-key-file", verificationKeyFp stakeKeys + ] ++ concatMap (\(prevTxId, prevIx) -> + [ "--prev-governance-action-tx-id", prettyShow prevTxId + , "--prev-governance-action-index", show prevIx + ]) prevAction ++ + [ "--anchor-url", "ipfs://" ++ proposalAnchorDataIpfsHash + , "--anchor-data-hash", proposalAnchorDataHash + , "--check-anchor-data" + , "--protocol-major-version", show targetMajor + , "--protocol-minor-version", "0" + , "--out-file", proposalFile + ] + + -- `transaction build` re-verifies the proposal's anchor, so it must also run + -- while the gateway stand-in is up. + txIn <- findLargestUtxoForPaymentKey epochStateView sbe wallet + void $ execCli' execConfig' + [ eraName, "transaction", "build" + , "--change-address", Text.unpack $ paymentKeyInfoAddr wallet + , "--tx-in", Text.unpack $ renderTxIn txIn + , "--proposal-file", proposalFile + , "--out-file", proposalBody + ] + ) + signedProposalTx <- signTx execConfig cEra baseDir "signed-proposal" + (File proposalBody) [Some $ paymentKeyInfoPair wallet] + submitTx execConfig cEra signedProposalTx + govActionTxId <- H.noteShowM $ retrieveTransactionId execConfig signedProposalTx + + govActionIndex <- retryUntilJustM epochStateView (WaitForEpochs (EpochInterval 2)) $ + maybeExtractGovernanceActionIndex govActionTxId <$> getEpochState epochStateView + + -- Hard fork initiation needs both the DRep and the SPO thresholds to be met. + drepVotes <- generateVoteFiles execConfig baseDir "drep-votes" govActionTxId govActionIndex + [ (defaultDRepKeyPair n, "yes") | n <- [1..3] ] + spoVotes <- SPO.generateVoteFiles ceo execConfig baseDir "spo-votes" govActionTxId govActionIndex + [ (defaultSpoKeys 1, "yes") ] + let voteFiles = drepVotes ++ spoVotes + + voteTxBodyFp <- H.note $ baseDir "votes.txbody" + voteTxIn <- findLargestUtxoForPaymentKey epochStateView sbe wallet + void $ execCli' execConfig $ + [ eraName, "transaction", "build" + , "--change-address", Text.unpack $ paymentKeyInfoAddr wallet + , "--tx-in", Text.unpack $ renderTxIn voteTxIn + ] ++ concat [ ["--vote-file", voteFile] | File voteFile <- voteFiles ] ++ + [ "--witness-override", show (length voteFiles + 1) + , "--out-file", voteTxBodyFp + ] + signedVoteTx <- signTx execConfig cEra baseDir "signed-votes" (File voteTxBodyFp) $ + Some (paymentKeyInfoPair wallet) + : Some (defaultSpoColdKeyPair 1) + : [ Some (defaultDRepKeyPair n) | n <- [1..3] ] + submitTx execConfig cEra signedVoteTx + + -- Ratification happens at the next epoch boundary and enactment at the one after. + H.note_ $ "waiting for protocol major version " <> show targetMajor + _ <- retryUntilM epochStateView (WaitForEpochs (EpochInterval 4)) + (getProtocolMajorVersion epochStateView ceo) + (== targetMajor) + + pure (govActionTxId, govActionIndex) + +-- | The major protocol version currently in the ledger's protocol parameters. +getProtocolMajorVersion + :: (HasCallStack, MonadTest m, MonadIO m, H.MonadAssertion m) + => EpochStateView + -> ConwayEraOnwards era + -> m Natural +getProtocolMajorVersion epochStateView ceo = withFrozenCallStack $ do + LedgerProtocolParameters pp <- getProtocolParams epochStateView ceo + pure $ conwayEraOnwardsConstraints ceo $ L.getVersion $ L.pvMajor $ pp ^. L.ppProtocolVersionL diff --git a/cardano-testnet/test/cardano-testnet-test/cardano-testnet-test.hs b/cardano-testnet/test/cardano-testnet-test/cardano-testnet-test.hs index 955728500db..4c5f21a4f23 100644 --- a/cardano-testnet/test/cardano-testnet-test/cardano-testnet-test.hs +++ b/cardano-testnet/test/cardano-testnet-test/cardano-testnet-test.hs @@ -35,6 +35,7 @@ import qualified Cardano.Testnet.Test.Gov.TreasuryDonation as Gov import qualified Cardano.Testnet.Test.Gov.TreasuryWithdrawal as Gov import qualified Cardano.Testnet.Test.MainnetParams import qualified Cardano.Testnet.Test.Manifest +import qualified Cardano.Testnet.Test.Node.DijkstraEra import qualified Cardano.Testnet.Test.Node.Shutdown import qualified Cardano.Testnet.Test.Parser import qualified Cardano.Testnet.Test.Rpc.EraSummary @@ -147,6 +148,7 @@ tests = do , T.testGroup "Cardano-testnet" [ ignoreOnWindows "Produces blocks" Cardano.Testnet.Test.RunTestnet.hprop_run_testnet , ignoreOnMacAndWindows "Enable Tracer" Cardano.Testnet.Test.EnableTracer.hprop_enable_tracer + , ignoreOnWindows "Hard fork to Dijkstra" Cardano.Testnet.Test.Node.DijkstraEra.hprop_hardfork_to_dijkstra , ignoreOnMacAndWindows "Supports dumping/loading config files" Cardano.Testnet.Test.DumpConfig.hprop_dump_config , ignoreOnMacAndWindows "Can have its start time modified" Cardano.Testnet.Test.UpdateTimeStamps.hprop_update_time_stamps , ignoreOnMacAndWindows "Can get on-chain parameters from blockfrost files" Cardano.Testnet.Test.MainnetParams.hprop_mainnet_params From f1b08ca5a1692524782738a248cd56634322147c Mon Sep 17 00:00:00 2001 From: Jordan Millar Date: Mon, 5 Oct 2026 16:53:04 -0400 Subject: [PATCH 2/6] cardano-testnet: add nested transaction swap test in Dijkstra Add `hprop_nested_transaction_swap` ("Nested transaction swap"). It mints 100 TokenA to Alice in Conway and funds Bob, hard forks the cluster into Dijkstra through governance (shared with the "Hard fork to Dijkstra" test), then settles a multi-asset swap with a nested transaction built with `cardano-cli dijkstra transaction sub-transaction`: * Alice's sub-transaction spends her 100 TokenA and pays herself 53 ADA (50 ADA price + the 3 ADA that travelled with the tokens); * Bob's sub-transaction spends 60 ADA and pays himself the 100 TokenA plus 7 ADA change, i.e. exactly 50 ADA; * the batcher's top-level transaction embeds both signed sub-transactions and pays the fee. Each sub-transaction is guarded by and signed with its owner's key only. The proof is read back from `query utxo`: Bob holds the 100 TokenA, Alice gained exactly 50 ADA, Bob lost exactly 50 ADA, the batcher paid the fee. The test needs a cardano-cli with the sub-transaction commands (IntersectMBO/cardano-cli#1453), pointed at via CARDANO_CLI, and cardano-ledger-dijkstra >= 0.4: before IntersectMBO/cardano-ledger#6129, UTxO-HD key fetching through `allInputsTxBodyF` did not load sub-transaction inputs and the ledger rejected the nested transaction with SubBadInputsUTxO. --- ...dan.millar_dijkstra_cluster_start_test.yml | 2 +- cardano-testnet/cardano-testnet.cabal | 1 + .../Test/Dijkstra/NestedTransaction.hs | 310 ++++++++++++++++++ .../Cardano/Testnet/Test/Node/DijkstraEra.hs | 114 ++++--- .../cardano-testnet-test.hs | 2 + 5 files changed, 391 insertions(+), 38 deletions(-) create mode 100644 cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/NestedTransaction.hs diff --git a/cardano-testnet/.changes/20260929_120000_jordan.millar_dijkstra_cluster_start_test.yml b/cardano-testnet/.changes/20260929_120000_jordan.millar_dijkstra_cluster_start_test.yml index 46d84c98577..5849e25c2a1 100644 --- a/cardano-testnet/.changes/20260929_120000_jordan.millar_dijkstra_cluster_start_test.yml +++ b/cardano-testnet/.changes/20260929_120000_jordan.millar_dijkstra_cluster_start_test.yml @@ -2,4 +2,4 @@ pr: 6709 kind: - test description: | - Added the `Hard fork to Dijkstra` integration test. It starts a cluster in Conway at protocol version 10, then hard forks it through governance the way mainnet will: a `HardForkInitiation` action to PV 11.0, voted for by the default DReps and SPO and enacted, followed by a chained `HardForkInitiation` action to PV 12.0. It then asserts that `query tip` and the ledger state served over node-to-client both report the Dijkstra era before checking block production and clean shutdown. The generated node configuration now sets `ExperimentalHardForksEnabled: true` for every era: without it the node declares PV11 as its maximum protocol version and rejects its own blocks with `ObsoleteNode` once the ledger reaches PV12. + Added two Dijkstra integration tests. `Hard fork to Dijkstra` starts a cluster in Conway at protocol version 10 and hard forks it through governance the way mainnet will: a `HardForkInitiation` action to PV 11.0, voted for by the default DReps and SPO and enacted, followed by a chained `HardForkInitiation` action to PV 12.0, then asserts that the node reports the Dijkstra era. `Nested transaction swap` additionally mints a native token in Conway and, once in Dijkstra, settles a multi-asset swap with a nested transaction: two independently signed sub-transactions (100 TokenA for 50 ADA) embedded in a top-level transaction signed only by the batcher, checked by reading the resulting UTxO set back from the node. It needs a `cardano-cli` with the `dijkstra transaction sub-transaction` commands (`CARDANO_CLI`). The generated node configuration now sets `ExperimentalHardForksEnabled: true` for every era, since without it the node declares PV11 as its maximum and rejects its own blocks with `ObsoleteNode` once the ledger reaches PV12. diff --git a/cardano-testnet/cardano-testnet.cabal b/cardano-testnet/cardano-testnet.cabal index 777c55ce0e8..482ec8838be 100644 --- a/cardano-testnet/cardano-testnet.cabal +++ b/cardano-testnet/cardano-testnet.cabal @@ -226,6 +226,7 @@ test-suite cardano-testnet-test Cardano.Testnet.Test.Cli.Transaction.BuildEstimate Cardano.Testnet.Test.Cli.Transaction.RegisterDeregisterStakeAddress Cardano.Testnet.Test.Cli.Transaction.WithdrawalReward + Cardano.Testnet.Test.Dijkstra.NestedTransaction Cardano.Testnet.Test.DumpConfig Cardano.Testnet.Test.EnableTracer Cardano.Testnet.Test.FoldEpochState diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/NestedTransaction.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/NestedTransaction.hs new file mode 100644 index 00000000000..6100b9514de --- /dev/null +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/NestedTransaction.hs @@ -0,0 +1,310 @@ +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE TypeApplications #-} + +module Cardano.Testnet.Test.Dijkstra.NestedTransaction + ( hprop_nested_transaction_swap + ) where + +import Cardano.Api + +import Cardano.Testnet +import Cardano.Testnet.Test.Node.DijkstraEra (assertNodeInDijkstra, hardForkToDijkstra) + +import Prelude + +import Control.Monad +import Control.Monad.Catch (MonadCatch) +import qualified Data.Aeson as A +import qualified Data.Aeson.Key as K +import qualified Data.Aeson.KeyMap as KM +import qualified Data.Aeson.Types as A +import qualified Data.ByteString.Lazy.Char8 as B +import Data.Default.Class (def) +import Data.List (find, sortOn) +import qualified Data.List.NonEmpty as NEL +import Data.Ord (Down (..)) +import qualified Data.Text as Text +import GHC.Stack +import System.Exit (ExitCode (..)) +import System.FilePath (()) + +import Testnet.Components.Query +import Testnet.Defaults (simpleScript) +import Testnet.Process.Run (execCli', execCliAny, mkExecConfig) +import Testnet.Property.Util (integrationRetryWorkspace) +import Testnet.Start.Types +import Testnet.Types + +import Hedgehog (Property) +import qualified Hedgehog as H +import qualified Hedgehog.Extras as H +import Hedgehog.Internal.Property (MonadTest) + +-- | A multi-asset swap settled with a Dijkstra nested transaction. +-- +-- Setup (Conway): the batcher wallet mints 100 @TokenA@ under a @sig@ simple script +-- and pays them to Alice, and pays Bob a 60 ADA UTxO. The cluster is then hard forked +-- into Dijkstra through governance. +-- +-- Swap (Dijkstra): Alice offers her 100 @TokenA@ for 50 ADA, i.e. 0.5 ADA per token. +-- +-- * Alice's sub-transaction spends her token UTxO and pays herself 53 ADA (the 50 ADA +-- price plus the 3 ADA that accompanied the tokens). It is guarded by, and signed +-- with, Alice's key only. +-- * Bob's sub-transaction spends his 60 ADA UTxO and pays himself the 100 @TokenA@ +-- (with 3 ADA) plus 7 ADA change, so he pays exactly 50 ADA. It is guarded by, and +-- signed with, Bob's key only. +-- * The batcher's top-level transaction embeds both signed sub-transactions, adds its +-- own input to pay the fee, and is signed by the batcher only. +-- +-- Neither sub-transaction balances on its own; the ledger only accepts the top-level +-- transaction because value is conserved across the batch. So if the transaction is +-- accepted, the exchange rate was honoured. The test then checks the resulting UTxO +-- set: Bob holds the 100 @TokenA@, Alice holds 50 ADA more, Bob 50 ADA less, and the +-- batcher paid the fee. +-- +-- Requires a @cardano-cli@ with the @dijkstra transaction sub-transaction@ commands +-- (IntersectMBO/cardano-cli#1453); point @CARDANO_CLI@ at it. +-- +-- Needs cardano-ledger-dijkstra >= 0.4: before that, UTxO-HD key fetching through +-- @allInputsTxBodyF@ did not load sub-transaction inputs and the ledger rejected the +-- nested transaction with @SubBadInputsUTxO@ (IntersectMBO/cardano-ledger#6129). +-- +-- Execute me with: +-- @DISABLE_RETRIES=1 cabal test cardano-testnet-test --test-options '-p "/Nested transaction swap/"'@ +hprop_nested_transaction_swap :: Property +hprop_nested_transaction_swap = integrationRetryWorkspace 2 "nested-transaction-swap" $ \tempAbsBasePath' -> H.runWithDefaultWatchdog_ $ do + conf@Conf { tempAbsPath } <- mkConf tempAbsBasePath' + let tempAbsPath' = unTmpAbsPath tempAbsPath + tempBaseAbsPath = makeTmpBaseAbsPath tempAbsPath + + work <- H.createDirectoryIfMissing $ tempAbsPath' "work" + + let creationOptions = def + { creationEra = AnyShelleyBasedEra ShelleyBasedEraConway + , creationGenesisOptions = def + { genesisEpochLength = 300 + , genesisActiveSlotsCoeff = 0.3 + } + } + + TestnetRuntime + { testnetMagic + , testnetNodes + , wallets = batcher:alice:bob:_ + , configurationFile + } <- createAndRunTestnet creationOptions def conf + + let node@TestnetNode{nodeSprocket} = NEL.head testnetNodes + socketPath = nodeSocketPath node + execConfig <- mkExecConfig tempBaseAbsPath nodeSprocket testnetMagic + epochStateView <- getEpochStateView configurationFile socketPath + + let batcherAddr = Text.unpack $ paymentKeyInfoAddr batcher + aliceAddr = Text.unpack $ paymentKeyInfoAddr alice + bobAddr = Text.unpack $ paymentKeyInfoAddr bob + + ------------------------------------------------------------------------------ + H.note_ "Conway: mint 100 TokenA to Alice and fund Bob with a 60 ADA UTxO" + ------------------------------------------------------------------------------ + setupDir <- H.createDirectoryIfMissing $ work "setup" + + batcherKeyHash <- keyHash execConfig (paymentKeyInfoPair batcher) + mintingScriptFp <- H.note $ setupDir "tokenA-policy.json" + H.writeFile mintingScriptFp $ Text.unpack $ simpleScript $ Text.pack batcherKeyHash + policyId <- filter (/= '\n') <$> execCli' execConfig + [ "conway", "transaction", "policyid", "--script-file", mintingScriptFp ] + let assetName = "546f6b656e41" -- "TokenA", lowercase hex as the CLI renders it in query output + tokenA n = show @Integer n <> " " <> policyId <> "." <> assetName + + setupTxIn <- findLargestUtxoForPaymentKey epochStateView ShelleyBasedEraConway batcher + setupBodyFp <- H.note $ setupDir "setup.txbody" + setupTxFp <- H.note $ setupDir "setup.tx" + void $ execCli' execConfig + [ "conway", "transaction", "build" + , "--change-address", batcherAddr + , "--tx-in", Text.unpack $ renderTxIn setupTxIn + , "--mint", tokenA 100 + , "--mint-script-file", mintingScriptFp + , "--tx-out", aliceAddr <> "+" <> show @Integer tokensAda <> "+" <> tokenA 100 + , "--tx-out", bobAddr <> "+" <> show @Integer bobFunds + , "--out-file", setupBodyFp + ] + void $ execCli' execConfig + [ "conway", "transaction", "sign" + , "--tx-body-file", setupBodyFp + , "--signing-key-file", signingKeyFp $ paymentKeyInfoPair batcher + , "--out-file", setupTxFp + ] + void $ execCli' execConfig [ "conway", "transaction", "submit", "--tx-file", setupTxFp ] + _ <- waitForBlocks epochStateView 2 + + aliceUtxosBefore <- queryUtxos execConfig policyId assetName aliceAddr + H.assertWith aliceUtxosBefore $ any ((== 100) . ueTokens) + + ------------------------------------------------------------------------------ + H.note_ "Hard fork into Dijkstra through governance" + ------------------------------------------------------------------------------ + hardForkToDijkstra execConfig epochStateView tempAbsPath work batcher alice + assertNodeInDijkstra execConfig + + ------------------------------------------------------------------------------ + H.note_ "Dijkstra: settle the swap with a nested transaction" + ------------------------------------------------------------------------------ + -- The sub-transaction commands only exist in cardano-cli with IntersectMBO/cardano-cli#1453. + -- Fail with a pointer rather than an option-parser error if the CLI in use lacks them. + subTxHelp <- execCliAny execConfig [ "dijkstra", "transaction", "sub-transaction", "--help" ] + case subTxHelp of + (ExitSuccess, _, _) -> pure () + _ -> H.failMessage callStack $ unlines + [ "The cardano-cli in use has no `dijkstra transaction sub-transaction` commands." + , "Point CARDANO_CLI at a build of IntersectMBO/cardano-cli#1453 (branch jordan/basic-sub-tx-integration)." + ] + swapDir <- H.createDirectoryIfMissing $ work "swap" + + aliceBefore <- queryUtxos execConfig policyId assetName aliceAddr + bobBefore <- queryUtxos execConfig policyId assetName bobAddr + batcherBefore <- queryUtxos execConfig policyId assetName batcherAddr + + aliceTokenUtxo <- H.nothingFail $ find ((== 100) . ueTokens) aliceBefore + bobAdaUtxo <- H.nothingFail $ find (\u -> ueLovelace u == bobFunds && ueTokens u == 0) bobBefore + batcherUtxo <- H.nothingFail $ listToMaybeHead $ sortOn (Down . ueLovelace) batcherBefore + + aliceKeyHash <- keyHash execConfig (paymentKeyInfoPair alice) + bobKeyHash <- keyHash execConfig (paymentKeyInfoPair bob) + + H.note_ "Alice's sub-transaction: 100 TokenA out, 50 ADA in (0.5 ADA per token)" + aliceSubUnsigned <- H.note $ swapDir "alice.sub.unsigned" + aliceSubSigned <- H.note $ swapDir "alice.sub.signed" + void $ execCli' execConfig + [ "dijkstra", "transaction", "sub-transaction", "build-raw" + , "--tx-in", ueTxIn aliceTokenUtxo + , "--tx-out", aliceAddr <> "+" <> show @Integer (tokensAda + price) + , "--guard-key-hash", aliceKeyHash + , "--out-file", aliceSubUnsigned + ] + void $ execCli' execConfig + [ "dijkstra", "transaction", "sub-transaction", "sign" + , "--sub-tx-file", aliceSubUnsigned + , "--signing-key-file", signingKeyFp $ paymentKeyInfoPair alice + , "--out-file", aliceSubSigned + ] + + H.note_ "Bob's sub-transaction: 50 ADA out, 100 TokenA in" + bobSubUnsigned <- H.note $ swapDir "bob.sub.unsigned" + bobSubSigned <- H.note $ swapDir "bob.sub.signed" + void $ execCli' execConfig + [ "dijkstra", "transaction", "sub-transaction", "build-raw" + , "--tx-in", ueTxIn bobAdaUtxo + , "--tx-out", bobAddr <> "+" <> show @Integer tokensAda <> "+" <> tokenA 100 + , "--tx-out", bobAddr <> "+" <> show @Integer (bobFunds - price - tokensAda) + , "--guard-key-hash", bobKeyHash + , "--out-file", bobSubUnsigned + ] + void $ execCli' execConfig + [ "dijkstra", "transaction", "sub-transaction", "sign" + , "--sub-tx-file", bobSubUnsigned + , "--signing-key-file", signingKeyFp $ paymentKeyInfoPair bob + , "--out-file", bobSubSigned + ] + + H.note_ "Batcher's top-level transaction embedding both sub-transactions and paying the fee" + topBodyFp <- H.note $ swapDir "swap.txbody" + topTxFp <- H.note $ swapDir "swap.tx" + void $ execCli' execConfig + [ "dijkstra", "transaction", "build-raw" + , "--tx-in", ueTxIn batcherUtxo + , "--tx-out", batcherAddr <> "+" <> show @Integer (ueLovelace batcherUtxo - fee) + , "--fee", show @Integer fee + , "--sub-transaction", aliceSubSigned + , "--sub-transaction", bobSubSigned + , "--out-file", topBodyFp + ] + void $ execCli' execConfig + [ "dijkstra", "transaction", "sign" + , "--tx-body-file", topBodyFp + , "--signing-key-file", signingKeyFp $ paymentKeyInfoPair batcher + , "--testnet-magic", show testnetMagic + , "--out-file", topTxFp + ] + H.noteM_ $ execCli' execConfig [ "dijkstra", "transaction", "submit", "--tx-file", topTxFp ] + _ <- waitForBlocks epochStateView 2 + + ------------------------------------------------------------------------------ + H.note_ "Proof: Bob holds the 100 TokenA and 50 ADA moved from Bob to Alice" + ------------------------------------------------------------------------------ + aliceAfter <- queryUtxos execConfig policyId assetName aliceAddr + bobAfter <- queryUtxos execConfig policyId assetName bobAddr + batcherAfter <- queryUtxos execConfig policyId assetName batcherAddr + + H.note_ "Bob received all the TokenA" + totalTokens bobAfter H.=== 100 + totalTokens aliceAfter H.=== 0 + + H.note_ "Alice was paid the asking price and Bob paid it" + totalLovelace aliceAfter H.=== totalLovelace aliceBefore + price + totalLovelace bobAfter H.=== totalLovelace bobBefore - price + + H.note_ "the batcher paid only the fee" + totalLovelace batcherAfter H.=== totalLovelace batcherBefore - fee + where + bobFunds, tokensAda, price, fee :: Integer + bobFunds = 60_000_000 -- Bob's UTxO to pay from + tokensAda = 3_000_000 -- ADA travelling with the tokens (covers the min-UTxO value) + price = 50_000_000 -- 100 TokenA at 0.5 ADA each + fee = 1_000_000 -- paid by the batcher; comfortably above the minimum fee + + listToMaybeHead :: [a] -> Maybe a + listToMaybeHead (x:_) = Just x + listToMaybeHead [] = Nothing + +-- | One entry of @cardano-cli query utxo --output-json@, reduced to what the swap cares about. +data UtxoEntry = UtxoEntry + { ueTxIn :: String + , ueLovelace :: Integer + , ueTokens :: Integer -- ^ quantity of the swap's token in this UTxO + } deriving Show + +totalLovelace, totalTokens :: [UtxoEntry] -> Integer +totalLovelace = sum . map ueLovelace +totalTokens = sum . map ueTokens + +-- | Query the UTxOs at an address and extract lovelace and the given asset's quantity. +queryUtxos + :: (HasCallStack, MonadTest m, MonadIO m, MonadCatch m) + => H.ExecConfig + -> String -- ^ Policy id of the asset to track + -> String -- ^ Hex-encoded asset name + -> String -- ^ Address + -> m [UtxoEntry] +queryUtxos execConfig policyId assetName addr = withFrozenCallStack $ do + out <- execCli' execConfig [ "query", "utxo", "--address", addr, "--output-json" ] + utxoJson <- H.leftFail $ A.eitherDecode $ B.pack out + H.leftFail $ A.parseEither (utxoEntries policyId assetName) utxoJson + +utxoEntries :: String -> String -> A.Value -> A.Parser [UtxoEntry] +utxoEntries policyId assetName = A.withObject "utxo set" $ \o -> + forM (KM.toList o) $ \(txIn, entry) -> + flip (A.withObject "utxo entry") entry $ \e -> do + value <- e A..: "value" + lovelace <- value A..: "lovelace" + mPolicy <- value A..:? K.fromString policyId + tokens <- case mPolicy of + Nothing -> pure 0 + Just policy -> policy A..:? K.fromString assetName A..!= 0 + pure UtxoEntry { ueTxIn = K.toString txIn, ueLovelace = lovelace, ueTokens = tokens } + +-- | Hash of a payment verification key, as accepted by @--guard-key-hash@. +keyHash + :: (HasCallStack, MonadTest m, MonadIO m, MonadCatch m) + => H.ExecConfig + -> KeyPair PaymentKey + -> m String +keyHash execConfig keys = withFrozenCallStack $ + filter (/= '\n') <$> execCli' execConfig + [ "address", "key-hash", "--payment-verification-key-file", verificationKeyFp keys ] diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Node/DijkstraEra.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Node/DijkstraEra.hs index 617707e3710..92d98d24c13 100644 --- a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Node/DijkstraEra.hs +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Node/DijkstraEra.hs @@ -9,6 +9,9 @@ module Cardano.Testnet.Test.Node.DijkstraEra ( hprop_hardfork_to_dijkstra + , hardForkToDijkstra + , getProtocolMajorVersion + , assertNodeInDijkstra ) where import Cardano.Api @@ -17,6 +20,7 @@ import Cardano.Api.Ledger (EpochInterval (..)) import qualified Cardano.Ledger.BaseTypes as L import qualified Cardano.Ledger.Core as L +import qualified Cardano.Ledger.Shelley.LedgerState as L import Cardano.Testnet import Cardano.Testnet.Test.Utils (nodesProduceBlocks) @@ -30,7 +34,6 @@ import qualified Data.ByteString.Lazy.Char8 as B import Data.Default.Class (def) import qualified Data.List.NonEmpty as NEL import qualified Data.Text as Text -import Data.Type.Equality ((:~:) (..)) import Data.Word (Word16) import GHC.Stack import Lens.Micro @@ -48,7 +51,6 @@ import Testnet.Process.Cli.SPO (createStakeKeyRegistrationCertificate) import Testnet.Process.Cli.Transaction (retrieveTransactionId, signTx, submitTx) import Testnet.Process.Run (addEnvVarsToConfig, execCli', mkExecConfig) import Testnet.Process.RunIO (liftIOAnnotated) -import Testnet.Property.Assert (assertErasEqual) import Testnet.Property.Util (integrationRetryWorkspace) import Testnet.Start.Types import Testnet.Types @@ -82,10 +84,7 @@ hprop_hardfork_to_dijkstra = integrationRetryWorkspace 2 "hardfork-to-dijkstra" work <- H.createDirectoryIfMissing $ tempAbsPath' "work" - let ceo = ConwayEraOnwardsConway - sbe = convert ceo - era = toCardanoEra sbe - eraName = eraToString era + let sbe = ShelleyBasedEraConway creationOptions = def { creationEra = AnyShelleyBasedEra sbe , creationGenesisOptions = def @@ -106,8 +105,37 @@ hprop_hardfork_to_dijkstra = integrationRetryWorkspace 2 "hardfork-to-dijkstra" execConfig <- mkExecConfig tempBaseAbsPath nodeSprocket testnetMagic epochStateView <- getEpochStateView configurationFile socketPath + hardForkToDijkstra execConfig epochStateView tempAbsPath work wallet0 wallet1 + + assertNodeInDijkstra execConfig + + H.note_ "the ledger state served by the node is a Dijkstra NewEpochState" + AnyNewEpochState sbe' _ _ <- getEpochState epochStateView + shelleyBasedEraConstraints sbe' $ + AnyShelleyBasedEra sbe' H.=== AnyShelleyBasedEra ShelleyBasedEraDijkstra + + nodesProduceBlocks tempAbsBasePath' runtime + +-- | Take a cluster that was started in Conway at protocol version 10 into the +-- Dijkstra era through governance: register a stake key to receive the action +-- deposits, then propose, vote and enact a hard fork to PV 11.0 (Conway +-- intra-era) followed by a chained hard fork to PV 12.0 (Dijkstra). +hardForkToDijkstra + :: (HasCallStack, MonadTest m, MonadIO m, MonadCatch m, H.MonadAssertion m, H.MonadBaseControl IO m) + => H.ExecConfig + -> EpochStateView + -> TmpAbsolutePath + -> FilePath -- ^ Working directory + -> PaymentKeyInfo -- ^ Wallet paying for the governance actions and votes + -> PaymentKeyInfo -- ^ Wallet paying for the stake key registration + -> m () +hardForkToDijkstra execConfig epochStateView tempAbsPath work wallet0 wallet1 = withFrozenCallStack $ do + let ceo = ConwayEraOnwardsConway + sbe = convert ceo + eraName = eraToString $ toCardanoEra sbe + H.note_ "the cluster starts in Conway at protocol version 10" - startPv <- getProtocolMajorVersion epochStateView ceo + startPv <- getProtocolMajorVersion epochStateView startPv H.=== 10 gov <- H.createDirectoryIfMissing $ work "governance" @@ -149,30 +177,25 @@ hprop_hardfork_to_dijkstra = integrationRetryWorkspace 2 "hardfork-to-dijkstra" _ <- waitForBlocks epochStateView 1 H.note_ "hard fork 10.0 -> 11.0 (Conway intra-era hard fork)" - hf11 <- proposeAndEnactHardFork execConfig epochStateView ceo (gov "hardfork-11") + hf11 <- proposeAndVoteHardFork execConfig epochStateView ceo (gov "hardfork-11") Nothing 11 stakeKeys wallet0 + H.note_ "waiting for protocol major version 11" + _ <- retryUntilM epochStateView (WaitForEpochs (EpochInterval 4)) + (getProtocolMajorVersion epochStateView) (== 11) H.note_ "hard fork 11.0 -> 12.0 (Conway -> Dijkstra)" - _ <- proposeAndEnactHardFork execConfig epochStateView ceo (gov "hardfork-12") + _ <- proposeAndVoteHardFork execConfig epochStateView ceo (gov "hardfork-12") (Just hf11) 12 stakeKeys wallet0 - - H.note_ "query tip reports the Dijkstra era" - tipStr <- H.noteM $ execCli' execConfig [ "query", "tip", "--output-json" ] - tip :: A.Object <- H.nothingFail $ A.decode $ B.pack tipStr - KM.lookup "era" tip H.=== Just (A.String "Dijkstra") - - H.note_ "the ledger state served by the node is in the Dijkstra era" - AnyNewEpochState actualEra _ _ <- getEpochState epochStateView - Refl <- H.leftFail $ assertErasEqual ShelleyBasedEraDijkstra actualEra - - nodesProduceBlocks tempAbsBasePath' runtime + H.note_ "waiting for protocol major version 12" + void $ retryUntilM epochStateView (WaitForEpochs (EpochInterval 4)) + (getProtocolMajorVersion epochStateView) (== 12) -- | Submit a @HardForkInitiation@ governance action to the given major protocol --- version (minor version 0), vote @yes@ on it with the three default DReps and --- the default SPO, and wait until the ledger's protocol parameters report the --- new major version. Returns the action's transaction id and index so that the --- next hard fork action can be chained to it. -proposeAndEnactHardFork +-- version (minor version 0) and vote @yes@ on it with the three default DReps and +-- the default SPO. Ratification happens at the next epoch boundary and enactment +-- at the one after; the caller waits for it. Returns the action's transaction id +-- and index so that the next hard fork action can be chained to it. +proposeAndVoteHardFork :: (HasCallStack, MonadTest m, MonadIO m, MonadCatch m, H.MonadAssertion m, H.MonadBaseControl IO m) => H.ExecConfig -> EpochStateView @@ -183,7 +206,7 @@ proposeAndEnactHardFork -> KeyPair StakeKey -- ^ Registered stake key that receives the deposit back -> PaymentKeyInfo -- ^ Wallet paying for the transactions -> m (TxId, Word16) -proposeAndEnactHardFork execConfig epochStateView ceo work prevAction targetMajor stakeKeys wallet = withFrozenCallStack $ do +proposeAndVoteHardFork execConfig epochStateView ceo work prevAction targetMajor stakeKeys wallet = withFrozenCallStack $ do let sbe = convert ceo era = toCardanoEra sbe cEra = AnyCardanoEra era @@ -267,20 +290,37 @@ proposeAndEnactHardFork execConfig epochStateView ceo work prevAction targetMajo : [ Some (defaultDRepKeyPair n) | n <- [1..3] ] submitTx execConfig cEra signedVoteTx - -- Ratification happens at the next epoch boundary and enactment at the one after. - H.note_ $ "waiting for protocol major version " <> show targetMajor - _ <- retryUntilM epochStateView (WaitForEpochs (EpochInterval 4)) - (getProtocolMajorVersion epochStateView ceo) - (== targetMajor) - pure (govActionTxId, govActionIndex) --- | The major protocol version currently in the ledger's protocol parameters. +-- | The major protocol version currently in the ledger's protocol parameters, +-- read from whatever era the node is in. This must not fix the era, because the +-- version being awaited is the one whose enactment moves the node into Dijkstra. getProtocolMajorVersion :: (HasCallStack, MonadTest m, MonadIO m, H.MonadAssertion m) => EpochStateView - -> ConwayEraOnwards era -> m Natural -getProtocolMajorVersion epochStateView ceo = withFrozenCallStack $ do - LedgerProtocolParameters pp <- getProtocolParams epochStateView ceo - pure $ conwayEraOnwardsConstraints ceo $ L.getVersion $ L.pvMajor $ pp ^. L.ppProtocolVersionL +getProtocolMajorVersion epochStateView = withFrozenCallStack $ do + AnyNewEpochState sbe nes _ <- getEpochState epochStateView + pure $ shelleyBasedEraConstraints sbe $ + L.getVersion $ L.pvMajor $ nes ^. L.nesEsL . L.curPParamsEpochStateL . L.ppProtocolVersionL + +-- | Assert that the node reports the Dijkstra era over node-to-client. +-- +-- Needs ouroboros-consensus >= 5.0: earlier versions enact protocol version 12 but +-- stay in Conway, because 'protocolInfoCardano' builds the Conway ledger config with +-- 'TriggerHardForkNotDuringThisExecution' and ignores the Dijkstra trigger. +assertNodeInDijkstra + :: (HasCallStack, MonadTest m, MonadIO m, MonadCatch m) + => H.ExecConfig + -> m () +assertNodeInDijkstra execConfig = withFrozenCallStack $ do + H.note_ "query tip reports the Dijkstra era" + tipStr <- H.noteM $ execCli' execConfig [ "query", "tip", "--output-json" ] + tip :: A.Object <- H.nothingFail $ A.decode $ B.pack tipStr + case KM.lookup "era" tip of + Just (A.String "Dijkstra") -> pure () + other -> H.failMessage callStack $ unlines + [ "The node is not in the Dijkstra era after protocol version 12 was enacted: " <> show other + , "The pinned ouroboros-consensus does not hard fork out of Conway (the Conway ledger config" + , "uses TriggerHardForkNotDuringThisExecution); ouroboros-consensus >= 5.0 is needed." + ] diff --git a/cardano-testnet/test/cardano-testnet-test/cardano-testnet-test.hs b/cardano-testnet/test/cardano-testnet-test/cardano-testnet-test.hs index 4c5f21a4f23..de4245bff53 100644 --- a/cardano-testnet/test/cardano-testnet-test/cardano-testnet-test.hs +++ b/cardano-testnet/test/cardano-testnet-test/cardano-testnet-test.hs @@ -20,6 +20,7 @@ import qualified Cardano.Testnet.Test.Cli.Transaction import qualified Cardano.Testnet.Test.Cli.Transaction.BuildEstimate import qualified Cardano.Testnet.Test.Cli.Transaction.RegisterDeregisterStakeAddress import qualified Cardano.Testnet.Test.Cli.Transaction.WithdrawalReward +import qualified Cardano.Testnet.Test.Dijkstra.NestedTransaction import qualified Cardano.Testnet.Test.DumpConfig import qualified Cardano.Testnet.Test.EnableTracer import qualified Cardano.Testnet.Test.FoldEpochState @@ -149,6 +150,7 @@ tests = do [ ignoreOnWindows "Produces blocks" Cardano.Testnet.Test.RunTestnet.hprop_run_testnet , ignoreOnMacAndWindows "Enable Tracer" Cardano.Testnet.Test.EnableTracer.hprop_enable_tracer , ignoreOnWindows "Hard fork to Dijkstra" Cardano.Testnet.Test.Node.DijkstraEra.hprop_hardfork_to_dijkstra + , ignoreOnWindows "Nested transaction swap" Cardano.Testnet.Test.Dijkstra.NestedTransaction.hprop_nested_transaction_swap , ignoreOnMacAndWindows "Supports dumping/loading config files" Cardano.Testnet.Test.DumpConfig.hprop_dump_config , ignoreOnMacAndWindows "Can have its start time modified" Cardano.Testnet.Test.UpdateTimeStamps.hprop_update_time_stamps , ignoreOnMacAndWindows "Can get on-chain parameters from blockfrost files" Cardano.Testnet.Test.MainnetParams.hprop_mainnet_params From 9f6784f8c5814b323c6ac903f4fe6b380431af0d Mon Sep 17 00:00:00 2001 From: Jordan Millar Date: Wed, 30 Sep 2026 09:47:57 -0400 Subject: [PATCH 3/6] cardano-testnet: drop guards from the nested transaction swap sub-transactions Spending a key-locked input already requires the owner's witness on the sub-transaction, so the --guard-key-hash options added nothing. --- .../Testnet/Test/Dijkstra/NestedTransaction.hs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/NestedTransaction.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/NestedTransaction.hs index 6100b9514de..c82bd1d9c7f 100644 --- a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/NestedTransaction.hs +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/NestedTransaction.hs @@ -54,11 +54,11 @@ import Hedgehog.Internal.Property (MonadTest) -- Swap (Dijkstra): Alice offers her 100 @TokenA@ for 50 ADA, i.e. 0.5 ADA per token. -- -- * Alice's sub-transaction spends her token UTxO and pays herself 53 ADA (the 50 ADA --- price plus the 3 ADA that accompanied the tokens). It is guarded by, and signed --- with, Alice's key only. +-- price plus the 3 ADA that accompanied the tokens). It is signed with Alice's key +-- only, which spending her input already requires; no guards are needed. -- * Bob's sub-transaction spends his 60 ADA UTxO and pays himself the 100 @TokenA@ --- (with 3 ADA) plus 7 ADA change, so he pays exactly 50 ADA. It is guarded by, and --- signed with, Bob's key only. +-- (with 3 ADA) plus 7 ADA change, so he pays exactly 50 ADA. It is signed with Bob's +-- key only. -- * The batcher's top-level transaction embeds both signed sub-transactions, adds its -- own input to pay the fee, and is signed by the batcher only. -- @@ -175,9 +175,6 @@ hprop_nested_transaction_swap = integrationRetryWorkspace 2 "nested-transaction- bobAdaUtxo <- H.nothingFail $ find (\u -> ueLovelace u == bobFunds && ueTokens u == 0) bobBefore batcherUtxo <- H.nothingFail $ listToMaybeHead $ sortOn (Down . ueLovelace) batcherBefore - aliceKeyHash <- keyHash execConfig (paymentKeyInfoPair alice) - bobKeyHash <- keyHash execConfig (paymentKeyInfoPair bob) - H.note_ "Alice's sub-transaction: 100 TokenA out, 50 ADA in (0.5 ADA per token)" aliceSubUnsigned <- H.note $ swapDir "alice.sub.unsigned" aliceSubSigned <- H.note $ swapDir "alice.sub.signed" @@ -185,7 +182,6 @@ hprop_nested_transaction_swap = integrationRetryWorkspace 2 "nested-transaction- [ "dijkstra", "transaction", "sub-transaction", "build-raw" , "--tx-in", ueTxIn aliceTokenUtxo , "--tx-out", aliceAddr <> "+" <> show @Integer (tokensAda + price) - , "--guard-key-hash", aliceKeyHash , "--out-file", aliceSubUnsigned ] void $ execCli' execConfig @@ -203,7 +199,6 @@ hprop_nested_transaction_swap = integrationRetryWorkspace 2 "nested-transaction- , "--tx-in", ueTxIn bobAdaUtxo , "--tx-out", bobAddr <> "+" <> show @Integer tokensAda <> "+" <> tokenA 100 , "--tx-out", bobAddr <> "+" <> show @Integer (bobFunds - price - tokensAda) - , "--guard-key-hash", bobKeyHash , "--out-file", bobSubUnsigned ] void $ execCli' execConfig @@ -299,7 +294,7 @@ utxoEntries policyId assetName = A.withObject "utxo set" $ \o -> Just policy -> policy A..:? K.fromString assetName A..!= 0 pure UtxoEntry { ueTxIn = K.toString txIn, ueLovelace = lovelace, ueTokens = tokens } --- | Hash of a payment verification key, as accepted by @--guard-key-hash@. +-- | Hash of a payment verification key, used in the @sig@ minting policy. keyHash :: (HasCallStack, MonadTest m, MonadIO m, MonadCatch m) => H.ExecConfig From 4ca8ee50ff993532b815a533aa7c6f771232fbfd Mon Sep 17 00:00:00 2001 From: Jordan Millar Date: Wed, 14 Oct 2026 12:26:35 -0400 Subject: [PATCH 4/6] cardano-testnet: cover Dijkstra sub-transactions and top-level guards Add basic native and Plutus V4 script, staking, governance and top-level guard properties. Exercise direct and reference witnesses and missing required guards. Use cardano-api 11.9 and pin cardano-cli PR #1453 for the sub-transaction commands. --- .../plutus-scripts-bench.cabal | 2 +- bench/tx-generator/tx-generator.cabal | 2 +- cabal.project | 15 +- cardano-node/cardano-node.cabal | 2 +- cardano-submit-api/cardano-submit-api.cabal | 2 +- ...dan.millar_dijkstra_cluster_start_test.yml | 10 +- cardano-testnet/cardano-testnet.cabal | 8 +- .../Cardano/Testnet/Test/Dijkstra/Common.hs | 322 ++++++++++++++++++ .../Testnet/Test/Dijkstra/Governance.hs | 64 ++++ .../Test/Dijkstra/NestedTransaction.hs | 20 +- .../Cardano/Testnet/Test/Dijkstra/Scripts.hs | 98 ++++++ .../Cardano/Testnet/Test/Dijkstra/Staking.hs | 58 ++++ .../Testnet/Test/Dijkstra/TopLevelGuards.hs | 187 ++++++++++ .../cardano-testnet-test.hs | 11 + flake.lock | 6 +- 15 files changed, 786 insertions(+), 21 deletions(-) create mode 100644 cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/Common.hs create mode 100644 cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/Governance.hs create mode 100644 cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/Scripts.hs create mode 100644 cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/Staking.hs create mode 100644 cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/TopLevelGuards.hs diff --git a/bench/plutus-scripts-bench/plutus-scripts-bench.cabal b/bench/plutus-scripts-bench/plutus-scripts-bench.cabal index 9d81a5b16a0..290e072a6f4 100644 --- a/bench/plutus-scripts-bench/plutus-scripts-bench.cabal +++ b/bench/plutus-scripts-bench/plutus-scripts-bench.cabal @@ -86,7 +86,7 @@ library -- IOG dependencies -------------------------- build-depends: - , cardano-api ^>=11.7 + , cardano-api ^>=11.9 , plutus-ledger-api ^>=1.65 , plutus-tx ^>=1.65 , plutus-tx-plugin ^>=1.65 diff --git a/bench/tx-generator/tx-generator.cabal b/bench/tx-generator/tx-generator.cabal index 8bdb56229f5..a5b6ced8b47 100644 --- a/bench/tx-generator/tx-generator.cabal +++ b/bench/tx-generator/tx-generator.cabal @@ -116,7 +116,7 @@ library , attoparsec-aeson , base16-bytestring , bytestring - , cardano-api ^>= 11.8 + , cardano-api ^>= 11.9 , cardano-binary , cardano-cli ^>= 11.3 , cardano-crypto-class diff --git a/cabal.project b/cabal.project index 112983b348f..82e0e3bc38f 100644 --- a/cabal.project +++ b/cabal.project @@ -14,7 +14,7 @@ repository cardano-haskell-packages -- you need to run if you change them index-state: , hackage.haskell.org 2026-09-02T14:05:07Z - , cardano-haskell-packages 2026-10-09T14:30:01Z + , cardano-haskell-packages 2026-10-13T14:57:01Z constraints: -- haskell.nix patch does not work for 1.6.8 @@ -70,8 +70,6 @@ package plutus-scripts-bench haddock-options: "--optghc=-fplugin-opt=PlutusTx.Plugin:defer-errors" allow-newer: - -- plutus-scripts-bench bounds cardano-api ^>=11.5 but we have 11.6 - , plutus-scripts-bench:cardano-api -- plutus-scripts-bench bounds plutus-* ^>=1.65 but we have 1.68 , plutus-scripts-bench:plutus-ledger-api , plutus-scripts-bench:plutus-tx @@ -149,3 +147,14 @@ if impl(ghc >= 9.14) -- IMPORTANT -- Do NOT add more source-repository-package stanzas here unless they are strictly -- temporary! Please read the section in CONTRIBUTING about updating dependencies. + +-- TEMPORARY: cardano-cli with the `dijkstra sub-transaction` commands +-- (IntersectMBO/cardano-cli#1453), needed by the Dijkstra integration properties. +-- Remove once #1453 is merged and released. +source-repository-package + type: git + location: https://github.com/IntersectMBO/cardano-cli + tag: 55cff1ebdd619551c3a270e95fe18701d89a5bf7 + --sha256: sha256-JT1HY+RvpVv5/AAYg3nisU6Klm1iE0tizyya4zJ7Zqg= + subdir: + cardano-cli diff --git a/cardano-node/cardano-node.cabal b/cardano-node/cardano-node.cabal index 4556a52116b..243e57f030f 100644 --- a/cardano-node/cardano-node.cabal +++ b/cardano-node/cardano-node.cabal @@ -126,7 +126,7 @@ library , async , base16-bytestring , bytestring - , cardano-api ^>= 11.8 + , cardano-api ^>= 11.9 , cardano-config ^>= 2.2 , cardano-crypto-class ^>=2.6 , cardano-crypto-wrapper diff --git a/cardano-submit-api/cardano-submit-api.cabal b/cardano-submit-api/cardano-submit-api.cabal index edfe51791ec..839ed21eec9 100644 --- a/cardano-submit-api/cardano-submit-api.cabal +++ b/cardano-submit-api/cardano-submit-api.cabal @@ -39,7 +39,7 @@ library , aeson , async , bytestring - , cardano-api ^>= 11.8 + , cardano-api ^>= 11.9 , cardano-binary , cardano-cli ^>= 11.3 , cardano-crypto-class ^>=2.6 diff --git a/cardano-testnet/.changes/20260929_120000_jordan.millar_dijkstra_cluster_start_test.yml b/cardano-testnet/.changes/20260929_120000_jordan.millar_dijkstra_cluster_start_test.yml index 5849e25c2a1..cf07d251eea 100644 --- a/cardano-testnet/.changes/20260929_120000_jordan.millar_dijkstra_cluster_start_test.yml +++ b/cardano-testnet/.changes/20260929_120000_jordan.millar_dijkstra_cluster_start_test.yml @@ -2,4 +2,12 @@ pr: 6709 kind: - test description: | - Added two Dijkstra integration tests. `Hard fork to Dijkstra` starts a cluster in Conway at protocol version 10 and hard forks it through governance the way mainnet will: a `HardForkInitiation` action to PV 11.0, voted for by the default DReps and SPO and enacted, followed by a chained `HardForkInitiation` action to PV 12.0, then asserts that the node reports the Dijkstra era. `Nested transaction swap` additionally mints a native token in Conway and, once in Dijkstra, settles a multi-asset swap with a nested transaction: two independently signed sub-transactions (100 TokenA for 50 ADA) embedded in a top-level transaction signed only by the batcher, checked by reading the resulting UTxO set back from the node. It needs a `cardano-cli` with the `dijkstra transaction sub-transaction` commands (`CARDANO_CLI`). The generated node configuration now sets `ExperimentalHardForksEnabled: true` for every era, since without it the node declares PV11 as its maximum and rejects its own blocks with `ObsoleteNode` once the ledger reaches PV12. + Added Dijkstra integration properties covering governance-driven hard forks from + Conway through protocol versions 11 and 12, a nested transaction token swap, + sub-transaction guards, minting and spending, staking, and governance. Native + and Plutus V4 script cases exercise direct and reference witnesses. Top-level + guard properties cover key, native and Plutus V4 requirements, including + missing-guard rejection and recovery. The generated node configuration enables + experimental hard forks so nodes can forge after reaching protocol version 12. + The tests use the cardano-cli pinned in cabal.project to PR #1453 for the + `dijkstra sub-transaction` commands. diff --git a/cardano-testnet/cardano-testnet.cabal b/cardano-testnet/cardano-testnet.cabal index 482ec8838be..ff36a13a80c 100644 --- a/cardano-testnet/cardano-testnet.cabal +++ b/cardano-testnet/cardano-testnet.cabal @@ -41,7 +41,7 @@ library , annotated-exception , ansi-terminal , bytestring - , cardano-api ^>= 11.8 + , cardano-api ^>= 11.9 , cardano-cli:{cardano-cli, cardano-cli-test-lib} ^>= 11.3 , cardano-crypto-class ^>=2.6 , cardano-crypto-wrapper @@ -226,7 +226,12 @@ test-suite cardano-testnet-test Cardano.Testnet.Test.Cli.Transaction.BuildEstimate Cardano.Testnet.Test.Cli.Transaction.RegisterDeregisterStakeAddress Cardano.Testnet.Test.Cli.Transaction.WithdrawalReward + Cardano.Testnet.Test.Dijkstra.Common + Cardano.Testnet.Test.Dijkstra.Governance Cardano.Testnet.Test.Dijkstra.NestedTransaction + Cardano.Testnet.Test.Dijkstra.Scripts + Cardano.Testnet.Test.Dijkstra.Staking + Cardano.Testnet.Test.Dijkstra.TopLevelGuards Cardano.Testnet.Test.DumpConfig Cardano.Testnet.Test.EnableTracer Cardano.Testnet.Test.FoldEpochState @@ -278,6 +283,7 @@ test-suite cardano-testnet-test , cardano-crypto-class , cardano-ledger-binary , cardano-ledger-conway + , cardano-ledger-dijkstra , cardano-ledger-core , cardano-ledger-shelley , cardano-prelude diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/Common.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/Common.hs new file mode 100644 index 00000000000..ff8edd1adf3 --- /dev/null +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/Common.hs @@ -0,0 +1,322 @@ +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE OverloadedStrings #-} + +-- | Small CLI helpers shared by the on-chain Dijkstra smoke properties. +module Cardano.Testnet.Test.Dijkstra.Common where + +import Cardano.Api hiding (label) +import qualified Cardano.Api.Experimental as Exp +import qualified Cardano.Api.Ledger as L +import Cardano.Testnet +import Cardano.Testnet.Test.Dijkstra.NestedTransaction (UtxoEntry (..), queryUtxos) +import Cardano.Testnet.Test.Node.DijkstraEra (assertNodeInDijkstra, hardForkToDijkstra) + +import Control.Monad (void) +import qualified Data.Aeson as A +import qualified Data.Aeson.Key as K +import qualified Data.Aeson.KeyMap as KM +import qualified Data.Aeson.Types as A +import qualified Data.ByteString.Char8 as BS +import qualified Data.ByteString.Lazy.Char8 as B +import Data.Default.Class (def) +import Data.List (maximumBy) +import qualified Data.List as List +import qualified Data.List.NonEmpty as NEL +import qualified Data.Map.Strict as Map +import Data.Ord (comparing) +import qualified Data.Set as Set +import qualified Data.Text as Text +import Lens.Micro ((^.)) +import System.FilePath (()) + +import Testnet.Components.Configuration (getShelleyGenesisHash) +import Testnet.Components.Query +import Testnet.Defaults (defaultGenesisFilepath) +import Testnet.Process.Run (execCli, execCli', mkExecConfig) +import Testnet.Property.Util (integrationRetryWorkspace) +import Testnet.Start.Cardano (liftToIntegration) +import Testnet.Start.Types +import Testnet.Types + +import Hedgehog (Property) +import qualified Hedgehog as H +import qualified Hedgehog.Extras as H + +-- The first wallet pays top-level fees and deposits; the second owns sub-inputs. +-- Keeping them separate also tests that sub-witnesses survive embedding. +data DijkstraTest = DijkstraTest + { exec :: H.ExecConfig + , epochView :: EpochStateView + , directory :: FilePath + , payer :: PaymentKeyInfo + , owner :: PaymentKeyInfo + , collateralWallet :: PaymentKeyInfo + , protocolParams :: FilePath + , collateral :: String + , v4Script :: FilePath + , testNetwork :: NetworkId + } + +data ScriptSource = ViaFile | ViaReference deriving (Eq, Show) + +-- | The same one-argument always-succeeding V4 program used by the CLI tests. +-- It intentionally tests witness plumbing, not business logic in the validator. +writeV4 :: FilePath -> H.Integration () +writeV4 path = H.writeFile path + "{\"type\":\"PlutusScriptV4\",\"description\":\"Always succeeds\",\"cborHex\":\"46450101002499\"}" + +withDijkstra :: String -> Bool -> (DijkstraTest -> H.Integration ()) -> Property +withDijkstra name guardrail action = integrationRetryWorkspace 2 name $ \tmp -> H.runWithDefaultWatchdog_ $ do + conf@Conf{tempAbsPath} <- mkConf tmp + work <- H.createDirectoryIfMissing $ tmp "work" + let script = work "always-succeeds-v4.plutus" + options = def + { creationEra = AnyShelleyBasedEra ShelleyBasedEraConway + , creationGenesisOptions = def { genesisEpochLength = 300, genesisActiveSlotsCoeff = 0.3 } + } + writeV4 script + liftToIntegration $ createTestnetEnv options conf + -- Set the constitution once, before startup, rather than enacting a separate + -- constitution in each governance scenario. Hard forks do not run guardrails. + if guardrail then do + guardrailHash <- trim <$> execCli ["hash", "script", "--script-file", script] + let genesis = tmp defaultGenesisFilepath ConwayEra + constitution <- H.readJsonFileOk genesis >>= field "constitution" + H.rewriteJsonFile genesis $ KM.insert "constitution" + (A.Object $ KM.insert "script" (A.String $ Text.pack guardrailHash) constitution) + hash <- getShelleyGenesisHash genesis "ConwayGenesisHash" + H.rewriteJsonFile (tmp "configuration.yaml") (hash <>) + else pure () + runtime <- liftToIntegration $ cardanoTestnet (creationNodes options) def conf + let node = NEL.head $ testnetNodes runtime + config <- mkExecConfig (makeTmpBaseAbsPath tempAbsPath) (nodeSprocket node) (testnetMagic runtime) + view <- getEpochStateView (configurationFile runtime) (nodeSocketPath node) + (batcher:alice:bob:_) <- pure $ wallets runtime + hardForkToDijkstra config view tempAbsPath work batcher alice + assertNodeInDijkstra config + let pp = work "protocol-parameters.json" + void $ execCli' config ["query", "protocol-parameters", "--out-file", pp] + -- A dedicated collateral output is never selected as an ordinary input. + let ctx = DijkstraTest config view work batcher alice bob pp "" script + (Testnet $ NetworkMagic $ fromIntegral $ testnetMagic runtime) + txid <- fund ctx "collateral" [(address bob, 50_000_000, [])] + action ctx{collateral = txid <> "#0"} + +trim :: String -> String +trim = filter (`notElem` ['\n', '\r']) + +address :: PaymentKeyInfo -> String +address = Text.unpack . paymentKeyInfoAddr + +skey :: PaymentKeyInfo -> FilePath +skey = signingKeyFp . paymentKeyInfoPair + +cli :: DijkstraTest -> [String] -> H.Integration String +cli ctx = execCli' (exec ctx) + +queryJson :: DijkstraTest -> [String] -> H.Integration A.Value +queryJson ctx args = cli ctx (["dijkstra", "query"] ++ args) >>= H.leftFail . A.eitherDecode . B.pack + +field :: A.FromJSON a => String -> A.Value -> H.Integration a +field key = H.leftFail . A.parseEither (A.withObject key (A..: K.fromString key)) + +parameter :: DijkstraTest -> String -> H.Integration Integer +parameter ctx key = H.readJsonFileOk (protocolParams ctx) >>= field key + +largest :: DijkstraTest -> PaymentKeyInfo -> H.Integration UtxoEntry +largest ctx wallet = do + entries <- queryUtxos (exec ctx) "" "" (address wallet) + H.assert $ not $ null entries + pure $ maximumBy (comparing ueLovelace) entries + +-- | Wait for a particular output, not merely for the next block. +waitTx :: DijkstraTest -> String -> H.Integration () +waitTx ctx txid = void $ retryUntilM (epochView ctx) (WaitForBlocks 10) + (queryJson ctx ["utxo", "--tx-in", txid <> "#0", "--output-json"]) + (\v -> case v of A.Object o -> not (KM.null o); _ -> False) + +assertSpent :: DijkstraTest -> String -> H.Integration () +assertSpent ctx input = do + value <- queryJson ctx ["utxo", "--tx-in", input, "--output-json"] + value H.=== A.Object mempty + +assertReference :: DijkstraTest -> String -> H.Integration () +assertReference ctx input = do + value <- queryJson ctx ["utxo", "--tx-in", input, "--output-json"] + H.assert $ case value of A.Object o -> KM.member (K.fromString input) o; _ -> False + +fee :: Integer +fee = 5_000_000 + +-- | Ordinary key-only setup transaction. Return its confirmed id. +fund :: DijkstraTest -> String -> [(String, Integer, [String])] -> H.Integration String +fund ctx label outputs = do + input <- largest ctx (payer ctx) + let body = directory ctx label <> ".body" + amount = ueLovelace input - sum [n | (_, n, _) <- outputs] - fee + H.assert $ amount > 0 + void $ cli ctx $ + ["dijkstra", "transaction", "build-raw", "--tx-in", ueTxIn input] + ++ concat [["--tx-out", addr <> "+" <> show n] ++ extras | (addr, n, extras) <- outputs] + ++ ["--tx-out", address (payer ctx) <> "+" <> show amount, "--fee", show fee, "--out-file", body] + signSubmit ctx body [skey $ payer ctx] + +signSubmit :: DijkstraTest -> FilePath -> [FilePath] -> H.Integration String +signSubmit ctx body keys = do + signed <- signBody ctx body keys + submitSigned ctx signed + +signBody :: DijkstraTest -> FilePath -> [FilePath] -> H.Integration FilePath +signBody ctx body keys = do + let signed = body <> ".signed" + void $ cli ctx $ ["dijkstra", "transaction", "sign", "--tx-body-file", body] + ++ concatMap (\key -> ["--signing-key-file", key]) keys ++ ["--out-file", signed] + pure signed + +submitSigned :: DijkstraTest -> FilePath -> H.Integration String +submitSigned ctx signed = do + txid <- trim <$> cli ctx ["dijkstra", "transaction", "txid", "--tx-file", signed, "--output-text"] + void $ cli ctx ["dijkstra", "transaction", "submit", "--tx-file", signed] + waitTx ctx txid + pure txid + +-- | Build/sign a sub-transaction and embed it in a separately signed top-level +-- transaction. Balance adjustment is withdrawals/refunds minus deposits; the +-- caller balances the sub-inputs, outputs and mint separately. +submitSub :: DijkstraTest -> String -> [String] -> [FilePath] -> Integer -> H.Integration String +submitSub ctx label args keys adjustment = do + H.note_ $ "Submit sub-transaction: " <> label + signed <- buildSignedSub ctx label args keys + input <- largest ctx (payer ctx) + body <- buildOuter ctx label signed input adjustment [] + txid <- signSubmit ctx body [skey $ payer ctx, skey $ collateralWallet ctx] + -- The batcher's confirmed change also checks deposit/refund/withdrawal accounting. + changeOutput <- queryJson ctx ["utxo", "--tx-in", txid <> "#0", "--output-json"] >>= field (txid <> "#0") + actual <- field "value" changeOutput >>= field "lovelace" + actual H.=== ueLovelace input - fee + adjustment + assertSpent ctx (ueTxIn input) + pure txid + +buildSignedSub :: DijkstraTest -> String -> [String] -> [FilePath] -> H.Integration FilePath +buildSignedSub ctx label args keys = do + let unsigned = directory ctx label <> ".unsigned" + signed = directory ctx label <> ".signed" + void $ cli ctx $ ["dijkstra", "sub-transaction", "build-raw"] ++ args + ++ ["--protocol-params-file", protocolParams ctx, "--out-file", unsigned] + before <- cli ctx ["dijkstra", "sub-transaction", "txid", "--sub-tx-file", unsigned, "--output-text"] + void $ cli ctx $ ["dijkstra", "sub-transaction", "sign", "--sub-tx-file", unsigned] + ++ concatMap (\key -> ["--signing-key-file", key]) keys ++ ["--out-file", signed] + after <- cli ctx ["dijkstra", "sub-transaction", "txid", "--signed-sub-tx-file", signed, "--output-text"] + before H.=== after + pure signed + +-- | Keep input selection separate so rejection tests can retry the same inputs. +buildOuter :: DijkstraTest -> String -> FilePath -> UtxoEntry -> Integer -> [String] -> H.Integration FilePath +buildOuter ctx label signed input adjustment extras = do + let body = directory ctx label <> ".body" + change = ueLovelace input - fee + adjustment + H.assert $ change > 0 + void $ cli ctx $ + [ "dijkstra", "transaction", "build-raw", "--tx-in", ueTxIn input + , "--tx-in-collateral", collateral ctx + , "--tx-out", address (payer ctx) <> "+" <> show change + , "--fee", show fee, "--signed-sub-tx-file", signed + , "--protocol-params-file", protocolParams ctx, "--out-file", body + ] ++ extras + pure body + +keyInput :: DijkstraTest -> H.Integration [String] +keyInput ctx = do + input <- largest ctx (owner ctx) + pure ["--tx-in", ueTxIn input, "--tx-out", address (owner ctx) <> "+" <> show (ueLovelace input)] + +scriptHash :: DijkstraTest -> FilePath -> H.Integration String +scriptHash ctx path = trim <$> cli ctx ["hash", "script", "--script-file", path] + +scriptAddress :: DijkstraTest -> FilePath -> H.Integration String +scriptAddress ctx path = do + -- The era-independent CLI address builder reads native scripts as Conway and + -- crashes on RequireGuard (and rejects V4). Construct the setup address with the API; + -- building, signing and submitting the tested sub-transactions still uses CLI. + hash <- scriptHash ctx path >>= H.evalEither . deserialiseFromRawBytesHex . BS.pack + pure $ Text.unpack $ serialiseAddress $ + makeShelleyAddress (testNetwork ctx) (PaymentCredentialByScript hash) NoStakeAddress + +referenceOutput :: DijkstraTest -> String -> FilePath -> H.Integration String +referenceOutput ctx label script = do + txid <- fund ctx label [(address $ collateralWallet ctx, 20_000_000, ["--tx-out-reference-script-file", script])] + pure $ txid <> "#0" + +-- CLI tuples are (steps, memory). Enough for the tiny program, below protocol limits. +units :: String +units = "(100000000,1000000)" + +v4Witness :: ScriptSource -> String -> FilePath -> String -> [String] +v4Witness ViaFile purpose script _ = + ["--" <> purpose <> "-script-file", script, "--" <> purpose <> "-redeemer-value", "0" + , "--" <> purpose <> "-execution-units", units] +v4Witness ViaReference purpose _ ref = + let prefix = if purpose == "tx-in" then "spending" else purpose + in ["--" <> prefix <> "-tx-in-reference", ref, "--" <> prefix <> "-plutus-script-v4" + , "--" <> prefix <> "-reference-tx-in-redeemer-value", "0" + , "--" <> prefix <> "-reference-tx-in-execution-units", units] + +-- Guard references use an ordinary read-only input and the script hash. +guardArgs :: ScriptSource -> FilePath -> String -> String -> [String] +guardArgs ViaFile script _ _ = ["--guard-script-file", script] +guardArgs ViaReference _ hash ref = ["--read-only-tx-in-reference", ref, "--guard-script-hash", hash] + +-- | Reference scenarios must not accidentally pass by embedding the script too. +assertReferenceOnly :: DijkstraTest -> String -> H.Integration () +assertReferenceOnly ctx label = do + Exp.UnsignedSubTx tx <- H.evalEither =<< H.evalIO + (readFileTextEnvelope (File $ directory ctx label <> ".unsigned")) + H.assert $ Map.null (tx ^. L.witsTxL . L.scriptTxWitsL) + H.assert $ not $ Set.null (tx ^. L.bodyTxL . L.referenceInputsTxBodyL) + +stakeAddress :: DijkstraTest -> FilePath -> H.Integration String +stakeAddress ctx script = trim <$> cli ctx ["dijkstra", "stake-address", "build", "--stake-script-file", script] + +registerStake :: DijkstraTest -> ScriptSource -> String -> FilePath -> String -> H.Integration String +registerStake ctx source label script ref = do + deposit <- parameter ctx "stakeAddressDeposit" + let cert = directory ctx label <> ".regcert" + void $ cli ctx ["dijkstra", "stake-address", "registration-certificate", "--stake-script-file", script + , "--key-reg-deposit-amt", show deposit, "--out-file", cert] + input <- keyInput ctx + void $ submitSub ctx label (input ++ ["--certificate-file", cert] ++ v4Witness source "certificate" script ref) + [skey $ owner ctx] (-deposit) + addr <- stakeAddress ctx script + entries <- stakeInfo ctx addr + length entries H.=== 1 + pure addr + +stakeInfo :: DijkstraTest -> String -> H.Integration [A.Value] +stakeInfo ctx addr = do + value <- queryJson ctx ["stake-address-info", "--address", addr] + H.evalEither $ A.parseEither A.parseJSON value + +rewardBalance :: DijkstraTest -> String -> H.Integration Integer +rewardBalance ctx addr = do + [entry] <- stakeInfo ctx addr + field "rewardAccountBalance" entry + +-- Read a specific proposal using its sub-transaction id. Nested governance +-- actions belong to that body, not to the batcher's top-level transaction id. +subId :: DijkstraTest -> String -> H.Integration String +subId ctx label = trim <$> cli ctx ["dijkstra", "sub-transaction", "txid" + , "--signed-sub-tx-file", directory ctx label <> ".signed", "--output-text"] + +proposalState :: DijkstraTest -> String -> H.Integration A.Value +proposalState ctx txid = do + proposals <- queryJson ctx ["gov-state"] >>= field "proposals" + H.nothingFail $ List.find matches (proposals :: [A.Value]) + where + matches (A.Object o) = case KM.lookup "actionId" o of + Just (A.Object action) -> KM.lookup "txId" action == Just (A.String $ Text.pack txid) + _ -> False + matches _ = False + +anchorArgs :: [String] +anchorArgs = ["--anchor-url", "https://example.com/dijkstra-smoke", "--anchor-data-hash", replicate 64 '0'] diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/Governance.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/Governance.hs new file mode 100644 index 00000000000..00e1a3d8787 --- /dev/null +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/Governance.hs @@ -0,0 +1,64 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Cardano.Testnet.Test.Dijkstra.Governance + ( hprop_sub_transaction_governance + ) where + +import Cardano.Testnet.Test.Dijkstra.Common + +import Control.Monad (forM_, void) +import qualified Data.Aeson as A +import qualified Data.Aeson.Key as K +import qualified Data.Aeson.KeyMap as KM +import System.FilePath (()) + +import Hedgehog (Property) +import qualified Hedgehog as H + +-- | V4 Proposing and Voting purposes, with real proposals and a registered +-- script DRep. Confirm recorded votes; ratification is not needed here. +-- Run with: --test-options '-p "/Dijkstra sub-transaction governance/"' +hprop_sub_transaction_governance :: Property +hprop_sub_transaction_governance = withDijkstra "dijkstra-governance" True $ \ctx -> do + let script = v4Script ctx + drepCert = directory ctx "drep.regcert" + ref <- referenceOutput ctx "governance-reference" script + hash <- scriptHash ctx script + returnAddr <- registerStake ctx ViaFile "return-account" script ref + drepDeposit <- parameter ctx "dRepDeposit" + actionDeposit <- parameter ctx "govActionDeposit" + void $ cli ctx ["dijkstra", "governance", "drep", "registration-certificate" + , "--drep-script-hash", hash, "--key-reg-deposit-amt", show drepDeposit, "--out-file", drepCert] + registrationInput <- keyInput ctx + void $ submitSub ctx "register-drep" + (registrationInput ++ ["--certificate-file", drepCert] ++ v4Witness ViaFile "certificate" script ref) + [skey $ owner ctx] (-drepDeposit) + forM_ [ViaFile, ViaReference] $ \source -> do + let label = show source + proposal = directory ctx label <> ".proposal" + vote = directory ctx label <> ".vote" + void $ cli ctx $ ["dijkstra", "governance", "action", "create-protocol-parameters-update", "--testnet" + , "--governance-action-deposit", show actionDeposit, "--deposit-return-stake-address", returnAddr + , "--constitution-script-hash", hash, "--min-fee-linear", "45"] + ++ anchorArgs ++ ["--out-file", proposal] + input <- keyInput ctx + void $ submitSub ctx (label <> "-propose") + (input ++ ["--proposal-file", proposal] ++ v4Witness source "proposal" script ref) + [skey $ owner ctx] (-actionDeposit) + actionId <- subId ctx (label <> "-propose") + state <- proposalState ctx actionId + action <- field "actionId" state + index <- field "govActionIx" action + void $ cli ctx ["dijkstra", "governance", "vote", "create", "--yes" + , "--governance-action-tx-id", actionId, "--governance-action-index", show (index :: Integer) + , "--drep-script-hash", hash, "--out-file", vote] + input' <- keyInput ctx + void $ submitSub ctx (label <> "-vote") + (input' ++ ["--vote-file", vote] ++ v4Witness source "vote" script ref) + [skey $ owner ctx] 0 + votes <- proposalState ctx actionId >>= field "dRepVotes" + KM.lookup (K.fromString $ "scriptHash-" <> hash) votes H.=== Just (A.String "VoteYes") + assertReference ctx ref + if source == ViaReference then + forM_ ["-propose", "-vote"] $ \suffix -> assertReferenceOnly ctx (label <> suffix) + else pure () diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/NestedTransaction.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/NestedTransaction.hs index c82bd1d9c7f..e480d3abde0 100644 --- a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/NestedTransaction.hs +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/NestedTransaction.hs @@ -8,6 +8,8 @@ module Cardano.Testnet.Test.Dijkstra.NestedTransaction ( hprop_nested_transaction_swap + , UtxoEntry (..) + , queryUtxos ) where import Cardano.Api @@ -68,7 +70,7 @@ import Hedgehog.Internal.Property (MonadTest) -- set: Bob holds the 100 @TokenA@, Alice holds 50 ADA more, Bob 50 ADA less, and the -- batcher paid the fee. -- --- Requires a @cardano-cli@ with the @dijkstra transaction sub-transaction@ commands +-- Requires a @cardano-cli@ with the @dijkstra sub-transaction@ commands -- (IntersectMBO/cardano-cli#1453); point @CARDANO_CLI@ at it. -- -- Needs cardano-ledger-dijkstra >= 0.4: before that, UTxO-HD key fetching through @@ -158,11 +160,11 @@ hprop_nested_transaction_swap = integrationRetryWorkspace 2 "nested-transaction- ------------------------------------------------------------------------------ -- The sub-transaction commands only exist in cardano-cli with IntersectMBO/cardano-cli#1453. -- Fail with a pointer rather than an option-parser error if the CLI in use lacks them. - subTxHelp <- execCliAny execConfig [ "dijkstra", "transaction", "sub-transaction", "--help" ] + subTxHelp <- execCliAny execConfig [ "dijkstra", "sub-transaction", "--help" ] case subTxHelp of (ExitSuccess, _, _) -> pure () _ -> H.failMessage callStack $ unlines - [ "The cardano-cli in use has no `dijkstra transaction sub-transaction` commands." + [ "The cardano-cli in use has no `dijkstra sub-transaction` commands." , "Point CARDANO_CLI at a build of IntersectMBO/cardano-cli#1453 (branch jordan/basic-sub-tx-integration)." ] swapDir <- H.createDirectoryIfMissing $ work "swap" @@ -179,13 +181,13 @@ hprop_nested_transaction_swap = integrationRetryWorkspace 2 "nested-transaction- aliceSubUnsigned <- H.note $ swapDir "alice.sub.unsigned" aliceSubSigned <- H.note $ swapDir "alice.sub.signed" void $ execCli' execConfig - [ "dijkstra", "transaction", "sub-transaction", "build-raw" + [ "dijkstra", "sub-transaction", "build-raw" , "--tx-in", ueTxIn aliceTokenUtxo , "--tx-out", aliceAddr <> "+" <> show @Integer (tokensAda + price) , "--out-file", aliceSubUnsigned ] void $ execCli' execConfig - [ "dijkstra", "transaction", "sub-transaction", "sign" + [ "dijkstra", "sub-transaction", "sign" , "--sub-tx-file", aliceSubUnsigned , "--signing-key-file", signingKeyFp $ paymentKeyInfoPair alice , "--out-file", aliceSubSigned @@ -195,14 +197,14 @@ hprop_nested_transaction_swap = integrationRetryWorkspace 2 "nested-transaction- bobSubUnsigned <- H.note $ swapDir "bob.sub.unsigned" bobSubSigned <- H.note $ swapDir "bob.sub.signed" void $ execCli' execConfig - [ "dijkstra", "transaction", "sub-transaction", "build-raw" + [ "dijkstra", "sub-transaction", "build-raw" , "--tx-in", ueTxIn bobAdaUtxo , "--tx-out", bobAddr <> "+" <> show @Integer tokensAda <> "+" <> tokenA 100 , "--tx-out", bobAddr <> "+" <> show @Integer (bobFunds - price - tokensAda) , "--out-file", bobSubUnsigned ] void $ execCli' execConfig - [ "dijkstra", "transaction", "sub-transaction", "sign" + [ "dijkstra", "sub-transaction", "sign" , "--sub-tx-file", bobSubUnsigned , "--signing-key-file", signingKeyFp $ paymentKeyInfoPair bob , "--out-file", bobSubSigned @@ -216,8 +218,8 @@ hprop_nested_transaction_swap = integrationRetryWorkspace 2 "nested-transaction- , "--tx-in", ueTxIn batcherUtxo , "--tx-out", batcherAddr <> "+" <> show @Integer (ueLovelace batcherUtxo - fee) , "--fee", show @Integer fee - , "--sub-transaction", aliceSubSigned - , "--sub-transaction", bobSubSigned + , "--signed-sub-tx-file", aliceSubSigned + , "--signed-sub-tx-file", bobSubSigned , "--out-file", topBodyFp ] void $ execCli' execConfig diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/Scripts.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/Scripts.hs new file mode 100644 index 00000000000..b3bccfa76d2 --- /dev/null +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/Scripts.hs @@ -0,0 +1,98 @@ +{-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE OverloadedStrings #-} + +module Cardano.Testnet.Test.Dijkstra.Scripts + ( hprop_sub_transaction_guards + , hprop_sub_transaction_plutus_v4 + ) where + +import Cardano.Testnet.Test.Dijkstra.Common +import Cardano.Testnet.Test.Dijkstra.NestedTransaction (UtxoEntry (..), queryUtxos) + +import Control.Monad (forM_, void) +import qualified Data.Aeson as A +import qualified Data.ByteString.Lazy.Char8 as B +import System.FilePath (()) + +import Hedgehog (Property) +import qualified Hedgehog as H +import qualified Hedgehog.Extras as H + +-- | A native RequireGuard script both locks a UTxO and controls a mint. Its +-- credential is authorized by a separate key, a native script, or a V4 script. +-- Run with: --test-options '-p "/Dijkstra sub-transaction guards/"' +hprop_sub_transaction_guards :: Property +hprop_sub_transaction_guards = withDijkstra "dijkstra-guards" False $ \ctx -> do + let key = directory ctx "guard.skey" + vkey = directory ctx "guard.vkey" + native = directory ctx "signature.json" + void $ cli ctx ["address", "key-gen", "--signing-key-file", key, "--verification-key-file", vkey] + keyHash <- trim <$> cli ctx ["address", "key-hash", "--payment-verification-key-file", vkey] + H.writeFile native $ B.unpack $ A.encode $ A.object ["type" A..= ("sig" :: String), "keyHash" A..= keyHash] + nativeHash <- scriptHash ctx native + plutusHash <- scriptHash ctx (v4Script ctx) + forM_ [ViaReference, ViaFile] $ \source -> do + nativeRef <- referenceOutput ctx (show source <> "-native-guard-ref") native + plutusRef <- referenceOutput ctx (show source <> "-plutus-guard-ref") (v4Script ctx) + let cases = + [ ("key", A.object ["keyHash" A..= keyHash], ["--guard-key-hash", keyHash], [key]) + , ("native", A.object ["scriptHash" A..= nativeHash], guardArgs source native nativeHash nativeRef, [key]) + , ("plutus", A.object ["scriptHash" A..= plutusHash] + , guardArgs source (v4Script ctx) plutusHash plutusRef + ++ ["--guard-redeemer-value", "0", "--guard-execution-units", units], []) + ] + forM_ cases $ \(kind, credential, guards, keys) -> do + let label = show source <> "-" <> kind + policy = directory ctx label <> "-require-guard.json" + H.note_ $ "Native RequireGuard spending/minting authorized by " <> kind <> ", " <> show source + H.writeFile policy $ B.unpack $ A.encode $ A.object ["type" A..= ("guard" :: String), "credential" A..= credential] + mintAndSpend ctx label source False policy guards keys + assertReference ctx nativeRef + assertReference ctx plutusRef + +-- | V4 Spending and Minting purposes in the same sub-transaction, using direct +-- witnesses and then reference witnesses. Both must execute on the ledger. +-- Run with: --test-options '-p "/Dijkstra sub-transaction Plutus V4/"' +hprop_sub_transaction_plutus_v4 :: Property +hprop_sub_transaction_plutus_v4 = withDijkstra "dijkstra-plutus-v4" False $ \ctx -> + forM_ [ViaFile, ViaReference] $ \source -> + mintAndSpend ctx (show source <> "-v4") source True (v4Script ctx) [] [] + +mintAndSpend :: DijkstraTest -> String -> ScriptSource -> Bool -> FilePath -> [String] -> [FilePath] -> H.Integration () +mintAndSpend ctx label source plutus script guards keys = do + addr <- scriptAddress ctx script + policy <- scriptHash ctx script + let datum = if plutus then ["--tx-out-inline-datum-value", "0"] else [] + asset = "1 " <> policy <> ".54" -- T + funding <- fund ctx (label <> "-fund") [(addr, 20_000_000, datum)] + ref <- referenceOutput ctx (label <> "-reference") script + let input = funding <> "#0" + spending = case (plutus, source) of + (True, ViaFile) -> v4Witness source "tx-in" script ref ++ ["--tx-in-inline-datum-present"] + (True, ViaReference) -> v4Witness source "tx-in" script ref ++ ["--spending-reference-tx-in-inline-datum-present"] + (False, ViaFile) -> ["--tx-in-script-file", script] + (False, ViaReference) -> ["--simple-script-tx-in-reference", ref] + minting = case (plutus, source) of + (True, ViaFile) -> v4Witness source "mint" script ref + (True, ViaReference) -> v4Witness source "mint" script ref ++ ["--policy-id", policy] + (False, ViaFile) -> ["--mint-script-file", script] + (False, ViaReference) -> ["--simple-minting-script-tx-in-reference", ref, "--policy-id", policy] + before <- queryUtxos (exec ctx) policy "54" (address $ owner ctx) + -- A validity lower bound, metadata and canonical output get one happy-path + -- exercise here, without separate permutations for each auxiliary option. + slot <- queryJson ctx ["tip"] >>= field "slot" + let metadata = directory ctx label <> "-metadata.json" + H.writeFile metadata "{\"1\":\"Dijkstra sub-transaction smoke test\"}" + void $ submitSub ctx label + (["--tx-in", input] ++ spending + ++ ["--mint", asset] ++ minting ++ guards + ++ [ "--tx-out", address (owner ctx) <> "+20000000+" <> asset + , "--invalid-before", show (slot :: Integer) + , "--metadata-json-file", metadata, "--out-canonical-cbor" + ]) keys 0 + assertSpent ctx input + after <- queryUtxos (exec ctx) policy "54" (address $ owner ctx) + sum (map ueTokens after) H.=== sum (map ueTokens before) + 1 + H.assert $ any (\u -> ueTokens u == 1 && ueLovelace u == 20_000_000) after + assertReference ctx ref + if source == ViaReference then assertReferenceOnly ctx label else pure () diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/Staking.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/Staking.hs new file mode 100644 index 00000000000..69cc364fda8 --- /dev/null +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/Staking.hs @@ -0,0 +1,58 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Cardano.Testnet.Test.Dijkstra.Staking + ( hprop_sub_transaction_staking + ) where + +import Cardano.Api.Ledger (EpochInterval (..)) +import Cardano.Testnet.Test.Dijkstra.Common + +import Control.Monad (forM_, void) +import System.FilePath (()) + +import Testnet.Components.Query + +import Hedgehog (Property) +import qualified Hedgehog as H + +-- | Certificates and real withdrawals, with direct and reference V4 witnesses. +-- Run with: --test-options '-p "/Dijkstra sub-transaction staking/"' +hprop_sub_transaction_staking :: Property +hprop_sub_transaction_staking = withDijkstra "dijkstra-staking" False $ \ctx -> do + ref <- referenceOutput ctx "stake-reference" (v4Script ctx) + deposit <- parameter ctx "stakeAddressDeposit" + actionDeposit <- parameter ctx "govActionDeposit" + lifetime <- parameter ctx "govActionLifetime" + forM_ [ViaFile, ViaReference] $ \source -> do + let label = show source + script = v4Script ctx + addr <- registerStake ctx source (label <> "-register") script ref + -- An info action cannot enact. Its returned deposit gives the registered + -- script account a positive balance without relying on pool reward timing. + let action = directory ctx label <> ".info" + void $ cli ctx $ ["dijkstra", "governance", "action", "create-info", "--testnet" + , "--governance-action-deposit", show actionDeposit, "--deposit-return-stake-address", addr] + ++ anchorArgs ++ ["--out-file", action] + input <- keyInput ctx + void $ submitSub ctx (label <> "-info") (input ++ ["--proposal-file", action]) + [skey $ owner ctx] (-actionDeposit) + reward <- retryUntilM (epochView ctx) (WaitForEpochs $ EpochInterval $ fromIntegral $ lifetime + 4) + (rewardBalance ctx addr) (> 0) + reward H.=== actionDeposit + input' <- keyInput ctx + void $ submitSub ctx (label <> "-withdraw") + (input' ++ ["--withdrawal", addr <> "+" <> show reward] ++ v4Witness source "withdrawal" script ref) + [skey $ owner ctx] reward + rewardBalance ctx addr >>= (H.=== 0) + let cert = directory ctx label <> ".deregcert" + void $ cli ctx ["dijkstra", "stake-address", "deregistration-certificate", "--stake-script-file", script + , "--key-reg-deposit-amt", show deposit, "--out-file", cert] + input'' <- keyInput ctx + void $ submitSub ctx (label <> "-deregister") + (input'' ++ ["--certificate-file", cert] ++ v4Witness source "certificate" script ref) + [skey $ owner ctx] deposit + stakeInfo ctx addr >>= (H.=== []) + assertReference ctx ref + if source == ViaReference then + forM_ ["-register", "-withdraw", "-deregister"] $ \suffix -> assertReferenceOnly ctx (label <> suffix) + else pure () diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/TopLevelGuards.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/TopLevelGuards.hs new file mode 100644 index 00000000000..bc29bcced2a --- /dev/null +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/TopLevelGuards.hs @@ -0,0 +1,187 @@ +{-# LANGUAGE GADTs #-} +{-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE OverloadedStrings #-} + +module Cardano.Testnet.Test.Dijkstra.TopLevelGuards + ( hprop_top_level_key_guards + , hprop_top_level_native_guards + , hprop_top_level_plutus_v4_guards + ) where + +import Cardano.Api hiding (label) +import qualified Cardano.Api.Experimental as Exp +import qualified Cardano.Api.Ledger as L + +import qualified Cardano.Ledger.Dijkstra.Scripts as L +import qualified Cardano.Ledger.Dijkstra.TxBody as L +import Cardano.Ledger.Keys (coerceKeyRole) +import qualified Cardano.Ledger.Plutus.Language as L +import Cardano.Testnet.Test.Dijkstra.Common +import Cardano.Testnet.Test.Dijkstra.NestedTransaction (UtxoEntry (..)) + +import Control.Monad (forM_, void, when) +import qualified Data.Aeson as A +import qualified Data.ByteString.Char8 as BS +import qualified Data.ByteString.Lazy.Char8 as B +import Data.List (isInfixOf) +import qualified Data.Map.Strict as Map +import qualified Data.Set as Set +import GHC.Exts (fromList) +import Lens.Micro ((&), (.~), (^.)) +import System.Exit (ExitCode (..)) +import System.FilePath (()) + +import Testnet.Process.Run (execCliAny) + +import Hedgehog (Property) +import qualified Hedgehog as H +import qualified Hedgehog.Extras as H + +-- The requirement belongs to the sub-transaction; its authorization belongs +-- to the enclosing transaction. Keep the guard key separate from both wallets. +data TopGuard = TopGuard + { credential :: L.Credential L.Guard + , requirementArgs :: [String] + , witnessScript :: Maybe (L.Script (ShelleyLedgerEra DijkstraEra)) + , witnessKeys :: [FilePath] + , isPlutus :: Bool + , referenceInput :: Maybe String + } + +hprop_top_level_key_guards :: Property +hprop_top_level_key_guards = withDijkstra "dijkstra-top-level-key-guards" False $ \ctx -> do + (hash, key) <- freshGuardKey ctx + PaymentKeyHash kh <- H.evalEither $ deserialiseFromRawBytesHex $ BS.pack hash + runGuardCase ctx "key" True $ TopGuard + (L.KeyHashObj $ coerceKeyRole kh) ["--top-level-guard-key-hash", hash] Nothing [key] False Nothing + +hprop_top_level_native_guards :: Property +hprop_top_level_native_guards = withDijkstra "dijkstra-top-level-native-guards" False $ \ctx -> do + (hash, key) <- freshGuardKey ctx + let path = directory ctx "signature.json" + H.writeFile path $ B.unpack $ A.encode $ A.object ["type" A..= ("sig" :: String), "keyHash" A..= hash] + script <- H.readJsonFileOk path + scriptCases ctx path (toShelleyScript $ ScriptInEra SimpleScriptInDijkstra $ SimpleScript script) [key] False + +hprop_top_level_plutus_v4_guards :: Property +hprop_top_level_plutus_v4_guards = withDijkstra "dijkstra-top-level-plutus-v4-guards" False $ \ctx -> do + script <- H.evalEither =<< H.evalIO (readFileTextEnvelope $ File $ v4Script ctx) + scriptCases ctx (v4Script ctx) + (toShelleyScript $ ScriptInEra PlutusScriptV4InDijkstra $ PlutusScript PlutusScriptV4 script) [] True + +freshGuardKey :: DijkstraTest -> H.Integration (String, FilePath) +freshGuardKey ctx = do + let key = directory ctx "top-guard.skey" + vkey = directory ctx "top-guard.vkey" + void $ cli ctx ["address", "key-gen", "--signing-key-file", key, "--verification-key-file", vkey] + hash <- trim <$> cli ctx ["address", "key-hash", "--payment-verification-key-file", vkey] + pure (hash, key) + +scriptCases :: DijkstraTest -> FilePath -> L.Script (ShelleyLedgerEra DijkstraEra) -> [FilePath] -> Bool -> H.Integration () +scriptCases ctx path script keys plutus = do + hash <- scriptHash ctx path + ScriptHash sh <- H.evalEither $ deserialiseFromRawBytesHex $ BS.pack hash + -- Exercise a success case before the omission/recovery case, so a failure + -- while rejecting malformed transactions does not hide the happy path. + forM_ [ViaReference, ViaFile] $ \source -> do + let label = show source + ref <- case source of + ViaFile -> pure Nothing + ViaReference -> Just <$> referenceOutput ctx "top-guard-reference" path + let requirement = case source of + ViaFile -> ["--top-level-guard-script-file", path] + ViaReference -> ["--top-level-guard-script-hash", hash] + datum = if plutus then ["--top-level-guard-datum-value", "42"] else [] + runGuardCase ctx label (source == ViaFile) $ + TopGuard (L.ScriptHashObj sh) (requirement ++ datum) (Just script) keys plutus ref + +runGuardCase :: DijkstraTest -> String -> Bool -> TopGuard -> H.Integration () +runGuardCase ctx label checkMissing guard = do + ownerInput <- largest ctx (owner ctx) + payerInput <- largest ctx (payer ctx) + sub <- buildSignedSub ctx label + (["--tx-in", ueTxIn ownerInput, "--tx-out", address (owner ctx) <> "+" <> show (ueLovelace ownerInput)] + ++ requirementArgs guard) + [skey $ owner ctx] + Exp.UnsignedSubTx subTx <- H.evalEither =<< H.evalIO + (readFileTextEnvelope $ File $ directory ctx label <> ".unsigned") + let expectedDatum = if isPlutus guard + then L.SJust $ toAlonzoData $ unsafeHashableScriptData $ ScriptDataNumber 42 + else L.SNothing + subTx ^. L.bodyTxL . L.requiredTopLevelGuardsL H.=== Map.singleton (credential guard) expectedDatum + subTx ^. L.bodyTxL . L.guardsTxBodyL H.=== mempty + subTx ^. L.witsTxL . L.scriptTxWitsL H.=== mempty + subTx ^. L.witsTxL . L.rdmrsTxWitsL H.=== mempty + let signingKeys = [skey $ payer ctx, skey $ collateralWallet ctx] ++ witnessKeys guard + protectedInputs = [ueTxIn ownerInput, ueTxIn payerInput, collateral ctx] + snapshot input = queryJson ctx ["utxo", "--tx-in", input, "--output-json"] + when checkMissing $ do + H.note_ "The node must reject the enclosing transaction without its required guard" + before <- mapM snapshot protectedInputs + body <- buildOuter ctx (label <> "-missing") sub payerInput 0 [] + signed <- signBody ctx body signingKeys + (exitCode, _, stderr) <- execCliAny (exec ctx) ["dijkstra", "transaction", "submit", "--tx-file", signed] + H.note_ stderr + H.assert $ case exitCode of ExitFailure _ -> True; ExitSuccess -> False + H.assert $ "MissingRequiredGuards" `isInfixOf` stderr + after <- mapM snapshot protectedInputs + before H.=== after + H.note_ "Satisfy the requirement and submit the same signed sub-transaction" + let extras = case credential guard of + L.KeyHashObj kh -> ["--required-signer-hash", BS.unpack $ serialiseToRawBytesHex $ PaymentKeyHash $ coerceKeyRole kh] + L.ScriptHashObj _ -> maybe [] (\ref -> ["--read-only-tx-in-reference", ref]) (referenceInput guard) + body <- buildOuter ctx label sub payerInput 0 extras + case witnessScript guard of + Nothing -> pure () + Just script -> addOuterScriptGuard ctx body guard script + signed <- signBody ctx body signingKeys + -- Check the final signed file: CLI signing must preserve the API-built guard. + ShelleyTx _ outer <- readOuter signed + outer ^. L.bodyTxL . L.guardsTxBodyL H.=== fromList [credential guard] + case referenceInput guard of + Nothing -> pure () + Just _ -> do + outer ^. L.witsTxL . L.scriptTxWitsL H.=== mempty + H.assert $ not $ Set.null $ outer ^. L.bodyTxL . L.referenceInputsTxBodyL + txid <- submitSigned ctx signed + forM_ [ueTxIn ownerInput, ueTxIn payerInput] $ assertSpent ctx + ownerId <- subId ctx label + assertOutput ctx (ownerId <> "#0") (address $ owner ctx) (ueLovelace ownerInput) + assertOutput ctx (txid <> "#0") (address $ payer ctx) (ueLovelace payerInput - fee) + assertReference ctx (collateral ctx) + forM_ (referenceInput guard) $ assertReference ctx + +assertOutput :: DijkstraTest -> String -> String -> Integer -> H.Integration () +assertOutput ctx input expectedAddress expectedLovelace = do + output <- queryJson ctx ["utxo", "--tx-in", input, "--output-json"] >>= field input + field "address" output >>= (H.=== expectedAddress) + (field "value" output >>= field "lovelace") >>= (H.=== expectedLovelace) + +readOuter :: FilePath -> H.Integration (Tx DijkstraEra) +readOuter path = H.evalEither =<< H.evalIO (readFileTextEnvelope $ File path) + +-- The pinned CLI exposes required guards in sub-transactions but cannot add +-- native/Plutus guards to the enclosing transaction. Construct those fields +-- through the API before CLI signing. The script integrity calculation follows +-- CLI's addGuardWitnesses: guard indices follow the body's ordered guard set. +addOuterScriptGuard :: DijkstraTest -> FilePath -> TopGuard -> L.Script (ShelleyLedgerEra DijkstraEra) -> H.Integration () +addOuterScriptGuard ctx body guard script = do + ShelleyTx _ tx <- readOuter body + let scripts = case referenceInput guard of + Nothing -> Map.singleton (L.hashScript script) script + Just _ -> mempty + -- This fixture has exactly one outer guard, at index zero. + redeemers = if isPlutus guard then L.Redeemers $ Map.singleton (L.GuardingPurpose $ L.AsIx 0) + (toAlonzoData $ unsafeHashableScriptData $ ScriptDataNumber 0, L.ExUnits 1_000_000 100_000_000) + else mempty + guarded = tx + & L.bodyTxL . L.guardsTxBodyL .~ fromList [credential guard] + & L.witsTxL . L.scriptTxWitsL .~ scripts + & L.witsTxL . L.rdmrsTxWitsL .~ redeemers + complete <- if isPlutus guard then do + pp <- H.readJsonFileOk (protocolParams ctx) + let integrity = getScriptIntegrityHash (BuildTxWith $ Just $ LedgerProtocolParameters pp) (Set.singleton L.PlutusV4) + (TxBodyScriptData AlonzoEraOnwardsDijkstra (guarded ^. L.witsTxL . L.datsTxWitsL) redeemers) + pure $ guarded & L.bodyTxL . L.scriptIntegrityHashTxBodyL .~ integrity + else pure guarded + H.evalEither =<< H.evalIO (writeTxFileTextEnvelope ShelleyBasedEraDijkstra (File body) (ShelleyTx ShelleyBasedEraDijkstra complete)) diff --git a/cardano-testnet/test/cardano-testnet-test/cardano-testnet-test.hs b/cardano-testnet/test/cardano-testnet-test/cardano-testnet-test.hs index de4245bff53..756ef55cb48 100644 --- a/cardano-testnet/test/cardano-testnet-test/cardano-testnet-test.hs +++ b/cardano-testnet/test/cardano-testnet-test/cardano-testnet-test.hs @@ -20,7 +20,11 @@ import qualified Cardano.Testnet.Test.Cli.Transaction import qualified Cardano.Testnet.Test.Cli.Transaction.BuildEstimate import qualified Cardano.Testnet.Test.Cli.Transaction.RegisterDeregisterStakeAddress import qualified Cardano.Testnet.Test.Cli.Transaction.WithdrawalReward +import qualified Cardano.Testnet.Test.Dijkstra.Governance as Dijkstra import qualified Cardano.Testnet.Test.Dijkstra.NestedTransaction +import qualified Cardano.Testnet.Test.Dijkstra.Scripts as Dijkstra +import qualified Cardano.Testnet.Test.Dijkstra.Staking as Dijkstra +import qualified Cardano.Testnet.Test.Dijkstra.TopLevelGuards as Dijkstra import qualified Cardano.Testnet.Test.DumpConfig import qualified Cardano.Testnet.Test.EnableTracer import qualified Cardano.Testnet.Test.FoldEpochState @@ -151,6 +155,13 @@ tests = do , ignoreOnMacAndWindows "Enable Tracer" Cardano.Testnet.Test.EnableTracer.hprop_enable_tracer , ignoreOnWindows "Hard fork to Dijkstra" Cardano.Testnet.Test.Node.DijkstraEra.hprop_hardfork_to_dijkstra , ignoreOnWindows "Nested transaction swap" Cardano.Testnet.Test.Dijkstra.NestedTransaction.hprop_nested_transaction_swap + , ignoreOnWindows "Dijkstra sub-transaction guards" Dijkstra.hprop_sub_transaction_guards + , ignoreOnWindows "Dijkstra sub-transaction Plutus V4" Dijkstra.hprop_sub_transaction_plutus_v4 + , ignoreOnWindows "Dijkstra sub-transaction staking" Dijkstra.hprop_sub_transaction_staking + , ignoreOnWindows "Dijkstra sub-transaction governance" Dijkstra.hprop_sub_transaction_governance + , ignoreOnWindows "Dijkstra top-level key guards" Dijkstra.hprop_top_level_key_guards + , ignoreOnWindows "Dijkstra top-level native guards" Dijkstra.hprop_top_level_native_guards + , ignoreOnWindows "Dijkstra top-level Plutus V4 guards" Dijkstra.hprop_top_level_plutus_v4_guards , ignoreOnMacAndWindows "Supports dumping/loading config files" Cardano.Testnet.Test.DumpConfig.hprop_dump_config , ignoreOnMacAndWindows "Can have its start time modified" Cardano.Testnet.Test.UpdateTimeStamps.hprop_update_time_stamps , ignoreOnMacAndWindows "Can get on-chain parameters from blockfrost files" Cardano.Testnet.Test.MainnetParams.hprop_mainnet_params diff --git a/flake.lock b/flake.lock index e136286893c..672d9326e3b 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "CHaP": { "flake": false, "locked": { - "lastModified": 1791557770, - "narHash": "sha256-sOy9L/Q5G2okYvZ4PlH/XEFQQFUOasifDfcMipqz3MY=", + "lastModified": 1791917071, + "narHash": "sha256-yI14qHreKU1r9BoxNH3IKiEbTI/xNPnPtAc9j7e0jwI=", "owner": "intersectmbo", "repo": "cardano-haskell-packages", - "rev": "0f444414db3931fb70d8667f4791dec49e1e23bf", + "rev": "d25333023603be57f516cd27ac8a239f5aca4552", "type": "github" }, "original": { From 3f27e44909adc42e39f2185acb1ddef0c8e5161b Mon Sep 17 00:00:00 2001 From: Jordan Millar Date: Fri, 16 Oct 2026 11:00:59 -0400 Subject: [PATCH 5/6] Pin cardano-cli PR 1473 and compatible ledger dependencies --- cabal.project | 62 +++++++++++++++++-- cardano-node/cardano-node.cabal | 2 +- .../20261016_ledger_6189_compatibility.yml | 3 + cardano-testnet/cardano-testnet.cabal | 2 +- cardano-testnet/src/Testnet/Runtime.hs | 5 +- .../Testnet/Test/Dijkstra/TopLevelGuards.hs | 2 +- 6 files changed, 66 insertions(+), 10 deletions(-) create mode 100644 cardano-testnet/.changes/20261016_ledger_6189_compatibility.yml diff --git a/cabal.project b/cabal.project index 82e0e3bc38f..0302d38de01 100644 --- a/cabal.project +++ b/cabal.project @@ -75,6 +75,16 @@ allow-newer: , plutus-scripts-bench:plutus-tx , plutus-scripts-bench:plutus-tx-plugin + -- Temporary ledger SRP versions exceed these released package bounds. + , cardano-config:cardano-ledger-alonzo + , cardano-config:cardano-ledger-conway + , cardano-config:cardano-ledger-core + , cardano-config:cardano-ledger-dijkstra + , cardano-config:cardano-ledger-shelley + , cardano-keys:cardano-ledger-core + , cardano-keys:cardano-ledger-shelley + , cardano-keys:cardano-protocol + if impl(ghc >= 9.14) -- some-1.1 adds EqP/OrdP superclasses to GEq/GCompare, which plutus-core -- 1.71.0.0 is not written against: @@ -148,13 +158,55 @@ if impl(ghc >= 9.14) -- Do NOT add more source-repository-package stanzas here unless they are strictly -- temporary! Please read the section in CONTRIBUTING about updating dependencies. --- TEMPORARY: cardano-cli with the `dijkstra sub-transaction` commands --- (IntersectMBO/cardano-cli#1453), needed by the Dijkstra integration properties. --- Remove once #1453 is merged and released. +-- TEMPORARY: cardano-cli with the Dijkstra top-level guard data decoder fix +-- (IntersectMBO/cardano-cli#1473). Remove these pins once compatible releases +-- of the CLI, ledger, API, and consensus are available. source-repository-package type: git location: https://github.com/IntersectMBO/cardano-cli - tag: 55cff1ebdd619551c3a270e95fe18701d89a5bf7 - --sha256: sha256-JT1HY+RvpVv5/AAYg3nisU6Klm1iE0tizyya4zJ7Zqg= + tag: 64aca1c00a65fd501e2229547045587cf686a8ce + --sha256: sha256-eyckGs9i6mRB+noz3yBEovwC0gAVI6hyOeV1NeiawDU= subdir: cardano-cli + +-- Temporary pin for the Plutus data decoder fix in https://github.com/IntersectMBO/cardano-ledger/pull/6189 +source-repository-package + type: git + location: https://github.com/IntersectMBO/cardano-ledger + tag: a2041c4bd73995fb4ab3b90db03fdf8ff464e104 + --sha256: 0syndgsgzhzg1wk3k4lf4anfvv9wvvzf437wk8c890mlk4g69vjn + subdir: + eras/allegra/impl + eras/alonzo/impl + eras/babbage/impl + eras/byron/crypto + eras/byron/ledger/impl + eras/conway/impl + eras/dijkstra/impl + eras/mary/impl + eras/shelley/impl + eras/shelley/test-suite + libs/cardano-data + libs/cardano-ledger-api + libs/cardano-ledger-binary + libs/cardano-ledger-core + libs/cardano-protocol + libs/cardano-protocol-tpraos + libs/non-integral + libs/small-steps + libs/vector-map + +-- Temporary cardano-api compatibility branch: srp/cardano-ledger-6189 +source-repository-package + type: git + location: https://github.com/IntersectMBO/cardano-api + tag: 326a8f02df4ff1bb8a862aba23ed7e0cbe40a32c + --sha256: 0gn5ad80qp7i5bvyjrsqrm2hq6ifmvcjhnzf6yys30286y94km5l + subdir: cardano-api + +-- Temporary ouroboros-consensus compatibility branch: srp/cardano-ledger-6189 +source-repository-package + type: git + location: https://github.com/IntersectMBO/ouroboros-consensus + tag: 7ef5dc2474d42eeccc8244c9d2a5379ea2405df7 + --sha256: 073rxsv3p5wr9n6kj4hbksa5mirmkpxasayv6h221v1frswp0mr7 diff --git a/cardano-node/cardano-node.cabal b/cardano-node/cardano-node.cabal index 243e57f030f..4f86ccfd45c 100644 --- a/cardano-node/cardano-node.cabal +++ b/cardano-node/cardano-node.cabal @@ -140,7 +140,7 @@ library , cardano-ledger-dijkstra , cardano-ledger-shelley , cardano-prelude - , cardano-protocol ^>= 0.2 + , cardano-protocol ^>= 0.3 , cardano-slotting >= 0.2 , cardano-rpc ^>= 11.5 , cborg ^>= 0.2.4 diff --git a/cardano-testnet/.changes/20261016_ledger_6189_compatibility.yml b/cardano-testnet/.changes/20261016_ledger_6189_compatibility.yml new file mode 100644 index 00000000000..691576ffe44 --- /dev/null +++ b/cardano-testnet/.changes/20261016_ledger_6189_compatibility.yml @@ -0,0 +1,3 @@ +kind: + - maintenance +description: Adapt epoch-state logging and Dijkstra guard tests to the ledger revision pinned for cardano-cli PR 1473, preserving the currentStakeDistribution JSON field through the new stake pool distribution getter. diff --git a/cardano-testnet/cardano-testnet.cabal b/cardano-testnet/cardano-testnet.cabal index ff36a13a80c..189970c7152 100644 --- a/cardano-testnet/cardano-testnet.cabal +++ b/cardano-testnet/cardano-testnet.cabal @@ -55,7 +55,7 @@ library , cardano-ledger-conway , cardano-ledger-core:{cardano-ledger-core, testlib} , cardano-ledger-dijkstra - , cardano-ledger-shelley + , cardano-ledger-shelley >= 1.21 , cardano-node , cardano-diffusion:ping ^>= 1.3 , cardano-prelude diff --git a/cardano-testnet/src/Testnet/Runtime.hs b/cardano-testnet/src/Testnet/Runtime.hs index 90c9f60d96c..850188c7729 100644 --- a/cardano-testnet/src/Testnet/Runtime.hs +++ b/cardano-testnet/src/Testnet/Runtime.hs @@ -47,6 +47,7 @@ import qualified Data.List as List import qualified Data.List.NonEmpty as NEL import GHC.Stack import qualified GHC.Stack as GHC +import Lens.Micro ((^.)) import Network.Socket (HostAddress, PortNumber) import Prettyprinter (unAnnotate) import qualified System.Directory as IO @@ -551,14 +552,14 @@ calculateEpochStateDiff current next = else ppDiff diffResult instance (L.EraTxOut ledgerera, L.EraGov ledgerera, L.EraCertState ledgerera, L.EraStake ledgerera) => ToJSON (L.NewEpochState ledgerera) where - toJSON (L.NewEpochState nesEL nesBprev nesBCur nesEs nesRu nesPd _stashedAvvm) = + toJSON nes@(L.NewEpochState nesEL nesBprev nesBCur nesEs nesRu _stashedAvvm) = object [ "currentEpoch" .= nesEL , "priorBlocks" .= nesBprev , "currentEpochBlocks" .= nesBCur , "currentEpochState" .= nesEs , "rewardUpdate" .= nesRu - , "currentStakeDistribution" .= nesPd + , "currentStakeDistribution" .= (nes ^. L.nesStakePoolDistrG) ] diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/TopLevelGuards.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/TopLevelGuards.hs index bc29bcced2a..650e733d853 100644 --- a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/TopLevelGuards.hs +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/TopLevelGuards.hs @@ -108,7 +108,7 @@ runGuardCase ctx label checkMissing guard = do let expectedDatum = if isPlutus guard then L.SJust $ toAlonzoData $ unsafeHashableScriptData $ ScriptDataNumber 42 else L.SNothing - subTx ^. L.bodyTxL . L.requiredTopLevelGuardsL H.=== Map.singleton (credential guard) expectedDatum + subTx ^. L.bodyTxL . L.requiredTopLevelGuardsTxBodyL H.=== Map.singleton (credential guard) expectedDatum subTx ^. L.bodyTxL . L.guardsTxBodyL H.=== mempty subTx ^. L.witsTxL . L.scriptTxWitsL H.=== mempty subTx ^. L.witsTxL . L.rdmrsTxWitsL H.=== mempty From d19ceeb7ba20627c591f611250e906c301afdb75 Mon Sep 17 00:00:00 2001 From: Jordan Millar Date: Fri, 16 Oct 2026 12:24:40 -0400 Subject: [PATCH 6/6] cardano-testnet: pin direct-deposit CLI and add live properties --- cabal.project | 11 +- .../20261015_dijkstra_direct_deposits.yml | 4 + cardano-testnet/cardano-testnet.cabal | 1 + .../Testnet/Test/Dijkstra/DirectDeposits.hs | 230 ++++++++++++++++++ .../cardano-testnet-test.hs | 9 + 5 files changed, 250 insertions(+), 5 deletions(-) create mode 100644 cardano-testnet/.changes/20261015_dijkstra_direct_deposits.yml create mode 100644 cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/DirectDeposits.hs diff --git a/cabal.project b/cabal.project index 0302d38de01..8981c1a0671 100644 --- a/cabal.project +++ b/cabal.project @@ -158,14 +158,15 @@ if impl(ghc >= 9.14) -- Do NOT add more source-repository-package stanzas here unless they are strictly -- temporary! Please read the section in CONTRIBUTING about updating dependencies. --- TEMPORARY: cardano-cli with the Dijkstra top-level guard data decoder fix --- (IntersectMBO/cardano-cli#1473). Remove these pins once compatible releases --- of the CLI, ledger, API, and consensus are available. +-- TEMPORARY: cardano-cli from srp/direct-deposits-ledger-6189, combining direct +-- ADA deposits in Dijkstra builders (IntersectMBO/cardano-cli#1469) with the +-- ledger compatibility fixes from #1473. Remove these pins once compatible +-- CLI, ledger, API, and consensus releases are available. source-repository-package type: git location: https://github.com/IntersectMBO/cardano-cli - tag: 64aca1c00a65fd501e2229547045587cf686a8ce - --sha256: sha256-eyckGs9i6mRB+noz3yBEovwC0gAVI6hyOeV1NeiawDU= + tag: 5900d772908c62bd40f16cb67ed7e1a9a01fd998 + --sha256: sha256-RfTI5dpzIilkRpchuXg2OT+8qaYeWzmQVe34brcBIXU= subdir: cardano-cli diff --git a/cardano-testnet/.changes/20261015_dijkstra_direct_deposits.yml b/cardano-testnet/.changes/20261015_dijkstra_direct_deposits.yml new file mode 100644 index 00000000000..220af601c57 --- /dev/null +++ b/cardano-testnet/.changes/20261015_dijkstra_direct_deposits.yml @@ -0,0 +1,4 @@ +pr: 6709 +kind: + - test +description: "Add live Dijkstra direct ADA deposit properties covering all three top-level builders, signed nested transactions, multiple recipients, repeated credits, V4 withdrawals with direct and reference witnesses, and rejection of unregistered or wrong-network recipients; pin cardano-cli to a compatibility branch combining PR #1469 with the ledger fixes from PR #1473." diff --git a/cardano-testnet/cardano-testnet.cabal b/cardano-testnet/cardano-testnet.cabal index 189970c7152..f77f5013681 100644 --- a/cardano-testnet/cardano-testnet.cabal +++ b/cardano-testnet/cardano-testnet.cabal @@ -227,6 +227,7 @@ test-suite cardano-testnet-test Cardano.Testnet.Test.Cli.Transaction.RegisterDeregisterStakeAddress Cardano.Testnet.Test.Cli.Transaction.WithdrawalReward Cardano.Testnet.Test.Dijkstra.Common + Cardano.Testnet.Test.Dijkstra.DirectDeposits Cardano.Testnet.Test.Dijkstra.Governance Cardano.Testnet.Test.Dijkstra.NestedTransaction Cardano.Testnet.Test.Dijkstra.Scripts diff --git a/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/DirectDeposits.hs b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/DirectDeposits.hs new file mode 100644 index 00000000000..65195511998 --- /dev/null +++ b/cardano-testnet/test/cardano-testnet-test/Cardano/Testnet/Test/Dijkstra/DirectDeposits.hs @@ -0,0 +1,230 @@ +{-# LANGUAGE GADTs #-} +{-# LANGUAGE NumericUnderscores #-} +{-# LANGUAGE OverloadedStrings #-} + +module Cardano.Testnet.Test.Dijkstra.DirectDeposits + ( DepositBuilder (..) + , hprop_direct_deposits_top_level + , hprop_direct_deposits_nested + , hprop_direct_deposits_withdraw + , hprop_direct_deposits_rejected + ) where + +import Cardano.Api hiding (label) +import qualified Cardano.Api.Ledger as L +import qualified Cardano.Ledger.Address as L +import qualified Cardano.Ledger.Dijkstra.TxBody as L +import Cardano.Testnet.Test.Dijkstra.Common +import Cardano.Testnet.Test.Dijkstra.NestedTransaction (UtxoEntry (..)) + +import Control.Monad (forM_, void, when, (>=>)) +import Data.Foldable (toList) +import Data.List (isInfixOf) +import qualified Data.Map.Strict as Map +import qualified Data.Text as Text +import Lens.Micro ((^.)) +import System.Exit (ExitCode (..)) +import System.FilePath (()) + +import Testnet.Process.Run (execCliAny) + +import Hedgehog (Property) +import qualified Hedgehog as H +import qualified Hedgehog.Extras as H + +data DepositBuilder = Raw | Online | Estimate deriving Show + +type Deposits = [(String, Integer)] + +-- Each builder gets its own testnet so a failure cannot hide another builder. +-- Undelegated recipients avoid epoch rewards changing the expected balances. +hprop_direct_deposits_top_level :: DepositBuilder -> Property +hprop_direct_deposits_top_level builder = withDijkstra ("dijkstra-deposits-" <> show builder) False $ \ctx -> do + (keyAccount, scriptAccount) <- registerRecipients ctx + forM_ [1, 2 :: Integer] $ \roundNumber -> do + let deposits = [(keyAccount, 2_000_000), (scriptAccount, 3_000_000)] + input <- largest ctx (payer ctx) + body <- buildDeposit ctx builder ("deposit-" <> show roundNumber) input deposits + signed <- signBody ctx body [skey $ payer ctx] + assertDeposits signed False deposits + txid <- submitSigned ctx signed + assertConservation ctx signed txid input 5_000_000 + forM_ deposits $ \(account, amount) -> rewardBalance ctx account >>= (H.=== roundNumber * amount) + +hprop_direct_deposits_nested :: Property +hprop_direct_deposits_nested = withDijkstra "dijkstra-deposits-nested" False $ \ctx -> do + (keyAccount, scriptAccount) <- registerRecipients ctx + forM_ [1, 2 :: Integer] $ \roundNumber -> do + let deposits = [(keyAccount, 2_000_000), (scriptAccount, 3_000_000)] + label = "nested-" <> show roundNumber + ownerInput <- largest ctx (owner ctx) + args <- keyInput ctx + sub <- buildSignedSub ctx label (args ++ depositArgs deposits) [skey $ owner ctx] + payerInput <- largest ctx (payer ctx) + body <- buildOuter ctx label sub payerInput (-5_000_000) [] + signed <- signBody ctx body [skey $ payer ctx, skey $ collateralWallet ctx] + assertDeposits signed True deposits + txid <- submitSigned ctx signed + assertConservation ctx signed txid payerInput 5_000_000 + assertSpent ctx (ueTxIn ownerInput) + -- The nested input is returned unchanged; only the outer payer funds deposits. + ownerId <- subId ctx label + ownerOutput <- queryJson ctx ["utxo", "--tx-in", ownerId <> "#0", "--output-json"] >>= field (ownerId <> "#0") + field "address" ownerOutput >>= (H.=== address (owner ctx)) + (field "value" ownerOutput >>= field "lovelace") >>= (H.=== ueLovelace ownerInput) + forM_ deposits $ \(account, amount) -> rewardBalance ctx account >>= (H.=== roundNumber * amount) + assertReference ctx (collateral ctx) + +hprop_direct_deposits_withdraw :: Bool -> Property +hprop_direct_deposits_withdraw useReference = withDijkstra ("dijkstra-deposits-withdraw-" <> show useReference) False $ \ctx -> do + let source = if useReference then ViaReference else ViaFile + script = v4Script ctx + amount = 7_000_000 + ref <- referenceOutput ctx "withdraw-reference" script + account <- registerStake ctx source "register" script ref + input <- largest ctx (payer ctx) + body <- buildDeposit ctx Raw "fund-account" input [(account, amount)] + signed <- signBody ctx body [skey $ payer ctx] + assertDeposits signed False [(account, amount)] + txid <- submitSigned ctx signed + assertConservation ctx signed txid input amount + rewardBalance ctx account >>= (H.=== amount) + args <- keyInput ctx + -- submitSub checks that the batcher's confirmed change includes the withdrawal. + void $ submitSub ctx "withdraw" + (args ++ ["--withdrawal", account <> "+" <> show amount] ++ v4Witness source "withdrawal" script ref) + [skey $ owner ctx] amount + rewardBalance ctx account >>= (H.=== 0) + assertReference ctx ref + assertReference ctx (collateral ctx) + when useReference $ assertReferenceOnly ctx "withdraw" + +hprop_direct_deposits_rejected :: Bool -> Property +hprop_direct_deposits_rejected nested = withDijkstra ("dijkstra-deposits-rejected-" <> show nested) False $ \ctx -> do + registered <- registerKeyAccount ctx "registered" + (missing, _, _) <- newKeyAccount ctx "missing" + StakeAddress _ credential <- H.evalMaybe $ deserialiseAddress AsStakeAddress (Text.pack registered) + let wrongNetwork = Text.unpack $ serialiseAddress $ StakeAddress L.Mainnet credential + prefix = if nested then "Sub" else "" + forM_ [("missing", missing, "DirectDepositAccountsMissing"), + ("network", wrongNetwork, "DirectDepositAddressesWithWrongNetwork")] $ \(label, target, reason) -> do + input <- largest ctx (payer ctx) + ownerInput <- largest ctx (owner ctx) + -- Include a valid recipient too: rejection must not partially credit it. + let deposits = [(registered, 2_000_000), (target, 3_000_000)] + body <- if nested then do + args <- keyInput ctx + sub <- buildSignedSub ctx label (args ++ depositArgs deposits) [skey $ owner ctx] + buildOuter ctx label sub input (-5_000_000) [] + else buildDeposit ctx Raw label input deposits + signed <- signBody ctx body [skey $ payer ctx, skey $ collateralWallet ctx] + assertDeposits signed nested deposits + let protected = [ueTxIn input, ueTxIn ownerInput, collateral ctx] + snapshot txin = queryJson ctx ["utxo", "--tx-in", txin, "--output-json"] + before <- mapM snapshot protected + balance <- rewardBalance ctx registered + (code, _, stderr) <- execCliAny (exec ctx) ["dijkstra", "transaction", "submit", "--tx-file", signed] + H.note_ stderr + H.assert $ case code of ExitFailure _ -> True; ExitSuccess -> False + H.assert $ (prefix <> reason) `isInfixOf` stderr + mapM snapshot protected >>= (H.=== before) + rewardBalance ctx registered >>= (H.=== balance) + stakeInfo ctx missing >>= (H.=== []) + -- Reuse the same funds successfully to distinguish validation from node failure. + input <- largest ctx (payer ctx) + let deposits = [(registered, 2_000_000)] + body <- if nested then do + args <- keyInput ctx + sub <- buildSignedSub ctx "recovery" (args ++ depositArgs deposits) [skey $ owner ctx] + buildOuter ctx "recovery" sub input (-2_000_000) [] + else buildDeposit ctx Raw "recovery" input deposits + signed <- signBody ctx body [skey $ payer ctx, skey $ collateralWallet ctx] + assertDeposits signed nested deposits + txid <- submitSigned ctx signed + assertConservation ctx signed txid input 2_000_000 + rewardBalance ctx registered >>= (H.=== 2_000_000) + +depositArgs :: Deposits -> [String] +depositArgs = concatMap $ \(account, amount) -> ["--direct-deposit", account <> "+" <> show amount] + +buildDeposit :: DijkstraTest -> DepositBuilder -> String -> UtxoEntry -> Deposits -> H.Integration FilePath +buildDeposit ctx builder label input deposits = do + let body = directory ctx label <> ".body" + change = ueLovelace input - fee - sum (map snd deposits) + (command, options) = case builder of + Raw -> ("build-raw", ["--tx-out", address (payer ctx) <> "+" <> show change, "--fee", show fee]) + Online -> ("build", ["--change-address", address (payer ctx), "--witness-override", "1"]) + Estimate -> ("build-estimate", + ["--change-address", address (payer ctx), "--total-utxo-value", show (ueLovelace input) + , "--shelley-key-witnesses", "1", "--protocol-params-file", protocolParams ctx]) + H.assert $ change > 0 + void $ cli ctx $ ["dijkstra", "transaction", command, "--tx-in", ueTxIn input] + ++ options ++ depositArgs deposits ++ ["--out-file", body] + pure body + +readTransaction :: FilePath -> H.Integration (Tx DijkstraEra) +readTransaction path = H.evalEither =<< H.evalIO (readFileTextEnvelope $ File path) + +assertDeposits :: FilePath -> Bool -> Deposits -> H.Integration () +assertDeposits signed nested deposits = do + expected <- L.DirectDeposits . Map.fromList <$> mapM (\(account, amount) -> do + addr <- H.evalMaybe $ deserialiseAddress AsStakeAddress (Text.pack account) + pure (toShelleyStakeAddr addr, Coin amount)) deposits + ShelleyTx _ tx <- readTransaction signed + let body = tx ^. L.bodyTxL + children = toList $ body ^. L.subTransactionsTxBodyL + if nested then do + body ^. L.directDepositsTxBodyL H.=== L.DirectDeposits Map.empty + length children H.=== 1 + forM_ children $ \child -> child ^. L.bodyTxL . L.directDepositsTxBodyL H.=== expected + else do + body ^. L.directDepositsTxBodyL H.=== expected + length children H.=== 0 + +-- Check confirmed outputs against the signed body, including the builder's fee. +-- Nested owners balance their own inputs/outputs, so this checks the outer payer. +assertConservation :: DijkstraTest -> FilePath -> String -> UtxoEntry -> Integer -> H.Integration () +assertConservation ctx signed txid input deposits = do + ShelleyTx _ tx <- readTransaction signed + let body = tx ^. L.bodyTxL + outputs = toList $ body ^. L.outputsTxBodyL + actualFee = body ^. L.feeTxBodyL + sum (map (^. L.coinTxOutL) outputs) + actualFee + Coin deposits H.=== Coin (ueLovelace input) + forM_ (zip [0 :: Int ..] outputs) $ \(index, output) -> do + let txin = txid <> "#" <> show index + confirmed <- queryJson ctx ["utxo", "--tx-in", txin, "--output-json"] >>= field txin + actual <- field "value" confirmed >>= field "lovelace" + Coin actual H.=== output ^. L.coinTxOutL + field "address" confirmed >>= (H.=== address (payer ctx)) + assertSpent ctx (ueTxIn input) + +registerRecipients :: DijkstraTest -> H.Integration (String, String) +registerRecipients ctx = do + key <- registerKeyAccount ctx "key-recipient" + script <- registerStake ctx ViaFile "script-recipient" (v4Script ctx) "" + forM_ [key, script] $ rewardBalance ctx >=> (H.=== 0) + pure (key, script) + +newKeyAccount :: DijkstraTest -> String -> H.Integration (String, FilePath, FilePath) +newKeyAccount ctx label = do + let vkey = directory ctx label <> ".vkey" + key = directory ctx label <> ".skey" + void $ cli ctx ["dijkstra", "stake-address", "key-gen", "--verification-key-file", vkey, "--signing-key-file", key] + account <- trim <$> cli ctx ["dijkstra", "stake-address", "build", "--stake-verification-key-file", vkey] + pure (account, vkey, key) + +registerKeyAccount :: DijkstraTest -> String -> H.Integration String +registerKeyAccount ctx label = do + (account, vkey, key) <- newKeyAccount ctx label + deposit <- parameter ctx "stakeAddressDeposit" + let cert = directory ctx label <> ".regcert" + body = directory ctx label <> ".regbody" + void $ cli ctx ["dijkstra", "stake-address", "registration-certificate", "--stake-verification-key-file", vkey + , "--key-reg-deposit-amt", show deposit, "--out-file", cert] + input <- largest ctx (payer ctx) + void $ cli ctx ["dijkstra", "transaction", "build-raw", "--tx-in", ueTxIn input + , "--tx-out", address (payer ctx) <> "+" <> show (ueLovelace input - fee - deposit) + , "--fee", show fee, "--certificate-file", cert, "--out-file", body] + void $ signSubmit ctx body [skey $ payer ctx, key] + rewardBalance ctx account >>= (H.=== 0) + pure account diff --git a/cardano-testnet/test/cardano-testnet-test/cardano-testnet-test.hs b/cardano-testnet/test/cardano-testnet-test/cardano-testnet-test.hs index 756ef55cb48..df2397ac8d9 100644 --- a/cardano-testnet/test/cardano-testnet-test/cardano-testnet-test.hs +++ b/cardano-testnet/test/cardano-testnet-test/cardano-testnet-test.hs @@ -20,6 +20,7 @@ import qualified Cardano.Testnet.Test.Cli.Transaction import qualified Cardano.Testnet.Test.Cli.Transaction.BuildEstimate import qualified Cardano.Testnet.Test.Cli.Transaction.RegisterDeregisterStakeAddress import qualified Cardano.Testnet.Test.Cli.Transaction.WithdrawalReward +import qualified Cardano.Testnet.Test.Dijkstra.DirectDeposits as Dijkstra import qualified Cardano.Testnet.Test.Dijkstra.Governance as Dijkstra import qualified Cardano.Testnet.Test.Dijkstra.NestedTransaction import qualified Cardano.Testnet.Test.Dijkstra.Scripts as Dijkstra @@ -158,6 +159,14 @@ tests = do , ignoreOnWindows "Dijkstra sub-transaction guards" Dijkstra.hprop_sub_transaction_guards , ignoreOnWindows "Dijkstra sub-transaction Plutus V4" Dijkstra.hprop_sub_transaction_plutus_v4 , ignoreOnWindows "Dijkstra sub-transaction staking" Dijkstra.hprop_sub_transaction_staking + , ignoreOnWindows "Dijkstra direct deposits build-raw" (Dijkstra.hprop_direct_deposits_top_level Dijkstra.Raw) + , ignoreOnWindows "Dijkstra direct deposits build" (Dijkstra.hprop_direct_deposits_top_level Dijkstra.Online) + , ignoreOnWindows "Dijkstra direct deposits build-estimate" (Dijkstra.hprop_direct_deposits_top_level Dijkstra.Estimate) + , ignoreOnWindows "Dijkstra direct deposits nested" Dijkstra.hprop_direct_deposits_nested + , ignoreOnWindows "Dijkstra direct deposits withdrawal (direct)" (Dijkstra.hprop_direct_deposits_withdraw False) + , ignoreOnWindows "Dijkstra direct deposits withdrawal (reference)" (Dijkstra.hprop_direct_deposits_withdraw True) + , ignoreOnWindows "Dijkstra direct deposits rejected (top-level)" (Dijkstra.hprop_direct_deposits_rejected False) + , ignoreOnWindows "Dijkstra direct deposits rejected (nested)" (Dijkstra.hprop_direct_deposits_rejected True) , ignoreOnWindows "Dijkstra sub-transaction governance" Dijkstra.hprop_sub_transaction_governance , ignoreOnWindows "Dijkstra top-level key guards" Dijkstra.hprop_top_level_key_guards , ignoreOnWindows "Dijkstra top-level native guards" Dijkstra.hprop_top_level_native_guards