Skip to content

Commit

Permalink
Merge #3456
Browse files Browse the repository at this point in the history
3456: Use `cardano-ledger` for all minimum UTxO calculations r=jonathanknowles a=jonathanknowles

## Issue Number

ADP-2144

## Summary

This PR:

-  uses the ledger function [`evaluateMinLovelaceOutput`](https://github.com/input-output-hk/cardano-ledger/blob/68a535603c80b7cf53425fd34558e23f9983dfe8/eras/shelley/impl/src/Cardano/Ledger/Shelley/API/Wallet.hs#L506) to replace the use of Cardano API function [`calculateMinimumUTxO`](https://github.com/input-output-hk/cardano-node/blob/42809ad3d420f0695eb147d4c66b90573d27cafd/cardano-api/src/Cardano/Api/Fees.hs#L1226) in all implementation code, for all eras.
- repurposes the Cardano API function [`calculateMinimumUTxO`](https://github.com/input-output-hk/cardano-node/blob/42809ad3d420f0695eb147d4c66b90573d27cafd/cardano-api/src/Cardano/Api/Fees.hs#L1226) for use as an oracle, to compare against when testing.
- removes all special-casing for the Babbage era, and replaces it with a simpler implementation that works in all eras.
- revises documentation for the `{compute,isBelow}minimumCoinForUTxO` functions to explain their intended purpose more clearly.

## Context

- The ledger function [`evaluateMinLovelaceOutput`](https://github.com/input-output-hk/cardano-ledger/blob/68a535603c80b7cf53425fd34558e23f9983dfe8/eras/shelley/impl/src/Cardano/Ledger/Shelley/API/Wallet.hs#L506) accepts an _era-specific_ protocol parameters object and, unlike like the Cardano API, does _not_ require that we convert to an era-agnostic protocol parameters object. Since values of the `MinimumUTxO` type already contain era-specific protocol parameters, this means we can avoid the extra complication of record conversion whenever we call:
    - `isBelowMinimumCoinForUTxO`
    - `computeMinimumCoinForUTxO`
    
## Performance

Mainnet (Alonzo):

```sh
$ time curl -X POST http://localhost:8091/v2/wallets/042094088ed439a7b14e811e0781a00185b921c2/payment-fees -d '{"payments":[{"address":"addr1qylgm2dh3vpv07cjfcyuu6vhaqhf8998qcx6s8ucpkly6f8l0dw5r75vk42mv3ykq8vyjeaanvpytg79xqzymqy5acmqgyxuyr","amount":{"quantity":100000000,"unit":"lovelace"}}]}' -H "Content-Type: application/json"
{"deposit":{"quantity":0,"unit":"lovelace"},"estimated_max":{"quantity":187633,"unit":"lovelace"},"estimated_min":{"quantity":169021,"unit":"lovelace"},"minimum_coins":[{"quantity":999978,"unit":"lovelace"}]}
real	0m0,029s
user	0m0,006s
sys	0m0,000s
```

Preprod (Alonzo):
```sh
$ time curl -X POST http://localhost:8090/v2/wallets/1f82e83772b7579fc0854bd13db6a9cce21ccd95/payment-fees \
> -d '{"payments":[{"address":"addr_test1qrtez7vn0d8xp495ggypmu2kyt7tt6qyva2spm0f5a3ewn0v474mcs4q8e9g55yknx3729kyg5dl69x5596ee9tvnynq7ffety","amount":{"quantity":1000000,"unit":"lovelace"}}]}' \
> -H "Content-Type: application/json"
{"deposit":{"quantity":0,"unit":"lovelace"},"estimated_max":{"quantity":169021,"unit":"lovelace"},"estimated_min":{"quantity":169021,"unit":"lovelace"},"minimum_coins":[{"quantity":999978,"unit":"lovelace"}]}
real	0m0,029s
user	0m0,006s
sys	0m0,000s
```

Preview (Babbage):
```sh
$ time curl -X POST http://localhost:8090/v2/wallets/1f82e83772b7579fc0854bd13db6a9cce21ccd95/payment-fees -d '{"payments":[{"address":"addr_test1qrtez7vn0d8xp495ggypmu2kyt7tt6qyva2spm0f5a3ewn0v474mcs4q8e9g55yknx3729kyg5dl69x5596ee9tvnynq7ffety","amount":{"quantity":100000000,"unit":"lovelace"}}]}' -H "Content-Type: application/json"
{"deposit":{"quantity":0,"unit":"lovelace"},"estimated_max":{"quantity":187809,"unit":"lovelace"},"estimated_min":{"quantity":169197,"unit":"lovelace"},"minimum_coins":[{"quantity":995610,"unit":"lovelace"}]}
real	0m0,036s
user	0m0,003s
sys	0m0,003s
```

Co-authored-by: Jonathan Knowles <[email protected]>
  • Loading branch information
iohk-bors[bot] and jonathanknowles authored Aug 24, 2022
2 parents 8037ec8 + 43a604a commit 88c76bb
Show file tree
Hide file tree
Showing 5 changed files with 263 additions and 216 deletions.
1 change: 1 addition & 0 deletions lib/shelley/cardano-wallet.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ library
Cardano.Wallet.Shelley.Launch.Cluster
Cardano.Wallet.Shelley.Logging
Cardano.Wallet.Shelley.MinimumUTxO
Cardano.Wallet.Shelley.MinimumUTxO.Internal
Cardano.Wallet.Shelley.Network
Cardano.Wallet.Shelley.Network.Blockfrost
Cardano.Wallet.Shelley.Network.Blockfrost.Conversion
Expand Down
34 changes: 31 additions & 3 deletions lib/shelley/src/Cardano/Wallet/Shelley/Compatibility/Ledger.hs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ module Cardano.Wallet.Shelley.Compatibility.Ledger
, toLedgerTokenPolicyId
, toLedgerTokenName
, toLedgerTokenQuantity
, toAlonzoTxOut
, toBabbageTxOut

-- * Conversions from ledger specification types to wallet types
, toWalletCoin
Expand All @@ -35,6 +33,13 @@ module Cardano.Wallet.Shelley.Compatibility.Ledger
-- types
, Convert (..)

-- * Conversions for transaction outputs
, toShelleyTxOut
, toAllegraTxOut
, toMaryTxOut
, toAlonzoTxOut
, toBabbageTxOut

) where

import Prelude
Expand Down Expand Up @@ -78,7 +83,7 @@ import GHC.Stack
import Numeric.Natural
( Natural )
import Ouroboros.Consensus.Shelley.Eras
( StandardCrypto )
( StandardAllegra, StandardCrypto, StandardMary, StandardShelley )

import qualified Cardano.Crypto.Hash.Class as Crypto
import qualified Cardano.Ledger.Address as Ledger
Expand All @@ -90,6 +95,7 @@ import qualified Cardano.Ledger.Crypto as Ledger
import qualified Cardano.Ledger.Keys as Ledger
import qualified Cardano.Ledger.Mary.Value as Ledger
import qualified Cardano.Ledger.Shelley.API as Ledger
import qualified Cardano.Ledger.Shelley.TxBody as Shelley
import qualified Cardano.Ledger.ShelleyMA.Timelocks as MA
import qualified Cardano.Wallet.Primitive.Types.Coin as Coin
import qualified Cardano.Wallet.Primitive.Types.TokenBundle as TokenBundle
Expand Down Expand Up @@ -248,6 +254,28 @@ instance Convert Address (Ledger.Addr StandardCrypto) where
]
toWallet = Address . Ledger.serialiseAddr

--------------------------------------------------------------------------------
-- Conversions for 'TxOut'
--------------------------------------------------------------------------------

toShelleyTxOut
:: TxOut
-> Shelley.TxOut StandardShelley
toShelleyTxOut (TxOut addr bundle) =
Shelley.TxOut (toLedger addr) (toLedger (TokenBundle.coin bundle))

toAllegraTxOut
:: TxOut
-> Shelley.TxOut StandardAllegra
toAllegraTxOut (TxOut addr bundle) =
Shelley.TxOut (toLedger addr) (toLedger (TokenBundle.coin bundle))

toMaryTxOut
:: TxOut
-> Shelley.TxOut StandardMary
toMaryTxOut (TxOut addr bundle) =
Shelley.TxOut (toLedger addr) (toLedger bundle)

toAlonzoTxOut
:: TxOut
-> Maybe (Hash "Datum")
Expand Down
227 changes: 60 additions & 167 deletions lib/shelley/src/Cardano/Wallet/Shelley/MinimumUTxO.hs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}

-- |
-- Copyright: © 2022 IOHK
Expand All @@ -14,201 +13,95 @@ module Cardano.Wallet.Shelley.MinimumUTxO

import Prelude

import Cardano.Ledger.Babbage.Rules.Utxo
( babbageMinUTxOValue )
import Cardano.Ledger.Serialization
( mkSized )
import Cardano.Wallet.Primitive.Types.Address
( Address (..) )
import Cardano.Wallet.Primitive.Types.Coin
( Coin (..) )
import Cardano.Wallet.Primitive.Types.MinimumUTxO
( MinimumUTxO (..), MinimumUTxOForShelleyBasedEra (..) )
( MinimumUTxO (..) )
import Cardano.Wallet.Primitive.Types.TokenBundle
( TokenBundle (..) )
import Cardano.Wallet.Primitive.Types.TokenMap
( TokenMap )
import Cardano.Wallet.Primitive.Types.Tx
( TxOut (..), txOutMaxCoin )
import Cardano.Wallet.Shelley.Compatibility
( toCardanoTxOut, unsafeLovelaceToWalletCoin, unsafeValueToLovelace )
import Cardano.Wallet.Shelley.Compatibility.Ledger
( toBabbageTxOut, toWalletCoin )
import Data.Function
( (&) )
import GHC.Stack
( HasCallStack )
import Ouroboros.Consensus.Cardano.Block
( StandardBabbage )

import qualified Cardano.Api.Shelley as Cardano
import qualified Cardano.Ledger.Babbage.PParams as Babbage
import qualified Cardano.Wallet.Primitive.Types.TokenBundle as TokenBundle
import qualified Cardano.Wallet.Shelley.MinimumUTxO.Internal as Internal

-- | Computes a minimum 'Coin' value for a 'TokenMap' that is destined for
-- inclusion in a transaction output.
--
computeMinimumCoinForUTxO
:: HasCallStack
=> MinimumUTxO
-> Address
-> TokenMap
-> Coin
computeMinimumCoinForUTxO = \case
MinimumUTxONone ->
\_addr _tokenMap -> Coin 0
MinimumUTxOConstant c ->
\_addr _tokenMap -> c
MinimumUTxOForShelleyBasedEraOf minUTxO ->
computeMinimumCoinForUTxOShelleyBasedEra minUTxO

-- | Computes a minimum 'Coin' value for a 'TokenMap' that is destined for
-- inclusion in a transaction output.
--
-- This function returns a value that is specific to a given Shelley-based era.
-- Importantly, a value that is valid in one era will not necessarily be valid
-- in another era.
-- The value returned is a /safe/ minimum, in the sense that any value above
-- the minimum should also satisfy the minimum UTxO rule. Consequently, when
-- assigning ada quantities to outputs, it should be safe to assign any value
-- that is greater than or equal to the value returned by this function.
--
computeMinimumCoinForUTxOShelleyBasedEra
:: HasCallStack
=> MinimumUTxOForShelleyBasedEra
computeMinimumCoinForUTxO
:: MinimumUTxO
-> Address
-> TokenMap
-> Coin
computeMinimumCoinForUTxOShelleyBasedEra
(MinimumUTxOForShelleyBasedEra era pp) addr tokenMap = case era of
-- Here we treat the Babbage era specially and use the ledger
-- to compute the minimum ada quantity, bypassing the Cardano
-- API. This appears to be significantly faster.
Cardano.ShelleyBasedEraBabbage ->
computeLedgerMinimumCoinForBabbage pp addr
(TokenBundle txOutMaxCoin tokenMap)
_ ->
unsafeCoinFromCardanoApiCalculateMinimumUTxOResult $
Cardano.calculateMinimumUTxO era
(embedTokenMapWithinPaddedTxOut era addr tokenMap)
(Cardano.fromLedgerPParams era pp)
computeMinimumCoinForUTxO minimumUTxO addr tokenMap =
case minimumUTxO of
MinimumUTxONone ->
Coin 0
MinimumUTxOConstant c ->
c
MinimumUTxOForShelleyBasedEraOf minimumUTxOShelley ->
-- It's very important that we do not underestimate minimum UTxO
-- quantities, as this may result in the creation of transactions
-- that are unacceptable to the ledger.
--
-- In the cases of change generation and wallet balance migration,
-- any underestimation would be particularly problematic, as
-- outputs are generated automatically, and users do not have
-- direct control over the ada quantities generated.
--
-- However, while we cannot underestimate minimum UTxO quantities,
-- we are at liberty to moderately overestimate them.
--
-- Since the minimum UTxO function is monotonically increasing
-- w.r.t. the size of the ada quantity, if we supply a 'TxOut' with
-- an ada quantity whose serialized length is the maximum possible
-- length, we can be confident that the resultant value can always
-- safely be increased.
--
Internal.computeMinimumCoinForUTxO_CardanoLedger
minimumUTxOShelley
(TxOut addr $ TokenBundle txOutMaxCoin tokenMap)

-- | Returns 'True' if and only if the given 'TokenBundle' has a 'Coin' value
-- that is below the minimum acceptable 'Coin' value.
--
-- This function should /only/ be used to validate existing 'Coin' values that
-- do not need to be modified in any way.
--
-- Increasing the 'Coin' value of an output can lead to an increase in the
-- serialized length of that output, which can in turn lead to an increase in
-- the minimum required 'Coin' value, since the minimum required 'Coin' value
-- is dependent on an output's serialized length.
--
-- Therefore, even if this function indicates that a given value 'Coin' value
-- 'c' satisfies the minimum UTxO rule, it should not be taken to imply that
-- all values greater than 'c' will also satisfy the minimum UTxO rule.
--
-- If you need to generate a value that can always safely be increased, use
-- the 'computeMinimumCoinForUTxO' function instead.
--
isBelowMinimumCoinForUTxO
:: MinimumUTxO
-> Address
-> TokenBundle
-> Bool
isBelowMinimumCoinForUTxO = \case
MinimumUTxONone ->
\_addr _tokenBundle ->
isBelowMinimumCoinForUTxO minimumUTxO addr tokenBundle =
case minimumUTxO of
MinimumUTxONone ->
False
MinimumUTxOConstant c ->
\_addr tokenBundle ->
MinimumUTxOConstant c ->
TokenBundle.getCoin tokenBundle < c
MinimumUTxOForShelleyBasedEraOf minUTxO ->
isBelowMinimumCoinForUTxOShelleyBasedEra minUTxO

-- | Returns 'True' if and only if the given 'TokenBundle' has a 'Coin' value
-- that is below the minimum acceptable 'Coin' value for a Shelley-based
-- era.
--
isBelowMinimumCoinForUTxOShelleyBasedEra
:: MinimumUTxOForShelleyBasedEra
-> Address
-> TokenBundle
-> Bool
isBelowMinimumCoinForUTxOShelleyBasedEra
(MinimumUTxOForShelleyBasedEra era pp) addr tokenBundle =
TokenBundle.getCoin tokenBundle <
-- Here we treat the Babbage era specially and use the ledger
-- to compute the minimum ada quantity, bypassing the Cardano
-- API. This appears to be significantly faster.
case era of
Cardano.ShelleyBasedEraBabbage ->
computeLedgerMinimumCoinForBabbage pp addr tokenBundle
_ ->
cardanoApiMinimumCoin
where
cardanoApiMinimumCoin :: Coin
cardanoApiMinimumCoin =
unsafeCoinFromCardanoApiCalculateMinimumUTxOResult $
Cardano.calculateMinimumUTxO era
(toCardanoTxOut era $ TxOut addr tokenBundle)
(Cardano.fromLedgerPParams era pp)

