Skip to content

Commit 6917544

Browse files
authored
Adjust UTxO set when building publish script txs (#1961)
Currently, Hydra needs to build 3 txs to publish scripts. Each is built using the faucet UTxO. This logic was adjusting the first selected UTxO repeatedly, causing the following [CI failure](https://github.com/cardano-scaling/hydra/actions/runs/14612733559/job/41012379950): ``` hydra-cluster: Illegal Value in TxOut: MaryValue (Coin (-64520700)) (MultiAsset (fromList [])) ``` Now: - we adjust the entire UTxO set on every tx build iteration, and then - we select all outputs associated with the key address (instead of just the first matching one). <!-- Describe your change here --> --- <!-- Consider each and tick it off one way or the other --> * [ ] 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 5e63c42 + 7e34ca3 commit 6917544

File tree

2 files changed

+66
-54
lines changed

2 files changed

+66
-54
lines changed

hydra-cluster/test/Test/Hydra/Cluster/FaucetSpec.hs

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,21 @@ import CardanoNode (withCardanoNodeDevnet)
88
import Control.Concurrent.Async (replicateConcurrently)
99
import Hydra.Cardano.Api (Coin (..), selectLovelace, txOutValue)
1010
import Hydra.Chain.CardanoClient (QueryPoint (..), queryUTxOFor)
11-
import Hydra.Cluster.Faucet (returnFundsToFaucet, seedFromFaucet)
11+
import Hydra.Cluster.Faucet (FaucetLog, publishHydraScriptsAs, returnFundsToFaucet, seedFromFaucet)
1212
import Hydra.Cluster.Fixture (Actor (..))
1313
import Hydra.Cluster.Scenarios (EndToEndLog (..))
1414
import Hydra.Cluster.Util (keysFor)
1515
import Hydra.Logging (Tracer, showLogsOnFailure)
1616
import Test.Hydra.Tx.Gen (genVerificationKey)
1717
import Test.QuickCheck (choose, elements, forAll, generate, withMaxSuccess)
1818

19-
setupDevnet :: ((Tracer IO EndToEndLog, RunningNode) -> IO a) -> IO a
19+
setupDevnet :: ((Tracer IO FaucetLog, RunningNode) -> IO a) -> IO a
2020
setupDevnet action =
2121
failAfter 30 $
2222
showLogsOnFailure "FaucetSpec" $ \tracer ->
2323
withTempDir "hydra-cluster" $ \tmpDir ->
2424
withCardanoNodeDevnet (contramap FromCardanoNode tracer) tmpDir $ \node ->
25-
action (tracer, node)
25+
action (contramap FromFaucet tracer, node)
2626

2727
spec :: Spec
2828
spec =
@@ -31,24 +31,23 @@ spec =
3131
it "should work concurrently when called multiple times with the same amount of lovelace" $ \(tracer, node) -> do
3232
utxos <- replicateConcurrently 10 $ do
3333
vk <- generate genVerificationKey
34-
seedFromFaucet node vk 1_000_000 (contramap FromFaucet tracer)
34+
seedFromFaucet node vk 1_000_000 tracer
3535
-- 10 unique outputs
3636
length (fold utxos) `shouldBe` 10
3737

3838
describe "returnFundsToFaucet" $ do
3939
it "does nothing if nothing to return" $ \(tracer, node) -> do
40-
returnFundsToFaucet (contramap FromFaucet tracer) node Alice
40+
returnFundsToFaucet tracer node Alice
4141

4242
it "seedFromFaucet and returnFundsToFaucet should work together" $ \(tracer, node@RunningNode{networkId, nodeSocket}) -> do
4343
withMaxSuccess 10 $
4444
forAll (Coin <$> choose (1000000, 10000000000)) $ \coin ->
4545
forAll (elements [Alice, Bob, Carol]) $ \actor -> do
46-
let faucetTracer = contramap FromFaucet tracer
4746
(vk, _) <- keysFor actor
4847
(faucetVk, _) <- keysFor Faucet
4948
initialFaucetFunds <- queryUTxOFor networkId nodeSocket QueryTip faucetVk
50-
void $ seedFromFaucet node vk coin faucetTracer
51-
returnFundsToFaucet faucetTracer node actor
49+
void $ seedFromFaucet node vk coin tracer
50+
returnFundsToFaucet tracer node actor
5251
remaining <- queryUTxOFor networkId nodeSocket QueryTip vk
5352
finalFaucetFunds <- queryUTxOFor networkId nodeSocket QueryTip faucetVk
5453
foldMap txOutValue remaining `shouldBe` mempty
@@ -62,3 +61,17 @@ spec =
6261
-- difference between starting faucet amount and final one should
6362
-- just be the amount of paid fees
6463
difference `shouldSatisfy` (< 400_000)
64+
65+
describe "publishHydraScriptsAs" $ do
66+
it "selects a suitable output" $ \(tracer, node@RunningNode{networkId, nodeSocket}) -> do
67+
-- NOTE: Note use 'Faucet' as this has a very big initial amount
68+
(vk, _) <- keysFor Alice
69+
-- NOTE: 83 ADA is just enough to pay for reference scripts deposits.
70+
forM_ [1_000_000, 2_000_000, 83_000_000] $ \c -> seedFromFaucet node vk c tracer
71+
utxoBefore <- queryUTxOFor networkId nodeSocket QueryTip vk
72+
73+
void $ publishHydraScriptsAs node Alice
74+
75+
-- Also, does not squash UTxO
76+
utxoAfter <- queryUTxOFor networkId nodeSocket QueryTip vk
77+
length utxoAfter `shouldBe` length utxoBefore

hydra-node/src/Hydra/Chain/ScriptRegistry.hs

Lines changed: 45 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,42 +5,40 @@ module Hydra.Chain.ScriptRegistry where
55
import Hydra.Prelude
66

77
import Cardano.Api.UTxO qualified as UTxO
8+
import Data.List ((!!))
89
import Hydra.Cardano.Api (
9-
AddressInEra,
10+
Coin,
11+
Era,
1012
EraHistory,
1113
Key (..),
1214
LedgerEra,
1315
NetworkId,
1416
PParams,
1517
PaymentKey,
16-
PlutusScript,
1718
PoolId,
18-
ShelleyWitnessSigningKey (WitnessPaymentKey),
1919
SigningKey,
2020
SocketPath,
2121
SystemStart,
2222
Tx,
23-
TxBody,
23+
TxBodyErrorAutoBalance,
2424
TxId,
2525
TxIn (..),
2626
TxIx (..),
2727
UTxO,
2828
WitCtx (..),
2929
examplePlutusScriptAlwaysFails,
30-
getTxBody,
31-
isKeyAddress,
32-
makeShelleyKeyWitness,
33-
makeSignedTransaction,
3430
mkScriptAddress,
3531
mkScriptRef,
32+
mkTxIn,
3633
mkTxOutAutoBalance,
3734
mkVkAddress,
3835
selectLovelace,
39-
throwErrorAsException,
40-
txOutAddress,
36+
toCtxUTxOTxOut,
4137
txOutValue,
38+
txOuts',
4239
pattern TxOutDatumNone,
4340
)
41+
import Hydra.Cardano.Api.Tx (signTx)
4442
import Hydra.Chain.CardanoClient (
4543
QueryPoint (..),
4644
awaitTransaction,
@@ -54,7 +52,6 @@ import Hydra.Chain.CardanoClient (
5452
submitTransaction,
5553
)
5654
import Hydra.Contract.Head qualified as Head
57-
import Hydra.Ledger.Cardano (adjustUTxO)
5855
import Hydra.Plutus (commitValidatorScript, initialValidatorScript)
5956
import Hydra.Tx (txId)
6057
import Hydra.Tx.ScriptRegistry (ScriptRegistry (..), newScriptRegistry)
@@ -105,54 +102,56 @@ publishHydraScripts networkId socketPath sk = do
105102
where
106103
vk = getVerificationKey sk
107104

105+
-- | Exception raised when building the script publishing transactions.
106+
data PublishScriptException
107+
= FailedToBuildPublishingTx (TxBodyErrorAutoBalance Era)
108+
| FailedToFindUTxOToCoverDeposit {totalDeposit :: Coin}
109+
deriving (Show)
110+
deriving anyclass (Exception)
111+
112+
-- | Builds a chain of script publishing transactions.
113+
-- Throws: PublishScriptException
108114
buildScriptPublishingTxs ::
115+
MonadThrow m =>
109116
PParams LedgerEra ->
110117
SystemStart ->
111118
NetworkId ->
112119
EraHistory ->
113120
Set PoolId ->
121+
-- | Outputs that can be spent by signing key.
114122
UTxO ->
123+
-- | Key owning funds to pay deposit and fees.
115124
SigningKey PaymentKey ->
116-
IO [Tx]
117-
buildScriptPublishingTxs pparams systemStart networkId eraHistory stakePools startUTxO sk =
118-
flip evalStateT (startUTxO, []) $
119-
forM scripts $ \script -> do
120-
(nextUTxO, _) <- get
121-
(tx, _, spentUTxO) <- liftIO $ buildScriptPublishingTx pparams systemStart networkId eraHistory stakePools changeAddress sk script nextUTxO
122-
modify' (\(_, existingTxs) -> (pickKeyAddressUTxO $ adjustUTxO tx spentUTxO, tx : existingTxs))
123-
pure tx
125+
m [Tx]
126+
buildScriptPublishingTxs pparams systemStart networkId eraHistory stakePools availableUTxO sk = do
127+
startUTxO <- findUTxO
128+
go startUTxO scriptOutputs
124129
where
125-
pickKeyAddressUTxO utxo = maybe mempty UTxO.singleton $ UTxO.findBy (\(_, txOut) -> isKeyAddress (txOutAddress txOut)) utxo
130+
-- Find a suitable utxo that covers at least the total deposit
131+
findUTxO =
132+
case UTxO.find (\o -> selectLovelace (txOutValue o) > totalDeposit) availableUTxO of
133+
Nothing -> throwIO FailedToFindUTxOToCoverDeposit{totalDeposit}
134+
Just (i, o) -> pure $ UTxO.singleton (i, o)
126135

127-
scripts = [initialValidatorScript, commitValidatorScript, Head.validatorScript]
136+
totalDeposit = sum $ selectLovelace . txOutValue <$> scriptOutputs
128137

129-
vk = getVerificationKey sk
138+
scriptOutputs =
139+
mkScriptTxOut . mkScriptRef
140+
<$> [initialValidatorScript, commitValidatorScript, Head.validatorScript]
130141

131-
changeAddress = mkVkAddress networkId vk
142+
-- Loop over all script outputs to create while re-spending the change output
143+
go _ [] = pure []
144+
go utxo (out : rest) = do
145+
tx <- case buildTransactionWithPParams' pparams systemStart eraHistory stakePools changeAddress utxo [] [out] of
146+
Left err -> throwIO $ FailedToBuildPublishingTx err
147+
Right tx -> pure $ signTx sk tx
148+
149+
let changeOutput = txOuts' tx !! 1
150+
utxo' = UTxO.singleton (mkTxIn tx 1, toCtxUTxOTxOut changeOutput)
151+
(tx :) <$> go utxo' rest
152+
153+
changeAddress = mkVkAddress networkId (getVerificationKey sk)
132154

133-
buildScriptPublishingTx ::
134-
PParams LedgerEra ->
135-
SystemStart ->
136-
NetworkId ->
137-
EraHistory ->
138-
Set PoolId ->
139-
AddressInEra ->
140-
SigningKey PaymentKey ->
141-
PlutusScript ->
142-
UTxO.UTxO ->
143-
IO (Tx, TxBody, UTxO.UTxO)
144-
buildScriptPublishingTx pparams systemStart networkId eraHistory stakePools changeAddress sk script utxo =
145-
let output = mkScriptTxOut <$> [mkScriptRef script]
146-
totalDeposit = sum (selectLovelace . txOutValue <$> output)
147-
utxoToSpend =
148-
maybe mempty UTxO.singleton $
149-
UTxO.find (\o -> selectLovelace (txOutValue o) > totalDeposit) utxo
150-
in case buildTransactionWithPParams' pparams systemStart eraHistory stakePools changeAddress utxoToSpend [] output of
151-
Left e -> throwErrorAsException e
152-
Right rawTx -> do
153-
let body = getTxBody rawTx
154-
pure (makeSignedTransaction [makeShelleyKeyWitness body (WitnessPaymentKey sk)] body, body, utxoToSpend)
155-
where
156155
mkScriptTxOut =
157156
mkTxOutAutoBalance
158157
pparams

0 commit comments

Comments
 (0)