Skip to content

Commit eb34819

Browse files
authored
Improve error reporting on missing script (#2506)
fix #2507 We had a user report where the user is confused about the deposit that was not coming through and the reason was missing script utxo in the API request for deposit. This PR introduces a check in the internal wallet that should provide more info for the users in this specific case of missing script. --- <!-- Consider each and tick it off one way or the other --> * [x] CHANGELOG updated or not needed * [x] Documentation updated or not needed * [x] Haddocks updated or not needed * [x] No new TODOs introduced or explained herafter
2 parents c95c6e7 + 4e0e43e commit eb34819

File tree

4 files changed

+83
-7
lines changed

4 files changed

+83
-7
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ changes.
1414

1515
- Hydra node now correctly handles deposits and decommits on chain rollbacks and handles its local state correctly in terms of keeping track of pending deposits. [#2491](https://github.com/cardano-scaling/hydra/pull/2491)
1616

17+
- Improved error reporting for transactions with missing script witnesses. Users now receive a clear error message indicating which script is missing from the transaction witnesses. [#2506](https://github.com/cardano-scaling/hydra/pull/2506)
18+
1719
- **BREAKING** A Hydra node will now start rejecting both network and client inputs once its view of the chain has been out of sync for more than 50% of the configured `--contestation-period`, based on **system wall-clock time**.
1820
- Added `NodeUnsynced` and `NodeSynced` state events and server outputs.
1921
- Added `RejectedInputBecauseUnsynced` client message.

hydra-node/src/Hydra/Chain/Direct/Handlers.hs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,15 @@ finalizeTx TinyWallet{sign, coverFee} ctx utxo userUTxO partialTx = do
259259
} ::
260260
PostTxError Tx
261261
)
262+
Left ErrMissingScript{scriptHash, purpose} ->
263+
throwIO
264+
( ScriptFailedInWallet
265+
{ redeemerPtr = purpose
266+
, failureReason = "Missing script witness for " <> scriptHash
267+
, failingTx = partialTx
268+
} ::
269+
PostTxError Tx
270+
)
262271
Left e ->
263272
throwIO
264273
( InternalWalletError

hydra-node/src/Hydra/Chain/Direct/Wallet.hs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import Cardano.Ledger.Api (
2525
BabbageEraTxBody,
2626
ConwayEra,
2727
PParams,
28-
TransactionScriptFailure,
28+
TransactionScriptFailure (..),
2929
Tx,
3030
bodyTxL,
3131
calcMinFeeTx,
@@ -65,6 +65,7 @@ import Data.List qualified as List
6565
import Data.Map.Strict qualified as Map
6666
import Data.Sequence.Strict ((|>))
6767
import Data.Set qualified as Set
68+
import Data.Text qualified as Text
6869
import Hydra.Cardano.Api (
6970
BlockHeader,
7071
ChainPoint,
@@ -218,6 +219,7 @@ data ErrCoverFee
218219
| ErrScriptExecutionFailed {redeemerPointer :: Text, scriptFailure :: Text}
219220
| ErrTranslationError (ContextError LedgerEra)
220221
| ErrConwayUpgradeError (TxUpgradeError ConwayEra)
222+
| ErrMissingScript {scriptHash :: Text, purpose :: Text}
221223
deriving stock (Show)
222224

223225
data ChangeError = ChangeError {inputBalance :: Coin, outputBalance :: Coin}
@@ -441,11 +443,21 @@ estimateScriptsCost pparams systemStart epochInfo utxo tx = do
441443
convertResult ptr = \case
442444
Right exUnits -> Right exUnits
443445
Left failure ->
444-
Left $
445-
ErrScriptExecutionFailed
446-
{ redeemerPointer = show ptr
447-
, scriptFailure = show failure
448-
}
446+
case failure of
447+
-- Missing script witness - provide helpful error message
448+
MissingScript _ scriptHash ->
449+
Left $
450+
ErrMissingScript
451+
{ scriptHash = Text.pack $ show scriptHash
452+
, purpose = Text.pack $ show ptr
453+
}
454+
-- Any other script execution failure
455+
_ ->
456+
Left $
457+
ErrScriptExecutionFailed
458+
{ redeemerPointer = Text.pack $ show ptr
459+
, scriptFailure = Text.pack $ show failure
460+
}
449461

450462
--
451463
-- Logs

hydra-node/test/Hydra/Chain/Direct/WalletSpec.hs

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@ import Hydra.Prelude
77
import Test.Hydra.Prelude
88

99
import Cardano.Api.UTxO qualified as UTxO
10-
import Cardano.Ledger.Api (AlonzoEraTxWits (rdmrsTxWitsL), ConwayEra, EraTx (getMinFeeTx, witsTxL), EraTxBody (feeTxBodyL, inputsTxBodyL), PParams, bodyTxL, coinTxOutL, outputsTxBodyL)
10+
import Cardano.Ledger.Alonzo.Scripts (AsIx (..))
11+
import Cardano.Ledger.Alonzo.TxWits (Redeemers (..))
12+
import Cardano.Ledger.Api (AlonzoEraTxWits (rdmrsTxWitsL), ConwayEra, EraTx (getMinFeeTx, witsTxL), EraTxBody (feeTxBodyL, inputsTxBodyL), PParams, bodyTxL, coinTxOutL, outputsTxBodyL, pattern SpendingPurpose)
1113
import Cardano.Ledger.Babbage.Tx (AlonzoTx (..))
1214
import Cardano.Ledger.Babbage.TxBody (BabbageTxOut (..))
1315
import Cardano.Ledger.BaseTypes qualified as Ledger
1416
import Cardano.Ledger.Coin (Coin (..))
1517
import Cardano.Ledger.Conway.TxBody (ConwayTxBody (..))
1618
import Cardano.Ledger.Core (Tx, Value)
1719
import Cardano.Ledger.Hashes (hashAnnotated)
20+
import Cardano.Ledger.Plutus (Data, ExUnits (..))
1821
import Cardano.Ledger.Shelley.API qualified as Ledger
1922
import Cardano.Ledger.Slot (EpochInfo)
2023
import Cardano.Ledger.Val (Val (..), invert)
@@ -44,6 +47,7 @@ import Hydra.Chain.CardanoClient (QueryPoint (..))
4447
import Hydra.Chain.Direct.Wallet (
4548
Address,
4649
ChainQuery,
50+
ErrCoverFee (..),
4751
TinyWallet (..),
4852
TxIn,
4953
TxOut,
@@ -88,6 +92,7 @@ spec = parallel $ do
8892
prop "sets min utxo values" prop_setsMinUTxOValue
8993
prop "balances transaction with fees" prop_balanceTransaction
9094
prop "prefers largest utxo" prop_picksLargestUTxOToPayTheFees
95+
prop "reports ErrMissingScript when script witness is missing" prop_detectsMissingScript
9196

9297
describe "newTinyWallet" $ do
9398
prop "initialises wallet by querying UTxO" $
@@ -410,3 +415,51 @@ knownInputBalance utxo = foldMap resolve . toList . view (bodyTxL . inputsTxBody
410415
outputBalance :: Tx LedgerEra -> Value LedgerEra
411416
outputBalance =
412417
foldMap getValue . view (bodyTxL . outputsTxBodyL)
418+
419+
-- | Test that coverFee detects missing script witnesses.
420+
-- Generates transactions that spend from script-locked UTxOs but omit the script witness.
421+
prop_detectsMissingScript :: Property
422+
prop_detectsMissingScript =
423+
forAllBlind genScriptSpendingTx $ \(tx, scriptUTxO) ->
424+
forAllBlind (reasonablySized genUTxO) $ \walletUTxO ->
425+
forAll arbitrary $ \(arbitraryData :: Data LedgerEra) -> do
426+
let
427+
-- Add a redeemer for the script input but DON'T add the script witness.
428+
-- This creates the missing script scenario: redeemer present but script absent.
429+
-- NB: ExUnits are irrelevant since script execution will fail due to missing script.
430+
redeemers = Redeemers $ Map.singleton (SpendingPurpose (AsIx 0)) (arbitraryData, ExUnits 0 0)
431+
txWithRedeemer = tx & witsTxL . rdmrsTxWitsL .~ redeemers
432+
433+
case coverFee_ Fixture.pparams Fixture.systemStart Fixture.epochInfo scriptUTxO walletUTxO txWithRedeemer of
434+
Left (ErrMissingScript scriptHash purpose) ->
435+
property True
436+
& counterexample "✓ Correctly detected missing script"
437+
& counterexample (" Script hash: " <> toString scriptHash)
438+
& counterexample (" Purpose: " <> toString purpose)
439+
Left otherError ->
440+
property False
441+
& counterexample ("Expected ErrMissingScript but got: " <> show otherError)
442+
Right _balancedTx ->
443+
property False
444+
& counterexample "Expected ErrMissingScript but transaction succeeded"
445+
where
446+
-- Generate a transaction that spends from a script-locked UTxO
447+
genScriptSpendingTx :: Gen (Tx LedgerEra, Map TxIn TxOut)
448+
genScriptSpendingTx = do
449+
-- Generate a dummy script hash
450+
scriptHash <- arbitrary
451+
452+
-- Create a script-locked output
453+
baseOutput <- arbitrary
454+
let scriptAddress = Ledger.Addr Ledger.Testnet (Ledger.ScriptHashObj scriptHash) Ledger.StakeRefNull
455+
scriptTxOut = baseOutput & (\(BabbageTxOut _ val dat ref) -> BabbageTxOut scriptAddress val dat ref)
456+
457+
-- Create an input spending from this script output
458+
scriptTxIn <- toLedgerTxIn <$> genTxIn
459+
460+
-- Generate a transaction with this input
461+
baseTx <- genLedgerTx
462+
let txSpendingScript = baseTx & bodyTxL . inputsTxBodyL .~ Set.singleton scriptTxIn
463+
lookupUTxO = Map.singleton scriptTxIn scriptTxOut
464+
465+
pure (txSpendingScript, lookupUTxO)

0 commit comments

Comments
 (0)