Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move important UTxO state transition functions to the top-level and test them #2848

Merged
merged 32 commits into from
Sep 3, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
88c9886
Move important UTxO state transition function to the top-level
sevanspowell Aug 25, 2021
740c71e
Clear up usage of knownTxO a little
sevanspowell Aug 27, 2021
05d18c6
Clear up language around "knownTxO" a bit more
sevanspowell Aug 27, 2021
46abf39
Further clear up language around "knownTxO"
sevanspowell Aug 27, 2021
94405f7
Substitute "difference" with "excluding"
sevanspowell Aug 27, 2021
78ffacb
Move filterOurUTxOs function to UTxO module and rename
sevanspowell Aug 27, 2021
d9674b6
Fix imports
sevanspowell Aug 27, 2021
7ce039d
Generalize comments and test names for `filterByAddress`.
jonathanknowles Aug 30, 2021
15afb10
Move `UTxO` type specific property tests to `UTxOSpec` module.
jonathanknowles Aug 30, 2021
5f589fc
Move `addressParity` function to `Address.Gen` module.
jonathanknowles Aug 30, 2021
d275095
Add property test `prop_filterByAddress_matchSome`.
jonathanknowles Aug 30, 2021
e49c2b5
Merge pull request #2860 from input-output-hk/jonathanknowles/adp-109…
sevanspowell Aug 31, 2021
7717363
Add spendTx
sevanspowell Aug 31, 2021
affd550
Add commentary and re-arrange property of spendTx
sevanspowell Sep 2, 2021
a4dab5c
isOurs' -> isOurAddress
sevanspowell Sep 2, 2021
34d15b8
Make sure not to filter entire UTxO
sevanspowell Sep 2, 2021
e15099a
Use Set.disjoint
sevanspowell Sep 2, 2021
efafab3
Formatting
sevanspowell Sep 2, 2021
41b9033
Use Arbitrary instances
sevanspowell Sep 2, 2021
75a4e45
Add coverage checks to tests
sevanspowell Sep 2, 2021
ffedc07
Fix grammar
sevanspowell Sep 2, 2021
fde40f8
Use `Arbitrary UTxO` instance to simplify `UTxOSpec`.
jonathanknowles Sep 3, 2021
7465e2b
Add `coarbitraryAddress` function to `Address.Gen`.
jonathanknowles Sep 3, 2021
355b3f3
Use `CoArbitrary Address` instance to strengthen UTxO property tests.
jonathanknowles Sep 3, 2021
5f9ff22
Add coverage checks to `UTxOSpec`.
jonathanknowles Sep 3, 2021
9eb9b66
Use `CoArbitrary Address` instance to strengthen `filterByAddress` te…
jonathanknowles Sep 3, 2021
9dda3c6
Make `genTx` sometimes generate transactions with zero outputs.
jonathanknowles Sep 3, 2021
804037c
Add coverage checks to individual `ModelSpec` properties.
jonathanknowles Sep 3, 2021
25d9b6f
Reduce long execution time of `prop_tx_utxo_coverage`.
jonathanknowles Sep 3, 2021
7cfc013
Strengthen coverage check for `prop_spendTx_balance_inequality`.
jonathanknowles Sep 3, 2021
672c0b2
Merge pull request #2867 from input-output-hk/jonathanknowles/apply-t…
jonathanknowles Sep 3, 2021
dc06d64
Merge branch 'master' into sevanspowell/adp-1092/apply-tx-testable
jonathanknowles Sep 3, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 94 additions & 34 deletions lib/core/src/Cardano/Wallet/Primitive/Model.hs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ module Cardano.Wallet.Primitive.Model
, applyBlock
, applyBlocks
, unsafeInitWallet
, applyTxToUTxO
, utxoFromTx

-- * Accessors
, currentTip
Expand Down Expand Up @@ -74,21 +76,20 @@ import Cardano.Wallet.Primitive.Types.Tx
, Tx (..)
, TxIn (..)
, TxMeta (..)
, TxOut (..)
, TxStatus (..)
, inputs
, txOutCoin
)
import Cardano.Wallet.Primitive.Types.UTxO
( Dom (..), UTxO (..), balance, excluding, restrictedBy )
( Dom (..), UTxO (..), balance, excluding, filterByAddressM )
import Control.DeepSeq
( NFData (..), deepseq )
import Control.Monad
( foldM, forM )
( foldM )
import Control.Monad.Extra
( mapMaybeM )
import Control.Monad.Trans.State.Strict
( State, evalState, runState, state )
( State, StateT, evalState, runState, state )
import Data.Functor
( (<&>) )
import Data.Generics.Internal.VL.Lens
Expand All @@ -98,7 +99,7 @@ import Data.Generics.Labels
import Data.List.NonEmpty
( NonEmpty (..) )
import Data.Maybe
( catMaybes, isJust )
( isJust )
import Data.Set
( Set )
import Fmt
Expand Down Expand Up @@ -320,6 +321,51 @@ totalUTxO
totalUTxO pending wallet@(Wallet _ _ s) =
availableUTxO pending wallet <> changeUTxO pending s

-- | Applies a transaction to a UTxO, moving it from one state from another.
-- When applying a transaction to a UTxO:
-- 1. We need to remove any unspents that have been spent in the transaction.
-- 2. Add any unspents that we've received via the transaction.
--
-- We don't consider "ownership" here (is this address ours?), only "do we
-- know about this address" (i.e. is it present in our UTxO?).
--
-- balance (applyTxToUTxO tx u) = balance u
-- + balance (utxoFromTx tx)
-- - balance (u `restrictedBy` inputs tx)
-- unUTxO (applyTxToUTxO tx u) = unUTxO u
-- `Map.union` unUTxO (utxoFromTx tx)
-- `Map.difference` unUTxO (u `restrictedBy` inputs tx)
applyTxToUTxO
:: Tx
-> UTxO
-> UTxO
applyTxToUTxO tx !u = do
let
-- When a transaction comes in, we now know about a set of new unspents.
newKnown = u <> utxoFromTx tx
-- But we also know that some of the new unspents we know about, as well
-- as (potentially) some old ones, may now be spent.
spentKnown =
Set.fromList (inputs tx) `Set.intersection` dom newKnown

newKnown `excluding` spentKnown

-- | Construct a UTxO corresponding to a given transaction. It is important for
-- the transaction outputs to be ordered correctly, since they become available
-- inputs for the subsequent blocks.
--
-- balance (utxoFromTx tx) = foldMap tokens (outputs tx)
utxoFromTx :: Tx -> UTxO
utxoFromTx Tx {txId, outputs} =
UTxO $ Map.fromList $ zip (TxIn txId <$> [0..]) outputs

isOurs' :: forall s m. (Monad m, IsOurs s Address) => Address -> StateT s m Bool
isOurs' addr = do
jonathanknowles marked this conversation as resolved.
Show resolved Hide resolved
mDerivationIndex <- state $ isOurs addr
case mDerivationIndex of
Nothing -> pure False
Just _ -> pure True
jonathanknowles marked this conversation as resolved.
Show resolved Hide resolved

{-------------------------------------------------------------------------------
Internals
-------------------------------------------------------------------------------}
Expand Down Expand Up @@ -386,16 +432,47 @@ prefilterBlock b u0 = runState $ do
=> ([(Tx, TxMeta)], UTxO)
-> Tx
-> State s ([(Tx, TxMeta)], UTxO)
applyTx (!txs, !u) tx = do
ourU <- state $ utxoOurs tx
let ourIns = Set.fromList (inputs tx) `Set.intersection` dom (u <> ourU)
let u' = (u <> ourU) `excluding` ourIns
applyTx (!txs, !prevUTxO) tx = do
-- The next UTxO state (apply a state transition) (e.g. remove
-- transaction outputs we've spent).
ourNextUTxO <- filterByAddressM isOurs' (applyTxToUTxO tx prevUTxO)
jonathanknowles marked this conversation as resolved.
Show resolved Hide resolved

ourWithdrawals <- Coin . sum . fmap (unCoin . snd) <$>
mapMaybeM ourWithdrawal (Map.toList $ withdrawals tx)
let received = balance ourU
let spent = balance (u `restrictedBy` ourIns) `TB.add` TB.fromCoin ourWithdrawals
let hasKnownInput = ourIns /= mempty
let hasKnownOutput = ourU /= mempty

let received = balance (ourNextUTxO `excluding` dom prevUTxO)
let spent =
balance (prevUTxO `excluding` dom ourNextUTxO)
`TB.add` TB.fromCoin ourWithdrawals

(ownedAndKnownTxIns, ownedAndKnownTxOuts) <- do
-- A new transaction expands the set of transaction inputs/outputs
-- we know about:
let known = prevUTxO <> utxoFromTx tx
-- But not all those transaction inputs/outputs belong to us:
ownedAndKnown <- filterByAddressM isOurs' known
jonathanknowles marked this conversation as resolved.
Show resolved Hide resolved
-- Also, the new transaction may spend some transaction
-- inputs/outputs. But we don't want to apply that logic yet. If we
-- do, any spent transaction input/output will be removed from our
-- knowledge base.
-- Therefore, because this is not technically an "Unspent TxO" set,
-- let's just return the TxIns and TxOuts, as the type "UTxO" will
-- create expectations which we explicitly aren't fulfilling:
let m = unUTxO ownedAndKnown
pure (Map.keys m, Map.elems m)

-- A transaction has a known input if one of the transaction inputs
-- matches a transaction input we know about.
let hasKnownInput =
Set.fromList (inputs tx)
`Set.intersection` (Set.fromList ownedAndKnownTxIns)
/= mempty
jonathanknowles marked this conversation as resolved.
Show resolved Hide resolved
-- A transaction has a known output if one of the transaction outputs
-- matches a transaction output we know about.
let hasKnownOutput =
Set.fromList (outputs tx)
`Set.intersection` (Set.fromList ownedAndKnownTxOuts)
/= mempty
jonathanknowles marked this conversation as resolved.
Show resolved Hide resolved
let hasKnownWithdrawal = ourWithdrawals /= mempty

-- NOTE 1: The only case where fees can be 'Nothing' is when dealing with
Expand Down Expand Up @@ -423,7 +500,7 @@ prefilterBlock b u0 = runState $ do
return $ if hasKnownOutput && not hasKnownInput then
let dir = Incoming in
( (tx { fee = actualFee dir }, mkTxMeta (TB.getCoin received) dir) : txs
, u'
, ourNextUTxO
)
else if hasKnownInput || hasKnownWithdrawal then
let
Expand All @@ -433,10 +510,10 @@ prefilterBlock b u0 = runState $ do
amount = distance adaSpent adaReceived
in
( (tx { fee = actualFee dir }, mkTxMeta amount dir) : txs
, u'
, ourNextUTxO
)
else
(txs, u)
(txs, prevUTxO)

-- | Get the change UTxO
--
Expand All @@ -451,21 +528,4 @@ changeUTxO
-> s
-> UTxO
changeUTxO pending = evalState $
mconcat <$> mapM (state . utxoOurs) (Set.toList pending)

-- | Construct our _next_ UTxO (possible empty) from a transaction by selecting
-- outputs that are ours. It is important for the transaction outputs to be
-- ordered correctly, since they become available inputs for the subsequent
-- blocks.
utxoOurs
:: IsOurs s Address
=> Tx
-> s
-> (UTxO, s)
utxoOurs tx = runState $ toUtxo <$> forM (zip [0..] (outputs tx)) filterOut
where
toUtxo = UTxO . Map.fromList . catMaybes
filterOut (ix, out) = do
state (isOurs $ address out) <&> \case
Just{} -> Just (TxIn (txId tx) ix, out)
Nothing -> Nothing
mconcat <$> mapM (filterByAddressM isOurs' . utxoFromTx) (Set.toList pending)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! 👍🏻

31 changes: 31 additions & 0 deletions lib/core/src/Cardano/Wallet/Primitive/Types/UTxO.hs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}

Expand Down Expand Up @@ -34,11 +36,15 @@ module Cardano.Wallet.Primitive.Types.UTxO
, restrictedBy
, restrictedTo
, size
, filterByAddressM
, filterByAddress
) where

import Prelude hiding
( null )

import Cardano.Wallet.Primitive.Types.Address
( Address )
import Cardano.Wallet.Primitive.Types.Coin
( Coin (..) )
import Cardano.Wallet.Primitive.Types.TokenBundle
Expand All @@ -49,6 +55,8 @@ import Control.DeepSeq
( NFData (..) )
import Data.Bifunctor
( first )
import Data.Functor.Identity
( runIdentity )
import Data.Generics.Internal.VL.Lens
( view )
import Data.Kind
Expand Down Expand Up @@ -136,6 +144,29 @@ null (UTxO u) = Map.null u
size :: UTxO -> Int
size (UTxO u) = Map.size u

-- | Limit a UTxO set to just the UTxOs that are ours, according to some
-- given function.
jonathanknowles marked this conversation as resolved.
Show resolved Hide resolved
--
-- filterByAddressM (const $ pure True) u = u
-- filterByAddressM (const $ pure False) u = mempty
-- filterByAddressM f mempty = mempty
-- balance (filterByAddressM f (applyTxToUTxO tx mempty)) =
-- foldMap (\o -> do
-- ours <- f (address o)
-- if ours then tokens o else mempty
-- ) (outputs tx)
filterByAddressM :: forall f. Monad f => (Address -> f Bool) -> UTxO -> f UTxO
filterByAddressM isOursF (UTxO m) =
UTxO <$> Map.traverseMaybeWithKey filterFunc m
where
filterFunc :: TxIn -> TxOut -> f (Maybe TxOut)
filterFunc _txin txout = do
ours <- isOursF $ view #address txout
pure $ if ours then Just txout else Nothing
jonathanknowles marked this conversation as resolved.
Show resolved Hide resolved

filterByAddress :: (Address -> Bool) -> UTxO -> UTxO
filterByAddress f = runIdentity . filterByAddressM (pure . f)

data UTxOStatistics = UTxOStatistics
{ histogram :: ![HistogramBar]
, allStakes :: !Word64
Expand Down
Loading