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

[CSL-2526] update changes after covering remaining fees #3815

Merged
merged 2 commits into from
Nov 6, 2018

Conversation

kderme
Copy link
Member

@kderme kderme commented Nov 2, 2018

Description

This pr tries to fix a bug on coin and fee selection, which was reported by QA.
The bug was caused because in some cases some changes were not included in the transaction, resulting in huge fees.

The CoinSelection works in many different stages:

  • first given some payees, some inputs are selected which cover the expenses (fees are ignored for now). Changes are computed at this stage.
  • then fees are computed and we reduce changes to cover the fees. If they are enough, we are done.
  • if not new inputs are selected.

The problem is that these additional inputs (last step) which cover the fees, may not be entirely consumed for the fees: we need to return what's left as change.

TODO:

  • Decide exactly how these changes will be returned: Do we create new additional change outputs or do we distribute them to the existing changes? If we decide the latter, do we need to equally distribute the additional change to all existing one, or just give everything to one of them?
    UPDATE: we split additional changes to existing change outputs. We do this in FromGeneric module (instead of the Generic or Fees) for 2 reason: in this module change outputs are flatten to a list (instead of list of lists) and also it is less polymorphic, which means we can directly use divide function from Core.
  • Another issue is the additional fees that we may need to pay for these additional utxos and changes. This issue becomes too complex.
  • Add a sanity check after the coin selection, which reports an internal error if fees are not within a reasonable range.
  • check round vs ceiling in the use of calculateTxSizeLinear.
  • add unit tests

Linked issue

https://iohk.myjetbrains.com/youtrack/issue/CSL-2526

QA Steps

Here are some steps to reproduce the bug with tome good probabilities:

  • Create a wallet.
  • send 1 tx with amount > 1 ADA to it.
  • send 2 txs with ammount in [100..1000] Lovelace to it.
  • try to send < 100 Lovelace from it.

After trying the last step with different amounts and addresses, at some point the bug will appear and fees will be > 1 ADA.

The bug appears in these cases because the inputs in [100..1000] are enough to cover the <100 tx, but not enough to cover the fees. So an additional input needs to be selected. If we are "lucky" this input will be the > 1 ADA in which case we will have huge fees (without the fix).

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)

Screenshots (if available)

before
bad

after
good

@kderme kderme requested review from edsko and KtorZ November 2, 2018 07:49
-> Value utxo
-> [CoinSelResult utxo]
returnAdditioncalChange [] _ = error "found empty CoinSelResult"
returnAdditioncalChange (cs : restcs) additionalChange =
Copy link
Contributor

Choose a reason for hiding this comment

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

typo here I believe returnAdditioncalChange -> returnAdditionalChange

