Skip to content

Commit

Permalink
Fix autobalancing when deregistering certificates.
Browse files Browse the repository at this point in the history
Add a test case for that.

Co-authored-by: sourabhxyz <[email protected]>
  • Loading branch information
carbolymer and sourabhxyz committed Jan 14, 2025
1 parent c5cb6b2 commit e0d4049
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 48 deletions.
55 changes: 31 additions & 24 deletions cardano-api/internal/Cardano/Api/Fees.hs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ import Data.Set (Set)
import qualified Data.Set as Set
import Data.Text (Text)
import GHC.Exts (IsList (..))
import GHC.Stack
import Lens.Micro ((.~), (^.))

-- | Type synonym for logs returned by the ledger's @evalTxExUnitsWithLogs@ function.
Expand Down Expand Up @@ -190,7 +191,8 @@ instance Error (TxFeeEstimationError era) where
-- | Use when you do not have access to the UTxOs you intend to spend
estimateBalancedTxBody
:: forall era
. MaryEraOnwards era
. HasCallStack
=> MaryEraOnwards era
-> TxBodyContent BuildTx era
-> L.PParams (ShelleyLedgerEra era)
-> Set PoolId
Expand Down Expand Up @@ -1004,6 +1006,7 @@ data FeeEstimationMode era
makeTransactionBodyAutoBalance
:: forall era
. ()
=> HasCallStack
=> ShelleyBasedEra era
-> SystemStart
-> LedgerEpochInfo
Expand Down Expand Up @@ -1044,18 +1047,39 @@ makeTransactionBodyAutoBalance
-- 3. update tx with fees
-- 4. balance the transaction and update tx change output

let totalValueAtSpendableUTxO = fromLedgerValue sbe . calculateIncomingUTxOValue . Map.elems $ unUTxO utxo
-- UTXO inputs, which inclue also non-ada assets
let totalValueAtUTxO =
fromLedgerValue sbe . calculateIncomingUTxOValue . Map.elems $ unUTxO utxo
change =
monoidForEraInEon (toCardanoEra sbe) $ \w ->
toLedgerValue w $ calculateChangeValue sbe totalValueAtSpendableUTxO txbodycontent
toLedgerValue w $ calculateChangeValue sbe totalValueAtUTxO txbodycontent

-- For the purpose of fees we just need to make a txbody of the right size in bytes. We do
-- not need the right values for the fee or change output. We use
-- "big enough" values for the change output and set so that the CBOR
-- encoding size of the tx will be big enough to cover the size of the final
-- output and fee. Yes this means this current code will only work for
-- final fee of less than around 4000 ada (2^32-1 lovelace) and change output
-- of less than around 18 trillion ada (2^64-1 lovelace).
-- However, since at this point we know how much non-Ada change to give
-- we can use the true values for that.
let maxLovelaceChange = L.Coin (2 ^ (64 :: Integer)) - 1
let maxLovelaceFee = L.Coin (2 ^ (32 :: Integer) - 1)
let changeWithMaxLovelace = change & A.adaAssetL sbe .~ maxLovelaceChange
let changeTxOut =
forShelleyBasedEraInEon
sbe
(lovelaceToTxOutValue sbe maxLovelaceChange)
(\w -> maryEraOnwardsConstraints w $ TxOutValueShelleyBased sbe changeWithMaxLovelace)

-- tx body used only for evaluating execution units, txout exact values do not matter much here
txbody0 <-
first TxBodyError
$ createTransactionBody
sbe
$ txbodycontent
& modTxOuts
(<> [TxOut changeaddr (TxOutValueShelleyBased sbe change) TxOutDatumNone ReferenceScriptNone])
(<> [TxOut changeaddr changeTxOut TxOutDatumNone ReferenceScriptNone])
exUnitsMapWithLogs <-
first TxBodyErrorValidityInterval $
evaluateTransactionExecutionUnits
Expand All @@ -1078,25 +1102,7 @@ makeTransactionBodyAutoBalance

txbodycontent1 <- substituteExecutionUnits exUnitsMap' txbodycontent

-- Make a txbody that we will use for calculating the fees. For the purpose
-- of fees we just need to make a txbody of the right size in bytes. We do
-- not need the right values for the fee or change output. We use
-- "big enough" values for the change output and set so that the CBOR
-- encoding size of the tx will be big enough to cover the size of the final
-- output and fee. Yes this means this current code will only work for
-- final fee of less than around 4000 ada (2^32-1 lovelace) and change output
-- of less than around 18 trillion ada (2^64-1 lovelace).
-- However, since at this point we know how much non-Ada change to give
-- we can use the true values for that.
let maxLovelaceChange = L.Coin (2 ^ (64 :: Integer)) - 1
let maxLovelaceFee = L.Coin (2 ^ (32 :: Integer) - 1)
let changeWithMaxLovelace = change & A.adaAssetL sbe .~ maxLovelaceChange
let changeTxOut =
forShelleyBasedEraInEon
sbe
(lovelaceToTxOutValue sbe maxLovelaceChange)
(\w -> maryEraOnwardsConstraints w $ TxOutValueShelleyBased sbe changeWithMaxLovelace)

-- Make a txbody that we will use for calculating the fees.
let (dummyCollRet, dummyTotColl) = maybeDummyTotalCollAndCollReturnOutput sbe txbodycontent changeaddr
txbody1 <-
first TxBodyError $ -- TODO: impossible to fail now
Expand Down Expand Up @@ -1578,7 +1584,8 @@ traverseScriptWitnesses =
traverse (\(item, eRes) -> eRes >>= (\res -> Right (item, res)))

calculateMinimumUTxO
:: ShelleyBasedEra era
:: HasCallStack
=> ShelleyBasedEra era
-> TxOut CtxTx era
-> Ledger.PParams (ShelleyLedgerEra era)
-> L.Coin
Expand Down
5 changes: 4 additions & 1 deletion cardano-api/internal/Cardano/Api/Tx/Body.hs
Original file line number Diff line number Diff line change
Expand Up @@ -1890,6 +1890,7 @@ instance Error TxBodyError where

createTransactionBody
:: ()
=> HasCallStack
=> ShelleyBasedEra era
-> TxBodyContent BuildTx era
-> Either TxBodyError (TxBody era)
Expand Down Expand Up @@ -2633,7 +2634,8 @@ convTotalCollateral txTotalCollateral =

convTxOuts
:: forall ctx era ledgerera
. ShelleyLedgerEra era ~ ledgerera
. HasCallStack
=> ShelleyLedgerEra era ~ ledgerera
=> ShelleyBasedEra era
-> [TxOut ctx era]
-> Seq.StrictSeq (Ledger.TxOut ledgerera)
Expand Down Expand Up @@ -2816,6 +2818,7 @@ guardShelleyTxInsOverflow txIns = do
-- all eras
mkCommonTxBody
:: ()
=> HasCallStack
=> ShelleyBasedEra era
-> TxIns BuildTx era
-> [TxOut ctx era]
Expand Down
4 changes: 3 additions & 1 deletion cardano-api/internal/Cardano/Api/Tx/Compatible.hs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import Data.Maybe.Strict
import Data.Monoid
import qualified Data.Sequence.Strict as Seq
import GHC.Exts (IsList (..))
import GHC.Stack
import Lens.Micro hiding (ix)

data AnyProtocolUpdate era where
Expand Down Expand Up @@ -205,7 +206,8 @@ createCompatibleSignedTx sbe ins outs witnesses txFee' anyProtocolUpdate anyVote
.~ shelleyBootstrapWitnesses

createCommonTxBody
:: ShelleyBasedEra era
:: HasCallStack
=> ShelleyBasedEra era
-> [TxIn]
-> [TxOut ctx era]
-> Lovelace
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module Test.Cardano.Api.Transaction.Autobalance
where

import Cardano.Api
import Cardano.Api.Address (toShelleyStakeCredential)
import Cardano.Api.Fees
import qualified Cardano.Api.Ledger as L
import qualified Cardano.Api.Ledger.Lens as L
Expand Down Expand Up @@ -59,13 +60,9 @@ prop_make_transaction_body_autobalance_return_correct_fee_for_multi_asset = H.pr
beo = convert ceo
meo = convert beo
sbe = convert ceo
era = toCardanoEra sbe
aeo <- H.nothingFail $ forEraMaybeEon @AlonzoEraOnwards era

systemStart <-
fmap SystemStart . H.evalIO $
DT.parseTimeM True DT.defaultTimeLocale "%Y-%m-%dT%H:%M:%S%QZ" "2021-09-01T00:00:00Z"
aeo = convert beo

systemStart <- parseSystemStart "2021-09-01T00:00:00Z"
let epochInfo = LedgerEpochInfo $ CS.fixedEpochInfo (CS.EpochSize 100) (CS.mkSlotLength 1000)

pparams <-
Expand All @@ -75,7 +72,7 @@ prop_make_transaction_body_autobalance_return_correct_fee_for_multi_asset = H.pr
(sh@(ScriptHash scriptHash), plutusWitness) <- loadPlutusWitness ceo
let policyId' = PolicyId sh
-- one UTXO with an asset - the same we're minting in the transaction
let utxos = mkUtxos beo scriptHash
let utxos = mkUtxos beo (Just scriptHash)
txInputs = map (,BuildTxWith (KeyWitness KeyWitnessForSpending)) . toList . M.keys . unUTxO $ utxos
txInputsCollateral = TxInsCollateral aeo $ toList . M.keys . unUTxO $ utxos
let address = mkAddress sbe scriptHash
Expand All @@ -90,12 +87,12 @@ prop_make_transaction_body_autobalance_return_correct_fee_for_multi_asset = H.pr
defaultTxBodyContent sbe
& setTxIns txInputs
& setTxInsCollateral txInputsCollateral
& setTxOuts (mkTxOutput beo address Nothing) -- include minted asset in txout manually
& setTxOuts (mkTxOutput beo address (L.Coin 2_000_000) Nothing) -- include minted asset in txout manually
& setTxMintValue txMint
& setTxProtocolParams (pure $ pure pparams)

-- tx body content with manually added asset to TxOut
let contentWithTxoutAsset = content & setTxOuts (mkTxOutput beo address (Just scriptHash))
let contentWithTxoutAsset = content & setTxOuts (mkTxOutput beo address (L.Coin 2_000_000) (Just scriptHash))

-- change txout only with ADA
(BalancedTxBody balancedContentWithTxoutAsset _ _ feeWithTxoutAsset) <-
Expand Down Expand Up @@ -137,19 +134,73 @@ prop_make_transaction_body_autobalance_return_correct_fee_for_multi_asset = H.pr
H.diff balancedContentWithTxoutAsset (\_ _ -> feeWithTxoutAsset == fee) balancedContent
feeWithTxoutAsset === fee

prop_make_transaction_body_autobalance_when_deregistering_certs :: Property
prop_make_transaction_body_autobalance_when_deregistering_certs = H.propertyOnce $ do
let ceo = ConwayEraOnwardsConway
beo = convert ceo
sbe = convert beo

systemStart <- parseSystemStart "2021-09-01T00:00:00Z"
let epochInfo = LedgerEpochInfo $ CS.fixedEpochInfo (CS.EpochSize 100) (CS.mkSlotLength 1000)

pparams <-
LedgerProtocolParameters
<$> H.readJsonFileOk "test/cardano-api-test/files/input/protocol-parameters/conway.json"

(ScriptHash scriptHash, _) <- loadPlutusWitness ceo

let utxos = mkUtxos beo Nothing
txInputs = map (,BuildTxWith (KeyWitness KeyWitnessForSpending)) . toList . M.keys . unUTxO $ utxos
txInputsTotalCoin = mconcat $ getTxOutCoin =<< M.elems (unUTxO utxos)
address = mkAddress sbe scriptHash
deregDeposit = L.Coin 20_000_000
txOutCoin = L.Coin 20_800_000

stakeCred <- forAll genStakeCredential
let certs =
[ ConwayCertificate ceo $
L.ConwayTxCertDeleg (L.ConwayUnRegCert (toShelleyStakeCredential stakeCred) (L.SJust deregDeposit))
]

content =
defaultTxBodyContent sbe
& setTxIns txInputs
& setTxOuts (mkTxOutput beo address txOutCoin Nothing)
& setTxProtocolParams (pure $ pure pparams)
& setTxCertificates (TxCertificates sbe certs (BuildTxWith []))

-- autobalanced body has assets and ADA in the change txout
(BalancedTxBody _ _ changeOut fee) <-
H.leftFail $
makeTransactionBodyAutoBalance
sbe
systemStart
epochInfo
pparams
mempty
[(stakeCred, deregDeposit)]
mempty
utxos
content
address
Nothing

changeCoin <- getTxOutCoin changeOut

H.note_ "Sanity check: inputs == outputs"
mconcat [deregDeposit, txInputsTotalCoin] === mconcat [txOutCoin, fee, changeCoin]

176_633 === fee

prop_make_transaction_body_autobalance_multi_asset_collateral :: Property
prop_make_transaction_body_autobalance_multi_asset_collateral = H.propertyOnce $ do
let ceo = ConwayEraOnwardsConway
beo = convert ceo
sbe = convert beo
meo = convert beo
era = toCardanoEra sbe
aeo <- H.nothingFail $ forEraMaybeEon @AlonzoEraOnwards era

systemStart <-
fmap SystemStart . H.evalIO $
DT.parseTimeM True DT.defaultTimeLocale "%Y-%m-%dT%H:%M:%S%QZ" "2021-09-01T00:00:00Z"
aeo = convert beo

systemStart <- parseSystemStart "2021-09-01T00:00:00Z"
let epochInfo = LedgerEpochInfo $ CS.fixedEpochInfo (CS.EpochSize 100) (CS.mkSlotLength 1000)

pparams <-
Expand All @@ -159,7 +210,7 @@ prop_make_transaction_body_autobalance_multi_asset_collateral = H.propertyOnce $
(sh@(ScriptHash scriptHash), plutusWitness) <- loadPlutusWitness ceo
let policyId' = PolicyId sh
-- one UTXO with an asset - the same we're minting in the transaction
let utxos = mkUtxos beo scriptHash
let utxos = mkUtxos beo (Just scriptHash)
txInputs = map (,BuildTxWith (KeyWitness KeyWitnessForSpending)) . toList . M.keys . unUTxO $ utxos
txInputsCollateral = TxInsCollateral aeo $ toList . M.keys . unUTxO $ utxos
let address = mkAddress sbe scriptHash
Expand All @@ -172,7 +223,7 @@ prop_make_transaction_body_autobalance_multi_asset_collateral = H.propertyOnce $
defaultTxBodyContent sbe
& setTxIns txInputs
& setTxInsCollateral txInputsCollateral
& setTxOuts (mkTxOutput beo address Nothing)
& setTxOuts (mkTxOutput beo address (L.Coin 2_000_000) Nothing)
& setTxMintValue txMint
& setTxProtocolParams (pure $ pure pparams)

Expand Down Expand Up @@ -306,8 +357,12 @@ textEnvTypes =
(ScriptInAnyLang (PlutusScriptLanguage PlutusScriptV3))
]

mkUtxos :: BabbageEraOnwards era -> L.ScriptHash L.StandardCrypto -> UTxO era
mkUtxos beo scriptHash = babbageEraOnwardsConstraints beo $ do
mkUtxos
:: BabbageEraOnwards era
-> Maybe (L.ScriptHash L.StandardCrypto)
-- ^ add an asset to the utxo if the script hash is provided
-> UTxO era
mkUtxos beo mScriptHash = babbageEraOnwardsConstraints beo $ do
let sbe = convert beo
UTxO
[
Expand All @@ -329,7 +384,10 @@ mkUtxos beo scriptHash = babbageEraOnwardsConstraints beo $ do
sbe
( L.MaryValue
(L.Coin 4_000_000)
(L.MultiAsset [(L.PolicyID scriptHash, [(L.AssetName "eeee", 1)])])
( L.MultiAsset $
fromList
[(L.PolicyID scriptHash, [(L.AssetName "eeee", 2)]) | scriptHash <- maybeToList mScriptHash]
)
)
)
TxOutDatumNone
Expand All @@ -352,17 +410,19 @@ mkAddress sbe scriptHash =
mkTxOutput
:: BabbageEraOnwards era
-> AddressInEra era
-> L.Coin
-- ^ output ADA
-> Maybe (L.ScriptHash L.StandardCrypto)
-- ^ there will be an asset in the txout if provided
-> [TxOut CtxTx era]
mkTxOutput beo address mScriptHash = babbageEraOnwardsConstraints beo $ do
mkTxOutput beo address coin mScriptHash = babbageEraOnwardsConstraints beo $ do
let sbe = convert beo
[ TxOut
address
( TxOutValueShelleyBased
sbe
( L.MaryValue
(L.Coin 2_000_000)
coin
( L.MultiAsset $
fromList
[(L.PolicyID scriptHash, [(L.AssetName "eeee", 2)]) | scriptHash <- maybeToList mScriptHash]
Expand All @@ -373,6 +433,21 @@ mkTxOutput beo address mScriptHash = babbageEraOnwardsConstraints beo $ do
ReferenceScriptNone
]

parseSystemStart :: (HasCallStack, MonadTest m, MonadIO m) => String -> m SystemStart
parseSystemStart timeString =
withFrozenCallStack $
fmap SystemStart . H.evalIO $
DT.parseTimeM True DT.defaultTimeLocale "%Y-%m-%dT%H:%M:%S%QZ" timeString

getTxOutCoin
:: forall era ctx m
. (HasCallStack, MonadFail m, IsMaryBasedEra era)
=> TxOut ctx era
-> m L.Coin
getTxOutCoin txout = withFrozenCallStack $ maryEraOnwardsConstraints (maryBasedEra @era) $ do
TxOut _ (TxOutValueShelleyBased _ (L.MaryValue changeCoin _)) _ _ <- pure txout
pure changeCoin

tests :: TestTree
tests =
testGroup
Expand All @@ -383,5 +458,8 @@ tests =
, testProperty
"makeTransactionBodyAutoBalance autobalances multi-asset collateral"
prop_make_transaction_body_autobalance_multi_asset_collateral
, testProperty
"makeTransactionBodyAutoBalance autobalances when deregistering certificates"
prop_make_transaction_body_autobalance_when_deregistering_certs
, testProperty "calcReturnAndTotalCollateral constraints hold" prop_calcReturnAndTotalCollateral
]

0 comments on commit e0d4049

Please sign in to comment.