Skip to content

Commit

Permalink
Add one-output fee estimation and update corresponding unit test
Browse files Browse the repository at this point in the history
  • Loading branch information
paweljakubas committed Apr 25, 2019
1 parent a930f1c commit 2cb36af
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 9 deletions.
70 changes: 69 additions & 1 deletion src/Cardano/Wallet/CoinSelection/Fee.hs
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,31 @@ module Cardano.Wallet.CoinSelection.Fee
, FeeOptions (..)
, FeeError(..)
, adjustForFees
, cardanoPolicy
, estimateCardanoFee
) where

import Prelude

import Cardano.Wallet.Binary
( encodeSignedTx )
import Cardano.Wallet.CoinSelection
( CoinSelection (..) )
import Cardano.Wallet.Primitive.Types
( Coin (..)
, Tx (..)
, TxIn
, TxOut (..)
, TxWitness
, UTxO (..)
, balance'
, distance
, invariant
, isValidCoin
, pickRandom
)
import Codec.CBOR.Write
( toLazyByteString )
import Control.Monad.Trans.Class
( lift )
import Control.Monad.Trans.Except
Expand All @@ -53,9 +61,10 @@ import Data.Word
import GHC.Generics
( Generic )

import qualified Data.ByteString.Lazy as BL
import qualified Data.List as L


import Debug.Trace
{-------------------------------------------------------------------------------
Fee Adjustment
-------------------------------------------------------------------------------}
Expand Down Expand Up @@ -341,3 +350,62 @@ computeFee (CoinSelection inps outs chgs) =
-- by definition, impossible; unless we messed up real hard.
collapse [] _ =
invariant "outputs are bigger than inputs" (undefined) (const False)

-- | A linear equation on the transaction size. Represents the @\s -> a + b*s@
-- function where @s@ is the transaction size in bytes, @a@ and @b@ are
-- constant coefficients.
data TxSizeLinear = TxSizeLinear Double Double
deriving (Eq, Show)

cardanoPolicy :: TxSizeLinear
cardanoPolicy = TxSizeLinear 155381 43.946

-- | Estimate the fee for a transaction that has @ins@ inputs
-- and @length outs@ outputs. The @outs@ lists holds the coin value
-- of each output.
--
-- NOTE: The average size of @Attributes AddrAttributes@ and
-- the transaction attributes @Attributes ()@ are both hard-coded
estimateCardanoFee
:: TxSizeLinear
-> (Tx, [TxWitness])
-> Fee
estimateCardanoFee (TxSizeLinear a b) txWithWitness@(Tx inps _, _) =
trace ("totalPayload: "<> show totalPayload
<> " length : " <> show (BL.length $ toLazyByteString $ encodeSignedTx txWithWitness)) $
Fee $ ceiling $ a + b*totalPayload
where
totalPayload :: Double
totalPayload = fromIntegral
$ boundAddrAttrSize + boundTxAttrSize + boundSignatureWitnessSize + payloadFromTxWithWitness + 4*(length inps - 1)

payloadFromTxWithWitness :: Int
payloadFromTxWithWitness = (fromIntegral . BL.length . toLazyByteString . encodeSignedTx) txWithWitness

-- | Size to use for a value of type @Attributes AddrAttributes@ when estimating
-- encoded transaction sizes. The minimum possible value is 2.
--
-- NOTE: When the /actual/ size exceeds this bounds, we may underestimate
-- tranasction fees and potentially generate invalid transactions.
--
-- `boundAddrAttrSize` needed to increase due to the inclusion of
-- `NetworkMagic` in `AddrAttributes`. The `Bi` instance of
-- `AddrAttributes` serializes `NetworkTestnet` as [(Word8,Int32)] and
-- `NetworkMainOrStage` as []; this should require a 5 Byte increase in
--`boundAddrAttrSize`. Because encoding in unit tests is not guaranteed
-- to be efficient, it was decided to increase by 7 Bytes to mitigate
-- against potential random test failures in the future.
boundAddrAttrSize :: Int
boundAddrAttrSize = 34 + 7 -- 7 bytes for potential NetworkMagic

-- | Size to use for a value of type @Attributes ()@ when estimating
-- encoded transaction sizes. The minimum possible value is 2.
--
-- NOTE: When the /actual/ size exceeds this bounds, we may underestimate
-- transaction fees and potentially generate invalid transactions.
boundTxAttrSize :: Int
boundTxAttrSize = 2

