IntersectMBO / plutus-apps

The Plutus application platform

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Emulator transaction balancing doesn't work after fix

MaximilianAlgehed opened this issue · comments

Summary

The balancer in the emulator does not compute the correct Ada required to cover the minimum Ada per output when minting tokens. Our best guess is that this is because it generates separate outputs for minted tokens and left-over tokens from consumed outputs, instead of merging them into a single output, but computes the Ada requirement as if they were merged.

This happens even after the fixes in PR#229.

Steps to reproduce the behavior

The following test fails in Spec.Contract in plutus-contract-tests (with a small change to the imports):

import Ledger.Scripts (datumHash, mintingPolicyHash, unitDatum, unitRedeemer)
import Ledger.Typed.Scripts.MonetaryPolicies qualified as MPS

balanceTxnMinAda' :: TestTree
balanceTxnMinAda' =
    let vA n = Value.singleton "ee" "A" n
        vB n = Value.singleton "ff" "B" n
        mps  = MPS.mkForwardingMintingPolicy vHash
        vL n = Value.singleton (Value.mpsSymbol $ mintingPolicyHash mps) "L" n
        options = defaultCheckOptions
            & changeInitialWalletValue w1 (<> vA 1 <> vB 2)
        vHash = validatorHash someValidator
        payToWallet w = Constraints.mustPayToPubKey (EM.mockWalletPaymentPubKeyHash w)
        mkTx lookups constraints = Constraints.adjustUnbalancedTx . either (error . show) id $ Constraints.mkTx @Void lookups constraints

        setupContract :: Contract () EmptySchema ContractError ()
        setupContract = do
            -- Make sure there is a utxo with 1 A, 1 B, and 4 ada at w2
            submitTxConfirmed $ mkTx mempty (payToWallet w2 (vA 1 <> vB 1 <> Value.scale 2 (Ada.toValue Ledger.minAdaTxOut)))
            -- Make sure there is a UTxO with 1 B and datum () at the script
            submitTxConfirmed $ mkTx mempty (Constraints.mustPayToOtherScript vHash unitDatum (vB 1))
            -- utxo0 @ wallet2 = 1 A, 1 B, 4 Ada
            -- utxo1 @ script  = 1 B, 2 Ada

        wallet2Contract :: Contract () EmptySchema ContractError ()
        wallet2Contract = do
            utxos <- utxosAt someAddress
            let txOutRef = head (Map.keys utxos)
                lookups = Constraints.unspentOutputs utxos
                        <> Constraints.otherScript someValidator
                        <> Constraints.mintingPolicy mps
                constraints = Constraints.mustSpendScriptOutput txOutRef unitRedeemer                                        -- spend utxo1
                            <> Constraints.mustPayToOtherScript vHash unitDatum (vB 1)                                       -- 2 ada and 1 B to script
                            <> Constraints.mustPayToOtherScript vHash (Datum $ PlutusTx.toBuiltinData (0 :: Integer)) (vB 1) -- 2 ada and 1 B to script (different datum)
                            <> Constraints.mustMintValue (vL 1) -- 1 L and 2 ada to wallet2
                            -- the leftover A doesn't get any ada here
                            -- the right solution would be to merge the 1 L and 2 Ada with the leftover A
                            --
                            -- Note: this bug is flaky - if you change the amount of A at the wallet to 899
                            -- this test passes, if you change it to 900 it fails, and at 901 it passes again.
                            -- This is most likely because the order in which UTxOs appear in the UTxO map depend
                            -- on their hash - so when UTxOs are pulled in to balance the UTxOs
                            -- containing Bs one of two things happen:
                            --  1. Either the precise amount of Ada required to balance existing utxos is found in the UTxO containing A (utxo0)
                            --  but we produce an extra UTxO that contains only A (that never gets any Ada) or
                            --  2. A utxo with a lot of ada is taken in first and is then squashed with the output utxo for As.
            submitTxConfirmed $ mkTx lookups constraints

        trace = do
            Trace.activateContractWallet w1 setupContract
            Trace.waitNSlots 10
            Trace.activateContractWallet w2 wallet2Contract
            Trace.waitNSlots 10

    in checkPredicateOptions options "balancing doesn't create outputs with no Ada (2)" assertNoFailedTransactions (void trace)

Actual Result

The test fails with a ValueContainsLessThanMinAda validaton error.

Expected Result

We expect the test to succeed.

Describe the approach you would take to fix this

We think the issue is in Wallet.Emulator.Wallet.handleBalanceTx.

System info

