Skip to content

Commit

Permalink
Remove the restriction that all minted assets must be spent or burned.
Browse files Browse the repository at this point in the history
This commit removes the restriction within that all minted assets must
be spent or burned. It also adds coverage checks to properties for
`performSelection` to verify that we are indeed covering the case where
some minted assets are not spent or burned.
  • Loading branch information
jonathanknowles committed Sep 15, 2021
1 parent 32b6d0f commit ac8e7c5
Show file tree
Hide file tree
Showing 3 changed files with 12 additions and 98 deletions.
8 changes: 0 additions & 8 deletions lib/core/src/Cardano/Wallet/Api/Server.hs
Expand Up @@ -361,7 +361,6 @@ import Cardano.Wallet.Primitive.CoinSelection.Balance
( SelectionResult (..)
, UnableToConstructChangeError (..)
, balanceMissing
, missingOutputAssets
, selectionDelta
)
import Cardano.Wallet.Primitive.Delegation.UTxO
Expand Down Expand Up @@ -3821,13 +3820,6 @@ instance IsServerError ErrSelectAssets where
, "must specify enough ada. Here are the problematic "
, "outputs:\n" <> pretty (indentF 2 $ blockListF xs)
]
Balance.OutputsInsufficient e ->
apiError err403 TokensMintedButNotSpentOrBurned $ mconcat
[ "I can't process this transaction because some "
, "minted values were not spent or burned. These "
, "are the values that should be spent or burned: "
, pretty . Flat $ missingOutputAssets e
]
Balance.UnableToConstructChange e ->
apiError err403 CannotCoverFee $ T.unwords
[ "I am unable to finalize the transaction, as there"
Expand Down
58 changes: 0 additions & 58 deletions lib/core/src/Cardano/Wallet/Primitive/CoinSelection/Balance.hs
Expand Up @@ -38,7 +38,6 @@ module Cardano.Wallet.Primitive.CoinSelection.Balance
, SelectionResult (..)
, SelectionError (..)
, BalanceInsufficientError (..)
, OutputsInsufficientError (..)
, SelectionInsufficientError (..)
, InsufficientMinCoinValueError (..)
, UnableToConstructChangeError (..)
Expand Down Expand Up @@ -115,7 +114,6 @@ module Cardano.Wallet.Primitive.CoinSelection.Balance
, distance
, mapMaybe
, balanceMissing
, missingOutputAssets
) where

import Prelude
Expand Down Expand Up @@ -507,56 +505,11 @@ data SelectionError
SelectionInsufficientError
| InsufficientMinCoinValues
(NonEmpty InsufficientMinCoinValueError)
| OutputsInsufficient
OutputsInsufficientError
| UnableToConstructChange
UnableToConstructChangeError
| EmptyUTxO
deriving (Generic, Eq, Show)

-- | Indicates that a portion of the minted assets were not spent or burned.
--
-- This situation occurs if the following inequality does not hold:
--
-- >>> assetsToMint `leq` (assetsToBurn <> requestedOutputAssets)
--
-- The existence of this error reflects a deliberate design choice: all minted
-- assets must either be explicitly spent or explicitly burned by the caller.
-- In future, we could revise this design to allow excess minted assets (those
-- that are neither spent nor burned) to be returned to the wallet as change.
--
data OutputsInsufficientError = OutputsInsufficientError
{ assetsToMint
:: !TokenMap
-- ^ The assets to mint
, assetsToBurn
:: !TokenMap
-- ^ The assets to burn
, requestedOutputAssets
:: !TokenMap
-- ^ The complete set of assets found within the user-specified outputs
} deriving (Generic, Eq, Show)

-- | Computes the portion of minted assets that are not spent or burned.
--
missingOutputAssets :: OutputsInsufficientError -> TokenMap
missingOutputAssets e =
-- We use 'difference' which will show us the quantities in 'assetsToMint'
-- that are not in 'assetsToBurn <> requestedOutputAssets'.
--
-- Any asset quantity present in 'assetsToBurn <> requestedOutputAssets'
-- but not present in 'assetsToMint' will simply be zeroed out, which is
-- the behaviour we want for this error report.
--
assetsToMint `TokenMap.difference`
(assetsToBurn `TokenMap.add` requestedOutputAssets)
where
OutputsInsufficientError
{ assetsToMint
, assetsToBurn
, requestedOutputAssets
} = e

-- | Indicates that the balance of selected UTxO entries was insufficient to
-- cover the balance required.
--
Expand Down Expand Up @@ -684,11 +637,6 @@ performSelection
-- ^ The selection goal to satisfy.
-> m (Either SelectionError (SelectionResult TokenBundle))
performSelection minCoinFor costFor bundleSizeAssessor criteria
-- Is the minted value all spent or burnt?
| not (assetsToMint `leq` (assetsToBurn <> requestedOutputAssets)) =
pure $ Left $ OutputsInsufficient $ OutputsInsufficientError
{assetsToMint, assetsToBurn, requestedOutputAssets}

