diff --git a/src/Internal/BalanceTx/BalanceTx.purs b/src/Internal/BalanceTx/BalanceTx.purs index a22a508d6..01659630a 100644 --- a/src/Internal/BalanceTx/BalanceTx.purs +++ b/src/Internal/BalanceTx/BalanceTx.purs @@ -22,6 +22,7 @@ import Cardano.Types , Transaction , TransactionBody , TransactionOutput + , TransactionUnspentOutput , UtxoMap , Value(Value) , _amount @@ -38,12 +39,15 @@ import Cardano.Types , _witnessSet ) import Cardano.Types.Address (Address) +import Cardano.Types.Address (getPaymentCredential) as Address import Cardano.Types.BigNum as BigNum import Cardano.Types.Coin as Coin +import Cardano.Types.Credential (asPubKeyHash) as Credential import Cardano.Types.OutputDatum (OutputDatum(OutputDatum)) -import Cardano.Types.TransactionBody (_votingProposals) +import Cardano.Types.TransactionBody (_collateral, _votingProposals) import Cardano.Types.TransactionInput (TransactionInput) -import Cardano.Types.TransactionUnspentOutput as TransactionUnspentOutputs +import Cardano.Types.TransactionUnspentOutput (_output) +import Cardano.Types.TransactionUnspentOutput as TransactionUnspentOutput import Cardano.Types.TransactionWitnessSet (_redeemers) import Cardano.Types.UtxoMap (pprintUtxoMap) import Cardano.Types.Value (getMultiAsset, mkValue, pprintValue) @@ -65,7 +69,10 @@ import Ctl.Internal.BalanceTx.Collateral ( addTxCollateral , addTxCollateralReturn ) -import Ctl.Internal.BalanceTx.Collateral.Select (selectCollateral) +import Ctl.Internal.BalanceTx.Collateral.Select + ( minRequiredCollateral + , selectCollateral + ) as Collateral import Ctl.Internal.BalanceTx.Constraints ( BalanceTxConstraintsBuilder , _collateralUtxos @@ -137,7 +144,7 @@ import Data.Array.NonEmpty import Data.Array.NonEmpty as NEA import Data.Bitraversable (ltraverse) import Data.Either (Either, hush, note) -import Data.Foldable (fold, foldMap, foldr, length, null, sum) +import Data.Foldable (foldMap, foldr, length, null, sum) import Data.Lens (view) import Data.Lens.Getter ((^.)) import Data.Lens.Setter ((%~), (.~), (?~)) @@ -147,6 +154,7 @@ import Data.Map (Map) import Data.Map ( empty , filter + , filterKeys , insert , isEmpty , lookup @@ -209,11 +217,6 @@ balanceTxWithConstraints transaction extraUtxos constraintsBuilder = <#> traverse (note CouldNotGetUtxos) >>> map (foldr Map.union Map.empty) -- merge all utxos into one map - unbalancedCollTx <- transactionWithNetworkId >>= - if Array.null (transaction ^. _witnessSet <<< _redeemers) - -- Don't set collateral if tx doesn't contain phase-2 scripts: - then pure - else setTransactionCollateral changeAddress let allUtxos :: UtxoMap allUtxos = @@ -223,6 +226,12 @@ balanceTxWithConstraints transaction extraUtxos constraintsBuilder = availableUtxos <- liftContract $ filterLockedUtxos allUtxos + unbalancedCollTx <- transactionWithNetworkId >>= + if Array.null (transaction ^. _witnessSet <<< _redeemers) + -- Don't set collateral if tx doesn't contain phase-2 scripts: + then pure + else setTransactionCollateral changeAddress availableUtxos + Logger.info (pprintUtxoMap allUtxos) "balanceTxWithConstraints: all UTxOs" Logger.info (pprintUtxoMap availableUtxos) "balanceTxWithConstraints: available UTxOs" @@ -253,8 +262,9 @@ balanceTxWithConstraints transaction extraUtxos constraintsBuilder = (transaction ^. _body <<< _networkId) pure (transaction # _body <<< _networkId ?~ networkId) -setTransactionCollateral :: Address -> Transaction -> BalanceTxM Transaction -setTransactionCollateral changeAddr transaction = do +setTransactionCollateral + :: Address -> UtxoMap -> Transaction -> BalanceTxM Transaction +setTransactionCollateral changeAddr availableUtxos transaction = do nonSpendableSet <- asksConstraints _nonSpendableInputs mbCollateralUtxos <- asksConstraints _collateralUtxos -- We must filter out UTxOs that are set as non-spendable in the balancer @@ -272,21 +282,49 @@ setTransactionCollateral changeAddr transaction = do when (not $ Array.null filteredUtxos) do logWarn' $ pprintTagSet "Some of the collateral UTxOs returned by the wallet were marked as non-spendable and ignored" - (pprintUtxoMap (TransactionUnspentOutputs.toUtxoMap filteredUtxos)) - pure spendableUtxos + (pprintUtxoMap (TransactionUnspentOutput.toUtxoMap filteredUtxos)) + let + collVal = + foldMap (Val.fromValue <<< view (_output <<< _amount)) + spendableUtxos + minRequiredCollateral = + BigNum.toBigInt $ + unwrap Collateral.minRequiredCollateral + if (Val.getCoin collVal < minRequiredCollateral) then do + logWarn' $ pprintTagSet + "Filtered collateral UTxOs do not cover the minimum required \ + \collateral, reselecting collateral using CTL algorithm." + (pprintUtxoMap (TransactionUnspentOutput.toUtxoMap spendableUtxos)) + let + isPkhUtxo txOut = isJust do + cred <- Address.getPaymentCredential $ (unwrap txOut).address + Credential.asPubKeyHash $ unwrap cred + availableUtxos' <- liftContract $ + Map.filter isPkhUtxo <<< Map.filterKeys isSpendable <$> + filterLockedUtxos availableUtxos + selectCollateral availableUtxos' + else pure spendableUtxos -- otherwise, get all the utxos, filter out unspendable, and select -- collateral using internal algo, that is also used in KeyWallet - Just utxoMap -> do - ProtocolParameters params <- liftContract getProtocolParameters - let - maxCollateralInputs = UInt.toInt $ params.maxCollateralInputs - mbCollateral = - Array.fromFoldable <$> - selectCollateral params.coinsPerUtxoByte maxCollateralInputs utxoMap - liftEither $ note (InsufficientCollateralUtxos utxoMap) mbCollateral + Just utxoMap -> selectCollateral utxoMap addTxCollateralReturn collateral (addTxCollateral collateral transaction) changeAddr +-- | Select collateral from the provided utxos using internal CTL +-- | collateral selection algorithm. +selectCollateral :: UtxoMap -> BalanceTxM (Array TransactionUnspentOutput) +selectCollateral utxos = do + pparams <- unwrap <$> liftContract getProtocolParameters + let + maxCollateralInputs = UInt.toInt $ pparams.maxCollateralInputs + mbCollateral = + Array.fromFoldable <$> Collateral.selectCollateral + pparams.coinsPerUtxoByte + maxCollateralInputs + utxos + liftEither $ note (InsufficientCollateralUtxos utxos) + mbCollateral + -------------------------------------------------------------------------------- -- Balancing Algorithm -------------------------------------------------------------------------------- @@ -346,11 +384,11 @@ runBalancer p = do isCip30 <- liftContract $ isCip30Wallet -- Get collateral inputs to mark them as unspendable. -- Some CIP-30 wallets don't allow to sign Txs that spend it. - nonSpendableCollateralInputs <- - if isCip30 then - liftContract $ Wallet.getWalletCollateral <#> - fold >>> map (unwrap >>> _.input) >>> Set.fromFoldable - else mempty + let + nonSpendableCollateralInputs = + if isCip30 then + Set.fromFoldable $ p.transaction ^. _body <<< _collateral + else mempty asksConstraints Constraints._nonSpendableInputs <#> append nonSpendableCollateralInputs >>> \nonSpendableInputs -> diff --git a/test/Testnet/Contract.purs b/test/Testnet/Contract.purs index 7bf1b3d65..bcfdb3823 100644 --- a/test/Testnet/Contract.purs +++ b/test/Testnet/Contract.purs @@ -166,7 +166,7 @@ import Data.Either (Either(Left, Right), hush, isLeft, isRight) import Data.Foldable (fold, foldM, length) import Data.Lens (view) import Data.Map as Map -import Data.Maybe (Maybe(Just, Nothing), fromJust, fromMaybe, isJust) +import Data.Maybe (Maybe(Just, Nothing), fromJust, fromMaybe, isJust, maybe) import Data.Newtype (unwrap, wrap) import Data.Traversable (traverse, traverse_) import Data.Tuple (Tuple(Tuple)) @@ -223,7 +223,7 @@ suite = do withWallets distribution \alice -> do withKeyWallet alice ManyAssets.contract test - "#1509 - Collateral set to one of the inputs in mustNotSpendUtxosWithOutRefs " + "#1509 - Collateral set to one of the inputs in mustNotSpendUtxosWithOutRefs" do let someUtxos = @@ -253,6 +253,57 @@ suite = do ) res `shouldSatisfy` isLeft + test + "#1581 - Fallback to CTL collateral selection when all collateral inputs are non-spendable" + do + let + distribution = + [ BigNum.fromInt 10_000_000 + , BigNum.fromInt 10_000_000 + ] + withWallets distribution \alice -> + withKeyWallet alice do + validator <- AlwaysSucceeds.alwaysSucceedsScript + let vhash = validatorHash validator + logInfo' "Attempt to lock value" + txId <- AlwaysSucceeds.payToAlwaysSucceeds vhash + awaitTxConfirmed txId + logInfo' "Try to spend locked values" + + scriptAddress <- mkAddress (wrap $ ScriptHashCredential vhash) + Nothing + utxos <- utxosAt scriptAddress + scriptUtxo <- + liftM + ( error + ( "The id " + <> show txId + <> " does not have output locked at: " + <> show scriptAddress + ) + ) + $ head (lookupTxHash txId utxos) + + unbalancedTx <- buildTx + [ SpendOutput scriptUtxo $ Just $ PlutusScriptOutput + (ScriptValue validator) + RedeemerDatum.unit + (Just $ DatumValue PlutusData.unit) + ] + + collUtxos <- getWalletCollateral + let + balancerConstraints = + maybe + mempty + (mustNotSpendUtxosWithOutRefs <<< Map.keys <<< toUtxoMap) + collUtxos + + balancedTx <- balanceTx unbalancedTx (toUtxoMap [ scriptUtxo ]) + balancerConstraints + balancedSignedTx <- signTransaction balancedTx + submitAndLog balancedSignedTx + test "#1480 - test that does nothing but fails" do let someUtxos =