OS: WSL 2 (this really shouldn't matter...)
Plutus: 7f53f18df

I have even simpler example, with gift contract (i.e. always succeeding validator) which now fails:

When I do

giftTrace :: EmulatorTrace ()
giftTrace = do
    c1 <- Trace.activateContractWallet w1 giftContract
    Trace.callEndpoint @"give" c1 1000000
    void $ Trace.waitNSlots 1

with

give :: AsContractError e => Integer -> Contract w s e ()
give amount = do
    let tx = Constraints.mustPayToOtherScript valHash Scripts.unitDatum $ Ada.lovelaceValueOf amount
    ledgerTx <- submitTx tx
    void $ awaitTxConfirmed (getCardanoTxId ledgerTx)
    logInfo @String $ printf "made a gift of %d lovelace" amount

it fails:

      [INFO] Slot 0: TxnValidate 98d5fbcefe21113b3f0390c1441e075b8a870cc5a8fa2a56dcde1d8247e41715
      [INFO] Slot 1: 00000000-0000-4000-8000-000000000000 {Wallet W872c}:
                       Contract instance started
      [INFO] Slot 1: 00000000-0000-4000-8000-000000000000 {Wallet W872c}:
                       Receive endpoint call on 'give' for Object (fromList [("contents",Array [Object (fromList [("getEndpointDescription",String "give")]),Object (fromList [("unEndpointValue",Number 1000000.0)])]),("tag",String "ExposeEndpointResp")])
      [INFO] Slot 1: W872cb83: Balancing an unbalanced transaction:
                                 Tx:
                                   Tx 156e33a2b2002c7fa42ed139604c824fa2e4395345bd838b7902f99bdeb47545:
                                     {inputs:
                                     collateral inputs:
                                     outputs:
                                       - Value (Map [(,Map [("",1000000)])]) addressed to
                                         ScriptCredential: 62bdc3d04d04376d516d31664944b25ce3affa76d17f8b5e1279b49d (no staking credential)
                                     mint: Value (Map [])
                                     fee: Value (Map [])
                                     mps:
                                     signatures:
                                     validity range: Interval {ivFrom = LowerBound NegInf True, ivTo = UpperBound PosInf True}
                                     data:
                                       <>}
                                 Requires signatures:
                                 Utxo index:
                                 Validity range:
                                   (-∞ , +∞)
      [WARNING] Slot 1: W872cb83: Validation error: Phase2 bc7fb8d2c5f914b3f44a1986ebb1dd88e104999e0453edc5a5c8ab6683adda4e: ValueContainsLessThanMinAda (Tx {txInputs = fromList [TxIn {txInRef = TxOutRef {txOutRefId = 98d5fbcefe21113b3f0390c1441e075b8a870cc5a8fa2a56dcde1d8247e41715, txOutRefIdx = 5}, txInType = Just ConsumePublicKeyAddress}], txCollateral = fromList [], txOutputs = [TxOut {txOutAddress = Address {addressCredential = PubKeyCredential a2c20c77887ace1cd986193e4e75babd8993cfd56995cd5cfce609c2, addressStakingCredential = Nothing}, txOutValue = Value (Map [(,Map [("",99000000)])]), txOutDatumHash = Nothing},TxOut {txOutAddress = Address {addressCredential = ScriptCredential 62bdc3d04d04376d516d31664944b25ce3affa76d17f8b5e1279b49d, addressStakingCredential = Nothing}, txOutValue = Value (Map [(,Map [("",1000000)])]), txOutDatumHash = Just 923918e403bf43c34b4ef6b48eb2ee04babed17320d8d1b9ff9ad086e86f44ec}], txMint = Value (Map []), txFee = Value (Map []), txValidRange = Interval {ivFrom = LowerBound NegInf True, ivTo = UpperBound PosInf True}, txMintScripts = fromList [], txSignatures = fromList [(8d9de88fbf445b7f6c3875a14daba94caee2ffcbc9ac211c95aba0a2f5711853,e7a40cadb0c73a7dc86c08b27f72194269e8395766412d820a0b7199fafbd1ce7f3e48ecfa26f504574cb6b39ac74ba77cf931fa4a2a4949da74a766584e8a0c)], txRedeemers = fromList [], txData = fromList [(923918e403bf43c34b4ef6b48eb2ee04babed17320d8d1b9ff9ad086e86f44ec,Datum {getDatum = Constr 0 []})]}) (Lovelace {getLovelace = 2000000})

@phadej Can you check if your example now works on main?

probably not as you have

minAdaTxOut :: Ada
minAdaTxOut = Ada.lovelaceOf 2_000_000

and my tests move only 1 Ada around.

Where from that fixed value originates?

@phadej "Normally, the minimum is 1 Ada, but transaction outputs that have datum are slightly more expensive than 1 Ada. So, to be on the safe side, we set the minimum Ada of each transaction output to 2 Ada."

I meant why that restriction exists at all on the cardano network.

@phadej "minUTXOvalue is a security parameter that prevents "flood attacks" making them costly. A flood attack is where malicious users could exploit the block size limit to overwhelm the blockchain with low-valued spam transactions, and cause delay in the verification of legitimate transactions."

Where from is that?

@MaximilianAlgehed thanks for the issue and the test!