-- | Signature of witness payload
boundSignatureWitnessSize :: Int
boundSignatureWitnessSize = 66
4 changes: 2 additions & 2 deletions test/data/Cardano/Wallet/fees
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ inputs|outputs|fee
[200000]|[9]|171905
[200000]|[11]|171905
[200000]|[15]|171905
[200000]|[99]|171993
[200000]|[101]|171993
[200000]|[500]|171993
[200000]|[999]|171993
[200000]|[1000]|171993
Expand Down Expand Up @@ -52,6 +50,8 @@ inputs|outputs|fee
[930206,8130,8130,8130,8130,10]|[750000]|212731
[930205,8130,8130,8130,8130,8130,10]|[750000]|220861
[50000,50000,50000,50000,50000,50000,50000,50000,50000,50000,50000,50000,50000,50000,50000,50000,50000,50000,50000,50000,50000,50000,5000]|[750000]|350941
[200000]|[99]|171993
[200000]|[101]|171993
[200000,1]|[1,1]|187770
[200000,1]|[1,100]|187945
[250000,250000]|[100000,100000]|188298
Expand Down
58 changes: 52 additions & 6 deletions test/unit/Cardano/Wallet/CoinSelection/FeeSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,29 @@ import Prelude
import Cardano.Wallet.CoinSelection
( CoinSelection (..), CoinSelectionOptions (..) )
import Cardano.Wallet.CoinSelection.Fee
( Fee (..), FeeError (..), FeeOptions (..), adjustForFees )
( Fee (..)
, FeeError (..)
, FeeOptions (..)
, adjustForFees
, cardanoPolicy
, estimateCardanoFee
)
import Cardano.Wallet.CoinSelection.Policy.LargestFirst
( largestFirst )
import Cardano.Wallet.CoinSelectionSpec
( CoinSelProp (..), genTxOut, genUTxO )
import Cardano.Wallet.Primitive.Types
( Coin (..), ShowFmt (..), TxOut (..), UTxO (..) )
( Address (..)
, Coin (..)
, Hash (..)
, ShowFmt (..)
, Tx (..)
, TxIn (..)
, TxOut (..)
, TxOut (..)
, TxWitness (..)
, UTxO (..)
)
import Control.Arrow
( left )
import Control.Monad
Expand All @@ -30,6 +46,12 @@ import Crypto.Random
( SystemDRG, getSystemDRG )
import Crypto.Random.Types
( withDRG )
import Data.ByteArray.Encoding
( Base (Base16), convertFromBase )
import Data.ByteString
( ByteString )
import Data.ByteString.Base58
( bitcoinAlphabet, decodeBase58 )
import Data.Char
( ord )
import Data.Csv
Expand Down Expand Up @@ -59,6 +81,7 @@ import Test.QuickCheck
import Text.Read
( read )

import qualified Cardano.Wallet.CoinSelection as CS
import qualified Data.ByteString.Lazy as BL
import qualified Data.Csv as CSV
import qualified Data.List as L
Expand All @@ -67,6 +90,8 @@ import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.Vector as V

import Debug.Trace

spec :: Spec
spec = do
describe "Fee calculation : unit tests" $ do
Expand Down Expand Up @@ -327,8 +352,8 @@ propReducedChanges drg (ShowFmt (FeeProp (CoinSelProp utxo txOuts) utxo' (fee, d
prop coinSel coinSel' = do
let chgs' = sum $ map getCoin $ change coinSel'
let chgs = sum $ map getCoin $ change coinSel
let inps' = inputs coinSel'
let inps = inputs coinSel
let inps' = CS.inputs coinSel'
let inps = CS.inputs coinSel
disjoin
[ chgs' `shouldSatisfy` (<= chgs)
, length inps' `shouldSatisfy` (>= length inps)
Expand Down Expand Up @@ -364,8 +389,29 @@ realisticFeeUnitTest = it "realistic fee calculations" $ do
let sum' = sum $ L.tail $ L.sort inps
in (sum' < (fee + sum outs))
checkFee :: FeeCase -> Expectation
checkFee (FeeCase _inps _outs fee) =
fee `shouldSatisfy` (> 10000)
checkFee (FeeCase inps outs fee) = do
let txIns = zipWith TxIn (replicate (length inps) inputId0) [0..]
let txOuts = zipWith TxOut (replicate (length outs) address0) (map Coin outs)
let tx = Tx txIns txOuts
let (Fee estFee) = estimateCardanoFee cardanoPolicy (tx, replicate (length inps) (PublicKeyWitness pkWitness))
trace ("estFee "<>show estFee) $ estFee `shouldBe` fee
inputId0 = hash16
"60dbb2679ee920540c18195a3d92ee9be50aee6ed5f891d92d51db8a76b02cd2"
address0 = addr58
"DdzFFzCqrhsug8jKBMV5Cr94hKY4DrbJtkUpqptoGEkovR2QSkcA\
\cRgjnUyegE689qBX6b2kyxyNvCL6mfqiarzRB9TRq8zwJphR31pr"
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"

-- | Make a Hash from a Base16 encoded string, without error handling.
hash16 :: ByteString -> Hash a
hash16 = either bomb Hash . convertFromBase Base16
where
bomb msg = error ("Could not decode test string: " <> msg)

-- | Make an Address from a Base58 encoded string, without error handling.
addr58 :: ByteString -> Address
addr58 = maybe (error "addr58: Could not decode") Address
. decodeBase58 bitcoinAlphabet


data FeeCase = FeeCase
Expand Down

0 comments on commit 2cb36af

Please sign in to comment.