Skip to content
This repository has been archived by the owner on Aug 18, 2020. It is now read-only.

[CBR-462] Fix 0 fee transaction #3704

Merged
merged 2 commits into from
Oct 4, 2018

Conversation

KtorZ
Copy link
Contributor

@KtorZ KtorZ commented Oct 3, 2018

Description

commit 527afc7
Author: KtorZ matthias.benkort@gmail.com
Date: Wed Oct 3 17:12:00 2018 +0200

[CBR-462] Throws right error when running out of UTxO in fee calculation

In this context, @CoinSelHardErrUtxoDepleted@ might be thrown by
`pickUtxo` as we iterate over the selected change outputs which here
means that we are running out of UTxOs to cover the fee.  Hence, we
ought to remap the error accordingly to @CoinSelHardErrCannotCoverFee@.

commit 78c113e
Author: KtorZ matthias.benkort@gmail.com
Date: Wed Oct 3 17:05:48 2018 +0200

[CBR-462] Fix fee estimation when transaction's amount matches account's balance

When calling

```
reduceChangeOutputs
  :: forall dom. CoinSelDom dom
  => Fee dom
  -> [Value dom]
  -> ([Value dom], Fee dom)
```

If the `[Value dom]` is a singleton array with a zero coin, we're going
to end up dividing by zero and silently obliterating fee as we do it.
This happens when making a transaction with no change, i.e., when the
transaction's amount matches exactly the source account's balance.

This commit provides a fix handling this special edge-case.

Linked issue

[CBR-462]

Type of change

  • 🐞 Bug fix (non-breaking change which fixes an issue)
  • 🛠 New feature (non-breaking change which adds functionality)
  • ⚠️ Breaking change (fix or feature that would cause existing functionality to change)
  • 🏭 Refactoring that does not change existing functionality but does improve things like code readability, structure etc
  • 🔨 New or improved tests for existing code
  • ⛑ git-flow chore (backport, hotfix, etc)

Developer checklist

  • I have read the style guide document, and my code follows the code style of this project.
  • If my code deals with exceptions, it follows the guidelines.
  • I have updated any documentation accordingly, if needed. Documentation changes can be reflected in opening a PR on cardanodocs.com, amending the inline Haddock comments, any relevant README file or one of the document listed in the docs directory.
  • [ ] CHANGELOG entry has been added and is linked to the correct PR on GitHub.

Testing checklist

  • I have added tests to cover my changes.
  • All new and existing tests passed.

QA Steps

Screenshots (if available)

…t's balance

When calling

```
reduceChangeOutputs
  :: forall dom. CoinSelDom dom
  => Fee dom
  -> [Value dom]
  -> ([Value dom], Fee dom)
```

If the `[Value dom]` is a singleton array with a zero coin, we're going
to end up dividing by zero and silently obliterating fee as we do it.
This happens when making a transaction with no change, i.e., when the
transaction's amount matches exactly the source account's balance.

This commit provides a fix handling this special edge-case.
In this context, @CoinSelHardErrUtxoDepleted@ might be thrown by
`pickUtxo` as we iterate over the selected change outputs which here
means that we are running out of UTxOs to cover the fee.  Hence, we
ought to remap the error accordingly to @CoinSelHardErrCannotCoverFee@.
if total == valueZero then
error "divyyFee: invalid set of coins, total is 0"
else
total
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't happen, but ... just-in-case :|

Copy link
Contributor

@adinapoli-iohk adinapoli-iohk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@KtorZ the divvyFee fix looks good, but let's see if we can keep the polymorphic type signature around 😉

-- with one 0 coin. This is artifically created during input selection when
-- the transaction's amount matches exactly the source's balance.
-- In such case, we can't really compute any ratio for fees and simply return
-- the whole fee back with the given change value.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add a final section to this comment explaining why we do that: we return the whole fee because at the end of the day "it doesn't matter": as we are doomed anyway (we have depleted our Utxo to cover the amount, no chances we have money to cover the fee, too) this will be caught "one layer up" and the whole coin selection algorithm will fail accordingly, as it should.

-> Fee (Dom utxo)
-> CoinSelT utxo e m (SelectedUtxo (Dom utxo))
-> CoinSelT utxo CoinSelHardErr m (SelectedUtxo (Dom utxo))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only thing I don't like about this PR: Edsko worked really hard to make the code completely generic, even in the error e, but here we are now specialising it to CoinSelHardErr. Is there any way we can keep it generic? It would be better, as I also don't have any clue if this would preclude along the lines the possibility to run more interesting simulations, for example.

