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

Commit f49976b

Browse files
[CBR-313] Round of refactoring after code review
1 parent a823d57 commit f49976b

File tree

6 files changed

+89
-51
lines changed

6 files changed

+89
-51
lines changed

src/Cardano/Wallet/API/V1/Handlers/Transactions.hs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ newTransaction aw payment@Payment{..} = do
5454

5555
-- TODO(adn) If the wallet is being restored, we need to disallow any @Payment@ from
5656
-- being submitted.
57-
57+
-- NOTE(adn) The 'SenderPaysFee' option will become configurable as part
58+
-- of CBR-291.
5859
res <- liftIO $ runProduction $ (WalletLayer.pay aw) (maybe mempty coerce pmtSpendingPassword)
5960
(toInputGrouping pmtGroupingPolicy)
6061
SenderPaysFee
@@ -63,6 +64,8 @@ newTransaction aw payment@Payment{..} = do
6364
Left err -> throwM err
6465
Right tx -> do
6566
now <- liftIO getCurrentTimestamp
67+
-- NOTE(adn) As part of [CBR-329], we could simply fetch the
68+
-- entire 'Transaction' as part of the TxMeta.
6669
return $ single Transaction {
6770
txId = V1 (hash tx)
6871
, txConfirmations = 0

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

Lines changed: 22 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ feeOptions CoinSelectionOptions{..} = FeeOptions{
168168
-- | Build a transaction
169169
type MkTx m = NonEmpty (Core.TxIn, Core.TxOutAux) -- ^ Transaction inputs
170170
-> NonEmpty Core.TxOutAux -- ^ Transaction outputs
171+
-> [Core.Coin] -- ^ Generated change.
171172
-> m (Either CoinSelHardErr Core.TxAux)
172173

173174
-- | Construct a standard transaction
@@ -176,10 +177,16 @@ type MkTx m = NonEmpty (Core.TxIn, Core.TxOutAux) -- ^ Transaction inputs
176177
-- multisignature transactions, etc.
177178
mkStdTx :: Monad m
178179
=> 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.
179184
-> (Core.Address -> Either CoinSelHardErr Core.SafeSigner)
180185
-> MkTx m
181-
mkStdTx pm hdwSigners inps outs =
182-
return $ Core.makeMPubKeyTxAddrs pm hdwSigners (fmap repack inps) outs
186+
mkStdTx pm genChange hdwSigners inps outs change = do
187+
chng <- genChange change
188+
let allOuts = foldl' (flip NE.cons) outs chng
189+
return $ Core.makeMPubKeyTxAddrs pm hdwSigners (fmap repack inps) allOuts
183190
where
184191
-- Repack a utxo-derived tuple into a format suitable for
185192
-- 'TxOwnedInputs'.
@@ -206,14 +213,13 @@ type PickUtxo m = Core.Coin -- ^ Fee to still cover
206213
-- just be run again on a new snapshot of the wallet DB.
207214
runCoinSelT :: forall m. Monad m
208215
=> CoinSelectionOptions
209-
-> m Core.Address
210216
-> PickUtxo m
211217
-> MkTx m
212218
-> (forall utxo. PickFromUtxo utxo
213219
=> NonEmpty (Output (Dom utxo))
214220
-> CoinSelT utxo CoinSelHardErr m [CoinSelResult (Dom utxo)])
215221
-> CoinSelPolicy Core.Utxo m Core.TxAux
216-
runCoinSelT opts genChangeAddr pickUtxo mkTx policy request utxo = do
222+
runCoinSelT opts pickUtxo mkTx policy request utxo = do
217223
mSelection <- unwrapCoinSelT policy' utxo
218224
case mSelection of
219225
Left err -> return (Left err)
@@ -222,22 +228,14 @@ runCoinSelT opts genChangeAddr pickUtxo mkTx policy request utxo = do
222228
inps = concatMap selectedEntries
223229
(additionalUtxo : map coinSelInputs css)
224230
outs = map coinSelOutput css
225-
changeOuts <- forM (concatMap coinSelChange css) $ \change -> do
226-
changeAddr <- genChangeAddr
227-
return Core.TxOutAux {
228-
Core.toaOut = Core.TxOut {
229-
Core.txOutAddress = changeAddr
230-
, Core.txOutValue = change
231-
}
232-
}
233231
let allInps = case inps of
234232
[] -> error "runCoinSelT: empty list of inputs"
235233
i:is -> i :| is
236-
allOuts = case outs ++ changeOuts of
237-
[] -> error "runCoinSelT: empty list of outputs"
238-
o:os -> o :| os
234+
originalOuts = case outs of
235+
[] -> error "runCoinSelT: empty list of outputs"
236+
o:os -> o :| os
239237
-- TODO: We should shuffle allOuts
240-
mkTx allInps allOuts
238+
mkTx allInps originalOuts (concatMap coinSelChange css)
241239
where
242240
policy' :: CoinSelT Core.Utxo CoinSelHardErr m
243241
([CoinSelResult Cardano], SelectedUtxo Cardano)
@@ -315,12 +313,11 @@ validateOutput out =
315313
-- | Random input selection policy
316314
random :: forall m. MonadRandom m
317315
=> CoinSelectionOptions
318-
-> m Core.Address -- ^ Generate change address
319316
-> MkTx m -- ^ Build and sign transaction
320317
-> Word64 -- ^ Maximum number of inputs
321318
-> CoinSelPolicy Core.Utxo m Core.TxAux
322-
random opts changeAddr mkTx maxInps =
323-
runCoinSelT opts changeAddr pickUtxo mkTx
319+
random opts mkTx maxInps =
320+
runCoinSelT opts pickUtxo mkTx
324321
$ Random.random Random.PrivacyModeOn maxInps . NE.toList
325322
where
326323
-- We ignore the size of the fee, and just pick randomly
@@ -332,12 +329,11 @@ random opts changeAddr mkTx maxInps =
332329
-- NOTE: Not for production use.
333330
largestFirst :: forall m. Monad m
334331
=> CoinSelectionOptions
335-
-> m Core.Address
336332
-> MkTx m
337333
-> Word64
338334
-> CoinSelPolicy Core.Utxo m Core.TxAux
339-
largestFirst opts changeAddr mkTx maxInps =
340-
runCoinSelT opts changeAddr pickUtxo mkTx
335+
largestFirst opts mkTx maxInps =
336+
runCoinSelT opts pickUtxo mkTx
341337
$ LargestFirst.largestFirst maxInps . NE.toList
342338
where
343339
pickUtxo :: PickUtxo m
@@ -355,6 +351,10 @@ largestFirst opts changeAddr mkTx maxInps =
355351
Cardano-specific fee-estimation.
356352
-------------------------------------------------------------------------------}
357353

354+
-- NOTE(adn): Once https://github.com/input-output-hk/cardano-sl/pull/3232
355+
-- will be merged, we should use the proper formula rather than the unrolled
356+
-- computation below.
357+
358358
{-| Estimate the size of a transaction, in bytes.
359359
360360
The magic numbers appearing in the formula have the following origins:

src/Cardano/Wallet/Kernel/DB/HdWallet/Read.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,6 @@ readHdAddressByCardanoAddress cardanoAddr = do
160160
aux . IxSet.getEQ cardanoAddr . readAllHdAddresses
161161
where
162162
aux :: IxSet HdAddress -> Either UnknownHdAddress HdAddress
163-
aux ixset = case IxSet.onlyOne ixset of
163+
aux ixset = case IxSet.getOne ixset of
164164
Just x -> Right x
165165
Nothing -> Left (UnknownHdCardanoAddress cardanoAddr)

src/Cardano/Wallet/Kernel/DB/Util/IxSet.hs

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ module Cardano.Wallet.Kernel.DB.Util.IxSet (
1717
, getEQ
1818
, member
1919
, size
20-
, onlyOne
20+
, getOne
2121
-- * Construction
2222
, fromList
2323
, omap
@@ -152,10 +152,8 @@ size = IxSet.size . unwrapIxSet
152152
-- one, i.e. only if it has @exactly@ one element in it. Usually this is
153153
-- used in tandem with 'getEQ' to witness the existence of exactly one element
154154
-- in the set indexed by a particular index.
155-
onlyOne :: IxSet a -> Maybe a
156-
onlyOne ixset = case IxSet.toList . unwrapIxSet $ ixset of
157-
[WrapOrdByPrimKey x] -> Just x
158-
_ -> Nothing
155+
getOne :: HasPrimKey a => IxSet a -> Maybe a
156+
getOne = fmap coerce . IxSet.getOne . unwrapIxSet
159157

160158
{-------------------------------------------------------------------------------
161159
Construction

src/Cardano/Wallet/Kernel/Transactions.hs

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import Universum
1616

1717
import Control.Lens (to)
1818
import Control.Retry (RetryPolicyM, RetryStatus, applyPolicy,
19-
constantDelay, limitRetries, retrying)
19+
fullJitterBackoff, limitRetries, retrying)
2020
import Crypto.Random (MonadRandom (..))
2121
import qualified Data.Set as Set
2222
import Test.QuickCheck (Arbitrary (..))
@@ -25,6 +25,7 @@ import Formatting (bprint, build, sformat, (%))
2525
import qualified Formatting.Buildable
2626

2727
import qualified Data.ByteArray as ByteArray
28+
import qualified Pos.Binary as Bi
2829

2930
import Cardano.Wallet.Kernel.CoinSelection.FromGeneric (Cardano,
3031
CoinSelectionOptions, estimateCardanoFee, mkStdTx)
@@ -100,10 +101,12 @@ pay activeWallet genChangeAddr signAddress opts accountId payees = do
100101
case tx of
101102
Left e -> return (Left $ PaymentNewTransactionError e)
102103
Right txAux -> do
104+
-- TODO(adn) As part of CBR-239 or CBR-324, we should
105+
-- ensure that 'newPending' inserts the transaction
106+
-- inside the TxMeta storage.
103107
succeeded <- newPending activeWallet accountId txAux
104108
case succeeded of
105109
Left e -> do
106-
print (sformat build e)
107110
-- If the next retry would bring us to the
108111
-- end of our allowed retries, we fail with
109112
-- a proper error
@@ -115,8 +118,9 @@ pay activeWallet genChangeAddr signAddress opts accountId payees = do
115118
PaymentNewPendingError e
116119
Right () -> return . Right . taTx $ txAux
117120
where
121+
-- See <https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter>
118122
retryPolicy :: RetryPolicyM IO
119-
retryPolicy = constantDelay 5000000 <> limitRetries 6
123+
retryPolicy = fullJitterBackoff 5000000 <> limitRetries 6
120124

121125
-- If this is a hard coin selection error we cannot recover, stop
122126
-- retrying. If we get a 'Tx' as output, stop retrying immediately.
@@ -142,14 +146,23 @@ newTransaction :: ActiveWallet
142146
newTransaction ActiveWallet{..} genChangeAddr signAddress options accountId payees = do
143147
snapshot <- getWalletSnapshot walletPassive
144148
let toTxOuts = fmap (\(a,c) -> TxOutAux (TxOut a c))
145-
let mkTx = mkStdTx walletProtocolMagic signAddress
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
146158
-- | NOTE(adn) This number was computed out of the work Matt Noonan did
147159
-- on the size estimation. See [CBR-318].
148160
let maxInputs = 350
149161
let availableUtxo = accountAvailableUtxo snapshot accountId
150-
res <- flip runReaderT payees . buildPayment $
162+
freshCounter <- newIORef 1
163+
let initialSeed = encodeUtf8 @Text @ByteString . sformat build $ hash payees
164+
res <- flip runReaderT (initialSeed, freshCounter) . buildPayment $
151165
CoinSelection.random options
152-
(liftIO genChangeAddr)
153166
mkTx
154167
maxInputs
155168
(toTxOuts payees)
@@ -163,17 +176,28 @@ newTransaction ActiveWallet{..} genChangeAddr signAddress options accountId paye
163176
-- when we estimate the fees and later create a transaction, the coin selection
164177
-- will always yield the same value, making the process externally-predicatable.
165178
newtype PayMonad a = PayMonad {
166-
buildPayment :: ReaderT (NonEmpty (Address, Coin)) IO a
167-
} deriving (Functor, Applicative, Monad, MonadIO, MonadReader (NonEmpty (Address, Coin)))
179+
buildPayment :: ReaderT (ByteString, IORef Word16) IO a
180+
} deriving ( Functor
181+
, Applicative
182+
, Monad
183+
, MonadIO
184+
, MonadReader (ByteString, IORef Word16)
185+
)
168186

169187
-- | \"Invalid\" 'MonadRandom' instance for 'PayMonad' which generates
170-
-- randomness using the hash of 'NonEmpty (Address, Coin)' as fixed seed.
188+
-- randomness using the hash of 'NonEmpty (Address, Coin)' as fixed seed,
189+
-- plus an internal counter used to shift the bits of such hash.
190+
-- This ensures that the coin selection algorithm runs in a random environment
191+
-- which is yet deterministically reproduceable by feeding the same set of
192+
-- payees.
171193
instance MonadRandom PayMonad where
172-
getRandomBytes _ =
173-
ask >>= return . ByteArray.convert
174-
. encodeUtf8 @Text @ByteString
175-
. sformat build
176-
. hash
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
177201

178202
{-------------------------------------------------------------------------------
179203
Estimating fees
@@ -208,9 +232,10 @@ estimateFees activeWallet@ActiveWallet{..} genChangeAddr signAddress options acc
208232
case res of
209233
Left e -> return . Left . EstFeesTxCreationFailed $ e
210234
Right tx -> -- calculate the fee as the difference between inputs and outputs.
211-
-- NOTE(adn) Apparently we shouldn't worry about the 'ExpenseRegulation'
212-
-- affecting the way we sum, as no matter who pays for the fee,
213-
-- the final difference won't be affected.
235+
-- NOTE(adn) In case of 'SenderPaysFee' is practice there might be a slightly
236+
-- increase of the projected fee in the case we are forced to pick "yet another input"
237+
-- to be able to pay the fee, which would, in turn, also increase the fee due to
238+
-- the extra input being picked.
214239
return $ Right
215240
$ sumOfInputs tx originalUtxo `unsafeSubCoin` sumOfOutputs tx
216241
where

test/unit/Test/Spec/CoinSelection.hs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,6 @@ errorWas predicate _ _ (STB hardErr) =
391391
-------------------------------------------------------------------------------}
392392

393393
type Policy = CoinSelectionOptions
394-
-> Gen Core.Address
395394
-> MkTx Gen
396395
-> Word64
397396
-> CoinSelPolicy Core.Utxo Gen Core.TxAux
@@ -404,8 +403,22 @@ type RunResult = ( Core.Utxo
404403
maxNumInputs :: Word64
405404
maxNumInputs = 300
406405

407-
mkTx :: Core.ProtocolMagic -> SecretKey -> MkTx Gen
408-
mkTx pm key = mkStdTx pm (\_addr -> Right (fakeSigner key))
406+
genChange :: Core.Utxo -> NonEmpty Core.TxOut -> [Core.Coin] -> Gen [Core.TxOutAux]
407+
genChange utxo payee css = forM css $ \change -> do
408+
changeAddr <- genUniqueChangeAddress utxo payee
409+
return Core.TxOutAux {
410+
Core.toaOut = Core.TxOut {
411+
Core.txOutAddress = changeAddr
412+
, Core.txOutValue = change
413+
}
414+
}
415+
416+
mkTx :: Core.Utxo
417+
-> NonEmpty Core.TxOut
418+
-> Core.ProtocolMagic
419+
-> SecretKey
420+
-> MkTx Gen
421+
mkTx utxo payee pm key = mkStdTx pm (genChange utxo payee) (\_addr -> Right (fakeSigner key))
409422

410423
payRestrictInputsTo :: Word64
411424
-> (InitialBalance -> Gen Core.Utxo)
@@ -425,8 +438,7 @@ payRestrictInputsTo maxInputs genU genP feeFunction adjustOptions bal amount pol
425438
res <- bimap STB identity <$>
426439
policy
427440
options
428-
(genUniqueChangeAddress utxo payee)
429-
(mkTx pm key)
441+
(mkTx utxo payee pm key)
430442
maxInputs
431443
(fmap Core.TxOutAux payee)
432444
utxo

0 commit comments

Comments
 (0)