-- Is the total available UTXO balance sufficient?
| not utxoBalanceSufficient =
pure $ Left $ BalanceInsufficient $ BalanceInsufficientError
Expand Down Expand Up @@ -733,12 +681,6 @@ performSelection minCoinFor costFor bundleSizeAssessor criteria
, utxoBalanceRequired
}

requestedOutputBalance :: TokenBundle
requestedOutputBalance = F.foldMap (view #tokens) outputsToCover

requestedOutputAssets :: TokenMap
requestedOutputAssets = view #tokens requestedOutputBalance

mkInputsSelected :: UTxOIndex -> NonEmpty (TxIn, TxOut)
mkInputsSelected =
fromMaybe invariantSelectAnyInputs . NE.nonEmpty . UTxOIndex.toList
Expand Down
Expand Up @@ -27,7 +27,6 @@ import Cardano.Wallet.Primitive.CoinSelection.Balance
, BalanceInsufficientError (..)
, InsufficientMinCoinValueError (..)
, MakeChangeCriteria (..)
, OutputsInsufficientError (..)
, RunSelectionParams (..)
, SelectionCriteria (..)
, SelectionDelta (..)
Expand Down Expand Up @@ -58,7 +57,6 @@ import Cardano.Wallet.Primitive.CoinSelection.Balance
, makeChangeForNonUserSpecifiedAssets
, makeChangeForUserSpecifiedAsset
, mapMaybe
, missingOutputAssets
, performSelection
, prepareOutputsWith
, reduceTokenQuantities
Expand Down Expand Up @@ -748,15 +746,19 @@ prop_performSelection_small minCoinValueFor costFor (Blind (Small criteria)) =
cover 2 (noAssetsAreBothSpentAndBurned)
"No assets are both spent and burned" $

-- Inspect the relationship between minted, burned, and spent assets:
cover 2 (allMintedAssetsEitherBurnedOrSpent)
"All minted assets were either spent or burned" $
cover 2 (not allMintedAssetsEitherBurnedOrSpent)
"Some minted assets were neither spent nor burned" $

prop_performSelection minCoinValueFor costFor (Blind criteria) $ \result ->
cover 10 (selectionUnlimited && selectionSufficient result)
"selection unlimited and sufficient"
. cover 4 (selectionLimited && selectionSufficient result)
"selection limited but sufficient"
. cover 10 (selectionLimited && selectionInsufficient result)
"selection limited and insufficient"
. cover 2 (outputsInsufficient result)
"A portion of the minted assets were not spent or burned"
where
utxoHasAtLeastOneAsset = not
. Set.null
Expand All @@ -771,11 +773,6 @@ prop_performSelection_small minCoinValueFor costFor (Blind (Small criteria)) =
. fmap (view #tokens)
$ outputsToCover criteria

outputsInsufficient :: PerformSelectionResult -> Bool
outputsInsufficient = \case
Left (OutputsInsufficient _) -> True
_ -> False

selectionLimited :: Bool
selectionLimited = case view #selectionLimit criteria of
MaximumInputLimit _ -> True
Expand All @@ -798,6 +795,12 @@ prop_performSelection_small minCoinValueFor costFor (Blind (Small criteria)) =
assetsSpentByUserSpecifiedOutputs =
F.foldMap (view (#tokens . #tokens)) (outputsToCover criteria)

allMintedAssetsEitherBurnedOrSpent :: Bool
allMintedAssetsEitherBurnedOrSpent =
view #assetsToMint criteria `leq` TokenMap.add
(view #assetsToBurn criteria)
(assetsSpentByUserSpecifiedOutputs)

someAssetsAreBothMintedAndBurned :: Bool
someAssetsAreBothMintedAndBurned
= TokenMap.isNotEmpty
Expand Down Expand Up @@ -969,8 +972,6 @@ prop_performSelection minCoinValueFor costFor (Blind criteria) coverage =
onFailure = \case
BalanceInsufficient e ->
onBalanceInsufficient e
OutputsInsufficient e ->
onOutputsInsufficient e
SelectionInsufficient e ->
onSelectionInsufficient e
InsufficientMinCoinValues es ->
Expand Down Expand Up @@ -1027,27 +1028,6 @@ prop_performSelection minCoinValueFor costFor (Blind criteria) coverage =
errorBalanceSelected =
F.foldMap (view #tokens . snd) errorInputsSelected

onOutputsInsufficient e = do
monitor $ counterexample $ unlines
[ "assets to mint:"
, pretty (Flat errorAssetsToMint)
, "assets to burn:"
, pretty (Flat errorAssetsToBurn)
, "requested output assets:"
, pretty (Flat errorRequestedOutputAssets)
, "assets minted but not spent or burned:"
, pretty (Flat $ missingOutputAssets e)
]
assert $ errorAssetsToMint == assetsToMint
assert $ errorAssetsToBurn == assetsToBurn
assert
$ not
$ errorAssetsToMint
`leq` (errorRequestedOutputAssets <> errorAssetsToBurn)
where
OutputsInsufficientError
errorAssetsToMint errorAssetsToBurn errorRequestedOutputAssets = e

onInsufficientMinCoinValues es = do
monitor $ counterexample $ unlines
[ show es
Expand Down

0 comments on commit ac8e7c5

Please sign in to comment.