Copy link
Contributor Author

@KtorZ KtorZ Oct 4, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure here. I just followed GHC complaints when started to "catchError" and "throwError". We can't keep the generic forall e here, because well, we use explicit CoinSelHardErr constructors in the function body :/

Exactly like functions around actually:

receiverPaysFee :: forall dom. CoinSelDom dom
=> Fee dom
-> [CoinSelResult dom]
-> Except CoinSelHardErr [CoinSelResult dom]
receiverPaysFee totalFee =

adjustForFees :: forall utxo m. (CoinSelDom (Dom utxo), Monad m)
=> FeeOptions (Dom utxo)
-> (Value (Dom utxo) ->
CoinSelT utxo CoinSelHardErr m (UtxoEntry (Dom utxo)))
-> [CoinSelResult (Dom utxo)]
-> CoinSelT utxo CoinSelHardErr m
([CoinSelResult (Dom utxo)], SelectedUtxo (Dom utxo))
adjustForFees feeOptions pickUtxo css = do

I believe this part can't really be 100% generic, unless we provide also "Generic errors"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, good point. coverRemainingFee is used by senderPaysFee which is already specialised, so I guess this is fine. Alright 😉

@adinapoli-iohk adinapoli-iohk merged commit 499bb3e into develop Oct 4, 2018
@adinapoli-iohk adinapoli-iohk deleted the KtorZ/CBR-462/fix-0-fee-transaction branch October 4, 2018 07:33
Copy link
Contributor

@edsko edsko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The need for error handling when trying to cover the fee is indeed an oversight on my part, apologies :) (More on that in #3710 though). However, I feel the change to divvy is not correct. The comment says "This is artificially created during input selection when the transaction's amount matches exactly the source's balance. In such case, we can't really compute any ratio for fees and simply return the whole fee back with the given change value." but this is not accurate. When during coin selection we have x left to cover, and we happen to have a UTxO entry available for exactly x, then indeed we may end up with a change output of precisely zero. This by itself is however not problematic, this isn't necessary an error case. It's entirely possible that we have additional UTxO entries available to cover the fee (or indeed we might just pay the fee from the output itself, if the recipient pays fees). So the comment is misleading. In a way the fix is not wrong, however; indeed, it could just be simplified to

divvyFee _ _   []  = error "divvyFee: empty list"
divvyFee f fee [a] = [(fee, a)]

After all, divvyFee is meant to proportionally allocate the fee, and if there is a single entry in the list, that single entry should the entire fee.

However, I feel this is fixing the problem in the wrong place, and will break again if, for example, coin selection was modified to create multiple change outputs (for instance, for obscurity), we may in principle end up with multiple change outputs of value zero, and we'd be back with a wrong calculation.

Instead, I think divvyFee should have a precondition that none of the outputs may be zero (just like it has a precondition the list shouldn't be empty). Then when we call divvyFee we have an obligation to satisfy this precondition. This happens (as far as I can tell) in two places: once, when we divvy the fee over all the outputs of the transaction; this will be a non-empty list and the values will be non-zero, so that's fine. Then we do it once more for each output, to divvy the fee (for that particular output) over the change values (of that output). This happens in reduceChangeOutputs; this function already takes care to satisfy the precondition of divvyFee to deal with the empty list of change outputs, but it doesn't deal with the zero change case. So I think reduceChangeOutputs should look at the list of change outputs it gets, filter by non-zero, then check if the resulting list is non-empty, and if so proceed as normal, and if not, have the same special case that it already has for the empty list.

KtorZ added a commit that referenced this pull request Oct 9, 2018
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.
KtorZ added a commit that referenced this pull request Oct 9, 2018
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.
KtorZ added a commit that referenced this pull request Oct 9, 2018
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.
KtorZ added a commit that referenced this pull request Oct 9, 2018
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.
KtorZ added a commit that referenced this pull request Oct 9, 2018
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.
KtorZ added a commit that referenced this pull request Nov 9, 2018
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.
KtorZ added a commit to input-output-hk/cardano-wallet-legacy that referenced this pull request Nov 9, 2018
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.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants