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

[RCD-45] & [RCD-44] Review fee calculation entirely #3861

Merged
merged 3 commits into from
Nov 19, 2018

Conversation

KtorZ
Copy link
Contributor

@KtorZ KtorZ commented Nov 16, 2018

Description

So, basically, by conflating a bit the selected entries and changes, a
lot of things become easier. At the price of one thing: fairness.

The previous code was splitting fee across change proportionally to
inputs. So here, I just split the fee across all changes, regardless of
the input.

One could see it as another type of fairness 🙃 ... But
that's also a lot simpler to handle, because we can just manipulate all
inputs and all changes directly and compute fee for those directly.

Linked issue

RCD-45
RCD-44

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)

Wallet unit tests
  Coin selection policies unit tests
    largestFirst
      one payee, SenderPaysFee, fee = 0
        +++ OK, passed 1000 tests.
      one payee, ReceiverPaysFee, fee = 0
        +++ OK, passed 1000 tests.
      multiple payees, SenderPaysFee, fee = 0
        +++ OK, passed 1000 tests.
      multiple payees, ReceiverPaysFee, fee = 0
        +++ OK, passed 1000 tests.
      one payee, SenderPaysFee, fee = 1 Lovelace
        +++ OK, passed 1000 tests.
      one payee, ReceiverPaysFee, fee = 1 Lovelace
        +++ OK, passed 1000 tests.
      multiple payees, SenderPaysFee, fee = 1 Lovelace
        +++ OK, passed 1000 tests.
      multiple payees, ReceiverPaysFee, fee = 1 Lovelace
        +++ OK, passed 1000 tests.
    random
      one payee, SenderPaysFee, fee = 0
        +++ OK, passed 2000 tests.
      one payee, ReceiverPaysFee, fee = 0
        +++ OK, passed 2000 tests.
      multiple payees, SenderPaysFee, fee = 0
        +++ OK, passed 2000 tests.
      multiple payees, ReceiverPaysFee, fee = 0
        +++ OK, passed 2000 tests.
      one payee, SenderPaysFee, fee = 1 Lovelace
        +++ OK, passed 2000 tests.
      multiple payees, SenderPaysFee, fee = 1 Lovelace
        +++ OK, passed 2000 tests.
      one payee, ReceiverPaysFee, fee = linear
        +++ OK, passed 2000 tests.
      multiple payees, ReceiverPaysFee, fee = linear
        +++ OK, passed 2000 tests.
      one payee, SenderPaysFee, fee = cardano
        +++ OK, passed 2000 tests.
      multiple payees, SenderPaysFee, fee = cardano
        +++ OK, passed 2000 tests.
      one payee, ReceiverPaysFee, fee = cardano
        +++ OK, passed 2000 tests.
      multiple payees, ReceiverPaysFee, fee = cardano
        +++ OK, passed 2000 tests.
    Expected failures
      Paying a redeem address should always be rejected
        +++ OK, passed 2000 tests.
      Paying somebody not having enough money should fail
        +++ OK, passed 2000 tests.
      Restricting too much the number of inputs results in an hard error for a single payee
        +++ OK, passed 2000 tests.
      Restricting too much the number of inputs results in an hard error for multiple payees
        +++ OK, passed 2000 tests.
    Fiddly Addresses
      multiple payees, SenderPaysFee, fee = cardano
        +++ OK, passed 200 tests.
      multiple payees, ReceiverPaysFee, fee = cardano
        +++ OK, passed 200 tests.
    Input Grouping
      Require grouping, fee = 0, one big group depletes the Utxo completely
        +++ OK, passed 2000 tests.
      Require grouping, fee = cardano, one big group depletes the Utxo completely
        +++ OK, passed 2000 tests.
      Require grouping, fee = 0, several groups allows the payment to be fullfilled
        +++ OK, passed 2000 tests.
      Prefer grouping, fee = 0
        +++ OK, passed 2000 tests.
      IgnoreGrouping, fee = 0 must not deplete the utxo
        +++ OK, passed 2000 tests.
    Estimating the maximum number of inputs
      estimateMaxTxInputs yields a lower bound.
        +++ OK, passed 100 tests.
      estimateMaxTxInputs yields a relatively tight bound.
        +++ OK, passed 100 tests.
      estimateHardMaxTxInputs is close to estimateMaxTxInputs.
        +++ OK, passed 1000 tests.

Finished in 123.3834 seconds
34 examples, 0 failures

How to merge

Send the message bors r+ to merge this PR. For more information, see
docs/how-to/bors.md.

@KtorZ KtorZ force-pushed the KtorZ/RCD-45-RCD-44/review-fee-calculation branch from 1e88b75 to dc35732 Compare November 16, 2018 18:17
@KtorZ KtorZ requested review from kderme and edsko November 16, 2018 18:17
@KtorZ KtorZ force-pushed the KtorZ/RCD-45-RCD-44/review-fee-calculation branch from dc35732 to 02bd85a Compare November 16, 2018 18:18
@KtorZ KtorZ changed the title Ktor z/rcd 45 rcd 44/review fee calculation [RCD-45] & [RCD-44] Review fee calculation entirely Nov 16, 2018
@KtorZ KtorZ force-pushed the KtorZ/RCD-45-RCD-44/review-fee-calculation branch from 02bd85a to 2dbcd7a Compare November 16, 2018 23:41
So, basically, by conflating a bit the selected entries and changes, a
lot of things become easier.  At the price of one thing: fairness.

The previous code was splitting fee across change proportionally to
inputs.  So here, I just split the fee across all changes, regardless of
the input. So everyone's got to pay the same part of for the
transaction.

One could see it as another type of fairness 🙃 ...  But
that's also a lot simpler to handle, because we can just manipulate all
inputs and all changes directly and compute fee for those directly.
@KtorZ KtorZ force-pushed the KtorZ/RCD-45-RCD-44/review-fee-calculation branch from 2dbcd7a to 01c16c7 Compare November 17, 2018 00:12
changes = changesRemoveDust (csoDustThreshold opts) changesWithDust
return . Right $ CoinSelFinalResult allInps
originalOuts
changes
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Checked moved inside adjustForFees

newRemaining = unsafeValueSub remaining piece -- unsafe because of div.
in case valueAdd piece a of
Just newChange -> newChange : go newRemaining as
Nothing -> a : go remaining as
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved in Generic/Fee and is now part of the senderPaysFee algorithm.

@@ -70,6 +70,7 @@ instance IsValue Core.Coin where
else a `Core.unsafeSubCoin` b
valueRatio = \ a b -> coinToDouble a / coinToDouble b
valueAdjust = \r d a -> coinFromDouble r (d * coinToDouble a)
valueDiv = divCoin
Copy link
Contributor Author

Choose a reason for hiding this comment

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

splitChange ranked higher up in the abstraction, so we need a way to divide Value dom into k pieces.


type PickUtxo m utxo
= Value (Dom utxo)
-> CoinSelT utxo CoinSelHardErr m (Maybe (UtxoEntry (Dom utxo)))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just a type-alias

, csrOutputs :: NonEmpty Core.TxOutAux
-- ^ Picked outputs
, csrChange :: [Core.Coin]
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved one level higher in the abstraction as CoinSelFinalResult dom

-- 2/
-- We try to cover fee with the available change by substracting equally
-- across all inputs. There's no fairness in that in the case of a
-- multi-account transaction. Everyone pays the same part.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

TODO: Amend this comment. Not really on the spot. We still divvy fee proportionally across all changes. Thus, the output:change ratio is more-or-less preserved. In the end, it may be less accurate but fairness still applies here.

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, just some requests for some comments clarifying a few points.

senderPaysFee pickUtxo upperBound css
where
upperBound = feeUpperBound feeOptions css
let inps = concatMap (selectedEntries . coinSelInputs) css
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested comment: We lose the relationship between the transaction outputs and their corresponding inputs/change outputs here. This is a decision we may wish to revisit later. For now however note that since (a) coin selection tries to establish a particular ratio between payment outputs and change outputs (currently it aims for an average of 1:1), and moreover (b) coin selection currently only generates a single change output per payment output, distributing the fee proportionally across all change outputs is roughly equivalent to distributing it proportionally over the payment outputs (roughly, not exactly, because the 1:1 proportion is best effort only, and may in some cases be wildly different). Note that for (a) we don't need the ratio to be 1:1, the above reasoning will remain true for any proportion 1:n. For (b) however, if coin selection starts creating multiple outputs, and this number may vary, then losing the connection between outputs and change outputs will mean that that some outputs may pay a larger percentage of the fee (depending on how many change outputs the algorithm happened to choose).