-- | Embeds a 'TokenMap' within a padded 'Cardano.TxOut' value.
--
-- When computing the minimum UTxO quantity for a given 'TokenMap', we do not
-- have access to an address or to an ada quantity.
--
-- However, in order to compute a minimum UTxO quantity through the Cardano
-- API, we must supply a 'TxOut' value with a valid address and ada quantity.
--
-- It's imperative that we do not underestimate minimum UTxO quantities, as
-- this may result in the creation of transactions that are unacceptable to
-- the ledger. In the case of change generation, this would be particularly
-- problematic, as change outputs are generated automatically, and users do
-- not have direct control over the ada quantities generated.
--
-- However, while we cannot underestimate minimum UTxO quantities, we are at
-- liberty to moderately overestimate them.
--
-- Since the minimum UTxO quantity function is monotonically increasing w.r.t.
-- the size of the address and ada quantity, if we supply a 'TxOut' with an
-- address and ada quantity whose serialized lengths are the maximum possible
-- lengths, we can be confident that the resultant value will not be an
-- underestimate.
--
embedTokenMapWithinPaddedTxOut
:: Cardano.ShelleyBasedEra era
-> Address
-> TokenMap
-> Cardano.TxOut Cardano.CtxTx era
embedTokenMapWithinPaddedTxOut era addr m =
toCardanoTxOut era $ TxOut addr $ TokenBundle txOutMaxCoin m

-- | Extracts a 'Coin' value from the result of calling the Cardano API
-- function 'calculateMinimumUTxO'.
--
unsafeCoinFromCardanoApiCalculateMinimumUTxOResult
:: HasCallStack
=> Either Cardano.MinimumUTxOError Cardano.Value
-> Coin
unsafeCoinFromCardanoApiCalculateMinimumUTxOResult = \case
Right value ->
-- We assume that the returned value is a non-negative ada quantity
-- with no other assets. If this assumption is violated, we have no
-- way to continue, and must raise an error:
value
& unsafeValueToLovelace
& unsafeLovelaceToWalletCoin
Left e ->
-- The 'Cardano.calculateMinimumUTxO' function should only return
-- an error if a required protocol parameter is missing.
--
-- However, given that values of 'MinimumUTxOForShelleyBasedEra'
-- can only be constructed by supplying an era-specific protocol
-- parameters record, it should be impossible to trigger this
-- condition.
--
-- Any violation of this assumption indicates a programming error.
-- If this condition is triggered, we have no way to continue, and
-- must raise an error:
--
error $ unwords
[ "unsafeCoinFromCardanoApiCalculateMinimumUTxOResult:"
, "unexpected error:"
, show e
]

-- | Uses the ledger to compute a minimum ada quantity for the Babbage era.
--
computeLedgerMinimumCoinForBabbage
:: Babbage.PParams StandardBabbage
-> Address
-> TokenBundle
-> Coin
computeLedgerMinimumCoinForBabbage pp addr tokenBundle =
toWalletCoin
$ babbageMinUTxOValue pp
$ mkSized
$ toBabbageTxOut (TxOut addr tokenBundle) Nothing
MinimumUTxOForShelleyBasedEraOf minimumUTxOShelley ->
TokenBundle.getCoin tokenBundle <
Internal.computeMinimumCoinForUTxO_CardanoLedger
minimumUTxOShelley
(TxOut addr tokenBundle)
Loading

0 comments on commit 88c76bb

Please sign in to comment.