-
Notifications
You must be signed in to change notification settings - Fork 631
[CBR-464] Raise Correct CoinSelHardErr
during coin selection
#3710
Conversation
This makes it available for other tests within this context.
…utput' The rational here is that, the outcome may depend of the context. We use this function in multiple places where running out of UTxO may have a different meaning. Therefore, instead of introducing the 'UtxoDepleted' error, we return a raw Maybe and let the caller decides what error should be thrown (here, most likely: CannotCoverFee or UtxoExhausted)
4c76bd8
to
7c4ed1c
Compare
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On the surface it LGTM, albeit I would be interested to hear with @edsko thinks as he might recall some edge case in the coin selection and tell us if our changes are sound or not. He should be back next week, so that's worth the wait 😉
@@ -267,15 +268,13 @@ newTransactionError e = case e of | |||
V1.TooBigTransaction | |||
|
|||
ex@(CoinSelHardErrUtxoExhausted balance _payment) -> | |||
case (readMaybe $ T.unpack balance) of | |||
-- NOTE balance & payment are "prettified" coins representation (e.g. "42 coin(s)") | |||
case (readMaybe $ T.unpack $ T.dropWhileEnd (not . C.isDigit) balance) of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This parsing is very unfortunate. I think the nicer solution is to add a valueToInt :: v -> Int
(or Word64
or whatever) and then replace the Text
in CoinSelHardErr
by Int
(or Word64
). That was never meant to be parsed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's what I understood from this indeed. Any arguments about having Coin
here instead of Int
since it conveys more meaning. I don't see any reasons why we would do an early conversion when throwing the error.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Coin
is not applicable, because that is specific to the Cardano domain. Other domains will not use Coin
for their representation of values.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ho true, forgot that the errors were generics here 👍
@@ -239,7 +239,7 @@ repack (txIn, aux) = (Core.toaOut aux, txIn) | |||
|
|||
-- | Pick an element from the UTxO to cover any remaining fee | |||
type PickUtxo m = Core.Coin -- ^ Fee to still cover | |||
-> CoinSelT Core.Utxo CoinSelHardErr m (Core.TxIn, Core.TxOutAux) | |||
-> CoinSelT Core.Utxo CoinSelHardErr m (Maybe (Core.TxIn, Core.TxOutAux)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this Maybe
here, I think it makes perfect sense. I think we can express easily that this function should never return an error by changing the signature to
type PickUtxo m = forall e. Core.Coin -- ^ Fee to still cover
-> CoinSelT Core.Utxo e m (Maybe (Core.TxIn, Core.TxOutAux))
without having to expose the underlying implementation.
@@ -288,7 +288,6 @@ runCoinSelT opts pickUtxo policy request utxo = do | |||
policy' :: CoinSelT Core.Utxo CoinSelHardErr m | |||
([CoinSelResult Cardano], SelectedUtxo Cardano) | |||
policy' = do | |||
when (Map.null utxo) $ throwError CoinSelHardErrUtxoDepleted |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great, I didn't like this special case and didn't understand why it was necessary :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Me neither 🙃 ... Hence the need for this PR.
The previous patches felt a bit like "monkey-patching" without really tackling the actual issue.
@@ -387,10 +386,10 @@ largestFirst opts maxInps = | |||
pickUtxo val = search . Map.toList =<< get | |||
where | |||
search :: [(Core.TxIn, Core.TxOutAux)] | |||
-> CoinSelT Core.Utxo CoinSelHardErr m (Core.TxIn, Core.TxOutAux) | |||
-> CoinSelT Core.Utxo CoinSelHardErr m (Maybe (Core.TxIn, Core.TxOutAux)) | |||
search [] = throwError CoinSelHardErrCannotCoverFee |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't this return Nothing
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed
inRange maxNumInputs (target privacyMode (outVal goal)) | ||
random privacyMode initMaxNumInputs goals = do | ||
balance <- gets utxoBalance | ||
when (balance == valueZero) $ throwError (errUtxoExhausted balance) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need this special case here? Surely if we reach a zero balance after, say, the first output, we'd have to deal with that anyway?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are correct. This isn't needed anymore with the introduction of the Maybe
for findRandomUTxO
and this get caught correctly by levels below 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome :D
@@ -98,18 +111,27 @@ atLeastNoFallback :: forall utxo m. (PickFromUtxo utxo, MonadRandom m) | |||
=> Word64 | |||
-> Value (Dom utxo) | |||
-> CoinSelT utxo CoinSelErr m (SelectedUtxo (Dom utxo)) | |||
atLeastNoFallback maxNumInputs targetMin = go emptySelection | |||
atLeastNoFallback maxNumInputs targetMin = do | |||
utxo <- get |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is correct, in the sense that it's good that we first get the initial UTxO, and use that when reporting the balance to the user; however, I'd feel a little happier if we just did initBalance <- utxoBalance <$> utxo
here and then thread that initBalance
around, because we don't need the entire UTxO (and this opens the door to misunderstandings later). I'm also somewhat worried that we don't report this initial balance correctly in other places, but that might not be justified.
Incidentally, @adinapoli-iohk tells me that for debugging the "balance at the point of failure" was actually a useful bit of information for him; that is now no longer available. Don't know how much it's worth trying to recover that.
7c4ed1c
to
c2a4272
Compare
c2a4272
to
85adb36
Compare
I didn't fix the parsing of the balance from the ex@(CoinSelHardErrUtxoExhausted balance _payment) ->
case (readMaybe $ T.unpack balance) of
-- NOTE balance & payment are "prettified" coins representation (e.g. "42 coin(s)")
case (readMaybe $ T.unpack $ T.dropWhileEnd (not . C.isDigit) balance) of I'll open a ticket for this as it requires a bit more change and is quite isolated from this particular PR. |
51c0a22
to
85adb36
Compare
Huhuh, unit tests aren't happy with the new invariant from |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
General braindump.
where | ||
-- divvyFee only accepts non-zero output values as it doesn't make sense | ||
-- to apply fees to those. | ||
(cs0, cs') = partition (== valueZero) cs |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need this partition
, at all? With @edsko we initially thought a simple filter
for (> valueZero)
might have been enough. What are we missing?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as I understand, we still want to return those zero value as part of the change, hence the change from
bimap identity unsafeFeeSum
to
bimap (++ cs0) unsafeFeeSum
We just don't run the divvyFee
repartition function on them... I mean, we would filter out the zero value at this stage, but I'd expect this function to return the same number of inputs I gave it, no ?
divvyFee f fee [a] | f a == valueZero = [(fee, a)] | ||
divvyFee f fee as = map (\a -> (feeForOut a, a)) as | ||
divvyFee _ _ [] = error "divvyFee: empty list" | ||
divvyFee f _ as | any ((== valueZero) . f) as = error "divvyFee: some outputs are null" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we still need this special guard? I suspect that now that we pushed the main logic fix inside reduceChangeOutputs
, we can try to be more liberal here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, this is an invariant we want to enforce. If we don't have that, then we might get a division by zero silently coerced to Word64
and turned to 0
.
λ> ceiling (0 * (14 / 0 :: Double)) :: Word64
0
So, I'll better leave a guard that explicitly fails and tell us that we did something wrong 👍 (like it seems to happen in the unit test 😅 )
85adb36
to
bcb7c49
Compare
Okay, fixed the |
Note that this revert a few things introduced in #3704 & #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.
bcb7c49
to
81e4323
Compare
pendingIns = Set.union | ||
(Pending.txIns $ c ^. cpPending) | ||
(Pending.txIns $ c ^. cpForeign) | ||
pendingIns = Pending.txIns $ c ^. cpPending |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reverting from #3672
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
Alles Goed! Not waiting for Darwin here 👍 |
…rect-coinselerr [CBR-464] Raise Correct `CoinSelHardErr` during coin selection
…hk/KtorZ/CBR-464/raise-correct-coinselerr [CBR-464] Raise Correct `CoinSelHardErr` during coin selection
Description
In a previous PR (#3704 - CBR-462), we patched the coin selection and remap the
UTxODepleted
error to aCannotCoverFee
error.From this, the frontend team discovered a new bug caused because again,
UTxODepleted
was thrown instead of a correctUTxOExhausted
.Instead of patching again and re-mapping the error to the correct one, I took a step back and slightly reviewed the original abstraction such that we would now have:
instead of
The rational here is that, the outcome may depend of the context. We use this function in
multiple places where running out of UTxO may have a different meaning. Therefore, instead
of introducing the 'UtxoDepleted' error, we return a raw Maybe and let the caller decides
what error should be thrown (here, most likely: CannotCoverFee or UtxoExhausted)
Note that in theory, we could simply have the following to make it obvious that
PickUtxo
can't fail:However, this exposes the
StrictStateT
fromCoinSelT
which is opaque.. So, we could go even further and have exposed instead.Linked issue
[CBR-464]
Type of change
Developer checklist
CHANGELOG entry has been added and is linked to the correct PR on GitHub.Testing checklist
QA Steps
Screenshots (if available)