-- 2/
-- We try to cover fee with the available change by substracting equally
-- across all inputs. There's no fairness in that in the case of a
-- multi-account transaction. Everyone pays the same part.
Copy link
Contributor

Choose a reason for hiding this comment

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

The "There's no fairness.." comment is not quite right, see my suggested comment above. Might want to refer back to that here.

let (change', fee') = reduceChangeOutputs fee (coinSelChange cs)
in (cs { coinSelChange = change' }, fee')

-- we should have (x + (sum ls) = sum result), but this check could overflow.
Copy link
Contributor

Choose a reason for hiding this comment

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

splitChange needs a comment what it does. Note that it makes no attempt to divvy the new output proportionally over the change outputs. This means that if we happen to pick a very large UTxO entry, adding this evenly rather than proportionally might skew the payment:change ratio a lot. Could consider defining this in terms of divvy instead (but if you do, we need to be wary of lower bounds/upper bounds in the division calculation, since the requirements may not be the same for subtracting stuff and adding stuff.). Can be reconsidered later.

= round $ calculateTxSizeLinear linearFeePolicy
$ hi $ estimateSize boundAddrAttrSize boundTxAttrSize ins outs
= ceiling $ calculateTxSizeLinear linearFeePolicy
$ hi $ estimateSize boundAddrAttrSize boundTxAttrSize ins outs
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Core nodes use ceiling in this computation and not round.

The 'estimateCardanoFee' was using 'round' but as pointed out by @duncan, core nodes use 'ceiling' which may cause some
divergence in the fee computation.
@KtorZ KtorZ force-pushed the KtorZ/RCD-45-RCD-44/review-fee-calculation branch from 4a32438 to b6c7208 Compare November 19, 2018 08:58
Copy link
Member

@kderme kderme left a comment

Choose a reason for hiding this comment

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

Simple dust filtering may fix the following issue: I had a wallet with 2 utxos: 1Ada and 10Lovelace. I tried to empty the wallet and the tx failed (not accepted on the network). This is the tx generated (note the output with amount = 0)
{"data":[
{
"id":"37bdc959ff2e35a85dd6abdb196b3431d46a5fb67ff2e3d1e197692188204661",
"confirmations":0,
"amount":1000010,"inputs":[{"address":"DdzFFzCqrhsnFXjLjbgFbZHmnwvexoQLXZkFQ9RQeGuNWsLEkUDFvyYZy1bBzfguya752AT9TbzXYXiVrYnU5CdXDzptuj3RA236SLY4","amount":10},{"address":"DdzFFzCqrhsnFXjLjbgFbZHmnwvexoQLXZkFQ9RQeGuNWsLEkUDFvyYZy1bBzfguya752AT9TbzXYXiVrYnU5CdXDzptuj3RA236SLY4","amount":1000000}],"outputs":[{"address":"DdzFFzCqrht7ZQTTrMXbvdQFt9k4FYJEwNdoRhucjiBCCQn2fGvt4Heu9CJCURPbD6c6ACKvTzw74o4uo2U1Wg2o785DoKuSPEArzLwv","amount":0},{"address":"DdzFFzCqrhsnryDPMxqHDS8zRe8Lrw1AtsXyJh25iZvt2tRzgtBKCdfBe2WUNCyUacuqFEykXKpALCY6eR3ENhxozg2mrE68iRUWXDTB","amount":819800}],"type":"foreign","direction":"outgoing","creationTime":"2018-11-19T10:21:55.06173","status":{"tag":"applying","data":{}}}

Copy link
Member

@kderme kderme left a comment

Choose a reason for hiding this comment

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

LGTM! I manually tested cases where multiple inputs are needed, cases where the wallet empties and combination of these cases. It always works ok.

Copy link
Member

@kderme kderme left a comment

Choose a reason for hiding this comment

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

LGTM! I manually tested cases where multiple inputs are needed, cases where the wallet empties and combination of these cases. It always works ok.

@KtorZ
Copy link
Contributor Author

KtorZ commented Nov 19, 2018

Merging this into release/2.0.0, will have another PR against develop which extends integration tests to reflect the scenarios fixed by this patch.

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.

3 participants