From 5c656db2c36f879178488876495a5b94b51e9f36 Mon Sep 17 00:00:00 2001 From: Dzmitry Shuysky Date: Mon, 19 Aug 2024 17:56:37 +0200 Subject: [PATCH 1/9] Remove old version of DistributeFunds algorithm --- src/Internal/Testnet/Contract.purs | 21 +- src/Internal/Testnet/DistributeFunds.purs | 479 ++++++++++---------- src/Internal/Testnet/DistributeFundsV2.purs | 266 ----------- test/Testnet/DistributeFunds.purs | 441 ++++++++---------- test/Testnet/DistributeFundsV2.purs | 205 --------- test/Unit.purs | 2 - 6 files changed, 452 insertions(+), 962 deletions(-) delete mode 100644 src/Internal/Testnet/DistributeFundsV2.purs delete mode 100644 test/Testnet/DistributeFundsV2.purs diff --git a/src/Internal/Testnet/Contract.purs b/src/Internal/Testnet/Contract.purs index 692e07067..581f0b2f5 100644 --- a/src/Internal/Testnet/Contract.purs +++ b/src/Internal/Testnet/Contract.purs @@ -55,8 +55,8 @@ import Ctl.Internal.Test.UtxoDistribution , encodeDistribution , keyWallets ) -import Ctl.Internal.Testnet.DistributeFundsV2 (DistrFundsParams) -import Ctl.Internal.Testnet.DistributeFundsV2 (Tx(Tx), makeDistributionPlan) as DistrFunds +import Ctl.Internal.Testnet.DistributeFunds (DistrFundsParams) +import Ctl.Internal.Testnet.DistributeFunds (Tx(Tx), makeDistributionPlan) as DistrFunds import Ctl.Internal.Testnet.Server ( StartedTestnetCluster , makeClusterContractEnv @@ -70,6 +70,7 @@ import Ctl.Internal.Testnet.Utils , whenError ) import Data.Array (concat, fromFoldable, zip) as Array +import Data.Bifunctor (lmap) import Data.Map (values) as Map import Effect.Aff (bracket) as Aff import Effect.Aff (try) @@ -304,6 +305,13 @@ execDistrFundsPlan withCardanoCliUtxos rounds = do ) roundsFixed +newtype KeyWalletShow = KeyWalletShow KeyWallet + +derive instance Newtype KeyWalletShow _ + +instance Show KeyWalletShow where + show _ = "(KeyWallet )" + makeDistrFundsPlan :: forall (distr :: Type) (wallets :: Type) . UtxoDistribution distr wallets @@ -321,15 +329,16 @@ makeDistrFundsPlan withCardanoCliUtxos genesisWallets distr = do "Impossible happened: could not decode wallets. Please report as bug" $ decodeWallets distr privateKeys let - kws = keyWallets (Proxy :: _ distr) wallets + kws = KeyWalletShow <$> keyWallets (Proxy :: _ distr) wallets targets = Array.concat $ sequence <$> Array.zip kws distrArray sources <- Array.concat <$> - parTraverse (\kw -> map (Tuple kw) <$> getGenesisUtxos kw) + parTraverse (\kw -> map (Tuple (KeyWalletShow kw)) <$> getGenesisUtxos kw) genesisWallets + -- traceM $ "genesis sources: " <> show sources distrPlan <- liftContractE $ DistrFunds.makeDistributionPlan distrFundsParams sources targets - pure $ wallets /\ distrPlan + pure $ wallets /\ (map (lmap unwrap) <$> distrPlan) where getGenesisUtxos :: KeyWallet -> Contract (Array BigInt) getGenesisUtxos genesisWallet = @@ -348,7 +357,7 @@ makeDistrFundsPlan withCardanoCliUtxos genesisWallets distr = do <<< Map.values -- FIXME: adjust values -distrFundsParams :: DistrFundsParams KeyWallet BigInt +distrFundsParams :: forall wallet. DistrFundsParams wallet BigInt distrFundsParams = { maxRounds: 3 , maxUtxosPerTx: 100 diff --git a/src/Internal/Testnet/DistributeFunds.purs b/src/Internal/Testnet/DistributeFunds.purs index c2f801c19..1990693b1 100644 --- a/src/Internal/Testnet/DistributeFunds.purs +++ b/src/Internal/Testnet/DistributeFunds.purs @@ -1,252 +1,273 @@ -module Ctl.Internal.Testnet.DistributeFunds - ( makeDistributionPlan - , parallelizedDistributionPlan - , SourceState(SourceState) - , Tx(Tx) - , _completeTxs - , _leftover - , _source - , _total - , _totalUtxos - , _tx - , _utxos - -- * Exported for testing purposes - , assignUtxo - , emptyTx - , initialSourceState - ) where - -import Contract.Prelude - -import Control.Alt ((<|>)) -import Data.Array as Array -import Data.Bifunctor (class Bifunctor, bimap) -import Data.Lens (Lens', view, (%~), (+~), (-~), (.~), (^.)) -import Data.Lens.Iso.Newtype (_Newtype) -import Data.Lens.Record (prop) +module Ctl.Internal.Testnet.DistributeFunds where + +import Prelude + +import Control.Monad.Rec.Class (Step(Done, Loop), tailRecM) +import Control.Safely (foldM) +import Data.Array (fromFoldable, snoc) as Array +import Data.Bifunctor (class Bifunctor) +import Data.Either (Either(Left, Right)) +import Data.Foldable + ( class Foldable + , foldMap + , foldlDefault + , foldr + , foldrDefault + ) +import Data.Generic.Rep (class Generic) import Data.List (List(Cons, Nil)) -import Data.List as List -import Data.Map (Map) -import Data.Map as Map -import Effect.Exception.Unsafe (unsafeThrow) -import Type.Proxy (Proxy(Proxy)) - -newtype Tx src target amount = Tx - { source :: { key :: src } - , total :: amount - , totalUtxos :: Int - , utxos :: List { key :: target, amount :: amount } +import Data.List (filter, fromFoldable) as List +import Data.Newtype (class Newtype, modify, unwrap, wrap) +import Data.Show.Generic (genericShow) +import Data.Traversable (class Traversable, sequenceDefault, traverse) +import Data.Tuple (uncurry) +import Data.Tuple.Nested (type (/\), (/\)) + +type DistrFundsParams wallet amount = + { maxRounds :: Int + , maxUtxosPerTx :: Int + , getUtxoMinAdaForWallet :: wallet -> amount + , feePerTx :: amount + } + +-- + +newtype Tx wallet amount = Tx + { srcWallet :: wallet + , numUtxos :: Int + , utxos :: List { wallet :: wallet, amount :: amount } } -derive instance Newtype (Tx s t a) _ -derive instance Generic (Tx s t a) _ -derive instance (Eq s, Eq t, Eq a) => Eq (Tx s t a) -derive instance (Ord s, Ord t, Ord a) => Ord (Tx s t a) -instance (Show s, Show t, Show a) => Show (Tx s t a) where +derive instance Generic (Tx wallet amount) _ +derive instance Newtype (Tx wallet amount) _ +derive instance (Eq wallet, Eq amount) => Eq (Tx wallet amount) + +instance (Show wallet, Show amount) => Show (Tx wallet amount) where show = genericShow -emptyTx - :: forall target amount - . amount - -> Tx Unit target amount -emptyTx total = Tx - { source: { key: unit } - , total - , totalUtxos: 0 +instance Functor (Tx wallet) where + map f (Tx tx) = + wrap $ tx + { utxos = + map (\utxo -> utxo { amount = f utxo.amount }) + tx.utxos + } + +instance Bifunctor Tx where + bimap f g (Tx tx) = + wrap $ tx + { srcWallet = f tx.srcWallet + , utxos = map (\utxo -> { wallet: f utxo.wallet, amount: g utxo.amount }) + tx.utxos + } + +instance Foldable (Tx wallet) where + foldl f a = foldlDefault f a + foldr f a = foldrDefault f a + foldMap f = foldMap (f <<< _.amount) <<< _.utxos <<< unwrap + +instance Traversable (Tx wallet) where + sequence = sequenceDefault + traverse f (Tx tx) = ado + utxos <- traverse + (\{ wallet, amount } -> { wallet, amount: _ } <$> f amount) + tx.utxos + in wrap $ tx { utxos = utxos } + +emptyTx :: forall wallet amount. wallet -> Tx wallet amount +emptyTx srcWallet = wrap + { srcWallet + , numUtxos: zero , utxos: Nil } -newtype SourceState src target amount = SourceState - { source :: src +isTxNonEmpty :: forall wallet amount. Tx wallet amount -> Boolean +isTxNonEmpty (Tx { numUtxos }) = numUtxos > zero + +-- + +type SourceState wallet amount = + { srcWallet :: wallet , leftover :: amount - , tx :: Tx Unit target amount - , completeTxs :: List (Tx Unit target amount) + , currentTx :: Tx wallet amount + } + +initSourceState + :: forall wallet amount + . wallet + -> amount + -> SourceState wallet amount +initSourceState srcWallet initFunds = + { srcWallet + , leftover: initFunds + , currentTx: emptyTx srcWallet } -derive instance Newtype (SourceState s t a) _ -derive instance Generic (SourceState s t a) _ -derive instance (Eq s, Eq t, Eq a) => Eq (SourceState s t a) -derive instance (Ord s, Ord t, Ord a) => Ord (SourceState s t a) -instance (Show s, Show t, Show a) => Show (SourceState s t a) where +resetSourceTx + :: forall wallet amount + . SourceState wallet amount + -> SourceState wallet amount +resetSourceTx src = src { currentTx = emptyTx src.srcWallet } + +-- + +data DistrFundsError wallet amount + = DistrFunds_MaxUtxosPerTxLowerLimitError + | DistrFunds_AssignUtxoError + { utxoToAssign :: wallet /\ amount + , currentSources :: List (SourceState wallet amount) + } + | DistrFunds_MaxRoundsExceededError + +derive instance Generic (DistrFundsError wallet amount) _ +derive instance (Eq wallet, Eq amount) => Eq (DistrFundsError wallet amount) + +instance (Show wallet, Show amount) => Show (DistrFundsError wallet amount) where show = genericShow -initialSourceState - :: forall src target amount - . Semiring amount - => { initialFunds :: amount, key :: src } - -> SourceState src target amount -initialSourceState { initialFunds, key } = SourceState - { source: key - , leftover: initialFunds - , tx: emptyTx zero - , completeTxs: Nil +type DistrFundsRoundResult wallet amount = + { sources :: List (SourceState wallet amount) + , deferredTargets :: List (wallet /\ amount) } -parallelizedDistributionPlan - :: forall src target amount - . Map src (Array (Tx Unit target amount)) - -> Array (Map src (Tx Unit target amount)) -parallelizedDistributionPlan _ = unsafeThrow "hello" +data AssignUtxoResult wallet amount + = AssignUtxo_Unassigned + | AssignUtxo_Deferred + | AssignUtxo_AssignedToSource (SourceState wallet amount) + +derive instance Generic (AssignUtxoResult wallet amount) _ +derive instance (Eq wallet, Eq amount) => Eq (AssignUtxoResult wallet amount) + +instance (Show wallet, Show amount) => Show (AssignUtxoResult wallet amount) where + show = genericShow makeDistributionPlan - :: forall src target amount - . Ord src - => Ord amount - => Ord target - => Ring amount - => Map src amount - -> Map target (Array amount) - -> { maxCoinPerTx :: amount - , maxTargetUtxosPerTx :: Int - } - -> Either - { err :: String - , acc :: List (SourceState src target amount) - } - (Map src (Array (Tx Unit target amount))) -makeDistributionPlan sources targets thresholds = do - let - targetsUtxosAsc :: List { key :: target, amount :: amount } - targetsUtxosAsc = List.sortBy (flip compare) - $ Map.toUnfoldable targets - >>= \(key /\ utxos) -> - { key, amount: _ } <$> List.fromFoldable utxos - - assigned :: Either _ (Map src (Array (Tx Unit target amount))) - assigned = do - sourcesTxs <- foldM - (flip $ assignUtxo thresholds) - ( initialSourceState <<< uncurry { key: _, initialFunds: _ } <$> - Map.toUnfoldable sources - ) - targetsUtxosAsc - let - finish src = - src - # (_tx .~ emptyTx zero) - # (_completeTxs %~ Cons (src ^. _tx)) - sourceToTxs = Map.fromFoldable - $ Tuple - <<< view _source - <*> Array.fromFoldable - <<< view _completeTxs - <<< finish - <$> sourcesTxs - pure sourceToTxs - assigned - -assignUtxo - :: forall target src amount + :: forall wallet amount . Ord amount => Ring amount - => { maxCoinPerTx :: amount - , maxTargetUtxosPerTx :: Int - } - -> { amount :: amount, key :: target } - -> List (SourceState src target amount) - -> Either - { err :: String - , acc :: List (SourceState src target amount) - } - (List (SourceState src target amount)) -assignUtxo _ _ Nil = Left - { err: "Ran out of sources", acc: Nil } -assignUtxo thresholds utxo acc@(Cons source sources) - | 0 >= thresholds.maxTargetUtxosPerTx = - Left { err: "maxTargetUtxosPerTx must be greater than 1", acc } - | utxo.amount >= thresholds.maxCoinPerTx = - Left - { err: "UTxO required amount is higher than the maxCoinPerTx threshold" - , acc + => DistrFundsParams wallet amount + -> Array (wallet /\ amount) + -> Array (wallet /\ amount) + -> Either (DistrFundsError wallet amount) (Array (Array (Tx wallet amount))) +makeDistributionPlan params initSources initTargets + | params.maxUtxosPerTx < one = Left DistrFunds_MaxUtxosPerTxLowerLimitError + | otherwise = + tailRecM worker + { sources: List.fromFoldable $ uncurry initSourceState <$> initSources + , targets: List.fromFoldable initTargets + , rounds: mempty + , roundIdx: zero } - | (source ^. _tx <<< _totalUtxos) - >= thresholds.maxTargetUtxosPerTx = - -- means that this Tx is complete - assignUtxo thresholds utxo - $ startNewTx source sources -- be careful: infinite loop - -- it will terminate because new tx has 0 utxos which is higher than 'maxTargetUtxosPerTx' - | (source ^. _tx <<< _total) + utxo.amount - > thresholds.maxCoinPerTx = - -- means that utxo cannot be fit in this Tx + where + worker { sources, targets, rounds, roundIdx } + | roundIdx == params.maxRounds = + Left DistrFunds_MaxRoundsExceededError + | otherwise = + runDistrFundsRound params sources targets <#> \res -> + let + completedTxs = List.filter isTxNonEmpty $ _.currentTx <$> + res.sources + rounds' = Array.snoc rounds $ Array.fromFoldable completedTxs + in + case res.deferredTargets of + Nil -> Done rounds' + _ -> Loop + { sources: resetSourceTx <$> res.sources + , targets: res.deferredTargets + , rounds: rounds' + , roundIdx: roundIdx + one + } + +runDistrFundsRound + :: forall wallet amount + . Ord amount + => Ring amount + => DistrFundsParams wallet amount + -> List (SourceState wallet amount) + -> List (wallet /\ amount) + -> Either (DistrFundsError wallet amount) + (DistrFundsRoundResult wallet amount) +runDistrFundsRound params initSources targets = + foldM + ( \distrFundsAcc target -> + let + assignUtxoRes /\ sourcesUpdated = + foldr (tryNextSource params target) (AssignUtxo_Unassigned /\ Nil) + distrFundsAcc.sources + in + case assignUtxoRes of + AssignUtxo_Unassigned -> + Left $ DistrFunds_AssignUtxoError + { utxoToAssign: target + , currentSources: distrFundsAcc.sources + } + AssignUtxo_Deferred -> + Right $ distrFundsAcc + { deferredTargets = Cons target distrFundsAcc.deferredTargets + } + AssignUtxo_AssignedToSource _ -> + Right $ distrFundsAcc + { sources = sourcesUpdated + } + ) + { sources: initSources + , deferredTargets: Nil + } + targets + +tryNextSource + :: forall wallet amount + . Ord amount + => Ring amount + => DistrFundsParams wallet amount + -> wallet /\ amount + -> SourceState wallet amount + -> AssignUtxoResult wallet amount /\ List (SourceState wallet amount) + -> AssignUtxoResult wallet amount /\ List (SourceState wallet amount) +tryNextSource params (targetWallet /\ amount) source (acc /\ sources) = + case acc of + AssignUtxo_AssignedToSource _ -> + -- utxo already assigned, skip other sources + acc /\ Cons source sources + _ -> let - -- try fit this utxo in any source - tryAnother = tryWithAnotherSource - "Cannot fit UTxO amount into the Tx" - (assignUtxo thresholds utxo) - source - sources - -- if no source can fit this utxo, create a new tx - startNew = assignUtxo thresholds utxo - $ startNewTx source sources -- be careful: infinite loop - -- it will terminate because either new Tx starting with 0 total can fit it - -- or the condition above will throw Left + targetNormalized = + targetWallet /\ max (params.getUtxoMinAdaForWallet targetWallet) + amount in - tryAnother <|> startNew - | source ^. _leftover < utxo.amount = - -- means that this source cannot fit this tx - -- should try with the rest of sources and fail otherwise - tryWithAnotherSource - "Not enough funds on sources" - (assignUtxo thresholds utxo) - source - sources + case acc, assignUtxoToSource params source targetNormalized of + AssignUtxo_Deferred, AssignUtxo_Unassigned -> + -- utxo marked as deferred that cannot fit into the current tx + -- should remain deferred + AssignUtxo_Deferred /\ Cons source sources + _, new@(AssignUtxo_AssignedToSource sourceUpdated) -> + new /\ Cons sourceUpdated sources + _, new -> + new /\ Cons source sources + +assignUtxoToSource + :: forall wallet amount + . Ord amount + => Ring amount + => DistrFundsParams wallet amount + -> SourceState wallet amount + -> wallet /\ amount + -> AssignUtxoResult wallet amount +assignUtxoToSource params source (targetWallet /\ amountNormalized) + | (source.leftover - params.feePerTx) < amountNormalized = + AssignUtxo_Unassigned + | (unwrap source.currentTx).numUtxos + one > params.maxUtxosPerTx = + AssignUtxo_Deferred | otherwise = - -- means that utxo can be fit into the current tx - let - source' = source - # (_leftover -~ utxo.amount) - # (_tx <<< _total +~ utxo.amount) - # (_tx <<< _totalUtxos +~ 1) - # (_tx <<< _utxos %~ Cons utxo) - in - Right $ Cons source' sources - --- * Helpers - --- helper for assignUtxo -tryWithAnotherSource - :: forall s f - . Bifunctor f - => String - -> (List s -> f { err :: String, acc :: List s } (List s)) - -> s - -> List s - -> f { err :: String, acc :: List s } (List s) -tryWithAnotherSource err self source sources = - bimap (\e -> e { err = err <> "/" <> e.err, acc = Cons source e.acc }) - (Cons source) - $ self sources - --- helper for assignUtxo -startNewTx - :: forall src target amount - . Semiring amount - => SourceState src target amount - -> List (SourceState src target amount) - -> List (SourceState src target amount) -startNewTx source sources = - List.snoc sources - $ (_tx .~ emptyTx zero) - $ (_completeTxs %~ Cons (source ^. _tx)) - $ source - -_totalUtxos :: forall s t a. Lens' (Tx s t a) Int -_totalUtxos = _Newtype <<< prop (Proxy :: _ "totalUtxos") - -_utxos :: forall s t a. Lens' (Tx s t a) (List { key :: t, amount :: a }) -_utxos = _Newtype <<< prop (Proxy :: _ "utxos") - -_total :: forall s t a. Lens' (Tx s t a) a -_total = _Newtype <<< prop (Proxy :: _ "total") - -_tx :: forall s t a. Lens' (SourceState s t a) (Tx Unit t a) -_tx = _Newtype <<< prop (Proxy :: _ "tx") - -_leftover :: forall s t a. Lens' (SourceState s t a) a -_leftover = _Newtype <<< prop (Proxy :: _ "leftover") - -_source :: forall s t a. Lens' (SourceState s t a) s -_source = _Newtype <<< prop (Proxy :: _ "source") - -_completeTxs :: forall s t a. Lens' (SourceState s t a) (List (Tx Unit t a)) -_completeTxs = _Newtype <<< prop (Proxy :: _ "completeTxs") + AssignUtxo_AssignedToSource $ source + { leftover = source.leftover - amountNormalized + , currentTx = modify + ( \tx -> tx + { numUtxos = tx.numUtxos + one + , utxos = Cons + { wallet: targetWallet, amount: amountNormalized } + tx.utxos + } + ) + source.currentTx + } diff --git a/src/Internal/Testnet/DistributeFundsV2.purs b/src/Internal/Testnet/DistributeFundsV2.purs deleted file mode 100644 index 4476cc86f..000000000 --- a/src/Internal/Testnet/DistributeFundsV2.purs +++ /dev/null @@ -1,266 +0,0 @@ -module Ctl.Internal.Testnet.DistributeFundsV2 where - -import Prelude - -import Control.Monad.Rec.Class (Step(Done, Loop), tailRecM) -import Control.Safely (foldM) -import Data.Array (fromFoldable, snoc) as Array -import Data.Bifunctor (class Bifunctor) -import Data.Either (Either(Left, Right)) -import Data.Foldable - ( class Foldable - , foldMap - , foldlDefault - , foldr - , foldrDefault - ) -import Data.Generic.Rep (class Generic) -import Data.List (List(Cons, Nil)) -import Data.List (filter, fromFoldable) as List -import Data.Newtype (class Newtype, modify, unwrap, wrap) -import Data.Show.Generic (genericShow) -import Data.Traversable (class Traversable, sequenceDefault, traverse) -import Data.Tuple (uncurry) -import Data.Tuple.Nested (type (/\), (/\)) - -type DistrFundsParams wallet amount = - { maxRounds :: Int - , maxUtxosPerTx :: Int - , getUtxoMinAdaForWallet :: wallet -> amount - , feePerTx :: amount - } - --- - -newtype Tx wallet amount = Tx - { srcWallet :: wallet - , numUtxos :: Int - , utxos :: List { wallet :: wallet, amount :: amount } - } - -derive instance Generic (Tx wallet amount) _ -derive instance Newtype (Tx wallet amount) _ -derive instance (Eq wallet, Eq amount) => Eq (Tx wallet amount) - -instance (Show wallet, Show amount) => Show (Tx wallet amount) where - show = genericShow - -instance Functor (Tx wallet) where - map f (Tx tx) = - wrap $ tx - { utxos = - map (\utxo -> utxo { amount = f utxo.amount }) - tx.utxos - } - -instance Bifunctor Tx where - bimap f g (Tx tx) = - wrap $ tx - { srcWallet = f tx.srcWallet - , utxos = map (\utxo -> { wallet: f utxo.wallet, amount: g utxo.amount }) - tx.utxos - } - -instance Foldable (Tx wallet) where - foldl f a = foldlDefault f a - foldr f a = foldrDefault f a - foldMap f = foldMap (f <<< _.amount) <<< _.utxos <<< unwrap - -instance Traversable (Tx wallet) where - sequence = sequenceDefault - traverse f (Tx tx) = ado - utxos <- traverse - (\{ wallet, amount } -> { wallet, amount: _ } <$> f amount) - tx.utxos - in wrap $ tx { utxos = utxos } - -emptyTx :: forall wallet amount. wallet -> Tx wallet amount -emptyTx srcWallet = wrap - { srcWallet - , numUtxos: zero - , utxos: Nil - } - -isTxNonEmpty :: forall wallet amount. Tx wallet amount -> Boolean -isTxNonEmpty (Tx { numUtxos }) = numUtxos > zero - --- - -type SourceState wallet amount = - { srcWallet :: wallet - , leftover :: amount - , currentTx :: Tx wallet amount - } - -initSourceState - :: forall wallet amount - . wallet - -> amount - -> SourceState wallet amount -initSourceState srcWallet initFunds = - { srcWallet - , leftover: initFunds - , currentTx: emptyTx srcWallet - } - -resetSourceTx - :: forall wallet amount - . SourceState wallet amount - -> SourceState wallet amount -resetSourceTx src = src { currentTx = emptyTx src.srcWallet } - --- - -data DistrFundsError - = DistrFunds_MaxUtxosPerTxLowerLimitError - | DistrFunds_AssignUtxoError - | DistrFunds_MaxRoundsExceededError - -derive instance Generic DistrFundsError _ -derive instance Eq DistrFundsError - -instance Show DistrFundsError where - show = genericShow - -type DistrFundsRoundResult wallet amount = - { sources :: List (SourceState wallet amount) - , deferredTargets :: List (wallet /\ amount) - } - -data AssignUtxoResult wallet amount - = AssignUtxo_Unassigned - | AssignUtxo_Deferred - | AssignUtxo_AssignedToSource (SourceState wallet amount) - -derive instance Generic (AssignUtxoResult wallet amount) _ -derive instance (Eq wallet, Eq amount) => Eq (AssignUtxoResult wallet amount) - -instance (Show wallet, Show amount) => Show (AssignUtxoResult wallet amount) where - show = genericShow - -makeDistributionPlan - :: forall wallet amount - . Ord amount - => Ring amount - => DistrFundsParams wallet amount - -> Array (wallet /\ amount) - -> Array (wallet /\ amount) - -> Either DistrFundsError (Array (Array (Tx wallet amount))) -makeDistributionPlan params initSources initTargets - | params.maxUtxosPerTx < one = Left DistrFunds_MaxUtxosPerTxLowerLimitError - | otherwise = - tailRecM worker - { sources: List.fromFoldable $ uncurry initSourceState <$> initSources - , targets: List.fromFoldable initTargets - , rounds: mempty - , roundIdx: zero - } - where - worker { sources, targets, rounds, roundIdx } - | roundIdx == params.maxRounds = - Left DistrFunds_MaxRoundsExceededError - | otherwise = - runDistrFundsRound params sources targets <#> \res -> - let - completedTxs = List.filter isTxNonEmpty $ _.currentTx <$> - res.sources - rounds' = Array.snoc rounds $ Array.fromFoldable completedTxs - in - case res.deferredTargets of - Nil -> Done rounds' - _ -> Loop - { sources: resetSourceTx <$> res.sources - , targets: res.deferredTargets - , rounds: rounds' - , roundIdx: roundIdx + one - } - -runDistrFundsRound - :: forall wallet amount - . Ord amount - => Ring amount - => DistrFundsParams wallet amount - -> List (SourceState wallet amount) - -> List (wallet /\ amount) - -> Either DistrFundsError (DistrFundsRoundResult wallet amount) -runDistrFundsRound params initSources targets = - foldM - ( \distrFundsAcc target -> - let - assignUtxoRes /\ sourcesUpdated = - foldr (tryNextSource params target) (AssignUtxo_Unassigned /\ Nil) - distrFundsAcc.sources - in - case assignUtxoRes of - AssignUtxo_Unassigned -> - Left DistrFunds_AssignUtxoError - AssignUtxo_Deferred -> - Right $ distrFundsAcc - { deferredTargets = Cons target distrFundsAcc.deferredTargets - } - AssignUtxo_AssignedToSource _ -> - Right $ distrFundsAcc - { sources = sourcesUpdated - } - ) - { sources: initSources - , deferredTargets: Nil - } - targets - -tryNextSource - :: forall wallet amount - . Ord amount - => Ring amount - => DistrFundsParams wallet amount - -> wallet /\ amount - -> SourceState wallet amount - -> AssignUtxoResult wallet amount /\ List (SourceState wallet amount) - -> AssignUtxoResult wallet amount /\ List (SourceState wallet amount) -tryNextSource params (targetWallet /\ amount) source (acc /\ sources) = - case acc of - AssignUtxo_AssignedToSource _ -> - -- utxo already assigned, skip other sources - acc /\ Cons source sources - _ -> - let - targetNormalized = - targetWallet /\ max (params.getUtxoMinAdaForWallet targetWallet) - amount - in - case acc, assignUtxoToSource params source targetNormalized of - AssignUtxo_Deferred, AssignUtxo_Unassigned -> - -- utxo marked as deferred that cannot fit into the current tx - -- should remain deferred - AssignUtxo_Deferred /\ Cons source sources - _, new@(AssignUtxo_AssignedToSource sourceUpdated) -> - new /\ Cons sourceUpdated sources - _, new -> - new /\ Cons source sources - -assignUtxoToSource - :: forall wallet amount - . Ord amount - => Ring amount - => DistrFundsParams wallet amount - -> SourceState wallet amount - -> wallet /\ amount - -> AssignUtxoResult wallet amount -assignUtxoToSource params source (targetWallet /\ amountNormalized) - | (source.leftover - params.feePerTx) < amountNormalized = - AssignUtxo_Unassigned - | (unwrap source.currentTx).numUtxos + one > params.maxUtxosPerTx = - AssignUtxo_Deferred - | otherwise = - AssignUtxo_AssignedToSource $ source - { leftover = source.leftover - amountNormalized - , currentTx = modify - ( \tx -> tx - { numUtxos = tx.numUtxos + one - , utxos = Cons - { wallet: targetWallet, amount: amountNormalized } - tx.utxos - } - ) - source.currentTx - } diff --git a/test/Testnet/DistributeFunds.purs b/test/Testnet/DistributeFunds.purs index 9971d7f10..cbb7efd0f 100644 --- a/test/Testnet/DistributeFunds.purs +++ b/test/Testnet/DistributeFunds.purs @@ -1,274 +1,207 @@ -module Test.Ctl.Testnet.DistributeFunds where +module Test.Ctl.Testnet.DistributeFunds + ( suite + ) where -import Contract.Prelude hiding (over) +import Prelude import Contract.Test.Mote (TestPlanM) import Ctl.Internal.Testnet.DistributeFunds - ( _completeTxs - , _leftover - , _source - , _total - , _totalUtxos - , _tx - , _utxos + ( AssignUtxoResult + ( AssignUtxo_Unassigned + , AssignUtxo_Deferred + , AssignUtxo_AssignedToSource + ) + , DistrFundsError(DistrFunds_AssignUtxoError) + , DistrFundsParams + , SourceState + , assignUtxoToSource + , initSourceState + , makeDistributionPlan + , runDistrFundsRound ) -import Ctl.Internal.Testnet.DistributeFunds as Distribute -import Data.Bifunctor (lmap) -import Data.Lens (over, set, view, (%~), (+~), (-~), (.~), (^.)) -import Data.List (List(Cons)) -import Data.List as List +import Data.Array (reverse) +import Data.Either (Either(Left, Right)) +import Data.List (fromFoldable) as List +import Data.Maybe (Maybe(Just, Nothing)) +import Data.Newtype (modify, wrap) +import Data.Tuple.Nested (type (/\), (/\)) +import Effect.Aff (Aff) import Mote (group, test) -import Test.Spec.Assertions (shouldEqual) +import Test.Spec.Assertions (shouldEqual, shouldSatisfy) suite :: TestPlanM (Aff Unit) Unit -suite = group "Testnet" $ group "Distribute Funds" do - group "assignUtxo" do - let - highThreshold = - { maxCoinPerTx: 999_999 - , maxTargetUtxosPerTx: 999_999 - } +suite = + group "DistributeFunds" do + group "assignUtxoToSource" do + test "Leaves utxo unassigned if it cannot be covered by source" + let + src0 = initSourceState "src0" 1000 + utxo = "target0" /\ 2000 + in + assignUtxoToSource defaultParams src0 utxo `shouldEqual` + AssignUtxo_Unassigned - test "Fails if sources do not have enough funds" do + test "Takes tx fee into account" + let + params = defaultParams { feePerTx = 10 } + src0 = initSourceState "src0" 1000 + utxo = "target0" /\ 1000 + in + assignUtxoToSource params src0 utxo `shouldEqual` + AssignUtxo_Unassigned - let - outcome = - Distribute.assignUtxo - highThreshold - { amount: 120, key: "utxo0" } - $ List.fromFoldable - [ Distribute.initialSourceState - { key: 1, initialFunds: 90 } - , Distribute.initialSourceState - { key: 2, initialFunds: 100 } - ] - lmap (const unit) outcome `shouldEqual` Left unit - test "Starts new Tx when reaches the limit of UTxOs" do - let - thresholds = highThreshold { maxTargetUtxosPerTx = 3 } + test "Marks utxo as deferred if maxUtxosPerTx is exceeded" + let + params = defaultParams { maxUtxosPerTx = 0 } + src0 = initSourceState "src0" 1000 + utxo = "target0" /\ 1000 + in + assignUtxoToSource params src0 utxo `shouldEqual` + AssignUtxo_Deferred - src0 :: Distribute.SourceState Int String Int - src0 = - Distribute.initialSourceState - { key: 0, initialFunds: 90 } - # (_tx <<< _total .~ 370) - # (_tx <<< _totalUtxos .~ 2) - # - ( _tx <<< _utxos .~ List.fromFoldable - [ { key: "tgt0", amount: 300 } - , { key: "tgt42", amount: 70 } - ] - ) + test "Correctly assigns utxos to source" + let + params = defaultParams { feePerTx = 100 } + src0 = initSourceState "src0" 2000 + utxo0 = "target0" /\ 1000 + utxo1 = "target1" /\ 500 + assignUtxo = flip (assignUtxoToSource params) + outcome = + (getSource <<< assignUtxo utxo1) + =<< getSource (assignUtxo utxo0 src0) + in + outcome `shouldEqual` Just + ( src0 + { leftover = 500 + , currentTx = modify + ( _ + { numUtxos = 2 + , utxos = List.fromFoldable + [ utxoToRec utxo1, utxoToRec utxo0 ] + } + ) + src0.currentTx + } + ) - src1 :: Distribute.SourceState Int String Int - src1 = Distribute.initialSourceState - { key: 1, initialFunds: 30 } + group "runDistrFundsRound" do + test "Fails if utxo cannot be covered by any source" + let + sources = List.fromFoldable + [ initSourceState "src0" 1000 + , initSourceState "src1" 2000 + , initSourceState "src2" 3000 + ] + utxos = List.fromFoldable + [ "target0" /\ 1000 + , "target1" /\ 3500 + ] + in + runDistrFundsRound defaultParams sources utxos `shouldSatisfy` + case _ of + Left (DistrFunds_AssignUtxoError _) -> true + _ -> false - utxo0 = { amount: 10, key: "utxo0" } - utxo1 = { amount: 6, key: "utxo1" } - acc0 = List.fromFoldable [ src0, src1 ] - outcome = do - acc1 <- Distribute.assignUtxo - thresholds - utxo0 - acc0 - acc2 <- Distribute.assignUtxo - thresholds - utxo1 - acc1 - pure acc2 + test "Deferrs utxos that cannot be assigned in the current round" + let + params = defaultParams { maxUtxosPerTx = 1 } + src0 = initSourceState "src0" 2000 + src1 = initSourceState "src1" 1000 + sources = List.fromFoldable [ src0, src1 ] + utxo0 = "target0" /\ 1600 + utxo1 = "target1" /\ 800 + utxo2 = "target2" /\ 400 + utxos = List.fromFoldable [ utxo0, utxo1, utxo2 ] + in + runDistrFundsRound params sources utxos `shouldEqual` + Right + { sources: List.fromFoldable + [ src0 + { leftover = 400 + , currentTx = modify + ( _ + { numUtxos = 1 + , utxos = List.fromFoldable [ utxoToRec utxo0 ] + } + ) + src0.currentTx + } + , src1 + { leftover = 200 + , currentTx = modify + ( _ + { numUtxos = 1 + , utxos = List.fromFoldable [ utxoToRec utxo1 ] + } + ) + src1.currentTx + } + ] + , deferredTargets: List.fromFoldable [ utxo2 ] + } - expected :: List (Distribute.SourceState Int String Int) - expected = List.fromFoldable - [ src1 - # (_leftover -~ utxo1.amount) - # - ( set _tx - $ Distribute.emptyTx utxo1.amount - # (_totalUtxos .~ 1) - # (_utxos .~ pure utxo1) - ) - , src0 - # (_leftover -~ utxo0.amount) - # (_tx .~ Distribute.emptyTx zero) - # - ( over _completeTxs - $ src0 - # (view _tx) - # (_total +~ utxo0.amount) - # (_totalUtxos .~ 3) - # (_utxos %~ Cons utxo0) - # Cons - ) - ] + group "makeDistributionPlan" do + test "Prepares simple funds distribution plan (2 rounds)" + let + params = defaultParams { maxUtxosPerTx = 2, maxRounds = 5 } - outcome `shouldEqual` Right expected - test "Tends to spend sources evenly" do - let - utxos0 = List.fromFoldable - [ { key: "01", amount: 2 } - , { key: "02", amount: 18 } - ] - utxos1 = List.fromFoldable - [ { key: "11", amount: 3 } - , { key: "12", amount: 17 } - ] - utxos2 = List.fromFoldable - [ { key: "21", amount: 9 } - , { key: "22", amount: 11 } - ] - utxos3 = List.fromFoldable - [ { key: "31", amount: 15 } - , { key: "32", amount: 5 } - ] - -- total = 80 - utxos = utxos0 <> utxos1 <> utxos2 <> utxos3 + utxos0 = [ "01" /\ 2, "02" /\ 18 ] + utxos1 = [ "11" /\ 3, "12" /\ 17 ] + utxos2 = [ "21" /\ 9, "22" /\ 11 ] + utxos3 = [ "31" /\ 15, "32" /\ 5 ] - -- they have exactly enough to fit all the utxos - src0 = Distribute.initialSourceState - { key: 0, initialFunds: 40 } - src1 = Distribute.initialSourceState - { key: 1, initialFunds: 40 } - sources = List.fromFoldable [ src0, src1 ] + -- total = 80 + utxos = utxos0 <> utxos1 <> utxos2 <> utxos3 - outcome = foldM - ( flip $ Distribute.assignUtxo highThreshold - { maxTargetUtxosPerTx = 2 } - ) - sources - utxos + -- sources have exactly enough funds to fit all the utxos + src0 = "src0" /\ 40 + src1 = "src1" /\ 40 + sources = [ src0, src1 ] + in + makeDistributionPlan params sources utxos `shouldEqual` + Right + [ [ wrap + { srcWallet: "src0" + , numUtxos: 2 + , utxos: List.fromFoldable $ utxoToRec <$> reverse utxos1 + } + , wrap + { srcWallet: "src1" + , numUtxos: 2 + , utxos: List.fromFoldable $ utxoToRec <$> reverse utxos0 + } + ] + , [ wrap + { srcWallet: "src0" + , numUtxos: 2 + , utxos: List.fromFoldable $ utxoToRec <$> utxos2 + } + , wrap + { srcWallet: "src1" + , numUtxos: 2 + , utxos: List.fromFoldable $ utxoToRec <$> utxos3 + } + ] + ] - -- Both must have 2 txs, 20 UTxO each. All sources funds must be spent. - expected = List.fromFoldable - [ src1 - # (_leftover .~ 0) - -- it would be put in completeTxs on the next iteration - # - ( set _tx - $ Distribute.emptyTx 20 - # (_totalUtxos .~ 2) - # (_utxos .~ List.reverse utxos3) - ) - # - ( set _completeTxs - $ Distribute.emptyTx 20 - # (_totalUtxos .~ 2) - # (_utxos .~ List.reverse utxos1) - # pure - ) - , src0 - # (_leftover .~ 0) - # - ( _completeTxs .~ List.fromFoldable - [ Distribute.emptyTx 20 - # (_totalUtxos .~ 2) - # (_utxos .~ List.reverse utxos2) - , Distribute.emptyTx 20 - # (_totalUtxos .~ 2) - # (_utxos .~ List.reverse utxos0) - ] - ) - ] - outcome `shouldEqual` Right expected +utxoToRec + :: forall wallet amount + . wallet /\ amount + -> { wallet :: wallet, amount :: amount } +utxoToRec (wallet /\ amount) = { wallet, amount } - -- It could be better: seach for a source that have a Tx with lowest total or lowest amount of UTxOs - -- But if it worths it? - test "Makes new Tx if utxo is impossible to fit in existing ones" do - let - src0 = - Distribute.initialSourceState - { key: 0, initialFunds: 900 } - # (_tx <<< _total .~ 120) - src1 = - Distribute.initialSourceState - { key: 1, initialFunds: 800 } - # (_tx <<< _total .~ 105) - utxo = { key: "utxo0", amount: 100 } - outcome = - Distribute.assignUtxo - highThreshold { maxCoinPerTx = 200 } - utxo - $ List.fromFoldable [ src0, src1 ] - expected = List.fromFoldable - [ src0 - , src1 - # (_leftover -~ utxo.amount) - # - ( set _tx - $ Distribute.emptyTx utxo.amount - # (_totalUtxos .~ 1) - # (_utxos .~ pure utxo) - ) - # (_completeTxs .~ pure (src1 ^. _tx)) - ] - outcome `shouldEqual` Right expected - test "Tries to fit UTxO in any constructing tx that can fit it" do - let - src0 = - Distribute.initialSourceState - { key: 0, initialFunds: 900 } - -- not enough to fit the utxo - # (_tx <<< _total .~ 120) - src1 = - Distribute.initialSourceState - { key: 1, initialFunds: 800 } - -- exactly enough to fit the utxo - # (_tx <<< _total .~ 100) - utxo = { key: "utxo0", amount: 100 } - outcome = - Distribute.assignUtxo - highThreshold { maxCoinPerTx = 200 } - utxo - $ List.fromFoldable [ src0, src1 ] - expected = List.fromFoldable - [ src0 - , src1 - # (_leftover -~ utxo.amount) - # (_tx <<< _total +~ utxo.amount) - # (_tx <<< _totalUtxos +~ 1) - # (_tx <<< _utxos .~ pure utxo) - ] - outcome `shouldEqual` Right expected - test "Tries to fit UTxO in any source tx that has enough funds" do - let - -- not enough - src0 = Distribute.initialSourceState - { key: 0, initialFunds: 200 } - src1 = Distribute.initialSourceState - { key: 1, initialFunds: 100 } - -- enough - src2 = Distribute.initialSourceState - { key: 2, initialFunds: 300 } - utxo = { key: "utxo0", amount: 250 } - outcome = map (List.sortBy $ comparing (view _source)) - $ Distribute.assignUtxo highThreshold utxo - $ List.fromFoldable [ src0, src1, src2 ] - expected = List.sortBy (comparing $ view _source) $ List.fromFoldable - [ src0 - , src1 - , src2 - # (_leftover -~ utxo.amount) - # - ( set _tx - $ Distribute.emptyTx utxo.amount - # (_totalUtxos .~ 1) - # (_utxos .~ pure utxo) - ) - ] - outcome `shouldEqual` Right expected - test "Fails if UTxO amount is higher than threshold" do - let - -- not enough - src0 = Distribute.initialSourceState - { key: 0, initialFunds: 300 } - src1 = Distribute.initialSourceState - { key: 1, initialFunds: 900 } - utxo = { key: "utxo0", amount: 250 } - outcome = - Distribute.assignUtxo - highThreshold { maxCoinPerTx = 200 } - utxo - $ List.fromFoldable [ src0, src1 ] - lmap (const unit) outcome `shouldEqual` Left unit - pure unit +getSource + :: forall wallet amount + . AssignUtxoResult wallet amount + -> Maybe (SourceState wallet amount) +getSource = case _ of + AssignUtxo_AssignedToSource src -> Just src + _ -> Nothing +defaultParams :: forall wallet. DistrFundsParams wallet Int +defaultParams = + { maxRounds: top + , maxUtxosPerTx: top + , getUtxoMinAdaForWallet: const zero + , feePerTx: zero + } diff --git a/test/Testnet/DistributeFundsV2.purs b/test/Testnet/DistributeFundsV2.purs deleted file mode 100644 index 6105937e5..000000000 --- a/test/Testnet/DistributeFundsV2.purs +++ /dev/null @@ -1,205 +0,0 @@ -module Test.Ctl.Testnet.DistributeFundsV2 - ( suite - ) where - -import Prelude - -import Contract.Test.Mote (TestPlanM) -import Ctl.Internal.Testnet.DistributeFundsV2 - ( AssignUtxoResult - ( AssignUtxo_Unassigned - , AssignUtxo_Deferred - , AssignUtxo_AssignedToSource - ) - , DistrFundsError(DistrFunds_AssignUtxoError) - , DistrFundsParams - , SourceState - , assignUtxoToSource - , initSourceState - , makeDistributionPlan - , runDistrFundsRound - ) -import Data.Array (reverse) -import Data.Either (Either(Left, Right)) -import Data.List (fromFoldable) as List -import Data.Maybe (Maybe(Just, Nothing)) -import Data.Newtype (modify, wrap) -import Data.Tuple.Nested (type (/\), (/\)) -import Effect.Aff (Aff) -import Mote (group, test) -import Test.Spec.Assertions (shouldEqual) - -suite :: TestPlanM (Aff Unit) Unit -suite = - group "DistributeFundsV2" do - group "assignUtxoToSource" do - test "Leaves utxo unassigned if it cannot be covered by source" - let - src0 = initSourceState "src0" 1000 - utxo = "target0" /\ 2000 - in - assignUtxoToSource defaultParams src0 utxo `shouldEqual` - AssignUtxo_Unassigned - - test "Takes tx fee into account" - let - params = defaultParams { feePerTx = 10 } - src0 = initSourceState "src0" 1000 - utxo = "target0" /\ 1000 - in - assignUtxoToSource params src0 utxo `shouldEqual` - AssignUtxo_Unassigned - - test "Marks utxo as deferred if maxUtxosPerTx is exceeded" - let - params = defaultParams { maxUtxosPerTx = 0 } - src0 = initSourceState "src0" 1000 - utxo = "target0" /\ 1000 - in - assignUtxoToSource params src0 utxo `shouldEqual` - AssignUtxo_Deferred - - test "Correctly assigns utxos to source" - let - params = defaultParams { feePerTx = 100 } - src0 = initSourceState "src0" 2000 - utxo0 = "target0" /\ 1000 - utxo1 = "target1" /\ 500 - assignUtxo = flip (assignUtxoToSource params) - outcome = - (getSource <<< assignUtxo utxo1) - =<< getSource (assignUtxo utxo0 src0) - in - outcome `shouldEqual` Just - ( src0 - { leftover = 500 - , currentTx = modify - ( _ - { numUtxos = 2 - , utxos = List.fromFoldable - [ utxoToRec utxo1, utxoToRec utxo0 ] - } - ) - src0.currentTx - } - ) - - group "runDistrFundsRound" do - test "Fails if utxo cannot be covered by any source" - let - sources = List.fromFoldable - [ initSourceState "src0" 1000 - , initSourceState "src1" 2000 - , initSourceState "src2" 3000 - ] - utxos = List.fromFoldable - [ "target0" /\ 1000 - , "target1" /\ 3500 - ] - in - runDistrFundsRound defaultParams sources utxos `shouldEqual` - Left DistrFunds_AssignUtxoError - - test "Deferrs utxos that cannot be assigned in the current round" - let - params = defaultParams { maxUtxosPerTx = 1 } - src0 = initSourceState "src0" 2000 - src1 = initSourceState "src1" 1000 - sources = List.fromFoldable [ src0, src1 ] - utxo0 = "target0" /\ 1600 - utxo1 = "target1" /\ 800 - utxo2 = "target2" /\ 400 - utxos = List.fromFoldable [ utxo0, utxo1, utxo2 ] - in - runDistrFundsRound params sources utxos `shouldEqual` - Right - { sources: List.fromFoldable - [ src0 - { leftover = 400 - , currentTx = modify - ( _ - { numUtxos = 1 - , utxos = List.fromFoldable [ utxoToRec utxo0 ] - } - ) - src0.currentTx - } - , src1 - { leftover = 200 - , currentTx = modify - ( _ - { numUtxos = 1 - , utxos = List.fromFoldable [ utxoToRec utxo1 ] - } - ) - src1.currentTx - } - ] - , deferredTargets: List.fromFoldable [ utxo2 ] - } - - group "makeDistributionPlan" do - test "Prepares simple funds distribution plan (2 rounds)" - let - params = defaultParams { maxUtxosPerTx = 2, maxRounds = 5 } - - utxos0 = [ "01" /\ 2, "02" /\ 18 ] - utxos1 = [ "11" /\ 3, "12" /\ 17 ] - utxos2 = [ "21" /\ 9, "22" /\ 11 ] - utxos3 = [ "31" /\ 15, "32" /\ 5 ] - - -- total = 80 - utxos = utxos0 <> utxos1 <> utxos2 <> utxos3 - - -- sources have exactly enough funds to fit all the utxos - src0 = "src0" /\ 40 - src1 = "src1" /\ 40 - sources = [ src0, src1 ] - in - makeDistributionPlan params sources utxos `shouldEqual` - Right - [ [ wrap - { srcWallet: "src0" - , numUtxos: 2 - , utxos: List.fromFoldable $ utxoToRec <$> reverse utxos1 - } - , wrap - { srcWallet: "src1" - , numUtxos: 2 - , utxos: List.fromFoldable $ utxoToRec <$> reverse utxos0 - } - ] - , [ wrap - { srcWallet: "src0" - , numUtxos: 2 - , utxos: List.fromFoldable $ utxoToRec <$> utxos2 - } - , wrap - { srcWallet: "src1" - , numUtxos: 2 - , utxos: List.fromFoldable $ utxoToRec <$> utxos3 - } - ] - ] - -utxoToRec - :: forall wallet amount - . wallet /\ amount - -> { wallet :: wallet, amount :: amount } -utxoToRec (wallet /\ amount) = { wallet, amount } - -getSource - :: forall wallet amount - . AssignUtxoResult wallet amount - -> Maybe (SourceState wallet amount) -getSource = case _ of - AssignUtxo_AssignedToSource src -> Just src - _ -> Nothing - -defaultParams :: forall wallet. DistrFundsParams wallet Int -defaultParams = - { maxRounds: top - , maxUtxosPerTx: top - , getUtxoMinAdaForWallet: const zero - , feePerTx: zero - } diff --git a/test/Unit.purs b/test/Unit.purs index 4250ac8a5..36b3835a6 100644 --- a/test/Unit.purs +++ b/test/Unit.purs @@ -29,7 +29,6 @@ import Test.Ctl.ProtocolParams as ProtocolParams import Test.Ctl.Serialization as Serialization import Test.Ctl.Serialization.Hash as Serialization.Hash import Test.Ctl.Testnet.DistributeFunds as Testnet.DistributeFunds -import Test.Ctl.Testnet.DistributeFundsV2 as Testnet.DistributeFundsV2 import Test.Ctl.Types.Interval as Types.Interval import Test.Ctl.Types.Ipv6 as Ipv6 import Test.Ctl.Types.TokenName as Types.TokenName @@ -49,7 +48,6 @@ main = interruptOnSignal SIGINT =<< launchAff do testPlan :: TestPlanM (Aff Unit) Unit testPlan = do Testnet.DistributeFunds.suite - Testnet.DistributeFundsV2.suite ApplyArgs.suite Ipv6.suite NativeScript.suite From fe3be88cfdbd99c32b805e635ade9edfdeffb181 Mon Sep 17 00:00:00 2001 From: Dzmitry Shuysky Date: Tue, 20 Aug 2024 15:21:05 +0200 Subject: [PATCH 2/9] Refactor Internal.CardanoCli.QueryHandle --- src/Internal/CardanoCli/QueryHandle.purs | 104 ++++++++++++++++++++++ src/Internal/CardanoCli/QueryHandler.purs | 93 ------------------- src/Internal/Testnet/Contract.purs | 2 +- 3 files changed, 105 insertions(+), 94 deletions(-) create mode 100644 src/Internal/CardanoCli/QueryHandle.purs delete mode 100644 src/Internal/CardanoCli/QueryHandler.purs diff --git a/src/Internal/CardanoCli/QueryHandle.purs b/src/Internal/CardanoCli/QueryHandle.purs new file mode 100644 index 000000000..b3d99e87b --- /dev/null +++ b/src/Internal/CardanoCli/QueryHandle.purs @@ -0,0 +1,104 @@ +module Internal.CardanoCli.QueryHandle + ( withCardanoCliCompletion + ) where + +import Prelude + +import Cardano.Types.Address (Address) +import Cardano.Types.TransactionInput (TransactionInput) +import Cardano.Types.TransactionOutput (TransactionOutput) +import Cardano.Types.UtxoMap (UtxoMap) +import Control.Alt ((<|>)) +import Control.Monad.Error.Class (try) +import Control.Monad.Except (ExceptT(ExceptT), runExceptT) +import Control.Monad.Reader (local) +import Ctl.Internal.CardanoCli + ( CardanoCliTxOutInfo + , CardanoNodeInstance + , cardanoCliTxOutInfoToUtxo + , queryUtxosViaCardanoCli + ) as CardanoCli +import Ctl.Internal.Contract.Monad (Contract, ContractEnv) +import Ctl.Internal.Service.Error (ClientError(ClientOtherError)) +import Data.Bifunctor (bimap) +import Data.Either (Either) +import Data.Lens (Lens', (%~)) +import Data.Lens.Record (prop) +import Data.Map as Map +import Data.Maybe (Maybe) +import Effect.Aff (Aff) +import Effect.Exception (Error) +import Effect.Exception (message) as Error +import Type.Proxy (Proxy(Proxy)) + +type UtxosAtQuery = Address -> Aff (Either ClientError UtxoMap) + +type GetUtxoByOrefQuery = + TransactionInput -> Aff (Either ClientError (Maybe TransactionOutput)) + +utxosAtLens :: Lens' ContractEnv UtxosAtQuery +utxosAtLens = + prop (Proxy :: _ "handle") + <<< prop (Proxy :: _ "utxosAt") + +getUtxoByOrefLens :: Lens' ContractEnv GetUtxoByOrefQuery +getUtxoByOrefLens = + prop (Proxy :: _ "handle") + <<< prop (Proxy :: _ "getUtxoByOref") + +withCardanoCliCompletion + :: forall a + . CardanoCli.CardanoNodeInstance + -> Address + -> Contract a + -> Contract a +withCardanoCliCompletion node genesisAddr = + local $ (utxosAtLens %~ completeUtxosAt node) >>> + (getUtxoByOrefLens %~ completeGetUtxoByOref node genesisAddr) + +-- | Complements the `utxosAt` result with utxos found via cardano-cli. +-- | In case of overlapping results, the utxos from the query layer are given +-- | preference. +-- | +-- | NOTE: It is assumed that utxos retrieved via cardano-cli do not include +-- | datum or reference scripts. +completeUtxosAt + :: CardanoCli.CardanoNodeInstance -> UtxosAtQuery -> UtxosAtQuery +completeUtxosAt node utxosAt address = + runExceptT do + queryLayerUtxos <- ExceptT $ utxosAt address + cardanoCliUtxos <- ExceptT $ queryUtxosViaCardanoCli node address + pure $ Map.union queryLayerUtxos cardanoCliUtxos + +-- | Complements the `getUtxoByOref` search space with utxos found via +-- | cardano-cli. If no utxo is found in the initial search, the lookup will be +-- | performed using utxos from cardano-cli. +-- | +-- | NOTE: It is assumed that utxos retrieved via cardano-cli do not include +-- | datum or reference scripts. +completeGetUtxoByOref + :: CardanoCli.CardanoNodeInstance + -> Address + -> GetUtxoByOrefQuery + -> GetUtxoByOrefQuery +completeGetUtxoByOref node address getUtxoByOref oref = + runExceptT do + mbUtxo <- ExceptT $ getUtxoByOref oref + cardanoCliUtxos <- ExceptT $ queryUtxosViaCardanoCli node address + pure $ mbUtxo <|> Map.lookup oref cardanoCliUtxos + +queryUtxosViaCardanoCli + :: CardanoCli.CardanoNodeInstance + -> Address + -> Aff (Either ClientError UtxoMap) +queryUtxosViaCardanoCli node address = + bimap toClientError toUtxoMap <$> + try (CardanoCli.queryUtxosViaCardanoCli node address) + where + toClientError :: Error -> ClientError + toClientError = ClientOtherError <<< Error.message + + toUtxoMap :: Array CardanoCli.CardanoCliTxOutInfo -> UtxoMap + toUtxoMap = + Map.fromFoldable + <<< map (CardanoCli.cardanoCliTxOutInfoToUtxo address) diff --git a/src/Internal/CardanoCli/QueryHandler.purs b/src/Internal/CardanoCli/QueryHandler.purs deleted file mode 100644 index 3ab6b0938..000000000 --- a/src/Internal/CardanoCli/QueryHandler.purs +++ /dev/null @@ -1,93 +0,0 @@ -module Internal.CardanoCli.QueryHandler - ( withCardanoCliCompletion - ) where - -import Contract.Prelude - -import Cardano.Types as Cardano.Types -import Cardano.Types.Address (Address) -import Cardano.Types.TransactionInput (TransactionInput) -import Cardano.Types.TransactionOutput (TransactionOutput) -import Contract.ClientError as Contract.ClientError -import Contract.Monad (Contract, ContractEnv) -import Control.Alt ((<|>)) -import Control.Monad.Error.Class (try) -import Control.Monad.Except (ExceptT(ExceptT), runExceptT) -import Control.Monad.Reader (local) -import Ctl.Internal.CardanoCli as CardanoCli -import Data.Bifunctor (bimap) -import Data.Lens (Lens', (%~)) -import Data.Lens.Record (prop) -import Data.Map as Map -import Effect.Exception (Error, message) -import Type.Proxy (Proxy(Proxy)) - -type UtxosAtQuery = - Cardano.Types.Address - -> Aff (Either Contract.ClientError.ClientError Cardano.Types.UtxoMap) - -type GetUtxoByOrefQuery = - TransactionInput - -> Aff (Either Contract.ClientError.ClientError (Maybe TransactionOutput)) - --- | Adds to the utxosAt results UTxOs found by cardano-cli but not found by the current 'utxosAt' query. --- UTxOs found by cardano-cli assumed to have no datum or script ref. -withCardanoCliCompletion - :: forall a - . CardanoCli.CardanoNodeInstance - -> Address - -> Contract a - -> Contract a -withCardanoCliCompletion node genesisAddr = - local $ (utxosAtL %~ completeUtxosAt node) >>> - (getUtxoByOrefL %~ completeGetUtxoByOref node genesisAddr) - -utxosAtL :: Lens' ContractEnv UtxosAtQuery -utxosAtL = prop (Proxy :: _ "handle") <<< prop (Proxy :: _ "utxosAt") - -getUtxoByOrefL :: Lens' ContractEnv GetUtxoByOrefQuery -getUtxoByOrefL = prop (Proxy :: _ "handle") <<< prop - (Proxy :: _ "getUtxoByOref") - --- | Adds to the results UTxOs found by cardano-cli but not found by the given 'utxosAt' query. --- UTxOs found by cardano-cli assumed to have no datum or script ref. -completeUtxosAt - :: CardanoCli.CardanoNodeInstance - -> UtxosAtQuery - -> UtxosAtQuery -completeUtxosAt node utxosAt address = runExceptT do - let - toCliError :: Error -> Contract.ClientError.ClientError - toCliError = Contract.ClientError.ClientOtherError <<< message - - toUtxoMap :: Array CardanoCli.CardanoCliTxOutInfo -> Cardano.Types.UtxoMap - toUtxoMap = Map.fromFoldable - <<< map (CardanoCli.cardanoCliTxOutInfoToUtxo address) - cardanoCliUtxos <- ExceptT - $ map (bimap toCliError toUtxoMap) - $ try - $ CardanoCli.queryUtxosViaCardanoCli node address - kupoUtxos <- ExceptT $ utxosAt address - pure $ Map.union kupoUtxos cardanoCliUtxos - --- | Adds to the results UTxOs found by cardano-cli but not found by the given 'getUtxoByOref' query. --- UTxOs found by cardano-cli assumed to have no datum or script ref. -completeGetUtxoByOref -- FIXME - :: CardanoCli.CardanoNodeInstance - -> Address - -> GetUtxoByOrefQuery - -> GetUtxoByOrefQuery -completeGetUtxoByOref node address getUtxoByOref oref = runExceptT do - let - toCliError :: Error -> Contract.ClientError.ClientError - toCliError = Contract.ClientError.ClientOtherError <<< message - - toUtxoMap :: Array CardanoCli.CardanoCliTxOutInfo -> Cardano.Types.UtxoMap - toUtxoMap = Map.fromFoldable - <<< map (CardanoCli.cardanoCliTxOutInfoToUtxo address) - cardanoCliUtxos <- ExceptT - $ map (bimap toCliError toUtxoMap) - $ try - $ CardanoCli.queryUtxosViaCardanoCli node address - mUtxo <- ExceptT $ getUtxoByOref oref - pure $ mUtxo <|> Map.lookup oref cardanoCliUtxos diff --git a/src/Internal/Testnet/Contract.purs b/src/Internal/Testnet/Contract.purs index 581f0b2f5..123394c9a 100644 --- a/src/Internal/Testnet/Contract.purs +++ b/src/Internal/Testnet/Contract.purs @@ -77,7 +77,7 @@ import Effect.Aff (try) import Effect.Exception (error) import Effect.Ref (Ref) import Effect.Ref (new, read, write) as Ref -import Internal.CardanoCli.QueryHandler (withCardanoCliCompletion) +import Internal.CardanoCli.QueryHandle (withCardanoCliCompletion) import Mote (bracket) as Mote import Mote.Description (Description(Group, Test)) import Mote.Monad (MoteT(MoteT), mapTest) From d334da302cde5f01e1ddce436d0b4294c04689f7 Mon Sep 17 00:00:00 2001 From: Dzmitry Shuysky Date: Tue, 20 Aug 2024 15:31:55 +0200 Subject: [PATCH 3/9] cardano-testnet: Set lovelace supply to uint64 max value --- src/Internal/Testnet/Server.purs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Internal/Testnet/Server.purs b/src/Internal/Testnet/Server.purs index 63e09f5b4..2e779b31e 100644 --- a/src/Internal/Testnet/Server.purs +++ b/src/Internal/Testnet/Server.purs @@ -10,6 +10,7 @@ module Ctl.Internal.Testnet.Server import Contract.Prelude hiding (log) import Cardano.Types (NetworkId(MainnetId)) +import Cardano.Types.BigNum (maxValue, toString) as BigNum import Contract.Config (Hooks, defaultSynchronizationParams, defaultTimeParams) import Contract.Monad (ContractEnv) import Control.Alt ((<|>)) @@ -318,18 +319,18 @@ spawnCardanoTestnet { cwd } params = do flag :: String -> String flag name = "--" <> name - option :: forall a. Show a => String -> a -> Array String - option name value = [ flag name, show value ] - options :: Array String options = join [ [ "cardano" ] - , option "testnet-magic" params.testnetMagic , [ flag $ show params.era ] - , option "slot-length" $ unwrap params.slotLength , maybe mempty (\epochSize -> [ flag "epoch-length", UInt.toString epochSize ]) params.epochSize + , [ flag "slot-length", show (unwrap params.slotLength) ] + , [ flag "testnet-magic", show params.testnetMagic ] + -- FIXME: max-lovelace-supply option has no effect, should be fixed upstream + -- https://github.com/IntersectMBO/cardano-node/issues/5953 + , [ flag "max-lovelace-supply", BigNum.toString BigNum.maxValue ] ] startCardanoTestnet From 0eae48277fb1effca9bcb9305c159a70fcf2c4f6 Mon Sep 17 00:00:00 2001 From: Dzmitry Shuysky Date: Tue, 20 Aug 2024 16:01:29 +0200 Subject: [PATCH 4/9] Revise cardano-testnet env variables --- src/Internal/Testnet/Server.purs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/Internal/Testnet/Server.purs b/src/Internal/Testnet/Server.purs index 2e779b31e..2c764d175 100644 --- a/src/Internal/Testnet/Server.purs +++ b/src/Internal/Testnet/Server.purs @@ -282,27 +282,22 @@ startTestnetCluster cfg cleanupRef = do } pure { process: ogmios, channels: ogmiosChannels } --- | Runs cardano-testnet executable with provided params. +-- | Spawns cardano-testnet process with provided parameters. spawnCardanoTestnet :: { cwd :: FilePath } -> TestnetClusterConfig -> Aff { testnet :: ManagedProcess, workspace :: FilePath } spawnCardanoTestnet { cwd } params = do env <- liftEffect Node.Process.getEnv - -- initCwd <- liftMaybe (error "Couldn't find INIT_CWD env variable") - -- $ Object.lookup "INIT_CWD" env let env' = Object.fromFoldable - [ "TMPDIR" /\ cwd -- only for 8.1.1; 8.7.2 puts it's testnet directory into cwd instead - -- , "CARDANO_NODE_SRC" /\ (initCwd <> "cardano-testnet-files") - , "CARDANO_CLI" /\ "cardano-cli" - , "CREATE_SCRIPT_CONTEXT" /\ "create-script-context" + [ "CARDANO_CLI" /\ "cardano-cli" , "CARDANO_NODE" /\ "cardano-node" - , "CARDANO_SUBMIT_API" /\ "cardano-submit-api" - , "CARDANO_NODE_CHAIRMAN" /\ "cardano-node-chairman" ] opts = defaultSpawnOptions - { cwd = Just cwd, env = Just $ Object.union env' env } + { cwd = Just cwd + , env = Just $ Object.union env' env + } workspaceRef <- liftEffect $ Ref.new mempty ps <- spawn "cardano-testnet" options opts $ Just From a471c719753ac5087cf429addac87123332e61be Mon Sep 17 00:00:00 2001 From: Dzmitry Shuysky Date: Tue, 20 Aug 2024 16:13:56 +0200 Subject: [PATCH 5/9] Include ctl runtime in nix shell only for x86_64-linux --- flake.nix | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/flake.nix b/flake.nix index 047c6de0b..02e114716 100644 --- a/flake.nix +++ b/flake.nix @@ -75,12 +75,9 @@ , ... }@inputs: let - supportedSystems = [ - "x86_64-linux" - "x86_64-darwin" - "aarch64-linux" - "aarch64-darwin" - ]; + linuxSystems = [ "x86_64-linux" "aarch64-linux" ]; + darwinSystems = [ "x86_64-darwin" "aarch64-darwin" ]; + supportedSystems = linuxSystems ++ darwinSystems; ogmiosVersion = "6.5.0"; kupoVersion = "2.9.0"; @@ -140,7 +137,7 @@ cp -rT ogmios $out ''; - psProjectFor = pkgs: + psProjectFor = pkgs: system: let projectName = "cardano-transaction-lib"; # `filterSource` will still trigger rebuilds with flakes, even if a @@ -161,18 +158,19 @@ packageJson = ./package.json; packageLock = ./package-lock.json; shell = { - withRuntime = true; + withRuntime = system == "x86_64-linux"; shellHook = exportOgmiosFixtures; packageLockOnly = true; - packages = with pkgs; [ - arion - fd - psmisc - nixpkgs-fmt - nodePackages.eslint - nodePackages.prettier - blockfrost-backend-ryo - ]; + packages = with pkgs; + (if (builtins.elem system linuxSystems) then [ psmisc ] else [ ]) ++ + [ + arion + fd + nixpkgs-fmt + nodePackages.eslint + nodePackages.prettier + blockfrost-backend-ryo + ]; }; }; exportOgmiosFixtures = @@ -297,11 +295,11 @@ devShells = perSystem (system: { # This is the default `devShell` and can be run without specifying # it (i.e. `nix develop`) - default = (psProjectFor (nixpkgsFor system)).devShell; + default = (psProjectFor (nixpkgsFor system) system).devShell; }); packages = perSystem (system: - (psProjectFor (nixpkgsFor system)).packages + (psProjectFor (nixpkgsFor system) system).packages ); apps = perSystem (system: From 1266638e8f821c48cdb34bc0bbb09998e056a70c Mon Sep 17 00:00:00 2001 From: Dzmitry Shuysky Date: Tue, 20 Aug 2024 16:38:43 +0200 Subject: [PATCH 6/9] Fix nix flake apps and checks --- flake.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/flake.nix b/flake.nix index 02e114716..71a350f8b 100644 --- a/flake.nix +++ b/flake.nix @@ -306,7 +306,7 @@ let pkgs = nixpkgsFor system; in - (psProjectFor pkgs).apps // { + (psProjectFor pkgs system).apps // { ctl-runtime = pkgs.launchCtlRuntime { }; ctl-runtime-blockfrost = pkgs.launchCtlRuntime { blockfrost.enable = true; }; default = self.apps.${system}.ctl-runtime; @@ -324,7 +324,7 @@ checks = perSystem (system: let pkgs = nixpkgsFor system; - psProject = psProjectFor pkgs; + psProject = psProjectFor pkgs system; in psProject.checks // { From c4ca4c8573041aab4350ad6e264998dda7ae8c6f Mon Sep 17 00:00:00 2001 From: Dzmitry Shuysky Date: Tue, 27 Aug 2024 16:56:37 +0200 Subject: [PATCH 7/9] Kill cardano-testnet nodes by port using fuser --- src/Internal/Testnet/Utils.purs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Internal/Testnet/Utils.purs b/src/Internal/Testnet/Utils.purs index 401191c03..be3c25d75 100644 --- a/src/Internal/Testnet/Utils.purs +++ b/src/Internal/Testnet/Utils.purs @@ -216,12 +216,13 @@ getNodePort { nodeDir } = =<< Node.FS.readTextFile UTF8 (nodeDir <> "/port") findNodeDirs :: { workdir :: FilePath } -> Effect (Array { | NodeLocation () }) -findNodeDirs { workdir } = - Node.FS.readdir workdir <#> \subdirs -> +findNodeDirs { workdir } = do + let poolsKeysDir = workdir <> "pools-keys" + Node.FS.readdir poolsKeysDir <#> \subdirs -> flip Array.mapMaybe subdirs \dirname -> do - idx <- Int.fromString =<< String.stripPrefix (Pattern "pools-keys/pool1") + idx <- Int.fromString =<< String.stripPrefix (Pattern "pool") dirname - pure { idx, workdir: workdir <> dirname, name: dirname } + pure { idx, workdir: poolsKeysDir <> dirname, name: dirname } findTestnetPaths :: { workdir :: FilePath } -> Effect (Either Error TestnetPaths) From fc3ae377fe86ae0bc4b11b9c62fd30fabb12ca57 Mon Sep 17 00:00:00 2001 From: Dzmitry Shuysky Date: Fri, 30 Aug 2024 15:27:58 +0200 Subject: [PATCH 8/9] Use newer cardano-testnet that respects --max-lovelace-supply option --- flake.lock | 271 ++++++++++++++++++++++++++++++++++------------------- flake.nix | 8 +- 2 files changed, 182 insertions(+), 97 deletions(-) diff --git a/flake.lock b/flake.lock index d6f263050..4127e3105 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "CHaP": { "flake": false, "locked": { - "lastModified": 1721915212, - "narHash": "sha256-itkbLG6DUX/L5XuoSXFPgPBf+9lFOM3ufc1T4BU4MYM=", + "lastModified": 1724915143, + "narHash": "sha256-SKluKP0iuRTYMQWzkxOtqW39+1zjw6oeZY+J8RJytGM=", "owner": "input-output-hk", "repo": "cardano-haskell-packages", - "rev": "2126fa53c45842719ee38040f4d5bee8fb17a09d", + "rev": "92b3a071083372209af9c89c936f4f184ad5e3f6", "type": "github" }, "original": { @@ -71,11 +71,11 @@ "CHaP_5": { "flake": false, "locked": { - "lastModified": 1721831314, - "narHash": "sha256-I1j5HPSbbh3l1D0C9oP/59YB4e+64K9NDRl7ueD1c/Y=", + "lastModified": 1724197463, + "narHash": "sha256-/ZHOKRX84tXckstr6rTYyjytF2yfrIpvGujRLyjZfUE=", "owner": "intersectmbo", "repo": "cardano-haskell-packages", - "rev": "8815ee7598bc39a02db8896b788f69accf892790", + "rev": "610a202920ffe1d371035d35053152e9a0c77fce", "type": "github" }, "original": { @@ -710,7 +710,7 @@ "ragenix": "ragenix_3", "std": "std_6", "terranix": "terranix_3", - "utils": "utils_17" + "utils": "utils_18" }, "locked": { "lastModified": 1661790449, @@ -801,7 +801,7 @@ "ops-lib": "ops-lib_4", "ragenix": "ragenix", "terranix": "terranix_2", - "utils": "utils_12", + "utils": "utils_13", "vulnix": "vulnix" }, "locked": { @@ -835,7 +835,7 @@ "ops-lib": "ops-lib_6", "ragenix": "ragenix_4", "terranix": "terranix_4", - "utils": "utils_26", + "utils": "utils_27", "vulnix": "vulnix_2" }, "locked": { @@ -2047,6 +2047,7 @@ "cardano-db-sync": "cardano-db-sync", "cardano-node-8.1.1": "cardano-node-8.1.1", "cardano-node-8.7.3": "cardano-node-8.7.3", + "crane": "crane_2", "devour-flake": "devour-flake", "devshell": "devshell_4", "flake-parts": "flake-parts_5", @@ -2055,15 +2056,16 @@ "nixpkgs": [ "nixpkgs" ], + "oura": "oura", "pre-commit-hooks-nix": "pre-commit-hooks-nix", "treefmt-nix": "treefmt-nix_3" }, "locked": { - "lastModified": 1722438671, - "narHash": "sha256-Nb8bROKPjRWFMsaHIK4BOvsTceL9klpF3Ucp/zHqRzM=", + "lastModified": 1724967699, + "narHash": "sha256-rsj28Jq7DX/YCzykfvy2LJ9y6AE37i3MCyoAFeRHyEM=", "owner": "mlabs-haskell", "repo": "cardano.nix", - "rev": "7e696a77440d14f161c8b426d90fecfdb70ad8d8", + "rev": "3eb9384b2e1c43d4dcdf2eb23d0210d5d7e612af", "type": "github" }, "original": { @@ -2096,20 +2098,20 @@ ], "ops-lib": "ops-lib_3", "std": "std_5", - "utils": "utils_7" + "utils": "utils_8" }, "locked": { - "lastModified": 1722955151, - "narHash": "sha256-pZUg2PbhK35QdMcEP0or6IyKXBr544KyebQ+xiNc6PE=", + "lastModified": 1724944858, + "narHash": "sha256-7zV11vZ4e81cDIpk9OpkAnYV9EA5WWH134iei3n8+S8=", "owner": "input-output-hk", "repo": "cardano-node", - "rev": "4f4e372a1641ac68cd09fb0339e6f55bef1ab85d", + "rev": "d7abccd4e90c38ff5cd4d6a7839689d888332056", "type": "github" }, "original": { "owner": "input-output-hk", "repo": "cardano-node", - "rev": "4f4e372a1641ac68cd09fb0339e6f55bef1ab85d", + "rev": "d7abccd4e90c38ff5cd4d6a7839689d888332056", "type": "github" } }, @@ -2555,7 +2557,7 @@ "nix-cache-proxy": "nix-cache-proxy", "nixpkgs": "nixpkgs_68", "poetry2nix": "poetry2nix", - "utils": "utils_21" + "utils": "utils_22" }, "locked": { "lastModified": 1647522107, @@ -2625,6 +2627,27 @@ "type": "github" } }, + "crane_2": { + "inputs": { + "nixpkgs": [ + "cardano-nix", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1721058578, + "narHash": "sha256-fs/PVa3H5dS1//4BjecWi3nitXm5fRObx0JxXIAo+JA=", + "owner": "ipetkov", + "repo": "crane", + "rev": "17e5109bb1d9fb393d70fba80988f7d70d1ded1a", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, "crystal": { "inputs": { "ameba-src": "ameba-src", @@ -2861,7 +2884,7 @@ "haskellNix", "nixpkgs-unstable" ], - "utils": "utils_28" + "utils": "utils_29" }, "locked": { "lastModified": 1670313550, @@ -2892,7 +2915,7 @@ "fenix", "nixpkgs" ], - "utils": "utils_8" + "utils": "utils_9" }, "locked": { "lastModified": 1638318651, @@ -2920,7 +2943,7 @@ "fenix", "nixpkgs" ], - "utils": "utils_14" + "utils": "utils_15" }, "locked": { "lastModified": 1638318651, @@ -2949,7 +2972,7 @@ "fenix", "nixpkgs" ], - "utils": "utils_22" + "utils": "utils_23" }, "locked": { "lastModified": 1638318651, @@ -3628,7 +3651,7 @@ "cicero", "nixpkgs" ], - "utils": "utils_18" + "utils": "utils_19" }, "locked": { "lastModified": 1644418487, @@ -5300,7 +5323,7 @@ "cicero", "nixpkgs" ], - "utils": "utils_19" + "utils": "utils_20" }, "locked": { "lastModified": 1642008295, @@ -5660,7 +5683,7 @@ "gomod2nix_3": { "inputs": { "nixpkgs": "nixpkgs_31", - "utils": "utils_6" + "utils": "utils_7" }, "locked": { "lastModified": 1655245309, @@ -5695,11 +5718,11 @@ "hackage-nix": { "flake": false, "locked": { - "lastModified": 1721953589, - "narHash": "sha256-ctYOxCvXQS5MPILV8YPyUhylKhgIhOM4Dc5g0vGNFbM=", + "lastModified": 1724977850, + "narHash": "sha256-awqEskjcqDqrT+Xgl9GK8LCPfxtzuwoLH12nbVoOjT8=", "owner": "input-output-hk", "repo": "hackage.nix", - "rev": "3f0675337984f15834fcd52b97fc766e30f4d684", + "rev": "7007cb02b7b0333f7e2c33dd8b5d1de5105d36b4", "type": "github" }, "original": { @@ -5759,11 +5782,11 @@ "hackageNix_4": { "flake": false, "locked": { - "lastModified": 1719794527, - "narHash": "sha256-qHo/KumtwAzPkfLWODu/6EFY/LeK+C7iPJyAUdT8tGA=", + "lastModified": 1724200761, + "narHash": "sha256-IDenOlZc5aph7Jz6xNQXGNnnx896hUYrsRU8mbE4bVw=", "owner": "input-output-hk", "repo": "hackage.nix", - "rev": "da2a3bc9bd1b3dd41bb147279529c471c615fd3e", + "rev": "11b43aaf3ff8018897f1b84a3fb60cce9ae7056d", "type": "github" }, "original": { @@ -6016,7 +6039,7 @@ "hls-2.6": "hls-2.6_2", "hls-2.7": "hls-2.7_2", "hls-2.8": "hls-2.8_2", - "hls-2.9": "hls-2.9", + "hls-2.9": "hls-2.9_2", "hpc-coveralls": "hpc-coveralls_10", "hydra": "hydra_12", "iserv-proxy": "iserv-proxy_6", @@ -6036,11 +6059,11 @@ "stackage": "stackage_9" }, "locked": { - "lastModified": 1721956799, - "narHash": "sha256-FU09PlekhkuocxDO2UN2aARdUflIGA36VP1EUra4b7c=", + "lastModified": 1724979052, + "narHash": "sha256-hoL0ofZI4L6w4413Wr4eZkMZzj+TyyDXF84FxEjppxU=", "owner": "input-output-hk", "repo": "haskell.nix", - "rev": "ccbd8ed7d4aff11e0507d19dc7c40601487c0bea", + "rev": "5837bf2e6dbbbc2fc60fe7d27c9a03d9f8e9e974", "type": "github" }, "original": { @@ -6227,6 +6250,7 @@ "hls-2.6": "hls-2.6", "hls-2.7": "hls-2.7", "hls-2.8": "hls-2.8", + "hls-2.9": "hls-2.9", "hpc-coveralls": "hpc-coveralls_5", "hydra": "hydra_5", "iserv-proxy": "iserv-proxy_5", @@ -6412,11 +6436,11 @@ "nixpkgs": "nixpkgs_99" }, "locked": { - "lastModified": 1719226092, - "narHash": "sha256-YNkUMcCUCpnULp40g+svYsaH1RbSEj6s4WdZY/SHe38=", + "lastModified": 1724947644, + "narHash": "sha256-MHHrHasTngp7EYQOObHJ1a/IsRF+wodHqOckhH6uZbk=", "owner": "hercules-ci", "repo": "hercules-ci-effects", - "rev": "11e4b8dc112e2f485d7c97e1cee77f9958f498f5", + "rev": "dba4367b9a9d9615456c430a6d6af716f6e84cef", "type": "github" }, "original": { @@ -6953,6 +6977,23 @@ } }, "hls-2.9": { + "flake": false, + "locked": { + "lastModified": 1718469202, + "narHash": "sha256-qnDx8Pk0UxtoPr7BimEsAZh9g2WuTuMB/kGqnmdryKs=", + "owner": "haskell", + "repo": "haskell-language-server", + "rev": "0c1817cb2babef0765e4e72dd297c013e8e3d12b", + "type": "github" + }, + "original": { + "owner": "haskell", + "ref": "2.9.0.1", + "repo": "haskell-language-server", + "type": "github" + } + }, + "hls-2.9_2": { "flake": false, "locked": { "lastModified": 1718469202, @@ -7783,7 +7824,7 @@ "devshell": "devshell_8", "inclusive": "inclusive_4", "nixpkgs": "nixpkgs_51", - "utils": "utils_13" + "utils": "utils_14" }, "locked": { "lastModified": 1652212694, @@ -7804,7 +7845,7 @@ "devshell": "devshell_18", "inclusive": "inclusive_12", "nixpkgs": "nixpkgs_80", - "utils": "utils_27" + "utils": "utils_28" }, "locked": { "lastModified": 1658302707, @@ -8730,7 +8771,7 @@ "cicero", "nixpkgs" ], - "utils": "utils_20" + "utils": "utils_21" }, "locked": { "lastModified": 1644317729, @@ -10595,11 +10636,11 @@ }, "nixpkgs-arion": { "locked": { - "lastModified": 1721996520, - "narHash": "sha256-R/d5Af+YT2i6/QlGKQ4mZt/kziI1D6KTXumRWkbX/+s=", + "lastModified": 1725022733, + "narHash": "sha256-M6tvBPNDilgXLh9Bfv4U0ih+TyrQReeYOLkY+U2idy8=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "cd3ac4d9337a8be63e48a38583c5978627f4daeb", + "rev": "79eb73f1c49fdfdf75f00b1ee086366380fbc323", "type": "github" }, "original": { @@ -12743,7 +12784,7 @@ "inputs": { "nix": "nix_8", "nixpkgs": "nixpkgs_46", - "utils": "utils_9" + "utils": "utils_10" }, "locked": { "lastModified": 1648128770, @@ -12766,7 +12807,7 @@ "inclusive": "inclusive_2", "nix": "nix_9", "nixpkgs": "nixpkgs_48", - "utils": "utils_10" + "utils": "utils_11" }, "locked": { "lastModified": 1648029666, @@ -12788,7 +12829,7 @@ "inclusive": "inclusive_5", "nix": "nix_11", "nixpkgs": "nixpkgs_60", - "utils": "utils_15" + "utils": "utils_16" }, "locked": { "lastModified": 1648029666, @@ -12810,7 +12851,7 @@ "inclusive": "inclusive_10", "nix": "nix_16", "nixpkgs": "nixpkgs_77", - "utils": "utils_24" + "utils": "utils_25" }, "locked": { "lastModified": 1648029666, @@ -12831,7 +12872,7 @@ "devshell": "devshell_7", "inclusive": "inclusive_3", "nixpkgs": "nixpkgs_49", - "utils": "utils_11" + "utils": "utils_12" }, "locked": { "lastModified": 1649836589, @@ -12852,7 +12893,7 @@ "devshell": "devshell_10", "inclusive": "inclusive_6", "nixpkgs": "nixpkgs_61", - "utils": "utils_16" + "utils": "utils_17" }, "locked": { "lastModified": 1658244176, @@ -12873,7 +12914,7 @@ "devshell": "devshell_17", "inclusive": "inclusive_11", "nixpkgs": "nixpkgs_78", - "utils": "utils_25" + "utils": "utils_26" }, "locked": { "lastModified": 1649836589, @@ -12893,7 +12934,7 @@ "inputs": { "nix": "nix_15", "nixpkgs": "nixpkgs_75", - "utils": "utils_23" + "utils": "utils_24" }, "locked": { "lastModified": 1648128770, @@ -13287,6 +13328,29 @@ "type": "github" } }, + "oura": { + "inputs": { + "crane": [ + "cardano-nix", + "crane" + ], + "utils": "utils_6" + }, + "locked": { + "lastModified": 1720226386, + "narHash": "sha256-oBvHLxWM2vN351flm7jWjwuatFEK6la/nX9fHNy9/hk=", + "owner": "txpipe", + "repo": "oura", + "rev": "d94068562d98f43aeef8e224111fbdaeb2bc186c", + "type": "github" + }, + "original": { + "owner": "txpipe", + "ref": "v1.8.6", + "repo": "oura", + "type": "github" + } + }, "paisano": { "inputs": { "nixpkgs": [ @@ -14451,11 +14515,11 @@ "stackage_9": { "flake": false, "locked": { - "lastModified": 1721952692, - "narHash": "sha256-UXiGzFWWOZMZRYkhS0oVaNK/v8Rr5PxxsM2qV1T6iJI=", + "lastModified": 1724717508, + "narHash": "sha256-FeGR8x/iFDB6zmu3pjRFVcXc6gD/jEct/aM1kZF9gWs=", "owner": "input-output-hk", "repo": "stackage.nix", - "rev": "73bfeeb1dccad2858f22f6f57b6571b10579ed2e", + "rev": "3cdad9ccd2f0232659e147b16ca979d08f77e63e", "type": "github" }, "original": { @@ -15462,6 +15526,21 @@ } }, "utils_10": { + "locked": { + "lastModified": 1601282935, + "narHash": "sha256-WQAFV6sGGQxrRs3a+/Yj9xUYvhTpukQJIcMbIi7LCJ4=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "588973065fce51f4763287f0fda87a174d78bf48", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "utils_11": { "locked": { "lastModified": 1633020561, "narHash": "sha256-4uAiRqL9nP3d/NQ8VBqjQ5iZypHaM+X/FyWpXVXkwTA=", @@ -15476,7 +15555,7 @@ "type": "github" } }, - "utils_11": { + "utils_12": { "locked": { "lastModified": 1633020561, "narHash": "sha256-4uAiRqL9nP3d/NQ8VBqjQ5iZypHaM+X/FyWpXVXkwTA=", @@ -15491,7 +15570,7 @@ "type": "github" } }, - "utils_12": { + "utils_13": { "locked": { "lastModified": 1638122382, "narHash": "sha256-sQzZzAbvKEqN9s0bzWuYmRaA03v40gaJ4+iL1LXjaeI=", @@ -15506,7 +15585,7 @@ "type": "github" } }, - "utils_13": { + "utils_14": { "locked": { "lastModified": 1633020561, "narHash": "sha256-4uAiRqL9nP3d/NQ8VBqjQ5iZypHaM+X/FyWpXVXkwTA=", @@ -15521,7 +15600,7 @@ "type": "github" } }, - "utils_14": { + "utils_15": { "locked": { "lastModified": 1637014545, "narHash": "sha256-26IZAc5yzlD9FlDT54io1oqG/bBoyka+FJk5guaX4x4=", @@ -15536,7 +15615,7 @@ "type": "github" } }, - "utils_15": { + "utils_16": { "locked": { "lastModified": 1633020561, "narHash": "sha256-4uAiRqL9nP3d/NQ8VBqjQ5iZypHaM+X/FyWpXVXkwTA=", @@ -15551,7 +15630,7 @@ "type": "github" } }, - "utils_16": { + "utils_17": { "locked": { "lastModified": 1633020561, "narHash": "sha256-4uAiRqL9nP3d/NQ8VBqjQ5iZypHaM+X/FyWpXVXkwTA=", @@ -15566,7 +15645,7 @@ "type": "github" } }, - "utils_17": { + "utils_18": { "locked": { "lastModified": 1638122382, "narHash": "sha256-sQzZzAbvKEqN9s0bzWuYmRaA03v40gaJ4+iL1LXjaeI=", @@ -15581,21 +15660,6 @@ "type": "github" } }, - "utils_18": { - "locked": { - "lastModified": 1633020561, - "narHash": "sha256-4uAiRqL9nP3d/NQ8VBqjQ5iZypHaM+X/FyWpXVXkwTA=", - "owner": "kreisys", - "repo": "flake-utils", - "rev": "2923532a276a5595ee64376ec1b3db6ed8503c52", - "type": "github" - }, - "original": { - "owner": "kreisys", - "repo": "flake-utils", - "type": "github" - } - }, "utils_19": { "locked": { "lastModified": 1633020561, @@ -15642,6 +15706,21 @@ } }, "utils_21": { + "locked": { + "lastModified": 1633020561, + "narHash": "sha256-4uAiRqL9nP3d/NQ8VBqjQ5iZypHaM+X/FyWpXVXkwTA=", + "owner": "kreisys", + "repo": "flake-utils", + "rev": "2923532a276a5595ee64376ec1b3db6ed8503c52", + "type": "github" + }, + "original": { + "owner": "kreisys", + "repo": "flake-utils", + "type": "github" + } + }, + "utils_22": { "locked": { "lastModified": 1644229661, "narHash": "sha256-1YdnJAsNy69bpcjuoKdOYQX0YxZBiCYZo4Twxerqv7k=", @@ -15656,7 +15735,7 @@ "type": "github" } }, - "utils_22": { + "utils_23": { "locked": { "lastModified": 1637014545, "narHash": "sha256-26IZAc5yzlD9FlDT54io1oqG/bBoyka+FJk5guaX4x4=", @@ -15671,7 +15750,7 @@ "type": "github" } }, - "utils_23": { + "utils_24": { "locked": { "lastModified": 1601282935, "narHash": "sha256-WQAFV6sGGQxrRs3a+/Yj9xUYvhTpukQJIcMbIi7LCJ4=", @@ -15686,7 +15765,7 @@ "type": "github" } }, - "utils_24": { + "utils_25": { "locked": { "lastModified": 1633020561, "narHash": "sha256-4uAiRqL9nP3d/NQ8VBqjQ5iZypHaM+X/FyWpXVXkwTA=", @@ -15701,7 +15780,7 @@ "type": "github" } }, - "utils_25": { + "utils_26": { "locked": { "lastModified": 1633020561, "narHash": "sha256-4uAiRqL9nP3d/NQ8VBqjQ5iZypHaM+X/FyWpXVXkwTA=", @@ -15716,7 +15795,7 @@ "type": "github" } }, - "utils_26": { + "utils_27": { "locked": { "lastModified": 1638122382, "narHash": "sha256-sQzZzAbvKEqN9s0bzWuYmRaA03v40gaJ4+iL1LXjaeI=", @@ -15731,7 +15810,7 @@ "type": "github" } }, - "utils_27": { + "utils_28": { "locked": { "lastModified": 1633020561, "narHash": "sha256-4uAiRqL9nP3d/NQ8VBqjQ5iZypHaM+X/FyWpXVXkwTA=", @@ -15746,7 +15825,7 @@ "type": "github" } }, - "utils_28": { + "utils_29": { "locked": { "lastModified": 1638122382, "narHash": "sha256-sQzZzAbvKEqN9s0bzWuYmRaA03v40gaJ4+iL1LXjaeI=", @@ -15822,15 +15901,12 @@ } }, "utils_7": { - "inputs": { - "systems": "systems_6" - }, "locked": { - "lastModified": 1710146030, - "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "lastModified": 1653893745, + "narHash": "sha256-0jntwV3Z8//YwuOjzhV2sgJJPt+HY6KhU7VZUL0fKZQ=", "owner": "numtide", "repo": "flake-utils", - "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "rev": "1ed9fb1935d260de5fe1c2f7ee0ebaae17ed2fa1", "type": "github" }, "original": { @@ -15840,12 +15916,15 @@ } }, "utils_8": { + "inputs": { + "systems": "systems_6" + }, "locked": { - "lastModified": 1637014545, - "narHash": "sha256-26IZAc5yzlD9FlDT54io1oqG/bBoyka+FJk5guaX4x4=", + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", "owner": "numtide", "repo": "flake-utils", - "rev": "bba5dcc8e0b20ab664967ad83d24d64cb64ec4f4", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", "type": "github" }, "original": { @@ -15856,11 +15935,11 @@ }, "utils_9": { "locked": { - "lastModified": 1601282935, - "narHash": "sha256-WQAFV6sGGQxrRs3a+/Yj9xUYvhTpukQJIcMbIi7LCJ4=", + "lastModified": 1637014545, + "narHash": "sha256-26IZAc5yzlD9FlDT54io1oqG/bBoyka+FJk5guaX4x4=", "owner": "numtide", "repo": "flake-utils", - "rev": "588973065fce51f4763287f0fda87a174d78bf48", + "rev": "bba5dcc8e0b20ab664967ad83d24d64cb64ec4f4", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 71a350f8b..b4ce89933 100644 --- a/flake.nix +++ b/flake.nix @@ -33,7 +33,13 @@ flake = false; }; - cardano-node.url = "github:input-output-hk/cardano-node/4f4e372a1641ac68cd09fb0339e6f55bef1ab85d"; + # The changes introduced in the PRs listed below have not yet been included + # in any cardano-node release. These updates are necessary to run + # cardano-testnet in the Conway era and be able to adjust max Lovelace + # supply. + # https://github.com/IntersectMBO/cardano-node/pull/5936 + # https://github.com/IntersectMBO/cardano-node/pull/5960 + cardano-node.url = "github:input-output-hk/cardano-node/d7abccd4e90c38ff5cd4d6a7839689d888332056"; # Repository with network parameters # NOTE(bladyjoker): Cardano configurations (yaml/json) often change format and break, that's why we pin to a specific known version. From a1cea534174095ab45367c1364b2051a5a72adb0 Mon Sep 17 00:00:00 2001 From: Dzmitry Shuysky Date: Mon, 2 Sep 2024 14:34:13 +0200 Subject: [PATCH 9/9] Fix cardano-testnet cleanup logic: kill cardano-node child processes --- src/Internal/Spawn.purs | 12 +- src/Internal/Testnet/Server.purs | 187 +++++++++++++++---------------- src/Internal/Testnet/Utils.purs | 65 +++++------ 3 files changed, 129 insertions(+), 135 deletions(-) diff --git a/src/Internal/Spawn.purs b/src/Internal/Spawn.purs index 7c9a08ef4..b208b0a52 100644 --- a/src/Internal/Spawn.purs +++ b/src/Internal/Spawn.purs @@ -8,6 +8,7 @@ module Ctl.Internal.Spawn , spawn , exec , stop + , stopProcessWithChildren , waitForStop , cleanupTmpDir , cleanupOnSigint @@ -176,6 +177,13 @@ stop (ManagedProcess _ child closedAVar) = do isAlive <- AVar.isEmpty <$> AVar.status closedAVar when isAlive $ liftEffect $ kill SIGINT child +stopProcessWithChildren :: ManagedProcess -> Aff Unit +stopProcessWithChildren managedProc@(ManagedProcess _ proc _) = do + void $ liftEffect $ Node.ChildProcess.execSync + ("pkill -TERM -P " <> show (unwrap $ Node.ChildProcess.pid proc)) + Node.ChildProcess.defaultExecSyncOptions + stop managedProc + -- | Waits until the process has cleanly stopped. waitForStop :: ManagedProcess -> Aff Unit waitForStop (ManagedProcess cmd _ closedAVar) = do @@ -195,12 +203,12 @@ onSignal :: Signal -> Effect Unit -> Effect OnSignalRef onSignal sig = onSignalImpl (Signal.toString sig) -- | Just as onSignal, but Aff. -waitForSignal :: Signal -> Aff Unit +waitForSignal :: Signal -> Aff Signal waitForSignal signal = makeAff \cont -> do isCanceledRef <- Ref.new false onSignalRef <- onSignal signal $ Ref.read isCanceledRef - >>= flip unless (cont $ Right unit) + >>= flip unless (cont $ Right signal) pure $ Canceler \err -> liftEffect do Ref.write true isCanceledRef removeOnSignal onSignalRef diff --git a/src/Internal/Testnet/Server.purs b/src/Internal/Testnet/Server.purs index 2c764d175..561c0aba1 100644 --- a/src/Internal/Testnet/Server.purs +++ b/src/Internal/Testnet/Server.purs @@ -24,46 +24,45 @@ import Ctl.Internal.Contract.Monad , stopContractEnv ) import Ctl.Internal.Contract.QueryBackend (mkCtlBackendParams) -import Ctl.Internal.Helpers ((<>)) +import Ctl.Internal.Helpers (concatPaths, (<>)) import Ctl.Internal.Logging (Logger, mkLogger, setupLogs) -import Ctl.Internal.QueryM.UniqueId (uniqueId) import Ctl.Internal.ServerConfig (ServerConfig) import Ctl.Internal.Spawn ( ManagedProcess(ManagedProcess) , NewOutputAction(NoOp, Success) , _rmdirSync , isPortAvailable - , killProcessWithPort , spawn , stop + , stopProcessWithChildren ) import Ctl.Internal.Testnet.Types - ( Node - , TestnetClusterConfig + ( TestnetClusterConfig , TestnetConfig , TestnetPaths ) import Ctl.Internal.Testnet.Utils ( EventSource + , TestnetCleanupRef , addCleanup , after , annotateError - , findNodeDirs , findTestnetPaths , getRuntime , onLine - , readNodes , runCleanup , scheduleCleanup , suppressAndLogErrors - , tmpdir + , tmpdirUnique , tryAndLogErrors + , waitFor , waitForClose , waitForError , waitForEvent , waitUntil ) import Ctl.Internal.Types.UsedTxOuts (newUsedTxOuts) +import Data.Array (head) as Array import Data.Log.Message (Message) import Data.Maybe (Maybe(Nothing, Just)) import Data.Set as Set @@ -73,22 +72,24 @@ import Data.String.Pattern (Pattern(Pattern)) import Data.Time.Duration (Milliseconds(Milliseconds)) import Data.UInt (UInt) import Data.UInt (toString) as UInt -import Effect.Aff (Aff, launchAff_) +import Effect.Aff (Aff) import Effect.Aff as Aff +import Effect.Aff.Class (class MonadAff) import Effect.Aff.Retry ( RetryPolicy , constantDelay , limitRetriesByCumulativeDelay , recovering ) +import Effect.Class (class MonadEffect) import Effect.Exception (Error, error, throw) import Effect.Ref (Ref) -import Effect.Ref (modify_, new, read, write) as Ref +import Effect.Ref (modify_, new) as Ref import Foreign.Object as Object import Node.ChildProcess (defaultSpawnOptions) import Node.ChildProcess as Node.ChildProcess import Node.Encoding (Encoding(UTF8)) -import Node.FS.Sync (exists, mkdir) as FSSync +import Node.FS.Sync (readdir) as FSSync import Node.FS.Sync as Node.FS import Node.Path (FilePath) import Node.Process as Node.Process @@ -156,13 +157,7 @@ startKupo -> Ref (Array (Aff Unit)) -> Aff (ManagedProcess /\ String) startKupo cfg params cleanupRef = do - tmpDir <- liftEffect tmpdir - randomStr <- liftEffect $ uniqueId "" - let - workdir = tmpDir <> randomStr <> "-kupo-db" - liftEffect do - workdirExists <- FSSync.exists workdir - unless workdirExists (FSSync.mkdir workdir) + workdir <- tmpdirUnique "kupo-db" childProcess <- after (spawnKupoProcess workdir) @@ -283,33 +278,21 @@ startTestnetCluster cfg cleanupRef = do pure { process: ogmios, channels: ogmiosChannels } -- | Spawns cardano-testnet process with provided parameters. -spawnCardanoTestnet - :: { cwd :: FilePath } - -> TestnetClusterConfig - -> Aff { testnet :: ManagedProcess, workspace :: FilePath } -spawnCardanoTestnet { cwd } params = do +spawnCardanoTestnet :: FilePath -> TestnetClusterConfig -> Aff ManagedProcess +spawnCardanoTestnet workdir params = do env <- liftEffect Node.Process.getEnv let env' = Object.fromFoldable [ "CARDANO_CLI" /\ "cardano-cli" , "CARDANO_NODE" /\ "cardano-node" + , "TMPDIR" /\ workdir ] opts = defaultSpawnOptions - { cwd = Just cwd + { cwd = Just workdir , env = Just $ Object.union env' env + , detached = true } - workspaceRef <- liftEffect $ Ref.new mempty - ps <- spawn "cardano-testnet" options opts $ - Just - ( \{ line } -> - case String.stripPrefix (Pattern "Workspace: ") (String.trim line) of - Nothing -> pure NoOp - Just workspace -> do - void $ Ref.write workspace workspaceRef - pure Success - ) - workspace <- liftEffect $ Ref.read workspaceRef - pure { testnet: ps, workspace } + spawn "cardano-testnet" options opts Nothing where flag :: String -> String flag name = "--" <> name @@ -330,7 +313,7 @@ spawnCardanoTestnet { cwd } params = do startCardanoTestnet :: TestnetClusterConfig - -> Ref (Array (Aff Unit)) + -> TestnetCleanupRef -> Aff { testnet :: ManagedProcess , channels :: @@ -338,68 +321,84 @@ startCardanoTestnet , stdout :: EventSource String } , workdirAbsolute :: FilePath - , nodes :: Array { | Node () } } startCardanoTestnet params cleanupRef = annotateError "startCardanoTestnet" do - tmpDir <- liftEffect tmpdir - { testnet, workspace } <- spawnCardanoTestnet { cwd: tmpDir } params + workdir <- tmpdirUnique "cardano-testnet" + testnet <- scheduleCleanup + cleanupRef + (spawnCardanoTestnet workdir params) + stopProcessWithChildren channels <- liftEffect $ getChannels testnet + workspace <- waitUntil (Milliseconds 100.0) $ findWorkspaceDir workdir + scheduleWorkspaceCleanup workspace + redirectStreams channels workspace + workspaceFromLogs <- waitForCardanoTestnetWorkspace channels.stdout + when (workspace /= workspaceFromLogs) do + runCleanup cleanupRef + throwError $ error "cardano-testnet workspace mismatch" + attachStdoutMonitors testnet + log "startCardanoTestnet:done" + pure { testnet, workdirAbsolute: workspace, channels } + where + findWorkspaceDir :: forall m. MonadEffect m => FilePath -> m (Maybe FilePath) + findWorkspaceDir workdir = + liftEffect $ map (concatPaths workdir) <<< Array.head <$> + FSSync.readdir workdir + + redirectStreams :: StdStreams -> FilePath -> Aff Unit + redirectStreams channels workspace = + void $ redirectChannels channels + { stdoutTo: + { log: Just $ workspace <> "cardano-testnet.stdout.log" + , console: Nothing + } + , stderrTo: + { log: Just $ workspace <> "cardano-testnet.stderr.log" + , console: Nothing + } + } - void $ Aff.forkAff $ annotateError "startCardanoTestnet:waitForErrorOrClose" - do - let - waitError = Just <$> waitForError testnet - waitClose = Nothing <$ waitForClose testnet - cause <- waitError <|> waitClose - runCleanup cleanupRef - throwError $ fromMaybe (error "cardano-testnet process has exited") cause - - nodes <- - waitUntil (Milliseconds 3000.0) $ liftEffect do - hush <$> tryAndLogErrors "startCardanoTestnet:waitForNodes" do - nodeDirs <- findNodeDirs { workdir: workspace } - readNodes { testnetDirectory: workspace, nodeDirs } - - liftEffect $ - for_ nodes \{ port } -> - addCleanup cleanupRef (killProcessWithPort port) - - -- clean up on SIGINT - do - shouldCleanup <- liftEffect - $ Node.Process.lookupEnv "TESTNET_CLEANUP_WORKDIR" - <#> case _ of - Just "0" -> false - _ -> true - when shouldCleanup - $ liftEffect - $ addCleanup cleanupRef - $ liftEffect do - log $ "Cleaning up workdir: " <> workspace + waitForCardanoTestnetWorkspace + :: forall m + . MonadAff m + => EventSource String + -> m FilePath + waitForCardanoTestnetWorkspace = + liftAff + <<< flip waitFor + (String.stripPrefix (Pattern "Workspace: ") <<< String.trim) + + attachStdoutMonitors :: ManagedProcess -> Aff Unit + attachStdoutMonitors testnet = + void $ Aff.forkAff $ + annotateError "startCardanoTestnet:attachStdoutMonitors" do + let + waitError = Just <$> waitForError testnet + waitClose = Nothing <$ waitForClose testnet + cause <- waitError <|> waitClose + runCleanup cleanupRef + throwError $ fromMaybe (error "cardano-testnet process has exited") + cause + + scheduleWorkspaceCleanup :: forall m. MonadEffect m => FilePath -> m Unit + scheduleWorkspaceCleanup workspace = + liftEffect do + shouldCleanup <- + Node.Process.lookupEnv "TESTNET_CLEANUP_WORKDIR" <#> + case _ of + Just "0" -> false + _ -> true + when shouldCleanup do + addCleanup cleanupRef $ liftEffect do + log $ "Cleaning up cardano-testnet workspace: " <> workspace _rmdirSync workspace - launchAff_ $ stop testnet - - _ <- redirectChannels - { stderr: channels.stderr, stdout: channels.stdout } - { stdoutTo: - { log: Just $ workspace <> "cardano-testnet.stdout.log" - , console: Nothing - } - , stderrTo: - { log: Just $ workspace <> "cardano-testnet.stderr.log" - , console: Nothing - } - } - log "startCardanoTestnet:done" - pure { testnet, workdirAbsolute: workspace, channels, nodes } +type StdStreams = + { stderr :: EventSource String + , stdout :: EventSource String + } -getChannels - :: ManagedProcess - -> Effect - { stderr :: EventSource String - , stdout :: EventSource String - } +getChannels :: ManagedProcess -> Effect StdStreams getChannels (ManagedProcess _ process _) = ado stdout <- onLine (Node.ChildProcess.stdout process) Just stderr <- onLine (Node.ChildProcess.stderr process) Just @@ -408,9 +407,7 @@ getChannels (ManagedProcess _ process _) = ado -- Note: it will not throw, so to check the computation result -- Fiber must be inspected. redirectChannels - :: { stderr :: EventSource String - , stdout :: EventSource String - } + :: StdStreams -> { stderrTo :: { log :: Maybe FilePath, console :: Maybe String } , stdoutTo :: { log :: Maybe FilePath, console :: Maybe String } } diff --git a/src/Internal/Testnet/Utils.purs b/src/Internal/Testnet/Utils.purs index be3c25d75..a5d1dc14a 100644 --- a/src/Internal/Testnet/Utils.purs +++ b/src/Internal/Testnet/Utils.purs @@ -1,7 +1,7 @@ module Ctl.Internal.Testnet.Utils - ( find811TestnetWorkir + ( EventSource(EventSource) + , TestnetCleanupRef , findNodeDirs - , EventSource(EventSource) , onLine , makeEventSource , findTestnetPaths @@ -10,11 +10,11 @@ module Ctl.Internal.Testnet.Utils , scheduleCleanup , addCleanup , tmpdir + , tmpdirUnique , runCleanup , tryAndLogErrors , suppressAndLogErrors , after - , is811TestnetDirectoryName , onTestnetEvent , parseEvent , readNodes @@ -25,7 +25,6 @@ module Ctl.Internal.Testnet.Utils , waitForError , waitForEvent , waitUntil - , waitForTestnet872Workdir , cleanupOnExit , annotateError ) where @@ -48,7 +47,7 @@ import Control.Monad.Error.Class ) import Control.Monad.Except (lift, runExceptT) import Control.Monad.Rec.Class (Step(Done, Loop), tailRecM) -import Control.Parallel (parallel, sequential) +import Control.Parallel (parOneOf, parallel, sequential) import Ctl.Internal.Helpers ((<>)) import Ctl.Internal.QueryM.UniqueId (uniqueId) import Ctl.Internal.Spawn @@ -69,10 +68,11 @@ import Ctl.Internal.Testnet.Types import Data.Array as Array import Data.Int as Int import Data.Map as Map -import Data.Posix.Signal (Signal(SIGINT)) +import Data.Posix.Signal (Signal(SIGINT, SIGTERM)) +import Data.Posix.Signal (toString) as Signal import Data.String (Pattern(Pattern)) import Data.String as String -import Data.Time.Duration (Milliseconds) +import Data.Time.Duration (class Duration, fromDuration) import Data.UInt (UInt) import Data.UInt as UInt import Effect.Aff (try) @@ -85,6 +85,7 @@ import Effect.Ref as Ref import Node.ChildProcess as Node.ChildProcess import Node.Encoding (Encoding(UTF8)) import Node.Encoding as Node.Encoding +import Node.FS.Sync (exists, mkdir) as FSSync import Node.FS.Sync as Node.FS import Node.FS.Sync as Node.FS.Sync import Node.Path (FilePath) @@ -92,31 +93,6 @@ import Node.Process as Process import Node.ReadLine as RL import Node.Stream (Readable) --- | For cardano-node 8.1.1 -is811TestnetDirectoryName :: Int -> FilePath -> Boolean -is811TestnetDirectoryName n = - isJust <<< String.stripPrefix (Pattern $ "testnet-" <> show n <> "-test-") - -find811TestnetWorkir - :: { tmpdir :: FilePath, dirIdx :: Int } -> Effect (Maybe FilePath) -find811TestnetWorkir { tmpdir: tmpDir, dirIdx } = - map (tmpDir <> _) - <<< Array.find (is811TestnetDirectoryName dirIdx) - <$> Node.FS.readdir tmpDir - -waitForTestnet872Workdir - :: EventSource String -> { tmpdir :: FilePath } -> Aff { workdir :: FilePath } -waitForTestnet872Workdir src = map { workdir: _ } - <<< waitFor src - <<< parseTestnet872Workdir - -parseTestnet872Workdir :: { tmpdir :: FilePath } -> String -> Maybe FilePath -parseTestnet872Workdir { tmpdir: tmpDir } = String.stripPrefix - $ Pattern - $ " Workspace: " - <> tmpDir - <> "/" - parseEvent :: String -> Maybe Event parseEvent = case _ of -- we can't know this way when 8.1.1 cardano-testnet is ready @@ -301,13 +277,15 @@ makeEventSource subscribeOnEvents filter = annotateError "make event source" do , outcome } -addCleanup :: Ref (Array (Aff Unit)) -> Aff Unit -> Effect Unit +type TestnetCleanupRef = Ref (Array (Aff Unit)) + +addCleanup :: TestnetCleanupRef -> Aff Unit -> Effect Unit addCleanup = map void <<< flip (Ref.modify <<< Array.cons <<< suppressAndLogErrors "[addCleanup][error]: ") scheduleCleanup :: forall a - . Ref (Array (Aff Unit)) + . TestnetCleanupRef -> Aff a -> (a -> Aff Unit) -> Aff a @@ -355,6 +333,16 @@ narrowEventSource filter (EventSource source) = annotateError -- https://github.com/Plutonomicon/cardano-transaction-lib/issues/726 foreign import tmpdir :: Effect String +tmpdirUnique :: forall m. MonadEffect m => String -> m FilePath +tmpdirUnique suffix = + liftEffect do + tmpDir <- tmpdir + randomStr <- uniqueId "" + let dir = tmpDir <> randomStr <> "-" <> suffix + dirExists <- FSSync.exists dir + unless dirExists $ FSSync.mkdir dir + pure dir + foreign import setLineHandler :: RL.Interface -> (String -> Effect Unit) -> Effect OnSignalRef @@ -459,11 +447,11 @@ waitForClose (ManagedProcess _ child _) = do cancel cont $ Left $ appendErrorMessage "waitForClose has been canceled" err -waitUntil :: forall a. Milliseconds -> Aff (Maybe a) -> Aff a +waitUntil :: forall a d. Duration d => d -> Aff (Maybe a) -> Aff a waitUntil checkingInterval fa = flip tailRecM unit \_ -> fa >>= case _ of Nothing -> do - Aff.delay checkingInterval + Aff.delay $ fromDuration checkingInterval pure $ Loop unit Just x -> pure $ Done x @@ -580,7 +568,8 @@ cleanupOnExit cleanupRef = do ) <|> ( handlers.onWaitForSignal - <$ parallel (waitForSignal SIGINT) + <$> parallel + (parOneOf [ waitForSignal SIGINT, waitForSignal SIGTERM ]) ) handler cleanup triggeredBy = do @@ -595,7 +584,7 @@ cleanupOnExit cleanupRef = do err liftEffect $ Process.exit 7 -- Failing irrecoverably , onBeforeExit: cleanup "before exit" - , onWaitForSignal: cleanup "SIGINT" + , onWaitForSignal: cleanup <<< Signal.toString } pure { fiber }