Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of fee adjustment #165

Merged
merged 8 commits into from
Apr 17, 2019

Conversation

paweljakubas
Copy link
Contributor

@paweljakubas paweljakubas commented Apr 9, 2019

Issue Number

#92

Overview

  • I have added fee interface and corresponding types
  • I have added fee adjustment implementation
  • I have added unit tests
  • I have added property tests

Comments

@paweljakubas paweljakubas self-assigned this Apr 9, 2019
@paweljakubas paweljakubas requested a review from KtorZ April 9, 2019 10:58
@paweljakubas paweljakubas force-pushed the paweljakubas/92/adding-fee-calculation branch from 010e51c to 4d4d18d Compare April 9, 2019 12:40
@paweljakubas paweljakubas changed the title First impl of Fee adjustment Implementation of fee adjustment Apr 9, 2019
-- of outputs (incl. change) was bigger than the total input value which is
-- by definition, impossible; unless we messed up real hard.
collapse [] _ =
invariant "outputs are bigger than inputs" (Coin 0) (const True)
Copy link
Member

Choose a reason for hiding this comment

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

I believe it should be const False here, and let's replace Coin 0 with undefined here ... this can never be reached.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

indeed

-> UTxO
-> m (Maybe ((TxIn, TxOut), UTxO))
pickUtxoRandom _ utxo =
runMaybeT $ pickRandom utxo
Copy link
Member

Choose a reason for hiding this comment

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

I think this function / indirection is unnecessary here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍



coinToDouble :: Coin -> Double
coinToDouble = fromIntegral . getCoin
Copy link
Member

Choose a reason for hiding this comment

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

We can probably simplify those above and not actually require Double here. We do know the underlying type for coins (Word64), so let's use that and arithmetic on integer (e.g. div or quot instead of /)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

-- the goal fixed by 'estimatedFee'. So, the actualFee just can't be lower
-- than the goal.
if ( actualFee < estimatedFee
|| (coinToDouble actualFee) / 2.0 > (coinToDouble estimatedFee)) then
Copy link
Member

Choose a reason for hiding this comment

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

same remark here about Double ... We know the underlying type, so let's favor this one.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

-- Provides the API of Fee calculation.


module Cardano.Wallet.Fee
Copy link
Member

Choose a reason for hiding this comment

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

No need for an extra module here, let's have that in Cardano.Wallet.CoinSelection 👍

Copy link
Contributor Author

Choose a reason for hiding this comment

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

relocated FeeOptions and FeeErrors to Cardano.Wallet.CoinSelection

removeDust = L.filter (> (dustThreshold opt))

reduceChangeOutputs
:: ([Coin] -> [Coin])
Copy link
Member

Choose a reason for hiding this comment

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

No need for the abstraction here, we can use removeDust directly from above.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

remFee <- lift $ runMaybeT $ coverRemainingFee remainingFee utxo
case remFee of
Nothing ->
throwE CannotCoverFee
Copy link
Member

Choose a reason for hiding this comment

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

Maybe we can have the error a bit more explicit here 🤔 ? Saying that we cannot cover for fee because there's not enough inputs? And actually communicate about the missing amount?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

added missing amount arg

map (\a -> (feeForOut a, a)) outs
where
totalOut :: Coin
totalOut = (Coin . sum . map getCoin) outs
Copy link
Member

Choose a reason for hiding this comment

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

This seems like it could overflow 🤔 ...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

outputs can add to more than upperBound of Coin. I used Word64 here, and checking in feeForOut just below overflowing

import GHC.Generics
( Generic )

newtype Fee = Fee { getFee :: Coin } deriving (Show, Eq)
Copy link
Member

Choose a reason for hiding this comment

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

I don't think we need the newtype here. Fee are just 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.

now no more Fee just Coin

@paweljakubas paweljakubas force-pushed the paweljakubas/92/adding-fee-calculation branch 2 times, most recently from ecd5b66 to fea97a0 Compare April 10, 2019 13:57
totalOuts :: Word64
totalOuts = (sum . map getCoin) outs

feeForOut :: Coin -> 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.

for the simplicity and error plucking I am backed with double

, fExtraUtxo = [1]
, fFee = 5
, fDust = 0
}) (Left $ CannotCoverFee 2)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

maybe should be CannotCoverFee 1 as fChngs and fExtraUtxo gives 4 ... now because of MaybeT usage signalling about missing coins before random picking started

Copy link
Contributor Author

Choose a reason for hiding this comment

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

will be corrected in coming version

, fExtraUtxo = [2]
, fFee = 5
, fDust = 0
}) (Left $ OutOfBoundFee 0 5)
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 one I do not understand. From senderPaysFee I have :

coinSel' = CoinSelection {inputs = [(TxIn {inputId = Hash {getHash = "\170\223\131K\130@\DLE2\202\159"}, inputIx = 0},TxOut {address = Address {getAddress = "ADDR03"}, coin = Coin {getCoin = 10}}),(TxIn {inputId = Hash {getHash = "\219\CANf\153\159\&9\189\154\&9\133"}, inputIx = 0},TxOut {address = Address {getAddress = "ADDR01"}, coin = Coin {getCoin = 2}})], outputs = [TxOut {address = Address {getAddress = "ADDR01"}, coin = Coin {getCoin = 7}}], change = [Coin {getCoin = 5}]}

and after computeFee coinSel' I have Coin 0 ... The computation is :

collapse : p>m : Coin {getCoin = 10} > Coin {getCoin = 7} ps:[Coin {getCoin = 2}] ms[Coin {getCoin = 5}]
collapse : p<m : Coin {getCoin = 3} < Coin {getCoin = 5} ps:[Coin {getCoin = 2}] ms[]
collapse : p==m : Coin {getCoin = 2} ps:[] ms[]
collapse : plus : []

I need to think about it - though it should be Right

Copy link
Contributor Author

Choose a reason for hiding this comment

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

addressed in new commit

@paweljakubas
Copy link
Contributor Author

added logs which I use in the comment to illustrate the point I do not understand yet. Will remove them before the last commit

@@ -69,6 +69,7 @@ library
Cardano.Wallet.Binary
Cardano.Wallet.Binary.Packfile
Cardano.Wallet.CoinSelection
Cardano.Wallet.CoinSelection.Fee
Copy link
Member

Choose a reason for hiding this comment

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

Let's completely remove this module and have it merged with Cardano.Wallet.CoinSelection

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍

@@ -11,6 +11,8 @@

module Cardano.Wallet.CoinSelection.Random
( random
, pickRandom
, distance
Copy link
Member

Choose a reason for hiding this comment

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

Maybe distance is something we want to have in Primitive.Types 🤔 ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

computeFee
:: CoinSelection
-> Coin
computeFee (CoinSelection inps outs chgs) =
Copy link
Member

Choose a reason for hiding this comment

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

Note that this whole algorithm here exists only because in the original implementation, we were in the generic realm, where there was no concrete instance for Coin. Here, we know that Coin = Coin Word64 with maxBound :: Coin being much much less than maxBound :: Word64; We could in practice makes the computation Word64 (or Natural) if we really really want the safety here, and at the end, convert it back to a Coin.

Also, we can pretty much safely assume (i.e. enforce with an invariant) that the resulting fee Coin is less than the max bound for Coin (otherwise that's a very very expensive transaction we have there!)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

thanks for this comment

@paweljakubas paweljakubas force-pushed the paweljakubas/92/adding-fee-calculation branch from fea97a0 to cde75b8 Compare April 11, 2019 11:51
@paweljakubas
Copy link
Contributor Author

added almost 25 unit tests and made sure they pass. Reshuffled the code as suggested by reviewer. Rebased. Next commit : properties tests

@paweljakubas paweljakubas force-pushed the paweljakubas/92/adding-fee-calculation branch 3 times, most recently from 6b85a3b to f06d2fa Compare April 11, 2019 16:23
@KtorZ KtorZ force-pushed the paweljakubas/92/adding-fee-calculation branch from 1acdbdb to c87b71d Compare April 14, 2019 19:23
@paweljakubas paweljakubas force-pushed the paweljakubas/92/adding-fee-calculation branch from c87b71d to 40a761b Compare April 15, 2019 08:02
@KtorZ KtorZ force-pushed the paweljakubas/92/adding-fee-calculation branch from 40a761b to 356d7f8 Compare April 16, 2019 01:37
@KtorZ
Copy link
Member

KtorZ commented Apr 16, 2019

There is a failing properties:

  • No fee gives back the same selection FAILED [1]

which doesn't fail all the time but only when small enough coins are generated. Seems like some dust remains in the final selection 🤔 ... I haven't much looked into that but, changing the coin arbitrary generator to choose (1, 10) would make the property fail at each run.

NOTE: I've removed / merged together some of the other properties to keep it simple and, only about properties that are rather useful to verify.

cc @paweljakubas

@paweljakubas
Copy link
Contributor Author

paweljakubas commented Apr 16, 2019

as it is now I have not encountered failure when running:

stack test --ta '-m "Fee calculation"' cardano-wallet:unit

Moreover, I see in propSameSelection responsible for the property No fee ..:

let feeOpt = feeOptions 0 0

which means dust threshold is 0

Nevertheless, I confirm that when coin is between (1,10) the test fails

I see that the below is expected :

change = [Coin {getCoin = 0},Coin {getCoin = 0},Coin {getCoin = 3},Coin {getCoin = 5},Coin {getCoin = 7},Coin {getCoin = 7},Coin {getCoin = 7},Coin {getCoin = 8}]})

but after adjustForFee we obtained :

change = [Coin {getCoin = 3},Coin {getCoin = 5},Coin {getCoin = 7},Coin {getCoin = 7},Coin {getCoin = 7},Coin {getCoin = 8}]})

meaning because of small coins Coin 0 changes were produced. We should not do it because we wipe out Coin 0 after fee adjustment even when dust threshold is 0

Note. Of course, when Coin is (1,10) we cannot cover fees that are (100000, 500000) so third property will find satisfying case. As expected :

  test/unit/Cardano/Wallet/CoinSelectionSpec.hs:274:9: 
  1) Cardano.Wallet.CoinSelection, Fee calculation properties, Adjusting for fee (/= 0) reduces the change outputs or increase inputs
       Gave up after 0 tests; 1000 discarded!

@KtorZ
Copy link
Member

KtorZ commented Apr 16, 2019

I am not sure about this last commit @paweljakubas: why do we even have 0 coins as part of the selection 🤔 ?

@paweljakubas
Copy link
Contributor Author

paweljakubas commented Apr 16, 2019

well I believe, largestFirst can create Coin 0 in change https://github.com/input-output-hk/cardano-wallet/blob/master/src/Cardano/Wallet/CoinSelection/LargestFirst.hs#L95 in contrast to random : https://github.com/input-output-hk/cardano-wallet/blob/master/src/Cardano/Wallet/CoinSelection/Random.hs#L219
and for small coins (1,10) we capture this case. So, because we create coin selection for adjustForFee using largestFirst it is possible that Coin 0 is in changes. ..

@KtorZ
Copy link
Member

KtorZ commented Apr 16, 2019

Indeed. I've moved the fix into the largest first implementation instead as it makes more sense to have it here and return valid coin selections in a first place!

@KtorZ KtorZ force-pushed the paweljakubas/92/adding-fee-calculation branch from 4b1d41f to 215f429 Compare April 16, 2019 17:13
@KtorZ KtorZ merged commit fde6196 into master Apr 17, 2019
@KtorZ KtorZ deleted the paweljakubas/92/adding-fee-calculation branch April 17, 2019 00:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants