Skip to content

Commit

Permalink
Add the TxWithUTxO data type.
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathanknowles committed May 3, 2024
1 parent a8130ad commit 3339855
Show file tree
Hide file tree
Showing 4 changed files with 287 additions and 18 deletions.
3 changes: 3 additions & 0 deletions lib/balance-tx/cardano-balance-tx.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ library internal
, cardano-slotting
, cardano-strict-containers
, cardano-wallet-primitive
, cardano-wallet-test-utils
, cborg
, containers
, deepseq
Expand Down Expand Up @@ -96,6 +97,8 @@ library internal
Internal.Cardano.Write.Tx.Sign
Internal.Cardano.Write.Tx.SizeEstimation
Internal.Cardano.Write.Tx.TimeTranslation
Internal.Cardano.Write.Tx.TxWithUTxO
Internal.Cardano.Write.Tx.TxWithUTxO.Gen
Internal.Cardano.Write.UTxOAssumptions

test-suite test
Expand Down
143 changes: 143 additions & 0 deletions lib/balance-tx/lib/internal/Internal/Cardano/Write/Tx/TxWithUTxO.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
{-# LANGUAGE ExplicitNamespaces #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}

-- |
-- Copyright: © 2024 Cardano Foundation
-- License: Apache-2.0
--
-- Provides the 'TxWithUTxO' data type.
--
module Internal.Cardano.Write.Tx.TxWithUTxO
( type TxWithUTxO
, pattern TxWithUTxO
, construct
, constructFiltered
, isValid
)
where

import Prelude

import Cardano.Ledger.Api
( AlonzoEraTxBody (collateralInputsTxBodyL)
, BabbageEraTxBody (referenceInputsTxBodyL)
, EraTx (bodyTxL)
, EraTxBody (TxBody, inputsTxBodyL)
)
import Cardano.Ledger.Api.Tx.Body
( allInputsTxBodyF
)
import Control.Lens
( over
, view
)
import Data.Either.Combinators
( maybeToLeft
)
import Data.Maybe
( fromMaybe
)
import Data.Semigroup.Cancellative
( LeftReductive (stripPrefix)
)
import Data.Set.NonEmpty
( NESet
)
import Internal.Cardano.Write.Tx
( IsRecentEra
, Tx
, TxIn
, UTxO (UTxO)
)

import qualified Data.Map.Strict as Map
import qualified Data.Set as Set
import qualified Data.Set.NonEmpty as NESet

-- | A transaction with an associated UTxO set.
--
-- Every input in the transaction is guaranteed to resolve to a UTxO within the
-- associated UTxO set.
--
-- The UTxO set may also contain additional UTxOs that are not referenced by
-- the transaction.
--
data TxWithUTxO era = UnsafeTxWithUTxO !(Tx era) !(UTxO era)

deriving instance IsRecentEra era => Eq (TxWithUTxO era)

instance IsRecentEra era => Show (TxWithUTxO era) where
show = fromMaybe "TxWithUTxO" . stripPrefix "Unsafe" . show

{-# COMPLETE TxWithUTxO #-}
pattern TxWithUTxO :: IsRecentEra era => Tx era -> UTxO era -> TxWithUTxO era
pattern TxWithUTxO tx utxo <- UnsafeTxWithUTxO tx utxo

-- | Constructs a 'TxWithUTxO' object from an existing transaction and UTxO set.
--
-- Construction succeeds if (and only if) every single input within the given
-- transaction resolves to a UTxO within the accompanying UTxO set.
--
-- Otherwise, if the transaction has any unresolvable inputs, this function
-- returns the non-empty set of those inputs.
--
construct
:: IsRecentEra era
=> Tx era
-> UTxO era
-> Either (NESet TxIn) (TxWithUTxO era)
construct tx utxo =
maybeToLeft txWithUTxO (unresolvableInputs txWithUTxO)
where
txWithUTxO = UnsafeTxWithUTxO tx utxo

-- | Constructs a 'TxWithUTxO' object from an existing transaction and UTxO set,
-- automatically filtering out any unresolvable inputs from the transaction.
--
-- A transaction input is unresolvable if (and only if) it does not resolve to
-- a UTxO within the given UTxO set.
--
constructFiltered
:: forall era. IsRecentEra era
=> Tx era
-> UTxO era
-> TxWithUTxO era
constructFiltered tx utxo@(UTxO utxoMap) = UnsafeTxWithUTxO txFiltered utxo
where
txFiltered :: Tx era
txFiltered = over bodyTxL removeUnresolvableInputs tx

removeUnresolvableInputs :: TxBody era -> TxBody era
removeUnresolvableInputs
= over inputsTxBodyL f
. over collateralInputsTxBodyL f
. over referenceInputsTxBodyL f
where
f = Set.filter (`Map.member` utxoMap)

-- | Indicates whether or not a given 'TxWithUTxO' object is valid.
--
-- A 'TxWithUTxO' object is valid if (and only if) all inputs within the
-- transaction resolve to a UTxO within the associated UTxO set.
--
isValid :: IsRecentEra era => TxWithUTxO era -> Bool
isValid = null . unresolvableInputs

-- | Finds the complete set of unresolvable transaction inputs.
--
-- A transaction input is unresolvable if (and only if) it does not resolve
-- to a UTxO within the associated UTxO set.
--
-- For a valid 'TxWithUTxO' object, this function will return 'Nothing'.
--
unresolvableInputs
:: forall era. IsRecentEra era
=> TxWithUTxO era
-> Maybe (NESet TxIn)
unresolvableInputs (TxWithUTxO tx (UTxO utxo))
= NESet.nonEmptySet
. Set.filter (`Map.notMember` utxo)
. view (bodyTxL . allInputsTxBodyF)
$ tx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
{-# LANGUAGE ExplicitNamespaces #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- |
-- Copyright: © 2024 Cardano Foundation
-- License: Apache-2.0
--
-- Provides generators and shrinkers for the 'TxWithUTxO' data type.
--
module Internal.Cardano.Write.Tx.TxWithUTxO.Gen
( generate
, generateWithMinimalUTxO
, generateWithSurplusUTxO
, shrink
)
where

import Prelude

import Cardano.Ledger.Api
( EraTx (bodyTxL)
)
import Cardano.Ledger.Api.Tx.Body
( allInputsTxBodyF
)
import Control.Lens
( view
)
import Internal.Cardano.Write.Tx
( IsRecentEra
, Tx
, TxIn
, TxOut
, UTxO (UTxO, unUTxO)
)
import Internal.Cardano.Write.Tx.TxWithUTxO
( pattern TxWithUTxO
, type TxWithUTxO
)
import Test.QuickCheck
( Gen
, oneof
)
import Test.QuickCheck.Extra
( genMapFromKeysWith
, genMapWith
, shrinkMapToSubmaps
)

import qualified Data.Map.Strict as Map
import qualified Internal.Cardano.Write.Tx.TxWithUTxO as TxWithUTxO

-- | Generates a 'TxWithUTxO' object.
--
-- The domain of the UTxO map is a superset of the transaction input set, but
-- it may or may not be a strict superset.
--
generate
:: IsRecentEra era
=> Gen (Tx era)
-> Gen (TxIn)
-> Gen (TxOut era)
-> Gen (TxWithUTxO era)
generate genTx genTxIn genTxOut =
oneof
[ generateWithMinimalUTxO genTx genTxIn genTxOut
, generateWithSurplusUTxO genTx genTxIn genTxOut
]

-- | Generates a 'TxWithUTxO' object that has a minimal UTxO set.
--
-- The domain of the UTxO map is exactly equal to the transaction input set.
--
generateWithMinimalUTxO
:: IsRecentEra era
=> Gen (Tx era)
-> Gen (TxIn)
-> Gen (TxOut era)
-> Gen (TxWithUTxO era)
generateWithMinimalUTxO genTx _genTxIn genTxOut = do
tx <- genTx
utxo <- UTxO <$> genMapFromKeysWith genTxOut (txInputs tx)
pure $ TxWithUTxO.constructFiltered tx utxo
where
txInputs = view (bodyTxL . allInputsTxBodyF)

-- | Generates a 'TxWithUTxO' object that has a non-minimal UTxO set.
--
-- The domain of the UTxO map is a strict superset of the transaction input set.
--
generateWithSurplusUTxO
:: forall era. ()
=> IsRecentEra era
=> Gen (Tx era)
-> Gen (TxIn)
-> Gen (TxOut era)
-> Gen (TxWithUTxO era)
generateWithSurplusUTxO genTx genTxIn genTxOut =
generateWithMinimalUTxO genTx genTxIn genTxOut >>= \case
TxWithUTxO tx minimalUTxO -> do
surplusUTxO <- genNonEmptyUTxO
pure $ TxWithUTxO.constructFiltered tx (minimalUTxO <> surplusUTxO)
where
genNonEmptyUTxO :: Gen (UTxO era)
genNonEmptyUTxO
= fmap UTxO
$ Map.insert <$> genTxIn <*> genTxOut <*> genMapWith genTxIn genTxOut

-- TODO: Parameterise this by `Tx era -> [Tx era]` shrinker.
shrink :: IsRecentEra era => TxWithUTxO era -> [TxWithUTxO era]
shrink (TxWithUTxO tx utxo) = do
utxoSubset <- UTxO <$> shrinkMapToSubmaps (unUTxO utxo)
pure $ TxWithUTxO.constructFiltered tx utxoSubset
44 changes: 26 additions & 18 deletions lib/balance-tx/test/spec/Internal/Cardano/Write/Tx/BalanceSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE EmptyCase #-}
{-# LANGUAGE ExplicitNamespaces #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
Expand All @@ -12,6 +13,7 @@
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
Expand Down Expand Up @@ -465,6 +467,12 @@ import qualified Data.Set.NonEmpty as NESet
import qualified Data.Text as T
import qualified Data.Text.IO as T
import qualified Internal.Cardano.Write.Tx as Write
import Internal.Cardano.Write.Tx.TxWithUTxO
( pattern TxWithUTxO
, type TxWithUTxO
)
import qualified Internal.Cardano.Write.Tx.TxWithUTxO as TxWithUTxO
import qualified Internal.Cardano.Write.Tx.TxWithUTxO.Gen as TxWithUTxO
import qualified Ouroboros.Consensus.HardFork.History as HF
import qualified Test.Hspec.Extra as Hspec

Expand Down Expand Up @@ -2204,24 +2212,24 @@ instance Arbitrary (MixedSign Value) where
shrink (MixedSign v) = MixedSign <$> shrink v

instance forall era. IsRecentEra era => Arbitrary (PartialTx era) where
arbitrary = do
tx <- genTxForBalancing
extraUTxO <- genExtraUTxO (txInputs tx)
let redeemers = []
let timelockKeyWitnessCounts = mempty
pure PartialTx {tx, extraUTxO, redeemers, timelockKeyWitnessCounts}
where
genExtraUTxO :: Set TxIn -> Gen (UTxO era)
genExtraUTxO = fmap UTxO . genMapFromKeysWith genTxOut
txInputs :: Tx era -> Set TxIn
txInputs tx = tx ^. bodyTxL . inputsTxBodyL
shrink partialTx@PartialTx {tx, extraUTxO} =
[ partialTx {extraUTxO = extraUTxO'}
| extraUTxO' <- shrinkInputResolution extraUTxO
] <>
[ restrictResolution (partialTx {tx = tx'})
| tx' <- shrinkTx tx
]
arbitrary =
mkPartialTx <$> TxWithUTxO.generate genTxForBalancing genTxIn genTxOut
shrink =
shrinkMapBy mkPartialTx unPartialTx TxWithUTxO.shrink

genTxIn :: Gen TxIn
genTxIn = fromWalletTxIn <$> W.genTxIn

mkPartialTx :: IsRecentEra era => TxWithUTxO era -> PartialTx era
mkPartialTx (TxWithUTxO tx extraUTxO) =
PartialTx {tx, extraUTxO, redeemers, timelockKeyWitnessCounts}
where
redeemers = []
timelockKeyWitnessCounts = mempty

unPartialTx :: IsRecentEra era => PartialTx era -> TxWithUTxO era
unPartialTx PartialTx {tx, extraUTxO} =
TxWithUTxO.constructFiltered tx extraUTxO

instance Arbitrary StdGenSeed where
arbitrary = StdGenSeed . fromIntegral @Int <$> arbitrary
Expand Down

0 comments on commit 3339855

Please sign in to comment.