Skip to content
This repository was archived by the owner on Mar 1, 2019. It is now read-only.

Commit 50839f9

Browse files
[CBR-313] Refactor runCoinSelT to not assemble a TxAux
This commit separates out the concern of running the coin selection vs the concern of assembling the final TxAux.
1 parent f49976b commit 50839f9

File tree

5 files changed

+186
-197
lines changed

5 files changed

+186
-197
lines changed

src/Cardano/Wallet/Kernel/CoinSelection/FromGeneric.hs

Lines changed: 29 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ module Cardano.Wallet.Kernel.CoinSelection.FromGeneric (
1212
, CoinSelectionOptions(..)
1313
, newOptions
1414
-- * Transaction building
15-
, MkTx
15+
, CoinSelFinalResult(..)
1616
, mkStdTx
1717
-- * Coin selection policies
1818
, random
@@ -166,26 +166,23 @@ feeOptions CoinSelectionOptions{..} = FeeOptions{
166166
-------------------------------------------------------------------------------}
167167

168168
-- | Build a transaction
169-
type MkTx m = NonEmpty (Core.TxIn, Core.TxOutAux) -- ^ Transaction inputs
170-
-> NonEmpty Core.TxOutAux -- ^ Transaction outputs
171-
-> [Core.Coin] -- ^ Generated change.
172-
-> m (Either CoinSelHardErr Core.TxAux)
173169

174170
-- | Construct a standard transaction
175171
--
176172
-- " Standard " here refers to the fact that we do not deal with redemption,
177173
-- multisignature transactions, etc.
178174
mkStdTx :: Monad m
179175
=> Core.ProtocolMagic
180-
-> ([Core.Coin] -> m [Core.TxOutAux])
181-
-- ^ Function to turn a list of change into proper Core.TxOutAux.
182-
-- This function is likely going to do some IO like creating and
183-
-- persisting change addresses into the database.
184176
-> (Core.Address -> Either CoinSelHardErr Core.SafeSigner)
185-
-> MkTx m
186-
mkStdTx pm genChange hdwSigners inps outs change = do
187-
chng <- genChange change
188-
let allOuts = foldl' (flip NE.cons) outs chng
177+
-> NonEmpty (Core.TxIn, Core.TxOutAux)
178+
-- ^ Selected inputs
179+
-> NonEmpty Core.TxOutAux
180+
-- ^ Selected outputs
181+
-> [Core.TxOutAux]
182+
-- ^ A list of change addresess, in the form of 'TxOutAux'(s).
183+
-> m (Either CoinSelHardErr Core.TxAux)
184+
mkStdTx pm hdwSigners inps outs change = do
185+
let allOuts = foldl' (flip NE.cons) outs change
189186
return $ Core.makeMPubKeyTxAddrs pm hdwSigners (fmap repack inps) allOuts
190187
where
191188
-- Repack a utxo-derived tuple into a format suitable for
@@ -201,6 +198,14 @@ mkStdTx pm genChange hdwSigners inps outs change = do
201198
type PickUtxo m = Core.Coin -- ^ Fee to still cover
202199
-> CoinSelT Core.Utxo CoinSelHardErr m (Core.TxIn, Core.TxOutAux)
203200

201+
data CoinSelFinalResult = CoinSelFinalResult {
202+
csrInputs :: NonEmpty (Core.TxIn, Core.TxOutAux)
203+
-- ^ Picked inputs
204+
, csrOutputs :: NonEmpty Core.TxOutAux
205+
-- ^ Picked outputs
206+
, csrChange :: [Core.Coin]
207+
}
208+
204209
-- | Run coin selection
205210
--
206211
-- NOTE: Final UTxO is /not/ returned: coin selection runs /outside/ any wallet
@@ -214,12 +219,11 @@ type PickUtxo m = Core.Coin -- ^ Fee to still cover
214219
runCoinSelT :: forall m. Monad m
215220
=> CoinSelectionOptions
216221
-> PickUtxo m
217-
-> MkTx m
218222
-> (forall utxo. PickFromUtxo utxo
219223
=> NonEmpty (Output (Dom utxo))
220224
-> CoinSelT utxo CoinSelHardErr m [CoinSelResult (Dom utxo)])
221-
-> CoinSelPolicy Core.Utxo m Core.TxAux
222-
runCoinSelT opts pickUtxo mkTx policy request utxo = do
225+
-> CoinSelPolicy Core.Utxo m CoinSelFinalResult
226+
runCoinSelT opts pickUtxo policy request utxo = do
223227
mSelection <- unwrapCoinSelT policy' utxo
224228
case mSelection of
225229
Left err -> return (Left err)
@@ -235,7 +239,9 @@ runCoinSelT opts pickUtxo mkTx policy request utxo = do
235239
[] -> error "runCoinSelT: empty list of outputs"
236240
o:os -> o :| os
237241
-- TODO: We should shuffle allOuts
238-
mkTx allInps originalOuts (concatMap coinSelChange css)
242+
return . Right $ CoinSelFinalResult allInps
243+
originalOuts
244+
(concatMap coinSelChange css)
239245
where
240246
policy' :: CoinSelT Core.Utxo CoinSelHardErr m
241247
([CoinSelResult Cardano], SelectedUtxo Cardano)
@@ -313,11 +319,10 @@ validateOutput out =
313319
-- | Random input selection policy
314320
random :: forall m. MonadRandom m
315321
=> CoinSelectionOptions
316-
-> MkTx m -- ^ Build and sign transaction
317322
-> Word64 -- ^ Maximum number of inputs
318-
-> CoinSelPolicy Core.Utxo m Core.TxAux
319-
random opts mkTx maxInps =
320-
runCoinSelT opts pickUtxo mkTx
323+
-> CoinSelPolicy Core.Utxo m CoinSelFinalResult
324+
random opts maxInps =
325+
runCoinSelT opts pickUtxo
321326
$ Random.random Random.PrivacyModeOn maxInps . NE.toList
322327
where
323328
-- We ignore the size of the fee, and just pick randomly
@@ -329,11 +334,10 @@ random opts mkTx maxInps =
329334
-- NOTE: Not for production use.
330335
largestFirst :: forall m. Monad m
331336
=> CoinSelectionOptions
332-
-> MkTx m
333337
-> Word64
334-
-> CoinSelPolicy Core.Utxo m Core.TxAux
335-
largestFirst opts mkTx maxInps =
336-
runCoinSelT opts pickUtxo mkTx
338+
-> CoinSelPolicy Core.Utxo m CoinSelFinalResult
339+
largestFirst opts maxInps =
340+
runCoinSelT opts pickUtxo
337341
$ LargestFirst.largestFirst maxInps . NE.toList
338342
where
339343
pickUtxo :: PickUtxo m

src/Cardano/Wallet/Kernel/Transactions.hs

Lines changed: 96 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ module Cardano.Wallet.Kernel.Transactions (
66
, NewTransactionError(..)
77
, PaymentError(..)
88
, EstimateFeesError(..)
9-
, mkSigner
109
, cardanoFee
1110
-- * Internal & testing use only
1211
, newTransaction
@@ -19,16 +18,20 @@ import Control.Retry (RetryPolicyM, RetryStatus, applyPolicy,
1918
fullJitterBackoff, limitRetries, retrying)
2019
import Crypto.Random (MonadRandom (..))
2120
import qualified Data.Set as Set
21+
import qualified Data.Vector as V
22+
import System.Random.MWC (GenIO, asGenIO, initialize, uniformVector)
2223
import Test.QuickCheck (Arbitrary (..))
2324

2425
import Formatting (bprint, build, sformat, (%))
2526
import qualified Formatting.Buildable
2627

2728
import qualified Data.ByteArray as ByteArray
28-
import qualified Pos.Binary as Bi
29+
import qualified Data.ByteString as B
2930

31+
import qualified Cardano.Wallet.Kernel.Addresses as Kernel
3032
import Cardano.Wallet.Kernel.CoinSelection.FromGeneric (Cardano,
31-
CoinSelectionOptions, estimateCardanoFee, mkStdTx)
33+
CoinSelFinalResult (..), CoinSelectionOptions,
34+
estimateCardanoFee, mkStdTx)
3235
import qualified Cardano.Wallet.Kernel.CoinSelection.FromGeneric as CoinSelection
3336
import Cardano.Wallet.Kernel.CoinSelection.Generic
3437
(CoinSelHardErr (..))
@@ -37,12 +40,15 @@ import Cardano.Wallet.Kernel.DB.HdWallet
3740
import qualified Cardano.Wallet.Kernel.DB.HdWallet as HD
3841
import Cardano.Wallet.Kernel.DB.HdWallet.Read
3942
(readHdAddressByCardanoAddress)
43+
import Cardano.Wallet.Kernel.Types (AccountId (..), WalletId (..))
4044
import Cardano.Wallet.Kernel.Util (paymentAmount, utxoBalance,
4145
utxoRestrictToInputs)
4246

4347
import Cardano.Wallet.Kernel (getWalletSnapshot, newPending)
4448
import Cardano.Wallet.Kernel.DB.Read as Getters
45-
import Cardano.Wallet.Kernel.Internal (ActiveWallet (..))
49+
import Cardano.Wallet.Kernel.Internal (ActiveWallet (..),
50+
walletKeystore)
51+
import qualified Cardano.Wallet.Kernel.Keystore as Keystore
4652

4753
import Pos.Core (Address, Coin, Tx (..), TxAux (..), TxOut (..),
4854
TxOutAux (..), unsafeSubCoin)
@@ -85,19 +91,16 @@ instance Buildable PaymentError where
8591
-- seconds, as well as internally retrying up to 5 times to propagate the
8692
-- transaction via 'newPending'.
8793
pay :: ActiveWallet
88-
-> IO Address
89-
-- ^ A computation to generate change 'Address'es.
90-
-> (Address -> Either CoinSelHardErr SafeSigner)
94+
-> PassPhrase
9195
-> CoinSelectionOptions
9296
-> HdAccountId
93-
-- ^ The source @(root, account) from where the payment was
94-
-- originated
97+
-- ^ The source HD Account from where the payment was originated
9598
-> NonEmpty (Address, Coin)
9699
-- ^ The payees
97100
-> IO (Either PaymentError Tx)
98-
pay activeWallet genChangeAddr signAddress opts accountId payees = do
101+
pay activeWallet spendingPassword opts accountId payees = do
99102
retrying retryPolicy shouldRetry $ \rs -> do
100-
tx <- newTransaction activeWallet genChangeAddr signAddress opts accountId payees
103+
(tx, _) <- newTransaction activeWallet spendingPassword opts accountId payees
101104
case tx of
102105
Left e -> return (Left $ PaymentNewTransactionError e)
103106
Right txAux -> do
@@ -131,58 +134,98 @@ pay activeWallet genChangeAddr signAddress opts accountId payees = do
131134

132135
-- | Creates a new 'TxAux' without submitting it to the network.
133136
newTransaction :: ActiveWallet
134-
-> IO Address
135-
-- ^ A computation to generate change 'Address'es.
136-
-> (Address -> Either CoinSelHardErr SafeSigner)
137-
-- ^ A function to sign each Address
137+
-> PassPhrase
138+
-- ^ The spending password.
138139
-> CoinSelectionOptions
139140
-- ^ The options describing how to tune the coin selection.
140141
-> HdAccountId
141-
-- ^ The source @(root, account) from where the payment was
142-
-- originated
142+
-- ^ The source HD account from where the payment should originate
143143
-> NonEmpty (Address, Coin)
144144
-- ^ The payees
145-
-> IO (Either NewTransactionError TxAux)
146-
newTransaction ActiveWallet{..} genChangeAddr signAddress options accountId payees = do
147-
snapshot <- getWalletSnapshot walletPassive
148-
let toTxOuts = fmap (\(a,c) -> TxOutAux (TxOut a c))
149-
let genChangeOuts css = forM css $ \change -> do
150-
changeAddr <- liftIO genChangeAddr
151-
return TxOutAux {
152-
toaOut = TxOut {
153-
txOutAddress = changeAddr
154-
, txOutValue = change
155-
}
156-
}
157-
let mkTx = mkStdTx walletProtocolMagic genChangeOuts signAddress
145+
-> IO (Either NewTransactionError TxAux, Utxo)
146+
newTransaction ActiveWallet{..} spendingPassword options accountId payees = do
147+
158148
-- | NOTE(adn) This number was computed out of the work Matt Noonan did
159149
-- on the size estimation. See [CBR-318].
160150
let maxInputs = 350
151+
152+
snapshot <- getWalletSnapshot walletPassive
161153
let availableUtxo = accountAvailableUtxo snapshot accountId
162-
freshCounter <- newIORef 1
163-
let initialSeed = encodeUtf8 @Text @ByteString . sformat build $ hash payees
164-
res <- flip runReaderT (initialSeed, freshCounter) . buildPayment $
154+
155+
initialEnv <- newEnvironment
156+
157+
-- STEP 1: Run coin selection.
158+
res <- flip runReaderT initialEnv . buildPayment $
165159
CoinSelection.random options
166-
mkTx
167160
maxInputs
168-
(toTxOuts payees)
161+
(fmap toTxOut payees)
169162
availableUtxo
170163
case res of
171-
Left err -> return . Left . CoinSelectionFailed $ err
172-
Right t -> return . Right $ t
164+
Left err -> return (Left . CoinSelectionFailed $ err, availableUtxo)
165+
Right (CoinSelFinalResult inputs outputs coins) -> do
166+
-- STEP 2: Generate the change addresses needed.
167+
changeAddresses <- genChangeOuts coins
168+
169+
-- STEP 3: Perform the signing and forge the final TxAux.
170+
let keystore = walletPassive ^. walletKeystore
171+
mbEsk <- Keystore.lookup (WalletIdHdRnd $ accountId ^. hdAccountIdParent) keystore
172+
let allWallets = hdWallets snapshot
173+
signAddress = mkSigner spendingPassword mbEsk allWallets
174+
mkTx = mkStdTx walletProtocolMagic signAddress
175+
176+
(, availableUtxo) . bimap CoinSelectionFailed identity
177+
<$> mkTx inputs outputs changeAddresses
178+
179+
where
180+
-- Generate an initial seed for the random generator using the hash of
181+
-- the payees, which ensure that the coin selection (and the fee estimation)
182+
-- is \"pseudo deterministic\" and replicable.
183+
newEnvironment :: IO Env
184+
newEnvironment =
185+
let initialSeed = V.fromList . map fromIntegral
186+
. B.unpack
187+
. encodeUtf8 @Text @ByteString
188+
. sformat build
189+
$ hash payees
190+
in Env <$> initialize initialSeed
191+
192+
toTxOut :: (Address, Coin) -> TxOutAux
193+
toTxOut (a, c) = TxOutAux (TxOut a c)
194+
195+
-- | Generates the list of change outputs from a list of change coins.
196+
genChangeOuts :: MonadIO m => [Coin] -> m [TxOutAux]
197+
genChangeOuts css = forM css $ \change -> do
198+
changeAddr <- liftIO genChangeAddr
199+
return TxOutAux {
200+
toaOut = TxOut {
201+
txOutAddress = changeAddr
202+
, txOutValue = change
203+
}
204+
}
205+
206+
-- | Monadic computation to generate a new change 'Address'. This will
207+
-- run after coin selection, when we create the final transaction as
208+
-- part of 'mkTx'.
209+
genChangeAddr :: IO Address
210+
genChangeAddr = do
211+
res <- Kernel.createAddress spendingPassword
212+
(AccountIdHdRnd accountId)
213+
walletPassive
214+
case res of
215+
Right addr -> pure addr
216+
Left err -> throwM err
173217

174218
-- | Special monad used to process the payments, which randomness is derived
175219
-- from a fixed seed obtained from hashing the payees. This guarantees that
176220
-- when we estimate the fees and later create a transaction, the coin selection
177221
-- will always yield the same value, making the process externally-predicatable.
178222
newtype PayMonad a = PayMonad {
179-
buildPayment :: ReaderT (ByteString, IORef Word16) IO a
180-
} deriving ( Functor
181-
, Applicative
182-
, Monad
183-
, MonadIO
184-
, MonadReader (ByteString, IORef Word16)
185-
)
223+
buildPayment :: ReaderT Env IO a
224+
} deriving ( Functor , Applicative , Monad , MonadIO, MonadReader Env)
225+
226+
-- | This 'Env' datatype is necessary to convince GHC that indeed we have
227+
-- a 'MonadReader' instance defined on 'GenIO' for the 'PayMonad'.
228+
newtype Env = Env { getEnv :: GenIO }
186229

