Skip to content

Commit

Permalink
Merge pull request #77 from input-output-hk/jonathanknowles/fix-migra…
Browse files Browse the repository at this point in the history
…tion

Prevent the migration algorithm from creating money.
  • Loading branch information
jonathanknowles authored May 12, 2020
2 parents aae26dd + 1ecdf26 commit 14ef17a
Show file tree
Hide file tree
Showing 3 changed files with 42 additions and 26 deletions.
29 changes: 16 additions & 13 deletions src/library/Cardano/CoinSelection/Algorithm/Migration.hs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import Cardano.CoinSelection
, CoinSelectionLimit (..)
, coinMapFromList
, coinMapToList
, coinMapValue
, sumChange
, sumInputs
)
Expand All @@ -42,13 +43,14 @@ import Cardano.CoinSelection.Fee
, FeeBalancingPolicy (..)
, FeeEstimator (..)
, FeeOptions (..)
, isDust
)
import Control.Monad.Trans.State
( State, evalState, get, put )
import Data.List.NonEmpty
( NonEmpty ((:|)) )
import Data.Maybe
( fromMaybe, mapMaybe )
( fromMaybe )
import Data.Word
( Word16 )
import GHC.Generics
Expand Down Expand Up @@ -113,19 +115,20 @@ selectCoins options (BatchSize batchSize) utxo =
-- Note that the selection may look a bit weird at first sight as it has
-- no outputs (we are paying everything to ourselves!).
mkCoinSelection :: [CoinMapEntry i] -> CoinSelection i o
mkCoinSelection inps = CoinSelection
{ inputs = coinMapFromList inps
, outputs = mempty
, change =
let chgs = mapMaybe (noDust . entryValue) inps
in if null chgs then [threshold] else chgs
}
mkCoinSelection inputEntries = CoinSelection {inputs, outputs, change}
where
threshold = unDustThreshold dustThreshold
noDust :: Coin -> Maybe Coin
noDust c
| c < threshold = Nothing
| otherwise = Just c
inputs = coinMapFromList inputEntries
outputs = mempty
change
| null nonDustInputCoins && totalInputValue >= smallestNonDustCoin =
[smallestNonDustCoin]
| otherwise =
nonDustInputCoins
nonDustInputCoins = filter
(not . isDust dustThreshold)
(entryValue <$> inputEntries)
smallestNonDustCoin = C.succ $ unDustThreshold dustThreshold
totalInputValue = coinMapValue inputs

-- | Attempt to balance the coin selection by reducing or increasing the
-- change values based on the computed fees.
Expand Down
16 changes: 14 additions & 2 deletions src/library/Cardano/CoinSelection/Fee.hs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ module Cardano.CoinSelection.Fee

-- * Dust Processing
, DustThreshold (..)
, isDust
, coalesceDust

-- # Internal Functions
Expand Down Expand Up @@ -119,6 +120,17 @@ newtype DustThreshold = DustThreshold { unDustThreshold :: Coin }
deriving stock (Eq, Generic, Ord)
deriving Show via (Quiet DustThreshold)

-- | Returns 'True' if and only if the given 'Coin' is a __dust coin__
-- according to the given 'DustThreshold'.
--
-- A coin is considered to be a dust coin if it is /less than or equal to/
-- the threshold.
--
-- See 'DustThreshold'.
--
isDust :: DustThreshold -> Coin -> Bool
isDust (DustThreshold dt) c = c <= dt

-- | Provides a function capable of __estimating__ the transaction fee required
-- for a given coin selection, according to the rules of a particular
-- blockchain.
Expand Down Expand Up @@ -618,10 +630,10 @@ distributeFee (Fee feeTotal) coinsUnsafe =
-- >>> all (/= Coin 0) (coalesceDust threshold coins)
--
coalesceDust :: DustThreshold -> NonEmpty Coin -> [Coin]
coalesceDust (DustThreshold threshold) coins =
coalesceDust threshold coins =
splitCoin valueToDistribute coinsToKeep
where
(coinsToKeep, coinsToRemove) = NE.partition (> threshold) coins
(coinsToRemove, coinsToKeep) = NE.partition (isDust threshold) coins
valueToDistribute = F.fold coinsToRemove

-- Splits up the given coin of value __@v@__, distributing its value over the
Expand Down
23 changes: 12 additions & 11 deletions src/test/Cardano/CoinSelection/Algorithm/MigrationSpec.hs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
Expand Down Expand Up @@ -117,27 +118,27 @@ spec = do

describe "selectCoins properties" $ do
it "No coin selection has outputs" $
property $ withMaxSuccess 10000 $ prop_onlyChangeOutputs
property $ withMaxSuccess 10_000 $ prop_onlyChangeOutputs
@(Wrapped TxIn) @Address

it "Every coin in the selection change >= minimum threshold coin" $
property $ withMaxSuccess 10000 $ prop_noLessThanThreshold
it "Every coin in the selection change > dust threshold" $
property $ withMaxSuccess 10_000 $ prop_allAboveThreshold
@(Wrapped TxIn) @Address

it "Total input UTxO value >= sum of selection change coins" $
property $ withMaxSuccess 10000 $ prop_inputsGreaterThanOutputs
property $ withMaxSuccess 10_000 $ prop_inputsGreaterThanOutputs
@(Wrapped TxIn) @Address

it "Every selection input is unique" $
property $ withMaxSuccess 10000 $ prop_inputsAreUnique
property $ withMaxSuccess 10_000 $ prop_inputsAreUnique
@(Wrapped TxIn) @Address

it "Every selection input is a member of the UTxO" $
property $ withMaxSuccess 10000 $ prop_inputsStillInUTxO
property $ withMaxSuccess 10_000 $ prop_inputsStillInUTxO
@(Wrapped TxIn) @Address

it "Every coin selection is well-balanced" $
property $ withMaxSuccess 10000 $ prop_wellBalanced
property $ withMaxSuccess 10_000 $ prop_wellBalanced
@(Wrapped TxIn) @Address

describe "selectCoins regressions" $ do
Expand Down Expand Up @@ -177,18 +178,18 @@ prop_onlyChangeOutputs feeOpts batchSize utxo = do
coinMapToList . outputs =<< selectCoins feeOpts batchSize utxo
property (allOutputs `shouldSatisfy` null)

-- | Every coin in the selection change >= minimum threshold coin
prop_noLessThanThreshold
-- | Every coin in the selection change > dust threshold
prop_allAboveThreshold
:: forall i o . (Ord i, Ord o)
=> FeeOptions i o
-> BatchSize
-> CoinMap i
-> Property
prop_noLessThanThreshold feeOpts batchSize utxo = do
prop_allAboveThreshold feeOpts batchSize utxo = do
let allChange = change
=<< selectCoins feeOpts batchSize utxo
let undersizedCoins =
filter (< threshold) allChange
filter (<= threshold) allChange
property (undersizedCoins `shouldSatisfy` null)
where
threshold = unDustThreshold $ dustThreshold feeOpts
Expand Down

0 comments on commit 14ef17a

Please sign in to comment.