Skip to content

Commit 7f277ba

Browse files
committed
Better reporting of negative balance in transaction balancing
1 parent 4529c4a commit 7f277ba

2 files changed

Lines changed: 83 additions & 61 deletions

File tree

cardano-api/src/Cardano/Api/Internal/Fees.hs

Lines changed: 63 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
{-# LANGUAGE FlexibleInstances #-}
55
{-# LANGUAGE GADTs #-}
66
{-# LANGUAGE LambdaCase #-}
7+
{-# LANGUAGE MultiWayIf #-}
78
{-# LANGUAGE NamedFieldPuns #-}
89
{-# LANGUAGE RankNTypes #-}
910
{-# LANGUAGE ScopedTypeVariables #-}
@@ -357,6 +358,7 @@ import Cardano.Api.Internal.Plutus
357358
import Cardano.Api.Internal.Pretty
358359
import Cardano.Api.Internal.ProtocolParameters
359360
import Cardano.Api.Internal.Query
361+
import Cardano.Api.Internal.ReexposeLedger qualified as L
360362
import Cardano.Api.Internal.Script
361363
import Cardano.Api.Internal.Tx.Body
362364
import Cardano.Api.Internal.Tx.Sign
@@ -370,8 +372,8 @@ import Cardano.Ledger.Alonzo.Scripts qualified as Alonzo
370372
import Cardano.Ledger.Api qualified as L
371373
import Cardano.Ledger.Coin qualified as L
372374
import Cardano.Ledger.Conway.Governance qualified as L
373-
import Cardano.Ledger.Core qualified as L
374375
import Cardano.Ledger.Credential as Ledger (Credential)
376+
import Cardano.Ledger.Mary.Value qualified as L
375377
import Cardano.Ledger.Plutus.Language qualified as Plutus
376378
import Cardano.Ledger.Val qualified as L
377379
import Ouroboros.Consensus.HardFork.History qualified as Consensus
@@ -395,6 +397,7 @@ import Data.Text (Text)
395397
import GHC.Exts (IsList (..))
396398
import GHC.Stack
397399
import Lens.Micro ((.~), (^.))
400+
import Prettyprinter (punctuate)
398401

399402
-- | Type synonym for logs returned by the ledger's @evalTxExUnitsWithLogs@ function.
400403
-- for scripts in transactions.
@@ -639,9 +642,10 @@ estimateBalancedTxBody
639642
let fakeUTxO = createFakeUTxO sbe txbodycontent1 $ selectLovelace availableUTxOValue
640643
balance =
641644
evaluateTransactionBalance sbe pparams poolids stakeDelegDeposits drepDelegDeposits fakeUTxO txbody2
645+
balanceTxOut = TxOut changeaddr balance TxOutDatumNone ReferenceScriptNone
642646
-- check if the balance is positive or negative
643647
-- in one case we can produce change, in the other the inputs are insufficient
644-
first TxFeeEstimationBalanceError $ balanceCheck sbe pparams changeaddr balance
648+
first TxFeeEstimationBalanceError $ balanceCheck sbe pparams balanceTxOut
645649

646650
-- Step 6. Check all txouts have the min required UTxO value
647651
forM_ (txOuts txbodycontent1) $
@@ -659,7 +663,7 @@ estimateBalancedTxBody
659663
{ txFee = TxFeeExplicit sbe fee
660664
, txOuts =
661665
accountForNoChange
662-
(TxOut changeaddr balance TxOutDatumNone ReferenceScriptNone)
666+
balanceTxOut
663667
(txOuts txbodycontent)
664668
, txReturnCollateral = retColl
665669
, txTotalCollateral = reqCol
@@ -673,7 +677,7 @@ estimateBalancedTxBody
673677
( BalancedTxBody
674678
finalTxBodyContent
675679
txbody3
676-
(TxOut changeaddr balance TxOutDatumNone ReferenceScriptNone)
680+
balanceTxOut
677681
fee
678682
)
679683

@@ -990,9 +994,7 @@ evaluateTransactionExecutionUnits
990994
-> LedgerProtocolParameters era
991995
-> UTxO era
992996
-> TxBody era
993-
-> Either
994-
(TransactionValidityError era)
995-
(Map ScriptWitnessIndex (Either ScriptExecutionError (EvalTxExecutionUnitsLog, ExecutionUnits)))
997+
-> Map ScriptWitnessIndex (Either ScriptExecutionError (EvalTxExecutionUnitsLog, ExecutionUnits))
996998
evaluateTransactionExecutionUnits era systemstart epochInfo pp utxo txbody =
997999
case makeSignedTransaction' era [] txbody of
9981000
ShelleyTx sbe tx' -> evaluateTransactionExecutionUnitsShelley sbe systemstart epochInfo pp utxo tx'
@@ -1006,14 +1008,12 @@ evaluateTransactionExecutionUnitsShelley
10061008
-> LedgerProtocolParameters era
10071009
-> UTxO era
10081010
-> L.Tx (ShelleyLedgerEra era)
1009-
-> Either
1010-
(TransactionValidityError era)
1011-
(Map ScriptWitnessIndex (Either ScriptExecutionError (EvalTxExecutionUnitsLog, ExecutionUnits)))
1011+
-> Map ScriptWitnessIndex (Either ScriptExecutionError (EvalTxExecutionUnitsLog, ExecutionUnits))
10121012
evaluateTransactionExecutionUnitsShelley sbe systemstart epochInfo (LedgerProtocolParameters pp) utxo tx =
10131013
caseShelleyToMaryOrAlonzoEraOnwards
1014-
(const (Right Map.empty))
1014+
(const Map.empty)
10151015
( \w ->
1016-
pure . fromLedgerScriptExUnitsMap w $
1016+
fromLedgerScriptExUnitsMap w $
10171017
alonzoEraOnwardsConstraints w $
10181018
L.evalTxExUnitsWithLogs pp tx (toLedgerUTxO sbe utxo) ledgerEpochInfo systemstart
10191019
)
@@ -1147,22 +1147,21 @@ data TxBodyErrorAutoBalance era
11471147
TxBodyScriptExecutionError [(ScriptWitnessIndex, ScriptExecutionError)]
11481148
| -- | One or more scripts were expected to fail validation, but none did.
11491149
TxBodyScriptBadScriptValidity
1150-
| -- | There is not enough ada to cover both the outputs and the fees.
1151-
-- The transaction should be changed to provide more input ada, or
1150+
| -- | There is not enough ada and non-ada to cover both the outputs and the fees.
1151+
-- The transaction should be changed to provide more input assets, or
11521152
-- otherwise adjusted to need less (e.g. outputs, script etc).
1153-
TxBodyErrorAdaBalanceNegative L.Coin
1153+
TxBodyErrorBalanceNegative L.Coin L.MultiAsset
11541154
| -- | There is enough ada to cover both the outputs and the fees, but the
11551155
-- resulting change is too small: it is under the minimum value for
11561156
-- new UTXO entries. The transaction should be changed to provide more
11571157
-- input ada.
11581158
TxBodyErrorAdaBalanceTooSmall
1159-
-- \^ Offending TxOut
1160-
11611159
TxOutInAnyEra
1160+
-- ^ Offending TxOut
1161+
L.Coin
11621162
-- ^ Minimum UTxO
11631163
L.Coin
11641164
-- ^ Tx balance
1165-
L.Coin
11661165
| -- | 'makeTransactionBodyAutoBalance' does not yet support the Byron era.
11671166
TxBodyErrorByronEraNotSupported
11681167
| -- | The 'ProtocolParameters' must provide the value for the min utxo
@@ -1173,11 +1172,10 @@ data TxBodyErrorAutoBalance era
11731172
TxBodyErrorValidityInterval (TransactionValidityError era)
11741173
| -- | The minimum spendable UTxO threshold has not been met.
11751174
TxBodyErrorMinUTxONotMet
1176-
-- \^ Offending TxOut
1177-
11781175
TxOutInAnyEra
1179-
-- ^ Minimum UTXO
1176+
-- ^ Offending TxOut
11801177
L.Coin
1178+
-- ^ Minimum UTXO
11811179
| TxBodyErrorNonAdaAssetsUnbalanced Value
11821180
| TxBodyErrorScriptWitnessIndexMissingFromExecUnitsMap
11831181
ScriptWitnessIndex
@@ -1201,12 +1199,14 @@ instance Error (TxBodyErrorAutoBalance era) where
12011199
]
12021200
TxBodyScriptBadScriptValidity ->
12031201
"One or more of the scripts were expected to fail validation, but none did."
1204-
TxBodyErrorAdaBalanceNegative lovelace ->
1205-
mconcat
1206-
[ "The transaction does not balance in its use of ada. The net balance "
1207-
, "of the transaction is negative: " <> pretty lovelace <> ". "
1208-
, "The usual solution is to provide more inputs, or inputs with more ada."
1202+
TxBodyErrorBalanceNegative lovelace assets ->
1203+
mconcat $
1204+
[ "The transaction does not balance in its use of assets. The net balance "
1205+
, "of the transaction is negative: "
12091206
]
1207+
<> punctuate ", " [pretty lovelace, pretty assets]
1208+
<> [ ". The usual solution is to provide more inputs, or inputs with more assets."
1209+
]
12101210
TxBodyErrorAdaBalanceTooSmall changeOutput minUTxO balance ->
12111211
mconcat
12121212
[ "The transaction does balance in its use of ada, however the net "
@@ -1365,8 +1365,16 @@ makeTransactionBodyAutoBalance
13651365
-- 4. balance the transaction and update tx change output
13661366

13671367
txbodyForChange <- first TxBodyError $ createTransactionBody sbe txbodycontent
1368-
let initialChangeTxOut =
1368+
let initialChangeTxOutValue =
13691369
evaluateTransactionBalance sbe pp poolids stakeDelegDeposits drepDelegDeposits utxo txbodyForChange
1370+
initialChangeTxOut =
1371+
TxOut
1372+
changeaddr
1373+
initialChangeTxOutValue
1374+
TxOutDatumNone
1375+
ReferenceScriptNone
1376+
1377+
balanceCheck sbe pp initialChangeTxOut
13701378

13711379
-- Tx body used only for evaluating execution units. Because txout exact
13721380
-- values do not matter much here, we are using an initial change value,
@@ -1378,16 +1386,16 @@ makeTransactionBodyAutoBalance
13781386
sbe
13791387
$ txbodycontent
13801388
& modTxOuts
1381-
(<> [TxOut changeaddr initialChangeTxOut TxOutDatumNone ReferenceScriptNone])
1382-
exUnitsMapWithLogs <-
1383-
first TxBodyErrorValidityInterval $
1384-
evaluateTransactionExecutionUnits
1385-
era
1386-
systemstart
1387-
history
1388-
lpp
1389-
utxo
1390-
txbody
1389+
(<> [initialChangeTxOut])
1390+
-- first TxBodyErrorValidityInterval $ -- TODO remove this?
1391+
let exUnitsMapWithLogs =
1392+
evaluateTransactionExecutionUnits
1393+
era
1394+
systemstart
1395+
history
1396+
lpp
1397+
utxo
1398+
txbody
13911399

13921400
let exUnitsMap = Map.map (fmap snd) exUnitsMapWithLogs
13931401

@@ -1419,7 +1427,7 @@ makeTransactionBodyAutoBalance
14191427
{ txFee = TxFeeExplicit sbe maxLovelaceFee
14201428
, txOuts =
14211429
txOuts txbodycontent
1422-
<> [TxOut changeaddr initialChangeTxOut TxOutDatumNone ReferenceScriptNone]
1430+
<> [initialChangeTxOut]
14231431
, txReturnCollateral = dummyCollRet
14241432
, txTotalCollateral = dummyTotColl
14251433
}
@@ -1468,11 +1476,12 @@ makeTransactionBodyAutoBalance
14681476
, txTotalCollateral = reqCol
14691477
}
14701478
let balance = evaluateTransactionBalance sbe pp poolids stakeDelegDeposits drepDelegDeposits utxo txbody2
1479+
balanceTxOut = TxOut changeaddr balance TxOutDatumNone ReferenceScriptNone
14711480
forM_ (txOuts txbodycontent1) $ \txout -> checkMinUTxOValue sbe txout pp
14721481

14731482
-- check if the balance is positive or negative
14741483
-- in one case we can produce change, in the other the inputs are insufficient
1475-
balanceCheck sbe pp changeaddr balance
1484+
balanceCheck sbe pp balanceTxOut
14761485

14771486
-- TODO: we could add the extra fee for the CBOR encoding of the change,
14781487
-- now that we know the magnitude of the change: i.e. 1-8 bytes extra.
@@ -1486,7 +1495,7 @@ makeTransactionBodyAutoBalance
14861495
{ txFee = TxFeeExplicit sbe fee
14871496
, txOuts =
14881497
accountForNoChange
1489-
(TxOut changeaddr balance TxOutDatumNone ReferenceScriptNone)
1498+
balanceTxOut
14901499
(txOuts txbodycontent)
14911500
, txReturnCollateral = retColl
14921501
, txTotalCollateral = reqCol
@@ -1500,7 +1509,7 @@ makeTransactionBodyAutoBalance
15001509
( BalancedTxBody
15011510
finalTxBodyContent
15021511
txbody3
1503-
(TxOut changeaddr balance TxOutDatumNone ReferenceScriptNone)
1512+
balanceTxOut
15041513
fee
15051514
)
15061515
where
@@ -1534,26 +1543,20 @@ checkMinUTxOValue sbe txout@(TxOut _ v _ _) bpp = do
15341543
balanceCheck
15351544
:: ShelleyBasedEra era
15361545
-> Ledger.PParams (ShelleyLedgerEra era)
1537-
-> AddressInEra era
1538-
-> TxOutValue era
1546+
-> TxOut CtxTx era
15391547
-> Either (TxBodyErrorAutoBalance era) ()
1540-
balanceCheck sbe bpparams changeaddr balance
1541-
| txOutValueToLovelace balance == 0 && onlyAda (txOutValueToValue balance) = return ()
1542-
| txOutValueToLovelace balance < 0 =
1543-
Left . TxBodyErrorAdaBalanceNegative $ txOutValueToLovelace balance
1544-
| otherwise =
1545-
case checkMinUTxOValue sbe (TxOut changeaddr balance TxOutDatumNone ReferenceScriptNone) bpparams of
1546-
Left (TxBodyErrorMinUTxONotMet txOutAny minUTxO) ->
1547-
Left $ TxBodyErrorAdaBalanceTooSmall txOutAny minUTxO (txOutValueToLovelace balance)
1548-
Left err -> Left err
1549-
Right _ -> Right ()
1550-
1551-
isNotAda :: AssetId -> Bool
1552-
isNotAda AdaAssetId = False
1553-
isNotAda _ = True
1554-
1555-
onlyAda :: Value -> Bool
1556-
onlyAda = null . toList . filterValue isNotAda
1548+
balanceCheck sbe bpparams txout@(TxOut _ balance _ _) = do
1549+
let outValue@(L.MaryValue coin multiAsset) = toMaryValue $ txOutValueToValue balance
1550+
isPositiveValue = L.pointwise (>) outValue mempty
1551+
if
1552+
| L.isZero outValue && L.isAdaOnly outValue -> pure () -- no change
1553+
| not isPositiveValue -> Left $ TxBodyErrorBalanceNegative coin multiAsset
1554+
| otherwise ->
1555+
case checkMinUTxOValue sbe txout bpparams of
1556+
Left (TxBodyErrorMinUTxONotMet txOutAny minUTxO) ->
1557+
Left $ TxBodyErrorAdaBalanceTooSmall txOutAny minUTxO coin
1558+
Left err -> Left err
1559+
Right _ -> Right ()
15571560

15581561
-- Calculation taken from validateInsufficientCollateral:
15591562
-- https://github.com/input-output-hk/cardano-ledger/blob/389b266d6226dedf3d2aec7af640b3ca4984c5ea/eras/alonzo/impl/src/Cardano/Ledger/Alonzo/Rules/Utxo.hs#L335

cardano-api/src/Cardano/Api/Internal/Orphans.hs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import Cardano.Chain.Update.Validation.Endorsement qualified as L.Endorsement
2929
import Cardano.Chain.Update.Validation.Interface qualified as L.Interface
3030
import Cardano.Chain.Update.Validation.Registration qualified as L.Registration
3131
import Cardano.Chain.Update.Validation.Voting qualified as L.Voting
32+
import Cardano.Crypto.Hash qualified as Crypto
3233
import Cardano.Ledger.Allegra.Rules qualified as L
3334
import Cardano.Ledger.Alonzo.PParams qualified as Ledger
3435
import Cardano.Ledger.Alonzo.Rules qualified as L
@@ -49,6 +50,7 @@ import Cardano.Ledger.Core qualified as L hiding (KeyHash)
4950
import Cardano.Ledger.HKD (NoUpdate (..))
5051
import Cardano.Ledger.Hashes qualified as L hiding (KeyHash)
5152
import Cardano.Ledger.Keys qualified as L.Keys
53+
import Cardano.Ledger.Mary.Value qualified as L
5254
import Cardano.Ledger.Shelley.API.Mempool qualified as L
5355
import Cardano.Ledger.Shelley.PParams qualified as Ledger
5456
import Cardano.Ledger.Shelley.Rules qualified as L
@@ -89,12 +91,13 @@ import Data.Monoid
8991
import Data.Text qualified as T
9092
import Data.Text.Encoding qualified as Text
9193
import Data.Typeable (Typeable)
92-
import GHC.Exts (IsList (..))
94+
import GHC.Exts (IsList (..), IsString (..))
9395
import GHC.Generics
9496
import GHC.Stack (HasCallStack)
9597
import GHC.TypeLits
9698
import Lens.Micro
9799
import Network.Mux qualified as Mux
100+
import Prettyprinter (punctuate, viaShow)
98101

99102
deriving instance Generic (L.ApplyTxError era)
100103

@@ -257,6 +260,22 @@ deriving newtype instance Num L.Coin
257260
instance Pretty L.Coin where
258261
pretty (L.Coin n) = pretty n <+> "Lovelace"
259262

263+
instance Pretty L.MultiAsset where
264+
pretty (L.MultiAsset assetsMap) =
265+
mconcat $
266+
punctuate
267+
", "
268+
[ pretty quantity <+> pretty pId <> "." <> pretty name
269+
| (pId, assets) <- toList assetsMap
270+
, (name, quantity) <- toList assets
271+
]
272+
273+
instance Pretty L.PolicyID where
274+
pretty (L.PolicyID (L.ScriptHash sh)) = pretty $ Crypto.hashToStringAsHex sh
275+
276+
instance Pretty L.AssetName where
277+
pretty = pretty . L.assetNameToTextAsHex
278+
260279
-- Orphan instances involved in the JSON output of the API queries.
261280
-- We will remove/replace these as we provide more API wrapper types
262281

0 commit comments

Comments
 (0)