187230
-- | \"Invalid\" 'MonadRandom' instance for 'PayMonad' which generates
188231
-- randomness using the hash of 'NonEmpty (Address, Coin)' as fixed seed,
@@ -191,13 +234,10 @@ newtype PayMonad a = PayMonad {
191234
-- which is yet deterministically reproduceable by feeding the same set of
192235
-- payees.
193236
instance MonadRandom PayMonad where
194-
getRandomBytes _ = do
195-
(initialSeed, counterRef) <- ask
196-
counterValue <- readIORef counterRef
197-
let randVal = ByteArray.convert . mappend (Bi.serialize' counterValue)
198-
$ initialSeed
199-
modifyIORef' counterRef succ
200-
return randVal
237+
getRandomBytes len = do
238+
gen <- asks getEnv
239+
randomBytes <- liftIO (asGenIO (flip uniformVector len) gen)
240+
return $ ByteArray.convert (B.pack $ V.toList randomBytes)
201241

202242
{-------------------------------------------------------------------------------
203243
Estimating fees
@@ -213,22 +253,17 @@ instance Arbitrary EstimateFeesError where
213253
arbitrary = EstFeesTxCreationFailed <$> arbitrary
214254

215255
estimateFees :: ActiveWallet
216-
-> IO Address
217-
-- ^ A computation to generate a new change 'Address.
218-
-> (Address -> Either CoinSelHardErr SafeSigner)
219-
-- ^ A function to sign each Address
256+
-> PassPhrase
257+
-- ^ The spending password.
220258
-> CoinSelectionOptions
221259
-- ^ The options describing how to tune the coin selection.
222260
-> HdAccountId
223-
-- ^ The source @(root, account) from where the payment was
224-
-- originated
261+
-- ^ The source HD Account from where the payment should originate
225262
-> NonEmpty (Address, Coin)
226263
-- ^ The payees
227264
-> IO (Either EstimateFeesError Coin)
228-
estimateFees activeWallet@ActiveWallet{..} genChangeAddr signAddress options accountId payees = do
229-
snapshot <- getWalletSnapshot walletPassive
230-
let originalUtxo = accountAvailableUtxo snapshot accountId
231-
res <- newTransaction activeWallet genChangeAddr signAddress options accountId payees
265+
estimateFees activeWallet@ActiveWallet{..} spendingPassword options accountId payees = do
266+
(res, originalUtxo) <- newTransaction activeWallet spendingPassword options accountId payees
232267
case res of
233268
Left e -> return . Left . EstFeesTxCreationFailed $ e
234269
Right tx -> -- calculate the fee as the difference between inputs and outputs.

0 commit comments

Comments
 (0)