Skip to content

Commit

Permalink
review fee calculation for addresses
Browse files Browse the repository at this point in the history
  • Loading branch information
KtorZ committed Apr 26, 2019
1 parent df9c1a0 commit 09b090f
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 60 deletions.
65 changes: 34 additions & 31 deletions src/Cardano/Wallet/CoinSelection/Fee.hs
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,20 @@ module Cardano.Wallet.CoinSelection.Fee
, FeeOptions (..)
, FeeError(..)
, TxSizeLinear (..)
, AddressScheme (..)
, Network (..)
, adjustForFees
, cardanoPolicy
, estimateCardanoFee
, calculateFee
) where

import Prelude

import Cardano.Environment
( Network (..) )
import Cardano.Wallet.CoinSelection
( CoinSelection (..) )
import Cardano.Wallet.Primitive.Types
( Coin (..)
( Address (..)
, Coin (..)
, TxIn
, TxOut (..)
, UTxO (..)
Expand Down Expand Up @@ -62,6 +63,7 @@ import GHC.Generics

import qualified Codec.CBOR.Encoding as CBOR
import qualified Codec.CBOR.Write as CBOR
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BL
import qualified Data.List as L

Expand Down Expand Up @@ -358,29 +360,27 @@ data TxSizeLinear =
TxSizeLinear (Quantity "lovelace" Double) (Quantity "lovelace/byte" Double)
deriving (Eq, Show)

data AddressScheme = Sequential | Random
deriving (Eq, Show)

data Network = Mainnet | Testnet
deriving (Eq, Show)

cardanoPolicy :: TxSizeLinear
cardanoPolicy = TxSizeLinear (Quantity 155381) (Quantity 43.946)

-- | Estimate the fee for a transaction that has @inps@ inputs
-- | Calculate the fee for a transaction that has @inps@ inputs
-- and @outps@ outputs as coming from coin selection.
--
-- NOTE: The average size of @Attributes AddrAttributes@ and
-- the transaction attributes @Attributes ()@ are both hard-coded
estimateCardanoFee
-- NOTE 1: The average size of @Attributes AddrAttributes@ and
-- The transaction attributes @Attributes ()@ are both hard-coded
--
-- NOTE 2: The function supposes that change addresses are only following a
-- sequential scheme.
calculateFee
:: TxSizeLinear
-> AddressScheme
-> Network
-> CoinSelection
-> Fee
estimateCardanoFee (TxSizeLinear (Quantity a) (Quantity b)) scheme net (CoinSelection inps outs chngs) =
calculateFee fee network (CoinSelection inps outs chngs) =
Fee $ ceiling (a + b*totalPayload)
where
TxSizeLinear (Quantity a) (Quantity b) = fee

-- With `n` the number of inputs
-- signed tx ----------------------------------- 9 + n * 42 + n * 139 + Σ sizeOf(output)
-- | list len 2 -- 1
Expand Down Expand Up @@ -437,14 +437,20 @@ estimateCardanoFee (TxSizeLinear (Quantity a) (Quantity b)) scheme net (CoinSele
-- | | attributes -- 12-19
-- | | word8 -- 1
-- | word32 -- 1-4
sizeOfTxOut :: Word64 -> Int
sizeOfTxOut =
let toAdd = case (scheme, net) of
(Sequential, Mainnet) -> 48
(Sequential, Testnet) -> 56
(Random, Mainnet) -> 69
(Random, Testnet) -> 77
in (+ toAdd) . sizeOf . CBOR.encodeWord64
sizeOfTxOut :: TxOut -> Int
sizeOfTxOut (TxOut (Address bytes) c) =
5 + BS.length bytes + sizeOfCoin c

-- Compute the size of a coin
sizeOfCoin :: Coin -> Int
sizeOfCoin = sizeOf . CBOR.encodeWord64 . getCoin

-- Compute the size of the change, we assume that change is necessarily
-- using a sequential scheme. For the rest, cf 'sizeOfTxOut'
sizeOfChange :: Coin -> Int
sizeOfChange c = case network of
Mainnet -> 5 + 1 + sizeOfCoin c
Testnet -> 5 + 8 + sizeOfCoin c

-- tx ---------------------------------- 6 + (n * 42) + Σ sizeOf(output)
-- | list len 3 -- 1
Expand All @@ -456,13 +462,10 @@ estimateCardanoFee (TxSizeLinear (Quantity a) (Quantity b)) scheme net (CoinSele
-- | break -- 1
-- | empty attributes -- 1
sizeOfTx :: Int
sizeOfTx =
let n = length inps
coinOuts = map (getCoin . coin) outs
coinChns = map getCoin chngs
coins = coinOuts ++ coinChns
cborOverhead = 6
in cborOverhead + sum (map sizeOfTxIn [0..(n-1)]) + sum (map sizeOfTxOut coins)
sizeOfTx = 6
+ sum (map sizeOfTxIn [0..(length inps-1)])
+ sum (map sizeOfTxOut outs)
+ sum (map sizeOfChange chngs)

-- witness ---------------------------------- 139
-- | list len 2 -- 1
Expand Down
87 changes: 58 additions & 29 deletions test/unit/Cardano/Wallet/CoinSelection/FeeSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ import Cardano.Wallet.Binary
import Cardano.Wallet.CoinSelection
( CoinSelection (..), CoinSelectionOptions (..) )
import Cardano.Wallet.CoinSelection.Fee
( AddressScheme (..)
, Fee (..)
( Fee (..)
, FeeError (..)
, FeeOptions (..)
, Network (..)
Expand Down Expand Up @@ -78,6 +77,9 @@ import Test.QuickCheck
, (===)
, (==>)
)
import Test.QuickCheck.Arbitrary.Generic
( genericArbitrary, genericShrink )


import qualified Cardano.Wallet.CoinSelection as CS
import qualified Data.ByteString.Lazy as BL
Expand Down Expand Up @@ -361,9 +363,9 @@ propReducedChanges drg (ShowFmt (FeeProp (CoinSelProp utxo txOuts) utxo' (fee, d
propFeeEstimation
:: FeeEstimation
-> Property
propFeeEstimation (FeeEstimation (ShowFmt sel)) =
propFeeEstimation (FeeEstimation network (ShowFmt sel)) =
let
(Fee estFee) = estimateCardanoFee cardanoPolicy Random Testnet sel
(Fee estFee) = estimateCardanoFee cardanoPolicy network sel
(TxSizeLinear (Quantity a) (Quantity b)) = cardanoPolicy
tx = fromCoinSelection sel
size = BL.length $ toLazyByteString $ encodeSignedTx tx
Expand All @@ -374,12 +376,32 @@ propFeeEstimation (FeeEstimation (ShowFmt sel)) =
fromCoinSelection :: CoinSelection -> (Tx, [TxWitness])
fromCoinSelection (CoinSelection inps outs chngs) =
let
txIns = zipWith TxIn (replicate (length inps) inputId0) [0..]
coins = map coin outs ++ chngs
txOuts = zipWith TxOut (replicate (length coins) address0) coins
wits = replicate (length inps) (PublicKeyWitness pkWitness)
dummyRndAddr = case network of
Mainnet ->
addr58
"DdzFFzCqrhsw6TjWD5zcSo1wzYJPRnweuAt1DPJccYxaBudJpNMG\
\qoQtvhFhiRrRM9L2qBEkjRgFtSTg84rUUytaSCwQ48nLmZEJBiLy"
Testnet ->
addr58
"DdzFFzCqrhsug8jKBMV5Cr94hKY4DrbJtkUpqptoGEkovR2QSkcA\
\cRgjnUyegE689qBX6b2kyxyNvCL6mfqiarzRB9TRq8zwJphR31pr"

dummySeqAddr = case network of
Mainnet ->
addr58
"Ae2tdPwUPEZLLeQYaBmNXiwrv9Mf13eauCxgHxZYF1EhDKMTKR5t\
\1dFrSCU"
Testnet ->
addr58
"2cWKMJemoBaiH2NVgNh9aPDt5wr7i1YWZ8ySbFkpx7nbbs3x38gB\
\ec4NJc8vSgEVRV2G7"

txIns = zipWith TxIn (replicate (length inps) dummyInput) [0..]
txChngs = zipWith TxOut (replicate (length chngs) dummySeqAddr) chngs
coins = map coin outs
wits = replicate (length inps) (PublicKeyWitness dummyWitness)
in
(Tx txIns txOuts, wits)
(Tx txIns (outs <> txChngs), wits)

-- | Make a Hash from a Base16 encoded string, without error handling.
hash16 :: Text -> Hash "Tx"
Expand All @@ -395,9 +417,8 @@ propFeeEstimation (FeeEstimation (ShowFmt sel)) =
error ("addr58: Could not decode because " <> err)
Right res -> res

inputId0 = hash16 "60dbb2679ee920540c18195a3d92ee9be50aee6ed5f891d92d51db8a76b02cd2"
address0 = addr58 "DdzFFzCqrhsug8jKBMV5Cr94hKY4DrbJtkUpqptoGEkovR2QSkcAcRgjnUyegE689qBX6b2kyxyNvCL6mfqiarzRB9TRq8zwJphR31pr"
pkWitness = "\130X@\226E\220\252\DLE\170\216\210\164\155\182mm$ePG\252\186\195\225_\b=\v\241=\255 \208\147[\239\RS\170|\214\202\247\169\229\205\187O_)\221\175\155?e\198\248\170\157-K\155\169z\144\174\ENQhX@\193\151*,\NULz\205\234\&1tL@\211\&2\165\129S\STXP\164C\176 Xvf\160|;\CANs{\SYN\204<N\207\154\130\225\229\t\172mbC\139\US\159\246\168x\163Mq\248\145)\160|\139\207-\SI"
dummyInput = hash16 "60dbb2679ee920540c18195a3d92ee9be50aee6ed5f891d92d51db8a76b02cd2"
dummyWitness = "\130X@\226E\220\252\DLE\170\216\210\164\155\182mm$ePG\252\186\195\225_\b=\v\241=\255 \208\147[\239\RS\170|\214\202\247\169\229\205\187O_)\221\175\155?e\198\248\170\157-K\155\169z\144\174\ENQhX@\193\151*,\NULz\205\234\&1tL@\211\&2\165\129S\STXP\164C\176 Xvf\160|;\CANs{\SYN\204<N\207\154\130\225\229\t\172mbC\139\US\159\246\168x\163Mq\248\145)\160|\139\207-\SI"


{-------------------------------------------------------------------------------
Expand Down Expand Up @@ -478,11 +499,24 @@ data FeeOutput = FeeOutput
Arbitrary Instances
-------------------------------------------------------------------------------}

newtype FeeEstimation = FeeEstimation (ShowFmt CoinSelection)
deriving (Eq, Show)
instance Arbitrary FeeProp where
shrink (FeeProp cc utxo opts) =
(\(cc', utxo') -> FeeProp cc' utxo' opts)
<$> zip (shrink cc) (shrink utxo)
arbitrary = do
cc <- arbitrary
utxo <- arbitrary
fee <- choose (100000, 500000)
dust <- choose (0, 10000)
return $ FeeProp cc utxo (fee, dust)

data FeeEstimation = FeeEstimation
{ fsNetwork :: Network
, fsCoinSelection :: ShowFmt CoinSelection
} deriving (Eq, Show)

instance Arbitrary FeeEstimation where
shrink (FeeEstimation (ShowFmt (CoinSelection inps outs chgs))) =
shrink (FeeEstimation net (ShowFmt sel@(CoinSelection inps outs chgs))) =
case (inps, outs, chgs) of
([_], [_], []) ->
[]
Expand All @@ -496,28 +530,23 @@ instance Arbitrary FeeEstimation where
outs'' = if length outs > 1 then drop 1 outs else outs
chgs'' = drop 1 chgs
in
FeeEstimation . ShowFmt <$>
FeeEstimation net . ShowFmt <$> filter (/= sel)
[ CoinSelection inps' outs' chgs'
, CoinSelection inps'' outs'' chgs''
]
arbitrary = do
chgs <- choose (0, 10)
>>= \n -> vectorOf n arbitrary
inps <- choose (0, 100)
inps <- choose (1, 100)
>>= \n -> vectorOf n arbitrary
>>= fmap (Map.toList . getUTxO) . genUTxO
outs <- choose (0, 100)
outs <- choose (1, 100)
>>= \n -> vectorOf n arbitrary
>>= genTxOut
return $ FeeEstimation $ ShowFmt $ CoinSelection inps outs chgs
FeeEstimation
<$> arbitrary
<*> pure (ShowFmt $ CoinSelection inps outs chgs)

instance Arbitrary FeeProp where
shrink (FeeProp cc utxo opts) =
(\(cc', utxo') -> FeeProp cc' utxo' opts)
<$> zip (shrink cc) (shrink utxo)
arbitrary = do
cc <- arbitrary
utxo <- arbitrary
fee <- choose (100000, 500000)
dust <- choose (0, 10000)
return $ FeeProp cc utxo (fee, dust)
instance Arbitrary Network where
shrink = genericShrink
arbitrary = genericArbitrary

0 comments on commit 09b090f

Please sign in to comment.