(css', ) <$> coverRemainingFee pickUtxo remainingFee
(additionalUtxo, additionalChange) <- coverRemainingFee pickUtxo remainingFee
let css'' = returnAdditioncalChange css' additionalChange
return (css'', additionalUtxo)
Copy link
Contributor

Choose a reason for hiding this comment

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

To be honest, I'd rather add that logic to runCoinSelT from Cardano.Wallet.Kernel.CoinSelection.FromGeneric.

When we construct the final result:

        let css  = map (coinSelRemoveDust (csoDustThreshold opts)) cssWithDust
            inps = concatMap selectedEntries
                     (additionalUtxo : map coinSelInputs css)
            outs = map coinSelOutput css
        let allInps = case inps of
                        []   -> error "runCoinSelT: empty list of inputs"
                        i:is -> i :| is
            originalOuts = case outs of
                               []   -> error "runCoinSelT: empty list of outputs"
                               o:os -> o :| os
        return . Right $ CoinSelFinalResult allInps
                                            originalOuts
                                            (concatMap coinSelChange css)

This would avoid fiddling with CoinSelResult here and "artificially" add the change. To my understanding, we've separated additional inputs yielded by the fee policy for a reason; We have conflated them with the CoinSelResult inputs already but we didn't. Instead, we reconcialte the CoinSelFinalResult later and therefore, I see this as a similar case.

Copy link
Member Author

@kderme kderme Nov 2, 2018

Choose a reason for hiding this comment

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

Should I create an additional change output just for these additional UTXO's instead of just adding it to the existing one? In this case should I also check if this new change is dust? Not sure 🤔

Copy link
Contributor

Choose a reason for hiding this comment

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

Cf @edsko inputs I believe. Let's distribute the extra change across all changes.

@KtorZ
Copy link
Contributor

KtorZ commented Nov 2, 2018

The change looks good; I simply think it's not applied to the right place.
Also, let's have some hard invariant check after computing the fees, to make sure that we aren't constructing a transaction with crazy fees.

@tatyanavych
Copy link
Contributor

We need a fix on release/2.0.0 branch ASAP

@commandodev
Copy link
Contributor

Are we planning to add a unit or property tests to exercise this bug and prove that it's fixed? I can't really see from the code changes here what the specific logic error was or that it has been fixed.

@KtorZ
Copy link
Contributor

KtorZ commented Nov 5, 2018

Yes we are. And this is why this PR is still blocked and under review.
@tatyanavych We'll provide a fix but we'll provide a proper one which includes:

  • guards to prevent this from happening again
  • an acceptance test
  • a valid patch that fits correctly with the existing code and abstraction

We aren't going to ship something to fix it again later 👍

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.

LGTM, nice catch, and I like the logic of the solution. One concern: by modifying a single change output with the remaining change, if that remaining change is very large, it becomes easily identifiable as the change output. Might wish to consider divvy to distribute it evenly over all change outputs.

@edsko
Copy link
Contributor

edsko commented Nov 5, 2018

Very minor concern: possibility of overflow when adding change to existing outputs.

@kderme
Copy link
Member Author

kderme commented Nov 5, 2018

Sanity check breaks unit tests :/

Copy link
Contributor

@KtorZ KtorZ left a comment

Choose a reason for hiding this comment

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

Some minor comments and some concerns about the hardcoded min fee.
This is a chain parameter that can in theory, change with chain updates. The value should come from the chain as much as possible.

let fees = computeFeesOfUnsignedTx tx
if isSane options fees
then return (snapshot, tx, fees, availableUtxo)
else throwError $ NewTransactionErrorCoinSelectionFailed CoinSelHardErrCannotCoverFee
Copy link
Contributor

Choose a reason for hiding this comment

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

The error would be a bit misleading here and I wouldn't define any particular new user-facing error either; this is an invariant violation. If we reach that case, it means we fucked up hard and everything should blow up!

@@ -229,7 +229,12 @@ newUnsignedTransaction ActiveWallet{..} options accountId payees = runExceptT $
-- that it may change in the future.
let attributes = def :: TxAttributes
let tx = UnsignedTx inputs outputs attributes coins
return (snapshot, tx, availableUtxo)

-- STEP 3: sanity test.
Copy link
Contributor

Choose a reason for hiding this comment

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

Worth extending the comment here explaining what that check is all about.

-- this is a bit ad-hoc, but the policy is taken from the node.
-- another solution is to use the policy from CoinSelectionOptions
-- and call it with 0 ins and outs, which should give the same number.
let minFees = Core.mkCoin 155381
Copy link
Contributor

Choose a reason for hiding this comment

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

This can (ought to) be extracted from the NodeStateAdaptor:

      -- | Get fee policy
    , getFeePolicy :: m TxFeePolicy

@kderme kderme force-pushed the kderme/CSL-2526 branch 5 times, most recently from ad1371c to e1b1cfa Compare November 6, 2018 10:50
Copy link
Contributor

@KtorZ KtorZ left a comment

Choose a reason for hiding this comment

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

LGTM 👍

@KtorZ KtorZ merged commit 11f220d into develop Nov 6, 2018
@KtorZ KtorZ deleted the kderme/CSL-2526 branch November 6, 2018 14:37
KtorZ added a commit that referenced this pull request Nov 9, 2018
[CSL-2526] update changes after covering remaining fees
KtorZ added a commit to input-output-hk/cardano-wallet-legacy that referenced this pull request Nov 9, 2018
…hk/kderme/CSL-2526

[CSL-2526] update changes after covering remaining fees
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants