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

Use tx max size & estimator to figure out the right input upper-bound in coin selection #536

Merged
merged 2 commits into from
Jul 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 15 additions & 11 deletions exe/wallet/http-bridge/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -65,21 +65,20 @@ import Cardano.Wallet.DaedalusIPC
import Cardano.Wallet.DB
( DBLayer )
import Cardano.Wallet.HttpBridge.Compatibility
( HttpBridge, Network (..), byronFeePolicy, byronSlotLength )
( HttpBridge
, Network (..)
, byronFeePolicy
, byronSlotLength
, byronTxMaxSize
)
import Cardano.Wallet.HttpBridge.Environment
( KnownNetwork (..) )
import Cardano.Wallet.HttpBridge.Primitive.Types
( Tx )
import Cardano.Wallet.Network
( ErrNetworkTip, NetworkLayer, defaultRetryPolicy, waitForConnection )
import Cardano.Wallet.Primitive.AddressDerivation
( KeyToAddress )
import Cardano.Wallet.Primitive.AddressDiscovery
( SeqState )
import Cardano.Wallet.Primitive.Fee
( FeePolicy )
import Cardano.Wallet.Primitive.Types
( Block )
import Cardano.Wallet.Version
( showVersion, version )
import Control.Applicative
Expand Down Expand Up @@ -277,19 +276,24 @@ cmdServe = command "serve" $ info (helper <*> cmd) $ mempty
-> DBLayer IO s t
-> IO (WalletLayer s t)
newWalletLayer (sb, tracer) db = do
(nl, block0, feePolicy) <- newNetworkLayer (sb, tracer)
(nl, bp) <- newNetworkLayer (sb, tracer)
let tl = HttpBridge.newTransactionLayer @n
let bp = BlockchainParameters block0 feePolicy byronSlotLength
Wallet.newWalletLayer tracer bp db nl tl

newNetworkLayer
:: (Switchboard Text, Trace IO Text)
-> IO (NetworkLayer t IO, Block Tx, FeePolicy)
-> IO (NetworkLayer t IO, BlockchainParameters t)
newNetworkLayer (sb, tracer) = do
nl <- HttpBridge.newNetworkLayer @n (getPort nodePort)
waitForService @ErrNetworkTip "http-bridge" (sb, tracer) nodePort $
waitForConnection nl defaultRetryPolicy
return (nl, HttpBridge.block0, byronFeePolicy)
let bp = BlockchainParameters
{ getGenesisBlock = HttpBridge.block0
, getFeePolicy = byronFeePolicy
, getSlotLength = byronSlotLength
, getTxMaxSize = byronTxMaxSize
}
return (nl, bp)

withDBLayer
:: CM.Configuration
Expand Down
16 changes: 10 additions & 6 deletions lib/core/src/Cardano/Wallet.hs
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ import Data.Text.Class
( toText )
import Data.Time.Clock
( diffTimeToPicoseconds, getCurrentTime )
import Data.Word
( Word16 )
import Fmt
( Buildable, blockListF, pretty, (+|), (+||), (|+), (||+) )

Expand Down Expand Up @@ -367,7 +369,11 @@ data BlockchainParameters t = BlockchainParameters
{ getGenesisBlock :: Block (Tx t)
-- ^ Very first block
, getFeePolicy :: FeePolicy
-- ^ Policy regarding transcation fee
, getSlotLength :: SlotLength
-- ^ Length, in seconds, of a slot
, getTxMaxSize :: Quantity "byte" Word16
-- ^ Maximum size of a transaction (soft or hard limit)
}

-- | Create a new instance of the wallet layer.
Expand All @@ -379,10 +385,7 @@ newWalletLayer
-> NetworkLayer t IO
-> TransactionLayer t
-> IO (WalletLayer s t)
newWalletLayer
tracer
(BlockchainParameters block0 feePolicy (SlotLength slotLength))
db nw tl = do
newWalletLayer tracer bp db nw tl = do
logDebugT $ "Wallet layer starting with: "
<> "block0: "+| block0 |+ ", "
<> "fee policy: "+|| feePolicy ||+""
Expand All @@ -404,6 +407,8 @@ newWalletLayer
, listTransactions = _listTransactions
}
where
BlockchainParameters block0 feePolicy (SlotLength slotLength) txMaxSize = bp

logDebugT :: MonadIO m => Text -> m ()
logDebugT = liftIO . logDebug tracer

Expand Down Expand Up @@ -632,10 +637,9 @@ newWalletLayer
Transactions
---------------------------------------------------------------------------}

-- FIXME Compute the options based on the transaction's size / inputs
coinSelOpts :: CoinSelectionOptions (ErrValidateSelection t)
coinSelOpts = CoinSelectionOptions
{ maximumNumberOfInputs = 10
{ maximumNumberOfInputs = estimateMaxNumberOfInputs tl txMaxSize
Copy link
Contributor

Choose a reason for hiding this comment

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

💯

, validate = validateSelection tl
}

Expand Down
6 changes: 4 additions & 2 deletions lib/core/src/Cardano/Wallet/Primitive/CoinSelection.hs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import Crypto.Number.Generate
import Data.Vector.Mutable
( IOVector )
import Data.Word
( Word64 )
( Word64, Word8 )
import Fmt
( Buildable (..), blockListF, blockListF', listF, nameF )
import GHC.Generics
Expand Down Expand Up @@ -80,9 +80,11 @@ instance Buildable CoinSelection where

data CoinSelectionOptions e = CoinSelectionOptions
{ maximumNumberOfInputs
:: Word64
:: Word8 -> Word8
-- ^ Maximum number of inputs allowed for a given number of outputs
, validate
:: CoinSelection -> Either e ()
-- ^ Returns any backend-specific error regarding coin selection
} deriving (Generic)

data ErrCoinSelection e
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,12 @@ largestFirst
-> ExceptT (ErrCoinSelection e) m (CoinSelection, UTxO)
largestFirst opt outs utxo = do
let descending = NE.toList . NE.sortBy (flip $ comparing coin)
let n = fromIntegral $ maximumNumberOfInputs opt
let nLargest = take n . L.sortBy (flip $ comparing (coin . snd)) . Map.toList . getUTxO
let nOuts = fromIntegral $ NE.length outs
let maxN = fromIntegral $ maximumNumberOfInputs opt (fromIntegral nOuts)
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

let nLargest = take maxN
. L.sortBy (flip $ comparing (coin . snd))
. Map.toList
. getUTxO
let guard = except . left ErrInvalidSelection . validate opt

case foldM atLeast (nLargest utxo, mempty) (descending outs) of
Expand All @@ -57,18 +61,17 @@ largestFirst opt outs utxo = do
let moneyRequested = sum $ (getCoin . coin) <$> (descending outs)
let utxoBalance = fromIntegral $ balance utxo
let nUtxo = fromIntegral $ L.length $ (Map.toList . getUTxO) utxo
let nOuts = fromIntegral $ NE.length outs

when (utxoBalance < moneyRequested)
$ throwE $ ErrNotEnoughMoney utxoBalance moneyRequested

when (nUtxo < nOuts)
$ throwE $ ErrUtxoNotEnoughFragmented nUtxo nOuts

when (fromIntegral n > nUtxo)
when (fromIntegral maxN > nUtxo)
$ throwE ErrInputsDepleted

throwE $ ErrMaximumInputsReached (fromIntegral n)
throwE $ ErrMaximumInputsReached (fromIntegral maxN)

-- Selecting coins to cover at least the specified value
-- The details of the algorithm are following:
Expand Down
48 changes: 25 additions & 23 deletions lib/core/src/Cardano/Wallet/Primitive/CoinSelection/Random.hs
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,14 @@ random
-> ExceptT (ErrCoinSelection e) m (CoinSelection, UTxO)
random opt outs utxo = do
let descending = NE.toList . NE.sortBy (flip $ comparing coin)
let nOuts = fromIntegral $ NE.length outs
let maxN = fromIntegral $ maximumNumberOfInputs opt nOuts
randomMaybe <- lift $ runMaybeT $
foldM makeSelection (opt, utxo, []) (descending outs)
foldM makeSelection (maxN, utxo, []) (descending outs)
case randomMaybe of
Just (opt', utxo', res) -> do
Just (maxN', utxo', res) -> do
(_, sel, remUtxo) <- lift $
foldM improveTxOut (opt', mempty, utxo') (reverse res)
foldM improveTxOut (maxN', mempty, utxo') (reverse res)
guard sel $> (sel, remUtxo)
Nothing ->
largestFirst opt outs utxo
Expand All @@ -130,14 +132,14 @@ random opt outs utxo = do

-- | Perform a random selection on a given output, without improvement.
makeSelection
:: forall m e. MonadRandom m
=> (CoinSelectionOptions e, UTxO, [([(TxIn, TxOut)], TxOut)])
:: forall m. MonadRandom m
=> (Word64, UTxO, [([(TxIn, TxOut)], TxOut)])
-> TxOut
-> MaybeT m (CoinSelectionOptions e, UTxO, [([(TxIn, TxOut)], TxOut)])
makeSelection (CoinSelectionOptions maxNumInputs fn, utxo0, selection) txout = do
-> MaybeT m (Word64, UTxO, [([(TxIn, TxOut)], TxOut)])
makeSelection (maxNumInputs, utxo0, selection) txout = do
(inps, utxo1) <- coverRandomly ([], utxo0)
return
( CoinSelectionOptions (maxNumInputs - fromIntegral (L.length inps)) fn
( maxNumInputs - fromIntegral (L.length inps)
, utxo1
, (inps, txout) : selection
)
Expand All @@ -156,14 +158,14 @@ makeSelection (CoinSelectionOptions maxNumInputs fn, utxo0, selection) txout = d

-- | Perform an improvement to random selection on a given output.
improveTxOut
:: forall m e. MonadRandom m
=> (CoinSelectionOptions e, CoinSelection, UTxO)
:: forall m. MonadRandom m
=> (Word64, CoinSelection, UTxO)
-> ([(TxIn, TxOut)], TxOut)
-> m (CoinSelectionOptions e, CoinSelection, UTxO)
improveTxOut (opt0, selection, utxo0) (inps0, txout) = do
(opt, inps, utxo) <- improve (opt0, inps0, utxo0)
-> m (Word64, CoinSelection, UTxO)
improveTxOut (maxN0, selection, utxo0) (inps0, txout) = do
(maxN, inps, utxo) <- improve (maxN0, inps0, utxo0)
return
( opt
( maxN
, selection <> CoinSelection
{ inputs = inps
, outputs = [txout]
Expand All @@ -175,22 +177,22 @@ improveTxOut (opt0, selection, utxo0) (inps0, txout) = do
target = mkTargetRange txout

improve
:: forall m e. MonadRandom m
=> (CoinSelectionOptions e, [(TxIn, TxOut)], UTxO)
-> m (CoinSelectionOptions e, [(TxIn, TxOut)], UTxO)
improve (opt@(CoinSelectionOptions maxN fn), inps, utxo)
:: forall m. MonadRandom m
=> (Word64, [(TxIn, TxOut)], UTxO)
-> m (Word64, [(TxIn, TxOut)], UTxO)
improve (maxN, inps, utxo)
| maxN >= 1 && balance' inps < targetAim target = do
runMaybeT (pickRandomT utxo) >>= \case
Nothing ->
return (opt, inps, utxo)
return (maxN, inps, utxo)
Just (io, utxo') | isImprovement io inps -> do
let inps' = io : inps
let opt' = CoinSelectionOptions (maxN - 1) fn
improve (opt', inps', utxo')
let maxN' = maxN - 1
improve (maxN', inps', utxo')
Just _ ->
return (opt, inps, utxo)
return (maxN, inps, utxo)
| otherwise =
return (opt, inps, utxo)
return (maxN, inps, utxo)

Copy link
Member Author

Choose a reason for hiding this comment

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

Note that most of the changes in this file are because the type for the maxN in the coin selection options was changed from Word64 to Word8 -> Word8. Therefore, the actual maxN is now computed in the root random (or largestFirst function, and passed down to the iteration function as a Word64 to be depleted.

isImprovement :: (TxIn, TxOut) -> [(TxIn, TxOut)] -> Bool
isImprovement io selected =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ propDeterministic
:: CoinSelProp
-> Property
propDeterministic (CoinSelProp utxo txOuts) = do
let opts = CoinSelectionOptions 100 noValidation
let opts = CoinSelectionOptions (const 100) noValidation
let resultOne = runIdentity $ runExceptT $ largestFirst opts txOuts utxo
let resultTwo = runIdentity $ runExceptT $ largestFirst opts txOuts utxo
resultOne === resultTwo
Expand All @@ -229,7 +229,7 @@ propAtLeast (CoinSelProp utxo txOuts) =
prop (CoinSelection inps _ _) =
L.length inps `shouldSatisfy` (>= NE.length txOuts)
selection = runIdentity $ runExceptT $
largestFirst (CoinSelectionOptions 100 noValidation) txOuts utxo
largestFirst (CoinSelectionOptions (const 100) noValidation) txOuts utxo

propInputDecreasingOrder
:: CoinSelProp
Expand All @@ -247,4 +247,4 @@ propInputDecreasingOrder (CoinSelProp utxo txOuts) =
(>= (getExtremumValue L.maximum utxo'))
getExtremumValue f = f . map (getCoin . coin . snd)
selection = runIdentity $ runExceptT $
largestFirst (CoinSelectionOptions 100 noValidation) txOuts utxo
largestFirst (CoinSelectionOptions (const 100) noValidation) txOuts utxo
Original file line number Diff line number Diff line change
Expand Up @@ -250,9 +250,10 @@ propFragmentation drg (CoinSelProp utxo txOuts) = do
prop (CoinSelection inps1 _ _, CoinSelection inps2 _ _) =
L.length inps1 `shouldSatisfy` (>= L.length inps2)
(selection1,_) = withDRG drg
(runExceptT $ random (CoinSelectionOptions 100 noValidation) txOuts utxo)
(runExceptT $ random opt txOuts utxo)
selection2 = runIdentity $ runExceptT $
largestFirst (CoinSelectionOptions 100 noValidation) txOuts utxo
largestFirst opt txOuts utxo
opt = CoinSelectionOptions (const 100) noValidation

propErrors
:: SystemDRG
Expand All @@ -266,6 +267,7 @@ propErrors drg (CoinSelProp utxo txOuts) = do
prop (err1, err2) =
err1 === err2
(selection1,_) = withDRG drg
(runExceptT $ random (CoinSelectionOptions 1 noValidation) txOuts utxo)
(runExceptT $ random opt txOuts utxo)
selection2 = runIdentity $ runExceptT $
largestFirst (CoinSelectionOptions 1 noValidation) txOuts utxo
largestFirst opt txOuts utxo
opt = (CoinSelectionOptions (const 1) noValidation)
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ instance Buildable CoinSelProp where

-- | A fixture for testing the coin selection
data CoinSelectionFixture = CoinSelectionFixture
{ maxNumOfInputs :: Word64
{ maxNumOfInputs :: Word8
-- ^ Maximum number of inputs that can be selected
, validateSelection :: CoinSelection -> Either ErrValidation ()
-- ^ A extra validation function on the resulting selection
Expand Down Expand Up @@ -178,7 +178,7 @@ coinSelectionUnitTest run lbl expected (CoinSelectionFixture n fn utxoF outsF) =
(utxo,txOuts) <- setup
result <- runExceptT $ do
(CoinSelection inps outs chngs, _) <-
run (CoinSelectionOptions n fn) txOuts utxo
run (CoinSelectionOptions (const n) fn) txOuts utxo
return $ CoinSelectionResult
{ rsInputs = map (getCoin . coin . snd) inps
, rsChange = map getCoin chngs
Expand Down
2 changes: 1 addition & 1 deletion lib/core/test/unit/Cardano/Wallet/Primitive/FeeSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,7 @@ genTxOut coins = do

genSelection :: NonEmpty TxOut -> Gen CoinSelection
genSelection outs = do
let opts = CS.CoinSelectionOptions 100 (const $ pure ())
let opts = CS.CoinSelectionOptions (const 100) (const $ pure ())
utxo <- vectorOf (NE.length outs * 3) arbitrary >>= genUTxO
case runIdentity $ runExceptT $ largestFirst opts outs utxo of
Left _ -> genSelection outs
Expand Down
15 changes: 9 additions & 6 deletions lib/core/test/unit/Cardano/WalletSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ import Data.Quantity
import Data.Time.Clock
( secondsToDiffTime )
import Data.Word
( Word32 )
( Word16, Word32 )
import GHC.Generics
( Generic )
import Test.Hspec
Expand Down Expand Up @@ -372,19 +372,22 @@ setupFixture (wid, wname, wstate) = do
db <- newDBLayer
let nl = error "NetworkLayer"
let tl = dummyTransactionLayer
let bp = BlockchainParameters block0 dummyPolicy dummySlotLength
let bp = BlockchainParameters block0 policy slotLength txMaxSize
wl <- newWalletLayer @_ @DummyTarget nullTracer bp db nl tl
res <- runExceptT $ createWallet wl wid wname wstate
let wal = case res of
Left _ -> []
Right walletId -> [walletId]
pure $ WalletLayerFixture db wl wal
where
dummyPolicy :: FeePolicy
dummyPolicy = LinearFee (Quantity 14) (Quantity 42)
policy :: FeePolicy
policy = LinearFee (Quantity 14) (Quantity 42)

dummySlotLength :: SlotLength
dummySlotLength = SlotLength $ secondsToDiffTime 1
slotLength :: SlotLength
slotLength = SlotLength $ secondsToDiffTime 1

txMaxSize :: Quantity "byte" Word16
txMaxSize = Quantity 8192

-- | A dummy transaction layer to see the effect of a root private key. It
-- implements a fake signer that still produces sort of witnesses
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ module Cardano.Wallet.HttpBridge.Compatibility
, block0
, byronFeePolicy
, byronSlotLength
, byronTxMaxSize
) where

import Prelude
Expand Down Expand Up @@ -62,6 +63,8 @@ import Data.Text.Class
( TextDecodingError (..) )
import Data.Time.Clock
( secondsToDiffTime )
import Data.Word
( Word16 )

import qualified Cardano.Wallet.HttpBridge.Binary as CBOR
import qualified Cardano.Wallet.HttpBridge.Primitive.Types as W
Expand Down Expand Up @@ -159,7 +162,10 @@ block0 = Block
byronFeePolicy :: FeePolicy
byronFeePolicy = LinearFee (Quantity 155381) (Quantity 43.946)


-- | Hard-coded slot duration
byronSlotLength :: SlotLength
byronSlotLength = SlotLength $ secondsToDiffTime 20

-- | Hard-coded max transaction size
byronTxMaxSize :: Quantity "byte" Word16
byronTxMaxSize = Quantity 8192
Loading