Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle non-spendable utxos when selecting collateral #1650

Draft
wants to merge 2 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 64 additions & 26 deletions src/Internal/BalanceTx/BalanceTx.purs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import Cardano.Types
, Transaction
, TransactionBody
, TransactionOutput
, TransactionUnspentOutput
, UtxoMap
, Value(Value)
, _amount
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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 ((%~), (.~), (?~))
Expand All @@ -147,6 +154,7 @@ import Data.Map (Map)
import Data.Map
( empty
, filter
, filterKeys
, insert
, isEmpty
, lookup
Expand Down Expand Up @@ -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 =
Expand All @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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
--------------------------------------------------------------------------------
Expand Down Expand Up @@ -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 ->
Expand Down
55 changes: 53 additions & 2 deletions test/Testnet/Contract.purs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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 =
Expand Down