-
Notifications
You must be signed in to change notification settings - Fork 12
Review Fee Sanity Check #199
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, this was the absolute check checking a value against a particular config, regardless of the underlying transaction. Now removed in favor of a relative one. |
||
|
||
|
||
-- | Size to use for a value of type @Attributes AddrAttributes@ when estimating | ||
-- encoded transaction sizes. The minimum possible value is 2. | ||
-- | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So this is the new check that should never be triggered. It's not an exception because that's not something we want to communicate to user in theory. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is it safe to skip minimum bound check? What if fees are too small? (the thing we were testing here https://github.com/input-output-hk/cardano-wallet/pull/199/files#diff-393909f5cc8109550702f2c76a1b4afdL405 ). Is it safe to assume that fees won't be less then estimated fees? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Safe for the users that's for sure. Safe for us, that's debatable. But indeed, the |
||
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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So this was move into the generic coin selection algorithm. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. except we don't check minimum bound (https://github.com/input-output-hk/cardano-wallet/pull/199/files#r245325794) which might be ok? EDIT: already answered cardano-foundation/cardano-wallet#199 (comment) |
||
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 | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The check is now "hardcoded" in the form of an invariant in the generic code.