Skip to content
This repository was archived by the owner on Mar 1, 2019. It is now read-only.

Commit de7201c

Browse files
committed
[CBR-464] Make sure we throw correct UtxoExhausted when needed
Note that this revert a few things introduced in input-output-hk/cardano-sl#3704 & input-output-hk/cardano-sl#3672. We moved the zero-output check from divvyFee to its callers as it makes more sense. Also, with the introduction of `Maybe` in the `PickUtxo` signature, we can remove the corner-case check for empty UTxO which now correctly get caught by layers below.
1 parent 1ac5465 commit de7201c

File tree

8 files changed

+77
-85
lines changed

8 files changed

+77
-85
lines changed

integration/TransactionSpecs.hs

Lines changed: 19 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -171,36 +171,20 @@ transactionSpecs wRef wc = beforeAll_ (setupLogging "wallet-new_transactionSpecs
171171
<> " error, got: "
172172
<> show err
173173

174-
randomTest "fails if you spend more money than your available balance" 1 $ do
175-
wallet <- run $ sampleWallet wRef wc
176-
(toAcct, toAddr) <- run $ firstAccountAndId wc wallet
177-
178-
let payment = Payment
179-
{ pmtSource = PaymentSource
180-
{ psWalletId = walId wallet
181-
, psAccountIndex = accIndex toAcct
182-
}
183-
, pmtDestinations = pure PaymentDistribution
184-
{ pdAddress = addrId toAddr
185-
, pdAmount = tooMuchCash (accAmount toAcct)
186-
}
187-
, pmtGroupingPolicy = Nothing
188-
, pmtSpendingPassword = Nothing
189-
}
190-
tooMuchCash (V1 c) = V1 (Core.mkCoin (Core.getCoin c * 2))
191-
etxn <- run $ postTransaction wc payment
192-
err <- liftIO (etxn `shouldPrism` _Left)
193-
case err of
194-
ClientWalletError (NotEnoughMoney (ErrAvailableBalanceIsInsufficient _)) ->
195-
return ()
174+
randomTest "fails if you don't have any money" 1 $ run $ do
175+
(wallet, account) <- fixtureWallet Nothing
176+
resp <- makePayment (Core.mkCoin 14) (wallet, account) =<< getRandomAddress
177+
let err = NotEnoughMoney (ErrAvailableBalanceIsInsufficient 0)
178+
expectFailure (ClientWalletError err) resp
196179

197-
_ ->
198-
liftIO $ expectationFailure $
199-
"Expected 'NotEnoughMoney ~ ErrAvailableBalanceIsInsufficient', got: "
200-
<> show err
180+
randomTest "fails if you spend more money than your available balance" 1 $ run $ do
181+
(wallet, account) <- fixtureWallet (Just $ Core.mkCoin 42)
182+
resp <- makePayment (Core.mkCoin 10000) (wallet, account) =<< getRandomAddress
183+
let err = NotEnoughMoney (ErrAvailableBalanceIsInsufficient 42)
184+
expectFailure (ClientWalletError err) resp
201185

202186
randomTest "fails if you can't cover fee with a transaction" 1 $ run $ do
203-
(wallet, account) <- fixtureWallet (Core.mkCoin 42)
187+
(wallet, account) <- fixtureWallet (Just $ Core.mkCoin 42)
204188
resp <- makePayment (Core.mkCoin 42) (wallet, account) =<< getRandomAddress
205189
let err = NotEnoughMoney ErrCannotCoverFee
206190
expectFailure (ClientWalletError err) resp
@@ -277,15 +261,18 @@ transactionSpecs wRef wc = beforeAll_ (setupLogging "wallet-new_transactionSpecs
277261
return (unV1 $ addrId toAddr)
278262

279263
fixtureWallet
280-
:: Core.Coin
264+
:: Maybe Core.Coin
281265
-> IO (Wallet, Account)
282-
fixtureWallet coin = do
266+
fixtureWallet mcoin = do
283267
genesis <- genesisWallet wc
284268
(genesisAccount, _) <- firstAccountAndId wc genesis
285269
wallet <- randomWallet CreateWallet >>= createWalletCheck wc
286270
(account, address) <- firstAccountAndId wc wallet
287-
txn <- makePayment coin (genesis, genesisAccount) (unV1 $ addrId address) >>= shouldPrismFlipped _Right
288-
pollTransactions wc (walId wallet) (accIndex account) (txId txn)
271+
case mcoin of
272+
Nothing -> return ()
273+
Just coin -> do
274+
txn <- makePayment coin (genesis, genesisAccount) (unV1 $ addrId address) >>= shouldPrismFlipped _Right
275+
pollTransactions wc (walId wallet) (accIndex account) (txId txn)
289276
return (wallet, account)
290277

291278
expectFailure
@@ -295,4 +282,4 @@ transactionSpecs wRef wc = beforeAll_ (setupLogging "wallet-new_transactionSpecs
295282
-> IO ()
296283
expectFailure want eresp = do
297284
resp <- eresp `shouldPrism` _Left
298-
want `shouldBe` resp
285+
resp `shouldBe` want

src/Cardano/Wallet/API/V1/ReifyWalletError.hs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import qualified Data.Text as T
1111
import Formatting (build, sformat)
1212
import Universum
1313

14+
import qualified Data.Char as C
1415
import Pos.Core (decodeTextAddress)
1516

1617
import Cardano.Wallet.API.V1.Types (V1 (..))
@@ -267,7 +268,8 @@ newTransactionError e = case e of
267268
V1.TooBigTransaction
268269

269270
ex@(CoinSelHardErrUtxoExhausted balance _payment) ->
270-
case (readMaybe $ T.unpack balance) of
271+
-- NOTE balance & payment are "prettified" coins representation (e.g. "42 coin(s)")
272+
case (readMaybe $ T.unpack $ T.dropWhileEnd (not . C.isDigit) balance) of
271273
Just coin ->
272274
V1.NotEnoughMoney (V1.ErrAvailableBalanceIsInsufficient coin)
273275
Nothing ->

src/Cardano/Wallet/Kernel/CoinSelection/FromGeneric.hs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -238,8 +238,12 @@ repack (txIn, aux) = (Core.toaOut aux, txIn)
238238
-------------------------------------------------------------------------------}
239239

240240
-- | Pick an element from the UTxO to cover any remaining fee
241-
type PickUtxo m = Core.Coin -- ^ Fee to still cover
242-
-> CoinSelT Core.Utxo CoinSelHardErr m (Maybe (Core.TxIn, Core.TxOutAux))
241+
--
242+
-- NOTE: This cannot fail (as suggested by `forall e.`) but still runs in
243+
-- `CoinSelT` for conveniency; this way, it interfaces quite nicely with other
244+
-- functions.
245+
type PickUtxo m = forall e. Core.Coin -- ^ Fee to still cover
246+
-> CoinSelT Core.Utxo e m (Maybe (Core.TxIn, Core.TxOutAux))
243247

244248
data CoinSelFinalResult = CoinSelFinalResult {
245249
csrInputs :: NonEmpty (Core.TxIn, Core.TxOutAux)
@@ -385,9 +389,9 @@ largestFirst opts maxInps =
385389
pickUtxo :: PickUtxo m
386390
pickUtxo val = search . Map.toList =<< get
387391
where
388-
search :: [(Core.TxIn, Core.TxOutAux)]
389-
-> CoinSelT Core.Utxo CoinSelHardErr m (Maybe (Core.TxIn, Core.TxOutAux))
390-
search [] = throwError CoinSelHardErrCannotCoverFee
392+
search :: forall e. [(Core.TxIn, Core.TxOutAux)]
393+
-> CoinSelT Core.Utxo e m (Maybe (Core.TxIn, Core.TxOutAux))
394+
search [] = return Nothing
391395
search ((i, o):ios)
392396
| Core.txOutValue (Core.toaOut o) >= val = return $ Just (i, o)
393397
| otherwise = search ios

src/Cardano/Wallet/Kernel/CoinSelection/Generic.hs

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -582,28 +582,18 @@ nLargestFromListBy f n = \xs ->
582582

583583
-- | Proportionally divide the fee over each output.
584584
--
585-
-- There's a special 'edge-case' when the given input is a singleton list
586-
-- with one 0 coin. This is artifically created during input selection when
587-
-- the transaction's amount matches exactly the source's balance.
588-
-- In such case, we can't really compute any ratio for fees and simply return
589-
-- the whole fee back with the given change value.
585+
-- Pre-condition 1: The given outputs list shouldn't be empty
586+
-- Pre-condition 2: None of the outputs should be null
590587
divvyFee :: forall dom a. CoinSelDom dom
591588
=> (a -> Value dom) -> Fee dom -> [a] -> [(Fee dom, a)]
592-
divvyFee _ _ [] = error "divvyFee: empty list"
593-
divvyFee f fee [a] | f a == valueZero = [(fee, a)]
594-
divvyFee f fee as = map (\a -> (feeForOut a, a)) as
589+
divvyFee _ _ [] = error "divvyFee: empty list"
590+
divvyFee f _ as | any ((== valueZero) . f) as = error "divvyFee: some outputs are null"
591+
divvyFee f fee as = map (\a -> (feeForOut a, a)) as
595592
where
596593
-- All outputs are selected from well-formed UTxO, so their sum cannot
597594
-- overflow
598595
totalOut :: Value dom
599-
totalOut =
600-
let
601-
total = unsafeValueSum (map f as)
602-
in
603-
if total == valueZero then
604-
error "divyyFee: invalid set of coins, total is 0"
605-
else
606-
total
596+
totalOut = unsafeValueSum (map f as)
607597

608598
-- The ratio will be between 0 and 1 so cannot overflow
609599
feeForOut :: a -> Fee dom

src/Cardano/Wallet/Kernel/CoinSelection/Generic/Fees.hs

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ feeFromChange :: forall dom. CoinSelDom dom
124124
-> [CoinSelResult dom]
125125
-> ([CoinSelResult dom], Fee dom)
126126
feeFromChange totalFee =
127-
bimap identity unsafeFeeSum
127+
bimap identity unsafeFeeSum
128128
. unzip
129129
. map go
130130
. divvyFee (outVal . coinSelRequest) totalFee
@@ -143,13 +143,15 @@ feeFromChange totalFee =
143143
-- as unchanged as possible
144144
reduceChangeOutputs :: forall dom. CoinSelDom dom
145145
=> Fee dom -> [Value dom] -> ([Value dom], Fee dom)
146-
reduceChangeOutputs totalFee [] = ([], totalFee)
147146
reduceChangeOutputs totalFee cs =
148-
bimap identity unsafeFeeSum
149-
. unzip
150-
. map go
151-
. divvyFee identity totalFee
152-
$ cs
147+
case divvyFeeSafe identity totalFee cs of
148+
Nothing ->
149+
(cs, totalFee)
150+
Just xs ->
151+
bimap identity unsafeFeeSum
152+
. unzip
153+
. map go
154+
$ xs
153155
where
154156
-- Reduce single change output, returning remaining fee
155157
go :: (Fee dom, Value dom) -> (Value dom, Fee dom)
@@ -171,6 +173,19 @@ feeUpperBound FeeOptions{..} css =
171173
numInputs = fromIntegral $ sum (map (sizeToWord . coinSelInputSize) css)
172174
outputs = concatMap coinSelOutputs css
173175

176+
-- | divvy fee across outputs, discarding zero-output if any. Returns `Nothing`
177+
-- when there's no more outputs after filtering, in which case, we just can't
178+
-- divvy fee.
179+
divvyFeeSafe
180+
:: forall dom a. CoinSelDom dom
181+
=> (a -> Value dom)
182+
-> Fee dom
183+
-> [a]
184+
-> Maybe [(Fee dom, a)]
185+
divvyFeeSafe f fee as = case filter ((/= valueZero) . f) as of
186+
[] -> Nothing
187+
as' -> Just (divvyFee f fee as')
188+
174189
{-------------------------------------------------------------------------------
175190
Pretty-printing
176191
-------------------------------------------------------------------------------}

src/Cardano/Wallet/Kernel/CoinSelection/Generic/Random.hs

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,23 +33,10 @@ random :: forall utxo m. (MonadRandom m, PickFromUtxo utxo)
3333
-> Word64 -- ^ Maximum number of inputs
3434
-> [Output (Dom utxo)] -- ^ Outputs to include
3535
-> CoinSelT utxo CoinSelHardErr m [CoinSelResult (Dom utxo)]
36-
random privacyMode initMaxNumInputs goals = do
37-
balance <- gets utxoBalance
38-
when (balance == valueZero) $ throwError (errUtxoExhausted balance)
39-
coinSelPerGoal selection initMaxNumInputs goals
36+
random privacyMode = coinSelPerGoal $ \maxNumInputs goal ->
37+
defCoinSelResult goal <$>
38+
inRange maxNumInputs (target privacyMode (outVal goal))
4039
where
41-
errUtxoExhausted :: Value (Dom utxo) -> CoinSelHardErr
42-
errUtxoExhausted balance = CoinSelHardErrUtxoExhausted
43-
(pretty balance)
44-
(pretty $ unsafeValueSum $ map outVal goals)
45-
46-
selection
47-
:: Word64
48-
-> Output (Dom utxo)
49-
-> CoinSelT utxo CoinSelHardErr m (CoinSelResult (Dom utxo))
50-
selection maxNumInputs goal = defCoinSelResult goal
51-
<$> inRange maxNumInputs (target privacyMode (outVal goal))
52-
5340
target :: PrivacyMode -> Value (Dom utxo) -> TargetRange (Dom utxo)
5441
target PrivacyModeOn val = fromMaybe (target PrivacyModeOff val)
5542
(idealRange val)
@@ -111,18 +98,27 @@ atLeastNoFallback :: forall utxo m. (PickFromUtxo utxo, MonadRandom m)
11198
=> Word64
11299
-> Value (Dom utxo)
113100
-> CoinSelT utxo CoinSelErr m (SelectedUtxo (Dom utxo))
114-
atLeastNoFallback maxNumInputs targetMin = go emptySelection
101+
atLeastNoFallback maxNumInputs targetMin = do
102+
balance <- gets utxoBalance
103+
go emptySelection balance
115104
where
116105
go :: SelectedUtxo (Dom utxo)
106+
-> Value (Dom utxo)
117107
-> CoinSelT utxo CoinSelErr m (SelectedUtxo (Dom utxo))
118-
go selected
108+
go selected balance
119109
| sizeToWord (selectedSize selected) > maxNumInputs =
120110
throwError $ CoinSelErrSoft CoinSelSoftErr
121111
| selectedBalance selected >= targetMin =
122112
return selected
123113
| otherwise = do
124-
io <- mapCoinSelErr CoinSelErrHard $ findRandomOutput
125-
go $ select io selected
114+
io <- findRandomOutput >>= maybe (throwError $ errUtxoExhausted balance) return
115+
go (select io selected) balance
116+
117+
errUtxoExhausted :: Value (Dom utxo) -> CoinSelErr
118+
errUtxoExhausted balance =
119+
CoinSelErrHard $ CoinSelHardErrUtxoExhausted
120+
(pretty balance)
121+
(pretty targetMin)
126122

127123
-- | Select random additional inputs with the aim of improving the change amount
128124
--

src/Cardano/Wallet/Kernel/DB/Spec/Read.hs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,7 @@ cpAvailableBalance :: IsCheckpoint c => c -> Core.Coin
5656
cpAvailableBalance c =
5757
fromMaybe subCoinErr balance'
5858
where
59-
pendingIns = Set.union
60-
(Pending.txIns $ c ^. cpPending)
61-
(Pending.txIns $ c ^. cpForeign)
59+
pendingIns = Pending.txIns $ c ^. cpPending
6260
spentUtxo = Core.utxoRestrictToInputs (c ^. cpUtxo) pendingIns
6361
spentBalance = Core.unsafeIntegerToCoin $ Core.utxoBalance spentUtxo
6462
balance' = Core.subCoin (c ^. cpUtxoBalance) spentBalance

test/unit/Test/Spec/CoinSelection/Generators.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ utxoSmallestEntry utxo =
267267
genPayees :: Core.Utxo -> Pay -> Gen (NonEmpty Core.TxOut)
268268
genPayees utxo payment = do
269269
let balance = toLovelaces payment
270-
halfOfUtxoSmallest = (Core.getCoin $ utxoSmallestEntry utxo) `div` 2
270+
halfOfUtxoSmallest = max 1 $ (Core.getCoin $ utxoSmallestEntry utxo) `div` 2
271271
genTxOut StakeGenOptions {
272272
stakeMaxValue = Just (Core.mkCoin halfOfUtxoSmallest)
273273
, stakeGenerationTarget = AtLeast

0 commit comments

Comments
 (0)