diff --git a/src/Cardano/Wallet/Kernel/CoinSelection/FromGeneric.hs b/src/Cardano/Wallet/Kernel/CoinSelection/FromGeneric.hs index 43ac46330..1282f0988 100644 --- a/src/Cardano/Wallet/Kernel/CoinSelection/FromGeneric.hs +++ b/src/Cardano/Wallet/Kernel/CoinSelection/FromGeneric.hs @@ -18,7 +18,6 @@ module Cardano.Wallet.Kernel.CoinSelection.FromGeneric ( , largestFirst -- * Estimating fees , estimateCardanoFee - , checkCardanoFeeSanity , boundAddrAttrSize , boundTxAttrSize -- * Estimating transaction limits @@ -44,7 +43,7 @@ import Pos.Chain.Txp as Core (TxIn, TxOutAux, Utxo, toaOut, import Pos.Core as Core (AddrAttributes, Address, Coin (..), TxSizeLinear, addCoin, calculateTxSizeLinear, checkCoin, divCoin, isRedeemAddress, maxCoinVal, mkCoin, subCoin, - txSizeLinearMinValue, unsafeMulCoin, unsafeSubCoin) + unsafeSubCoin) import Pos.Core.Attributes (Attributes) import Pos.Crypto (Signature) @@ -147,8 +146,6 @@ data InputGrouping = data CoinSelectionOptions = CoinSelectionOptions { csoEstimateFee :: Int -> NonEmpty Core.Coin -> Core.Coin -- ^ A function to estimate the fees. - , csoFeesSanityCheck :: Core.Coin -> Bool - -- ^ A function we can use to check if fees are not too big or too small. , csoInputGrouping :: InputGrouping -- ^ A preference regarding input grouping. , csoExpenseRegulation :: ExpenseRegulation @@ -162,10 +159,9 @@ data CoinSelectionOptions = CoinSelectionOptions { -- | Creates new 'CoinSelectionOptions' using 'NoGrouping' as default -- 'InputGrouping' and 'SenderPaysFee' as default 'ExpenseRegulation'. newOptions :: (Int -> NonEmpty Core.Coin -> Core.Coin) - -> (Core.Coin -> Bool) -> CoinSelectionOptions -newOptions estimateFee check = CoinSelectionOptions { + -> CoinSelectionOptions +newOptions estimateFee = CoinSelectionOptions { csoEstimateFee = estimateFee - , csoFeesSanityCheck = check , csoInputGrouping = IgnoreGrouping , csoExpenseRegulation = SenderPaysFee , csoDustThreshold = Core.mkCoin 0 @@ -396,15 +392,6 @@ estimateCardanoFee linearFeePolicy ins outs = ceiling $ calculateTxSizeLinear linearFeePolicy $ hi $ estimateSize boundAddrAttrSize boundTxAttrSize ins outs -checkCardanoFeeSanity :: TxSizeLinear -> Coin -> Bool -checkCardanoFeeSanity linearFeePolicy fees = - let - maxCoeff :: Int = 2 - minFees = Core.mkCoin $ floor $ txSizeLinearMinValue linearFeePolicy - in - (fees >= minFees) && (fees <= Core.unsafeMulCoin minFees maxCoeff) - - -- | Size to use for a value of type @Attributes AddrAttributes@ when estimating -- encoded transaction sizes. The minimum possible value is 2. -- diff --git a/src/Cardano/Wallet/Kernel/CoinSelection/Generic.hs b/src/Cardano/Wallet/Kernel/CoinSelection/Generic.hs index 4579e6bee..eb32e33fa 100644 --- a/src/Cardano/Wallet/Kernel/CoinSelection/Generic.hs +++ b/src/Cardano/Wallet/Kernel/CoinSelection/Generic.hs @@ -12,6 +12,7 @@ module Cardano.Wallet.Kernel.CoinSelection.Generic ( , Rounding(..) , Fee(..) , adjustFee + , valueSum , unsafeFeeSum , utxoEntryVal , sizeOfEntries @@ -156,6 +157,9 @@ newtype Fee dom = Fee { getFee :: Value dom } adjustFee :: (Value dom -> Value dom) -> Fee dom -> Fee dom adjustFee f = Fee . f . getFee +valueSum :: CoinSelDom dom => [Value dom] -> Maybe (Value dom) +valueSum = foldM valueAdd valueZero + unsafeFeeSum :: CoinSelDom dom => [Fee dom] -> Fee dom unsafeFeeSum = Fee . unsafeValueSum . map getFee diff --git a/src/Cardano/Wallet/Kernel/CoinSelection/Generic/Fees.hs b/src/Cardano/Wallet/Kernel/CoinSelection/Generic/Fees.hs index 639380eb4..a1cc99f7a 100644 --- a/src/Cardano/Wallet/Kernel/CoinSelection/Generic/Fees.hs +++ b/src/Cardano/Wallet/Kernel/CoinSelection/Generic/Fees.hs @@ -97,15 +97,33 @@ adjustForFees feeOptions pickUtxo css = do SenderPaysFee -> senderPaysFee pickUtxo feeOptions inps outs chgs - let neInps = case inps' of - [] -> error "adjustForFees: empty list of inputs" - i:is -> i :| is - - let neOuts = case outs' of - [] -> error "adjustForFees: empty list of outputs" - o:os -> o :| os - - return $ CoinSelFinalResult neInps neOuts chgs' + let estimatedFee = getFee $ feeUpperBound feeOptions inps outs chgs + let actualFee = getFee $ computeFee inps' outs' chgs' + -- NOTE + -- We enforce the following invariant: + -- + -- estimatedFee < actualFee < 2 * estimatedFee + -- + -- This coefficient (2*...) is mostly taken out of nowhere, but if anything + -- go beyond that upper bound, we would know that our algorithm for fee + -- reconciliation below is messed up. + -- Similarly, the algorithm tries to take money from inputs until it reaches + -- the goal fixed by 'estimatedFee'. So, the actualFee just can't be lower + -- than the goal. + -- + -- (PS: using `valueDiv` instead of `valueMul` to avoid overflow) + if (actualFee < estimatedFee || actualFee `valueDiv` 2 > estimatedFee) then + error $ "adjustForFees: fee out of bounds: " <> pretty actualFee <> " while expecting ~" <> pretty estimatedFee + else do + let neInps = case inps' of + [] -> error "adjustForFees: empty list of inputs" + i:is -> i :| is + + let neOuts = case outs' of + [] -> error "adjustForFees: empty list of outputs" + o:os -> o :| os + + return $ CoinSelFinalResult neInps neOuts chgs' {------------------------------------------------------------------------------- @@ -287,6 +305,47 @@ feeUpperBound FeeOptions{..} inps outs chgs = numInputs = fromIntegral $ sizeToWord $ selectedSize $ foldl' (flip select) emptySelection inps outputs = map outVal outs <> chgs +-- Computing actual fee is a bit tricky in the generic realm because we don't +-- know what type representation is used by the underlying implementation. So, +-- we can't just sum up all the input and substract the sub of all outputs +-- (incl. change) because we'll risk an overflow with each sum. Instead, we +-- reduce the input value iteratively, coin by coin using a safe distance +-- between coins that are known to be within bounds. +-- The algorithm converge because we know that by construction, there are less +-- outputs than inputs. In essence, this computes: +-- +-- fees = ∑ inputs - (∑ outputs + ∑ changes) +computeFee + :: forall dom. (CoinSelDom dom) + => [UtxoEntry dom] + -> [Output dom] + -> [Value dom] + -> Fee dom +computeFee inps outs chgs = + Fee $ collapse (map utxoEntryVal inps) (map outVal outs <> chgs) + where + -- Some remaining inputs together. At this point, we've removed + -- all outputs and changes, so what's left are simply the actual fees. + -- It's unrealistic to imagine them being bigger than the max coin value. + collapse plus [] = case valueSum plus of + Nothing -> error "fees are bigger than max coin value" + Just a -> a + + -- In order to safely compute fees at this level, we need make sure we don't + -- overflow. Therefore, we remove outputs to inputs until there's no outputs + -- left to remove. + collapse (p:ps) (m:ms) + | p > m = let p' = valueDist p m in collapse (p':ps) ms + | p < m = let m' = valueDist p m in collapse ps (m':ms) + | otherwise = collapse ps ms + + -- This branch can only happens if we've depleted all our inputs and there + -- are still some outputs left to remove from them. If means the total value + -- of outputs (incl. change) was bigger than the total input value which is + -- by definition, impossible; unless we messed up real hard. + collapse [] _ = + error "invariant violation: outputs are bigger than inputs" + -- | divvy fee across outputs, discarding zero-output if any. Returns `Nothing` -- when there's no more outputs after filtering, in which case, we just can't diff --git a/src/Cardano/Wallet/Kernel/Transactions.hs b/src/Cardano/Wallet/Kernel/Transactions.hs index b312e326c..5430733e3 100644 --- a/src/Cardano/Wallet/Kernel/Transactions.hs +++ b/src/Cardano/Wallet/Kernel/Transactions.hs @@ -11,7 +11,6 @@ module Cardano.Wallet.Kernel.Transactions ( , EstimateFeesError(..) , RedeemAdaError(..) , cardanoFee - , cardanoFeeSanity , mkStdTx , prepareUnsignedTxWithSources , submitSignedTx @@ -44,8 +43,7 @@ import Cardano.Crypto.Wallet (DerivationIndex) import qualified Cardano.Wallet.Kernel.Addresses as Kernel import Cardano.Wallet.Kernel.CoinSelection.FromGeneric (CoinSelFinalResult (..), CoinSelectionOptions (..), - checkCardanoFeeSanity, estimateCardanoFee, - estimateMaxTxInputs) + estimateCardanoFee, estimateMaxTxInputs) import qualified Cardano.Wallet.Kernel.CoinSelection.FromGeneric as CoinSelection import Cardano.Wallet.Kernel.CoinSelection.Generic (CoinSelHardErr (..)) @@ -230,13 +228,8 @@ newUnsignedTransaction ActiveWallet{..} options accountId payees = runExceptT $ -- that it may change in the future. let attributes = def :: TxAttributes let tx = UnsignedTx inputs outputs attributes coins - - -- STEP 3: Sanity test. Here we check whether our fees are within a reasonable - -- range. let fees = computeFeesOfUnsignedTx tx - if csoFeesSanityCheck options fees - then return (snapshot, tx, fees, availableUtxo) - else error $ "fees out of bound " <> show fees + return (snapshot, tx, fees, availableUtxo) where -- Generate an initial seed for the random generator using the hash of -- the payees, which ensure that the coin selection (and the fee estimation) @@ -640,12 +633,6 @@ cardanoFee (TxFeePolicyTxSizeLinear policy) inputs outputs = cardanoFee TxFeePolicyUnknown{} _ _ = error "cardanoFee: unknown policy" -cardanoFeeSanity :: TxFeePolicy -> Coin -> Bool -cardanoFeeSanity (TxFeePolicyTxSizeLinear policy) fees = - checkCardanoFeeSanity policy fees -cardanoFeeSanity TxFeePolicyUnknown{} _ = - error "cardanoFeeSanity: unknown policy" - {------------------------------------------------------------------------------- Ada redemption diff --git a/src/Cardano/Wallet/WalletLayer/Kernel/Active.hs b/src/Cardano/Wallet/WalletLayer/Kernel/Active.hs index 4fac44c90..b6dabf238 100644 --- a/src/Cardano/Wallet/WalletLayer/Kernel/Active.hs +++ b/src/Cardano/Wallet/WalletLayer/Kernel/Active.hs @@ -222,7 +222,7 @@ setupPayment :: Monad m ) setupPayment policy grouping regulation payment = do rootId <- fromRootId wId - let opts = (newOptions (Kernel.cardanoFee policy) (Kernel.cardanoFeeSanity policy)) { + let opts = (newOptions (Kernel.cardanoFee policy)) { csoExpenseRegulation = regulation , csoInputGrouping = grouping } diff --git a/test/unit/Test/Spec/CoinSelection.hs b/test/unit/Test/Spec/CoinSelection.hs index efffee190..dc09ff675 100644 --- a/test/unit/Test/Spec/CoinSelection.hs +++ b/test/unit/Test/Spec/CoinSelection.hs @@ -49,9 +49,10 @@ import Cardano.Wallet.Kernel.Util.Core (paymentAmount, utxoBalance, import Pos.Crypto.Signing.Safe (fakeSigner) import Test.Pos.Configuration (withProvidedMagicConfig) import Test.Spec.CoinSelection.Generators (InitialBalance (..), - Pay (..), genFiddlyPayees, genFiddlyUtxo, genGroupedUtxo, - genPayee, genPayees, genRedeemPayee, - genUniqueChangeAddress, genUtxoWithAtLeast) + Pay (..), genFiddlyPayees, genFiddlyUtxo, + genFragmentedUtxo, genGroupedUtxo, genPayee, genPayees, + genRedeemPayee, genUniqueChangeAddress, + genUtxoWithAtLeast) {-# ANN module ("HLint: ignore Reduce duplication" :: Text) #-} @@ -63,15 +64,9 @@ import Test.Spec.CoinSelection.Generators (InitialBalance (..), freeLunch :: Int -> NonEmpty Core.Coin -> Core.Coin freeLunch _ _ = Core.mkCoin 0 -freeLunchCheck :: Core.Coin -> Bool -freeLunchCheck c = c == Core.mkCoin 0 - -- | The smallest fee possible. minFee :: Int -> NonEmpty Core.Coin -> Core.Coin -minFee _ _ = Core.mkCoin 1 - -minFeeCheck :: Core.Coin -> Bool -minFeeCheck c = c == Core.mkCoin 1 +minFee _ outs = Core.mkCoin (fromIntegral $ NE.length outs) -- | An hopefully-accurate estimate of the Tx fees in Cardano. cardanoFee :: Int -> NonEmpty Core.Coin -> Core.Coin @@ -80,21 +75,10 @@ cardanoFee inputs outputs = Core.mkCoin $ where linearFeePolicy = TxSizeLinear (Coeff 155381) (Coeff 43.946) -cardanoFeeCheck :: Core.Coin -> Bool -cardanoFeeCheck fees = - let - minFees = Core.mkCoin 155381 - maxCoeff :: Int = 2 - in - (fees >= minFees) && (fees <= Core.unsafeMulCoin minFees maxCoeff) - -- | A simple linear fee proportional in the #inputs & #outputs. linearFee :: Int -> NonEmpty Core.Coin -> Core.Coin linearFee inputsLen outputs = Core.mkCoin (fromIntegral $ inputsLen + length outputs) -linearFeeCheck :: Core.Coin -> Bool -linearFeeCheck _ = True - -- | For some reason the version of 'QuickCheck' we are using doesn't seem -- to export 'withMaxSuccess'. withMaxSuccess :: Int -> Spec -> Spec @@ -503,18 +487,17 @@ payRestrictInputsTo :: ProtocolMagic -> (InitialBalance -> Gen Core.Utxo) -> (Core.Utxo -> Pay -> Gen (NonEmpty Core.TxOut)) -> (Int -> NonEmpty Core.Coin -> Core.Coin) - -> (Core.Coin -> Bool) -> (CoinSelectionOptions -> CoinSelectionOptions) -> InitialBalance -> Pay -> Policy -> Gen RunResult -payRestrictInputsTo pm maxInputs genU genP feeFunction feeSanity adjustOptions bal amount policy = +payRestrictInputsTo pm maxInputs genU genP feeFunction adjustOptions bal amount policy = withProvidedMagicConfig pm $ \genesisConfig _ _ -> do utxo <- genU bal payee <- genP utxo amount key <- arbitrary - let options = adjustOptions (newOptions feeFunction feeSanity) + let options = adjustOptions (newOptions feeFunction) res <- policy options maxInputs (fmap Core.TxOutAux payee) @@ -534,7 +517,6 @@ pay :: ProtocolMagic -> (InitialBalance -> Gen Core.Utxo) -> (Core.Utxo -> Pay -> Gen (NonEmpty Core.TxOut)) -> (Int -> NonEmpty Core.Coin -> Core.Coin) - -> (Core.Coin -> Bool) -> (CoinSelectionOptions -> CoinSelectionOptions) -> InitialBalance -> Pay @@ -544,7 +526,6 @@ pay pm = payRestrictInputsTo pm maxNumInputs payOne :: ProtocolMagic -> (Int -> NonEmpty Core.Coin -> Core.Coin) - -> (Core.Coin -> Bool) -> (CoinSelectionOptions -> CoinSelectionOptions) -> InitialBalance -> Pay @@ -556,7 +537,6 @@ payOne pm = pay pm genUtxoWithAtLeast genPayee payOne' :: ProtocolMagic -> (Core.Utxo -> Pay -> Gen (NonEmpty Core.TxOut)) -> (Int -> NonEmpty Core.Coin -> Core.Coin) - -> (Core.Coin -> Bool) -> (CoinSelectionOptions -> CoinSelectionOptions) -> InitialBalance -> Pay @@ -566,7 +546,6 @@ payOne' pm payeeGenerator = pay pm genUtxoWithAtLeast payeeGenerator payBatch :: ProtocolMagic -> (Int -> NonEmpty Core.Coin -> Core.Coin) - -> (Core.Coin -> Bool) -> (CoinSelectionOptions -> CoinSelectionOptions) -> InitialBalance -> Pay @@ -591,65 +570,65 @@ spec = describe "Coin selection policies unit tests" $ do withMaxSuccess 5 $ describe "largestFirst" $ do prop "one payee, SenderPaysFee, fee = 0" $ \pm -> forAll ( - payOne pm freeLunch freeLunchCheck identity (InitialLovelace 1000) (PayLovelace 100) largestFirst + payOne pm freeLunch identity (InitialLovelace 1000) (PayLovelace 100) largestFirst ) $ \(utxo, payee, res) -> paymentSucceeded utxo payee res prop "one payee, ReceiverPaysFee, fee = 0" $ \pm -> forAll ( - payOne pm freeLunch freeLunchCheck receiverPays (InitialLovelace 1000) (PayLovelace 100) largestFirst + payOne pm freeLunch receiverPays (InitialLovelace 1000) (PayLovelace 100) largestFirst ) $ \(utxo, payee, res) -> paymentSucceeded utxo payee res prop "multiple payees, SenderPaysFee, fee = 0" $ \pm -> forAll ( - payBatch pm freeLunch freeLunchCheck identity (InitialLovelace 1000) (PayLovelace 100) largestFirst + payBatch pm freeLunch identity (InitialLovelace 1000) (PayLovelace 100) largestFirst ) $ \(utxo, payee, res) -> paymentSucceeded utxo payee res prop "multiple payees, ReceiverPaysFee, fee = 0" $ \pm -> forAll ( - payBatch pm freeLunch freeLunchCheck receiverPays (InitialLovelace 1000) (PayLovelace 100) largestFirst + payBatch pm freeLunch receiverPays (InitialLovelace 1000) (PayLovelace 100) largestFirst ) $ \(utxo, payee, res) -> paymentSucceeded utxo payee res -- Minimal fee prop "one payee, SenderPaysFee, fee = 1 Lovelace" $ \pm -> forAll ( - payOne pm minFee minFeeCheck identity (InitialLovelace 1000) (PayLovelace 100) largestFirst + payOne pm minFee identity (InitialLovelace 1000) (PayLovelace 100) largestFirst ) $ \(utxo, payee, res) -> paymentSucceeded utxo payee res prop "one payee, ReceiverPaysFee, fee = 1 Lovelace" $ \pm -> forAll ( - payOne pm minFee minFeeCheck receiverPays (InitialLovelace 1000) (PayLovelace 100) largestFirst + payOne pm minFee receiverPays (InitialLovelace 1000) (PayLovelace 100) largestFirst ) $ \(utxo, payee, res) -> paymentSucceeded utxo payee res prop "multiple payees, SenderPaysFee, fee = 1 Lovelace" $ \pm -> forAll ( - payBatch pm minFee minFeeCheck identity (InitialLovelace 1000) (PayLovelace 100) largestFirst + payBatch pm minFee identity (InitialLovelace 1000) (PayLovelace 100) largestFirst ) $ \(utxo, payee, res) -> paymentSucceeded utxo payee res prop "multiple payees, ReceiverPaysFee, fee = 1 Lovelace" $ \pm -> forAll ( - payBatch pm minFee minFeeCheck receiverPays (InitialLovelace 1000) (PayLovelace 100) largestFirst + payBatch pm minFee receiverPays (InitialLovelace 1000) (PayLovelace 100) largestFirst ) $ \(utxo, payee, res) -> paymentSucceeded utxo payee res withMaxSuccess 5 $ describe "random" $ do prop "one payee, SenderPaysFee, fee = 0" $ \pm -> forAll ( - payOne pm freeLunch freeLunchCheck identity (InitialLovelace 1000) (PayLovelace 100) random + payOne pm freeLunch identity (InitialLovelace 1000) (PayLovelace 100) random ) $ \(utxo, payee, res) -> paymentSucceeded utxo payee res prop "one payee, ReceiverPaysFee, fee = 0" $ \pm -> forAll ( - payOne pm freeLunch freeLunchCheck receiverPays (InitialLovelace 1000) (PayLovelace 100) random + payOne pm freeLunch receiverPays (InitialLovelace 1000) (PayLovelace 100) random ) $ \(utxo, payee, res) -> paymentSucceeded utxo payee res prop "multiple payees, SenderPaysFee, fee = 0" $ \pm -> forAll ( - payBatch pm freeLunch freeLunchCheck identity (InitialLovelace 1000) (PayLovelace 100) random + payBatch pm freeLunch identity (InitialLovelace 1000) (PayLovelace 100) random ) $ \(utxo, payee, res) -> paymentSucceeded utxo payee res prop "multiple payees, ReceiverPaysFee, fee = 0" $ \pm -> forAll ( - payBatch pm freeLunch freeLunchCheck receiverPays (InitialLovelace 1000) (PayLovelace 100) random + payBatch pm freeLunch receiverPays (InitialLovelace 1000) (PayLovelace 100) random ) $ \(utxo, payee, res) -> paymentSucceeded utxo payee res -- minimal fee. It doesn't make sense to use it for 'ReceiverPaysFee', because -- rounding will essentially cause the computed @epsilon@ will be 0 for each -- output. For those cases, we use the 'linear' fee policy. prop "one payee, SenderPaysFee, fee = 1 Lovelace" $ \pm -> forAll ( - payOne pm minFee minFeeCheck identity (InitialLovelace 1000) (PayLovelace 100) random + payOne pm minFee identity (InitialLovelace 1000) (PayLovelace 100) random ) $ \(utxo, payee, res) -> paymentSucceededWith utxo payee res [feeWasPayed SenderPaysFee] prop "multiple payees, SenderPaysFee, fee = 1 Lovelace" $ \pm -> forAll ( - payBatch pm minFee minFeeCheck identity (InitialLovelace 1000) (PayLovelace 100) random + payBatch pm minFee identity (InitialLovelace 1000) (PayLovelace 100) random ) $ \(utxo, payee, res) -> paymentSucceededWith utxo payee res [feeWasPayed SenderPaysFee] -- linear fee prop "one payee, ReceiverPaysFee, fee = linear" $ \pm -> forAll ( - payOne pm linearFee linearFeeCheck receiverPays (InitialLovelace 1000) (PayLovelace 100) random + payOne pm linearFee receiverPays (InitialLovelace 1000) (PayLovelace 100) random ) $ \(utxo, payee, res) -> paymentSucceededWith utxo payee res [feeWasPayed ReceiverPaysFee] prop "multiple payees, ReceiverPaysFee, fee = linear" $ \pm -> forAll ( - payBatch pm linearFee linearFeeCheck receiverPays (InitialLovelace 1000) (PayLovelace 100) random + payBatch pm linearFee receiverPays (InitialLovelace 1000) (PayLovelace 100) random ) $ \(utxo, payee, res) -> paymentSucceededWith utxo payee res [feeWasPayed ReceiverPaysFee] @@ -659,37 +638,37 @@ spec = -- like attributes, and trying to setup syntetic experiments with -- less than 1ADA (10^6 lovelaces) is probably counter-productive prop "one payee, SenderPaysFee, fee = cardano" $ \pm -> forAll ( - payOne pm cardanoFee cardanoFeeCheck identity (InitialADA 1000) (PayADA 100) random + payOne pm cardanoFee identity (InitialADA 1000) (PayADA 100) random ) $ \(utxo, payee, res) -> paymentSucceededWith utxo payee res [feeWasPayed SenderPaysFee] prop "multiple payees, SenderPaysFee, fee = cardano" $ \pm -> forAll ( - payBatch pm cardanoFee cardanoFeeCheck identity (InitialADA 1000) (PayADA 100) random + pay pm genFragmentedUtxo genPayees cardanoFee identity (InitialADA 1000) (PayADA 100) random ) $ \(utxo, payee, res) -> paymentSucceededWith utxo payee res [feeWasPayed SenderPaysFee] prop "one payee, ReceiverPaysFee, fee = cardano" $ \pm -> forAll ( - payOne pm cardanoFee cardanoFeeCheck receiverPays (InitialADA 1000) (PayADA 100) random + payOne pm cardanoFee receiverPays (InitialADA 1000) (PayADA 100) random ) $ \(utxo, payee, res) -> paymentSucceededWith utxo payee res [feeWasPayed ReceiverPaysFee] prop "multiple payees, ReceiverPaysFee, fee = cardano" $ \pm -> forAll ( - payBatch pm cardanoFee cardanoFeeCheck receiverPays (InitialADA 1000) (PayADA 100) random + payBatch pm cardanoFee receiverPays (InitialADA 1000) (PayADA 100) random ) $ \(utxo, payee, res) -> paymentSucceededWith utxo payee res [feeWasPayed ReceiverPaysFee] withMaxSuccess 5 $ describe "Expected failures" $ do prop "Paying a redeem address should always be rejected" $ \pm -> forAll ( - payOne' pm genRedeemPayee linearFee linearFeeCheck receiverPays (InitialLovelace 1000) (PayLovelace 100) random + payOne' pm genRedeemPayee linearFee receiverPays (InitialLovelace 1000) (PayLovelace 100) random ) $ \(utxo, payee, res) -> paymentFailedWith utxo payee res [errorWas outputWasRedeem] prop "Paying somebody not having enough money should fail" $ \pm -> forAll ( - payBatch pm linearFee linearFeeCheck receiverPays (InitialLovelace 10) (PayLovelace 100) random + payBatch pm linearFee receiverPays (InitialLovelace 10) (PayLovelace 100) random ) $ \(utxo, payee, res) -> do paymentFailedWith utxo payee res [errorWas notEnoughMoney] prop "Restricting too much the number of inputs results in an hard error for a single payee" $ \pm -> forAll ( - payRestrictInputsTo pm 1 genUtxoWithAtLeast genPayee freeLunch freeLunchCheck identity (InitialLovelace 200) (PayLovelace 100) random + payRestrictInputsTo pm 1 genUtxoWithAtLeast genPayee freeLunch identity (InitialLovelace 200) (PayLovelace 100) random ) $ \(utxo, payee, res) -> do paymentFailedWith utxo payee res [errorWas maxInputsReached] prop "Restricting too much the number of inputs results in an hard error for multiple payees" $ \pm -> forAll ( - payRestrictInputsTo pm 1 genUtxoWithAtLeast genPayees freeLunch freeLunchCheck identity (InitialLovelace 200) (PayLovelace 100) random + payRestrictInputsTo pm 1 genUtxoWithAtLeast genPayees freeLunch identity (InitialLovelace 200) (PayLovelace 100) random ) $ \(utxo, payee, res) -> do paymentFailedWith utxo payee res [errorWas maxInputsReached] @@ -699,11 +678,11 @@ spec = -- the average in Cardano. withMaxSuccess 5 $ describe "Fiddly Addresses" $ do prop "multiple payees, SenderPaysFee, fee = cardano" $ \pm -> forAll ( - pay pm genFiddlyUtxo genFiddlyPayees cardanoFee cardanoFeeCheck identity (InitialADA 1000) (PayADA 100) random + pay pm genFiddlyUtxo genFiddlyPayees cardanoFee identity (InitialADA 1000) (PayADA 100) random ) $ \(utxo, payee, res) -> paymentSucceededWith utxo payee res [feeWasPayed SenderPaysFee] prop "multiple payees, ReceiverPaysFee, fee = cardano" $ \pm -> forAll ( - pay pm genFiddlyUtxo genFiddlyPayees cardanoFee cardanoFeeCheck receiverPays (InitialADA 1000) (PayADA 100) random + pay pm genFiddlyUtxo genFiddlyPayees cardanoFee receiverPays (InitialADA 1000) (PayADA 100) random ) $ \(utxo, payee, res) -> paymentSucceededWith utxo payee res [feeWasPayed ReceiverPaysFee] @@ -719,23 +698,23 @@ spec = -- the associated inputs paying into the address we just picked. withMaxSuccess 5 $ describe "Input Grouping" $ do prop "Require grouping, fee = 0, one big group depletes the Utxo completely" $ \pm -> forAll ( - pay pm (genGroupedUtxo 1) genPayee freeLunch freeLunchCheck requireGrouping (InitialLovelace 1000) (PayLovelace 10) random + pay pm (genGroupedUtxo 1) genPayee freeLunch requireGrouping (InitialLovelace 1000) (PayLovelace 10) random ) $ \(utxo, payee, res) -> do paymentSucceededWith utxo payee res [utxoWasDepleted] prop "Require grouping, fee = cardano, one big group depletes the Utxo completely" $ \pm -> forAll ( - pay pm (genGroupedUtxo 1) genPayee freeLunch freeLunchCheck requireGrouping (InitialADA 1000) (PayADA 10) random + pay pm (genGroupedUtxo 1) genPayee freeLunch requireGrouping (InitialADA 1000) (PayADA 10) random ) $ \(utxo, payee, res) -> do paymentSucceededWith utxo payee res [utxoWasDepleted] prop "Require grouping, fee = 0, several groups allows the payment to be fullfilled" $ \pm -> forAll ( - pay pm (genGroupedUtxo 10) genPayee freeLunch freeLunchCheck requireGrouping (InitialLovelace 1000) (PayLovelace 10) random + pay pm (genGroupedUtxo 10) genPayee freeLunch requireGrouping (InitialLovelace 1000) (PayLovelace 10) random ) $ \(utxo, payee, res) -> do paymentSucceeded utxo payee res prop "Prefer grouping, fee = 0" $ \pm -> forAll ( - payOne pm freeLunch freeLunchCheck preferGrouping (InitialLovelace 1000) (PayLovelace 10) random + payOne pm freeLunch preferGrouping (InitialLovelace 1000) (PayLovelace 10) random ) $ \(utxo, payee, res) -> do paymentSucceeded utxo payee res prop "IgnoreGrouping, fee = 0 must not deplete the utxo" $ \pm -> forAll ( - pay pm (genGroupedUtxo 1) genPayee freeLunch freeLunchCheck ignoreGrouping (InitialLovelace 1000) (PayLovelace 10) random + pay pm (genGroupedUtxo 1) genPayee freeLunch ignoreGrouping (InitialLovelace 1000) (PayLovelace 10) random ) $ \(utxo, payee, res) -> do paymentSucceededWith utxo payee res [utxoWasNotDepleted] diff --git a/test/unit/Test/Spec/CoinSelection/Generators.hs b/test/unit/Test/Spec/CoinSelection/Generators.hs index 9ead2ed9e..56a3724e8 100644 --- a/test/unit/Test/Spec/CoinSelection/Generators.hs +++ b/test/unit/Test/Spec/CoinSelection/Generators.hs @@ -14,6 +14,7 @@ module Test.Spec.CoinSelection.Generators ( , Pay(..) , genUniqueChangeAddress , genUtxoWithAtLeast + , genFragmentedUtxo , genRedeemPayee ) where @@ -204,6 +205,21 @@ genUtxoWithAtLeast payment = do , allowRedeemAddresses = False } +-- | Generate a very fragment Utxo with @at least@ the supplied amount of money. +genFragmentedUtxo :: InitialBalance -> Gen Core.Utxo +genFragmentedUtxo payment = do + let balance = toLovelaces payment + twoPercentOf = balance `div` 50 + genUtxo $ StakeGenOptions { + stakeMaxValue = Just (Core.mkCoin twoPercentOf) + , stakeGenerationTarget = AtLeast + , stakeNeeded = Core.mkCoin (toLovelaces payment) + , stakeMaxEntries = Just 1000 + , fiddlyAddresses = False + , allowRedeemAddresses = False + } + + {------------------------------------------------------------------------------- Dealing with grouping -------------------------------------------------------------------------------} diff --git a/test/unit/Test/Spec/GetTransactions.hs b/test/unit/Test/Spec/GetTransactions.hs index f10d33d5a..d11f3d4db 100644 --- a/test/unit/Test/Spec/GetTransactions.hs +++ b/test/unit/Test/Spec/GetTransactions.hs @@ -56,7 +56,6 @@ import Cardano.Wallet.Kernel.DB.TxMeta import qualified Cardano.Wallet.Kernel.DB.Util.IxSet as IxSet import Cardano.Wallet.Kernel.Internal import qualified Cardano.Wallet.Kernel.Keystore as Keystore -import qualified Cardano.Wallet.Kernel.NodeStateAdaptor as Node import qualified Cardano.Wallet.Kernel.PrefilterTx as Kernel import qualified Cardano.Wallet.Kernel.Read as Kernel import qualified Cardano.Wallet.Kernel.Transactions as Kernel @@ -65,13 +64,12 @@ import Cardano.Wallet.WalletLayer (ActiveWalletLayer (..), walletPassiveLayer) import qualified Cardano.Wallet.WalletLayer as WalletLayer import qualified Cardano.Wallet.WalletLayer.Kernel.Accounts as Accounts -import qualified Cardano.Wallet.WalletLayer.Kernel.Active as Active import qualified Cardano.Wallet.WalletLayer.Kernel.Conv as Kernel.Conv import Cardano.Wallet.WalletLayer.Kernel.Transactions (toTransaction) import qualified Test.Spec.Addresses as Addresses import Test.Spec.CoinSelection.Generators (InitialBalance (..), - Pay (..), genPayeeWithNM, genUtxoWithAtLeast) + Pay (..), genUtxoWithAtLeast) import qualified Test.Spec.Fixture as Fixture import qualified Test.Spec.NewPayment as NewPayment import Test.Spec.TxMetaStorage (Isomorphic (..), genMeta) @@ -146,62 +144,6 @@ prepareFixtures nm initialBalance = do , fixturePw = pw } -prepareUTxoFixtures :: NetworkMagic - -> [Word64] - -> Fixture.GenActiveWalletFixture Fix -prepareUTxoFixtures nm coins = do - let (_, esk) = safeDeterministicKeyGen (B.pack $ replicate 32 0x42) mempty - let newRootId = eskToHdRootId nm esk - newRoot <- initHdRoot <$> pure newRootId - <*> pure (WalletName "A wallet") - <*> pure NoSpendingPassword - <*> pure AssuranceLevelNormal - <*> (InDb <$> pick arbitrary) - - newAccountId <- HdAccountId newRootId <$> deriveIndex (pick . choose) HdAccountIx HardDerivation - utxo <- foldlM (\acc coin -> do - newIndex <- deriveIndex (pick . choose) HdAddressIx HardDerivation - txIn <- pick $ Core.TxInUtxo <$> arbitrary <*> arbitrary - let Just (addr, _) = deriveLvl2KeyPair nm - (IsBootstrapEraAddr True) - (ShouldCheckPassphrase True) - mempty - esk - (newAccountId ^. hdAccountIdIx . to getHdAccountIx) - (getHdAddressIx newIndex) - return $ M.insert txIn (TxOutAux (TxOut addr coin)) acc - ) M.empty (mkCoin <$> coins) - return $ \keystore aw -> do - let pw = Kernel.walletPassive aw - Keystore.insert newRootId esk keystore - let accounts = Kernel.prefilterUtxo newRootId esk utxo - hdAccountId = Kernel.defaultHdAccountId newRootId - hdAddress = Kernel.defaultHdAddress nm esk emptyPassphrase newRootId - - void $ liftIO $ update (pw ^. wallets) (CreateHdWallet newRoot hdAccountId hdAddress accounts) - return $ Fix { - fixtureHdRootId = newRootId - , fixtureHdRoot = newRoot - , fixtureAccountId = newAccountId - , fixtureESK = esk - , fixtureUtxo = utxo - } - -withUtxosFixture :: MonadIO m - => ProtocolMagic - -> [Word64] - -> ( Keystore.Keystore - -> WalletLayer.ActiveWalletLayer m - -> Kernel.ActiveWallet - -> Fix - -> IO a - ) - -> PropertyM IO a -withUtxosFixture pm coins cc = - Fixture.withActiveWalletFixture pm (prepareUTxoFixtures nm coins) cc - where - nm = makeNetworkMagic pm - withFixture :: MonadIO m => ProtocolMagic -> InitialBalance @@ -263,75 +205,9 @@ getAccountBalanceNow pw Fix{..} = do constantFee :: Word64 -> Int -> NonEmpty Coin -> Coin constantFee c _ _ = mkCoin c -constantFeeCheck :: Word64 -> Coin -> Bool -constantFeeCheck c c' = mkCoin c == c' - spec :: Spec spec = do describe "GetTransactions" $ do - prop "utxo fixture creates the correct balance" $ withMaxSuccess 5 $ - monadicIO $ do - pm <- pick arbitrary - withUtxosFixture @IO pm [1,2,3] $ \_keystore _activeLayer aw f@Fix{..} -> do - let pw = Kernel.walletPassive aw - balance <- getAccountBalanceNow pw f - balance `shouldBe` 6 - - prop "sanity tests checks" $ withMaxSuccess 5 $ - monadicIO $ do - pm <- pick arbitrary - Fixture.withPassiveWalletFixture @IO pm (return $ \_ -> return ()) $ \_ _ pw _ -> do - policy <- Node.getFeePolicy (pw ^. Kernel.walletNode) - let checker = Kernel.cardanoFeeSanity policy . mkCoin - checker 100 `shouldBe` False - checker 155380 `shouldBe` False - checker 155381 `shouldBe` True - checker 213345 `shouldBe` True - checker (2 * 155381) `shouldBe` True - checker (2 * 155381 + 1) `shouldBe` False - checker 755381 `shouldBe` False - - prop "pay works normally for coin selection with additional utxos and changes" $ withMaxSuccess 5 $ - monadicIO $ do - pm <- pick arbitrary - let nm = makeNetworkMagic pm - distr <- fmap (\(TxOut addr coin) -> V1.PaymentDistribution (V1.WalAddress addr) (V1.WalletCoin coin)) - <$> pick (genPayeeWithNM nm mempty (PayLovelace 100)) - withUtxosFixture @IO pm [300, 400, 500, 600, 5000000] $ \_keystore _activeLayer aw f@Fix{..} -> do - let pw = Kernel.walletPassive aw - -- get the balance before the payment - coinsBefore <- getAccountBalanceNow pw f - -- do the payment - let src = V1.PaymentSource (Kernel.Conv.toRootId fixtureHdRootId) - (V1.unsafeMkAccountIndex $ getHdAccountIx $ fixtureAccountId ^. hdAccountIdIx) - payment = V1.Payment src distr Nothing Nothing - Right _ <- Active.pay aw emptyPassphrase PreferGrouping SenderPaysFee payment - -- get the balance after the payment. - coinsAfter <- getAccountBalanceNow pw f - -- sanity check. - policy <- Node.getFeePolicy (pw ^. Kernel.walletNode) - let checker = Kernel.cardanoFeeSanity policy . mkCoin - -- payment is very small so difference is almost equa to fees. - coinsBefore - coinsAfter `shouldSatisfy` checker - - prop "estimateFees looks sane for coin selection with additional utxos and changes" $ withMaxSuccess 5 $ - monadicIO $ do - pm <- pick arbitrary - let nm = makeNetworkMagic pm - distr <- fmap (\(TxOut addr coin) -> V1.PaymentDistribution (V1.WalAddress addr) (V1.WalletCoin coin)) - <$> pick (genPayeeWithNM nm mempty (PayLovelace 100)) - withUtxosFixture @IO pm [300, 400, 500, 600, 5000000] $ \_keystore _activeLayer aw Fix{..} -> do - let pw = Kernel.walletPassive aw - -- do the payment - let src = V1.PaymentSource (Kernel.Conv.toRootId fixtureHdRootId) - (V1.unsafeMkAccountIndex $ getHdAccountIx $ fixtureAccountId ^. hdAccountIdIx) - payment = V1.Payment src distr Nothing Nothing - Right c <- Active.estimateFees aw PreferGrouping SenderPaysFee payment - -- sanity check. - policy <- Node.getFeePolicy (pw ^. Kernel.walletNode) - let checker = Kernel.cardanoFeeSanity policy - c `shouldSatisfy` checker - prop "scenario: Layer.CreateAddress -> TxMeta.putTxMeta -> Layer.getTransactions works properly." $ withMaxSuccess 5 $ monadicIO $ do testMetaSTB <- pick genMeta @@ -593,7 +469,7 @@ spec = do payAux :: Kernel.ActiveWallet -> HdAccountId -> NonEmpty (Address, Coin) -> Word64 -> IO (Core.Tx, TxMeta) payAux aw hdAccountId payees fees = do - let opts = (newOptions (constantFee fees) (constantFeeCheck fees)) { + let opts = (newOptions (constantFee fees)) { csoExpenseRegulation = SenderPaysFee , csoInputGrouping = IgnoreGrouping } diff --git a/test/unit/Test/Spec/NewPayment.hs b/test/unit/Test/Spec/NewPayment.hs index cf81422ce..00a7a7774 100644 --- a/test/unit/Test/Spec/NewPayment.hs +++ b/test/unit/Test/Spec/NewPayment.hs @@ -148,9 +148,6 @@ withFixture pm initialBalance toPay cc = constantFee :: Int -> NonEmpty Coin -> Coin constantFee _ _ = mkCoin 10 -constantFeeCheck :: Coin -> Bool -constantFeeCheck c = c == mkCoin 10 - -- | Helper function to facilitate payments via the Layer or Servant. withPayment :: MonadIO n => ProtocolMagic @@ -200,7 +197,7 @@ spec = describe "NewPayment" $ do pm <- pick arbitrary withFixture @IO pm (InitialADA 10000) (PayLovelace 10) $ \_ _ aw Fixture{..} -> do policy <- Node.getFeePolicy (Kernel.walletPassive aw ^. Kernel.walletNode) - let opts = (newOptions (Kernel.cardanoFee policy) (Kernel.cardanoFeeSanity policy)) { + let opts = (newOptions (Kernel.cardanoFee policy)) { csoExpenseRegulation = SenderPaysFee , csoInputGrouping = IgnoreGrouping } @@ -217,7 +214,7 @@ spec = describe "NewPayment" $ do pm <- pick arbitrary withFixture @IO pm (InitialADA 10000) (PayADA 1) $ \_ _ aw Fixture{..} -> do policy <- Node.getFeePolicy (Kernel.walletPassive aw ^. Kernel.walletNode) - let opts = (newOptions (Kernel.cardanoFee policy) (Kernel.cardanoFeeSanity policy)) { + let opts = (newOptions (Kernel.cardanoFee policy)) { csoExpenseRegulation = ReceiverPaysFee , csoInputGrouping = IgnoreGrouping } @@ -260,7 +257,7 @@ spec = describe "NewPayment" $ do monadicIO $ do pm <- pick arbitrary withFixture @IO pm (InitialADA 10000) (PayADA 1) $ \_ _ aw Fixture{..} -> do - let opts = (newOptions constantFee constantFeeCheck) { + let opts = (newOptions constantFee) { csoExpenseRegulation = SenderPaysFee , csoInputGrouping = IgnoreGrouping } @@ -279,7 +276,7 @@ spec = describe "NewPayment" $ do monadicIO $ do pm <- pick arbitrary withFixture @IO pm (InitialADA 10000) (PayADA 1) $ \_ _ aw Fixture{..} -> do - let opts = (newOptions constantFee constantFeeCheck) { + let opts = (newOptions constantFee) { csoExpenseRegulation = SenderPaysFee , csoInputGrouping = IgnoreGrouping } @@ -298,7 +295,7 @@ spec = describe "NewPayment" $ do pm <- pick arbitrary withFixture @IO pm (InitialADA 10000) (PayADA 1) $ \_ _ aw Fixture{..} -> do policy <- Node.getFeePolicy (Kernel.walletPassive aw ^. Kernel.walletNode) - let opts = (newOptions (Kernel.cardanoFee policy) (Kernel.cardanoFeeSanity policy)) { + let opts = (newOptions (Kernel.cardanoFee policy)) { csoExpenseRegulation = SenderPaysFee , csoInputGrouping = IgnoreGrouping }