Skip to content

Commit c1c759b

Browse files
authored
Bring in example that just spends from a script in L2 and closes (#1796)
This brings in the bulk of the work from #1742 ; it demonstrates that one can spend from a script on L2. This is useful infrastructure to, say, test a custom ledger operation and check that the Hydra can deal with that on it's own ledger, but still close and go back to L1 successfully. This is a useful test in any case. Note that it contains a `TODO` around a bug we saw with autobalancing, that will hopefully be fixed in subsequent versions of `cardano-api`. We make two further additions: - We attempt to zero-out a few more fee fields in the protocol params - We provide `buildTransactionWithPParams` to explicitly set the pparams instead of using the **L1 pparams**. This is an important observation!
2 parents 64428c6 + c5bc555 commit c1c759b

File tree

5 files changed

+231
-17
lines changed

5 files changed

+231
-17
lines changed

hydra-cluster/hydra-cluster.cabal

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,11 @@ library
8585
build-depends:
8686
, aeson
8787
, async
88-
, base >=4.7 && <5
88+
, base >=4.7 && <5
8989
, bytestring
90+
, cardano-ledger-alonzo
91+
, cardano-ledger-api
92+
, cardano-ledger-core
9093
, cardano-slotting
9194
, containers
9295
, contra-tracer

hydra-cluster/src/Hydra/Cluster/Scenarios.hs

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

99
import Cardano.Api.UTxO qualified as UTxO
10+
import Cardano.Ledger.Alonzo.Tx (hashScriptIntegrity)
11+
import Cardano.Ledger.Api.PParams (AlonzoEraPParams, PParams, getLanguageView)
12+
import Cardano.Ledger.Api.Tx (EraTx, bodyTxL, datsTxWitsL, rdmrsTxWitsL, witsTxL)
13+
import Cardano.Ledger.Api.Tx qualified as Ledger
14+
import Cardano.Ledger.Api.Tx.Body (AlonzoEraTxBody, scriptIntegrityHashTxBodyL)
15+
import Cardano.Ledger.Api.Tx.Wits (AlonzoEraTxWits)
16+
import Cardano.Ledger.Plutus.Language (Language (PlutusV3))
1017
import CardanoClient (
1118
QueryPoint (QueryTip),
1219
RunningNode (..),
1320
buildTransaction,
21+
buildTransactionWithPParams,
1422
queryTip,
1523
queryUTxOFor,
1624
submitTx,
1725
waitForUTxO,
1826
)
1927
import CardanoNode (NodeLog)
2028
import Control.Concurrent.Async (mapConcurrently_)
21-
import Control.Lens ((^..), (^?))
29+
import Control.Lens ((.~), (^.), (^..), (^?))
2230
import Data.Aeson (Value, object, (.=))
2331
import Data.Aeson qualified as Aeson
2432
import Data.Aeson.Lens (key, values, _JSON, _String)
@@ -37,12 +45,17 @@ import Hydra.Cardano.Api (
3745
Coin (..),
3846
File (File),
3947
Key (SigningKey),
48+
KeyWitnessInCtx (KeyWitnessForSpending),
4049
PaymentKey,
4150
Tx,
4251
TxId,
4352
UTxO,
4453
addTxIns,
54+
addTxInsCollateral,
55+
addTxOuts,
56+
createAndValidateTransactionBody,
4557
defaultTxBodyContent,
58+
fromLedgerTx,
4659
getTxBody,
4760
getTxId,
4861
getVerificationKey,
@@ -51,16 +64,24 @@ import Hydra.Cardano.Api (
5164
mkScriptAddress,
5265
mkScriptDatum,
5366
mkScriptWitness,
67+
mkTxIn,
68+
mkTxOutAutoBalance,
5469
mkTxOutDatumHash,
5570
mkVkAddress,
5671
scriptWitnessInCtx,
5772
selectLovelace,
73+
setTxProtocolParams,
5874
signTx,
75+
toLedgerTx,
5976
toScriptData,
6077
txOutValue,
78+
txOuts',
6179
utxoFromTx,
6280
writeFileTextEnvelope,
6381
pattern BuildTxWith,
82+
pattern KeyWitness,
83+
pattern LedgerProtocolParameters,
84+
pattern PlutusScriptWitness,
6485
pattern ReferenceScriptNone,
6586
pattern ScriptWitness,
6687
pattern TxOut,
@@ -73,6 +94,7 @@ import Hydra.Cluster.Mithril (MithrilLog)
7394
import Hydra.Cluster.Options (Options)
7495
import Hydra.Cluster.Util (chainConfigFor, keysFor, modifyConfig, setNetworkId)
7596
import Hydra.Ledger.Cardano (mkSimpleTx, mkTransferTx, unsafeBuildTransaction)
97+
import Hydra.Ledger.Cardano.Evaluate (maxTxExecutionUnits)
7698
import Hydra.Logging (Tracer, traceWith)
7799
import Hydra.Options (DirectChainConfig (..), networkId, startChainFrom)
78100
import Hydra.Tx (HeadId, IsTx (balance), Party, txId)
@@ -381,6 +403,145 @@ singlePartyCommitsFromExternal tracer workDir node hydraScriptsTxId =
381403
where
382404
RunningNode{nodeSocket, blockTime} = node
383405

406+
singlePartyUsesScriptOnL2 ::
407+
Tracer IO EndToEndLog ->
408+
FilePath ->
409+
RunningNode ->
410+
[TxId] ->
411+
IO ()
412+
singlePartyUsesScriptOnL2 tracer workDir node hydraScriptsTxId =
413+
( `finally`
414+
do
415+
returnFundsToFaucet tracer node Alice
416+
returnFundsToFaucet tracer node AliceFunds
417+
)
418+
$ do
419+
refuelIfNeeded tracer node Alice 250_000_000
420+
aliceChainConfig <- chainConfigFor Alice workDir nodeSocket hydraScriptsTxId [] $ UnsafeContestationPeriod 1
421+
let hydraNodeId = 1
422+
let hydraTracer = contramap FromHydraNode tracer
423+
withHydraNode hydraTracer aliceChainConfig workDir hydraNodeId aliceSk [] [1] $ \n1 -> do
424+
send n1 $ input "Init" []
425+
headId <- waitMatch (10 * blockTime) n1 $ headIsInitializingWith (Set.fromList [alice])
426+
427+
(walletVk, walletSk) <- keysFor AliceFunds
428+
429+
-- Create money on L1
430+
let commitAmount = 100_000_000
431+
utxoToCommit <- seedFromFaucet node walletVk commitAmount (contramap FromFaucet tracer)
432+
433+
-- Push it into L2
434+
requestCommitTx n1 utxoToCommit
435+
<&> signTx walletSk >>= \tx -> do
436+
submitTx node tx
437+
438+
-- Check UTxO is present in L2
439+
waitFor hydraTracer (10 * blockTime) [n1] $
440+
output "HeadIsOpen" ["utxo" .= toJSON utxoToCommit, "headId" .= headId]
441+
442+
pparamsReq <-
443+
parseUrlThrow ("GET " <> hydraNodeBaseUrl n1 <> "/protocol-parameters")
444+
>>= httpJSON
445+
let pparams = getResponseBody pparamsReq
446+
447+
-- Send the UTxO to a script; in preparation for running the script
448+
let serializedScript = dummyValidatorScript
449+
let scriptAddress = mkScriptAddress networkId serializedScript
450+
let scriptOutput =
451+
mkTxOutAutoBalance
452+
pparams
453+
scriptAddress
454+
(lovelaceToValue 0)
455+
(mkTxOutDatumHash ())
456+
ReferenceScriptNone
457+
458+
Right tx <- buildTransactionWithPParams pparams networkId nodeSocket (mkVkAddress networkId walletVk) utxoToCommit [] [scriptOutput]
459+
460+
let signedL2tx = signTx walletSk tx
461+
send n1 $ input "NewTx" ["transaction" .= signedL2tx]
462+
463+
waitMatch 10 n1 $ \v -> do
464+
guard $ v ^? key "tag" == Just "SnapshotConfirmed"
465+
guard $
466+
toJSON signedL2tx
467+
`elem` (v ^.. key "snapshot" . key "confirmed" . values)
468+
469+
-- Now, spend the money from the script
470+
let scriptWitness =
471+
BuildTxWith $
472+
ScriptWitness scriptWitnessInCtx $
473+
PlutusScriptWitness
474+
serializedScript
475+
(mkScriptDatum ())
476+
(toScriptData ())
477+
maxTxExecutionUnits
478+
479+
let txIn = mkTxIn signedL2tx 0
480+
let remainder = mkTxIn signedL2tx 1
481+
482+
let outAmt = foldMap txOutValue (txOuts' tx)
483+
let body =
484+
defaultTxBodyContent
485+
& addTxIns [(txIn, scriptWitness), (remainder, BuildTxWith $ KeyWitness KeyWitnessForSpending)]
486+
& addTxInsCollateral [remainder]
487+
& addTxOuts [TxOut (mkVkAddress networkId walletVk) outAmt TxOutDatumNone ReferenceScriptNone]
488+
& setTxProtocolParams (BuildTxWith $ Just $ LedgerProtocolParameters pparams)
489+
490+
-- TODO: Instead of using `createAndValidateTransactionBody`, we
491+
-- should be able to just construct the Tx with autobalancing via
492+
-- `buildTransactionWithBody`. Unfortunately this is broken in the
493+
-- version of cardano-api that we presently use; in a future upgrade
494+
-- of that library we can try again.
495+
-- tx' <- either (failure . show) pure =<< buildTransactionWithBody networkId nodeSocket (mkVkAddress networkId walletVk) body utxoToCommit
496+
txBody <- either (failure . show) pure (createAndValidateTransactionBody body)
497+
498+
let spendTx' = makeSignedTransaction [] txBody
499+
spendTx = fromLedgerTx $ recomputeIntegrityHash pparams [PlutusV3] (toLedgerTx spendTx')
500+
let signedTx = signTx walletSk spendTx
501+
502+
send n1 $ input "NewTx" ["transaction" .= signedTx]
503+
504+
waitMatch 10 n1 $ \v -> do
505+
guard $ v ^? key "tag" == Just "SnapshotConfirmed"
506+
guard $
507+
toJSON signedTx
508+
`elem` (v ^.. key "snapshot" . key "confirmed" . values)
509+
510+
-- And check that we can close and fanout the head successfully
511+
send n1 $ input "Close" []
512+
deadline <- waitMatch (10 * blockTime) n1 $ \v -> do
513+
guard $ v ^? key "tag" == Just "HeadIsClosed"
514+
v ^? key "contestationDeadline" . _JSON
515+
remainingTime <- diffUTCTime deadline <$> getCurrentTime
516+
waitFor hydraTracer (remainingTime + 3 * blockTime) [n1] $
517+
output "ReadyToFanout" ["headId" .= headId]
518+
send n1 $ input "Fanout" []
519+
waitMatch (10 * blockTime) n1 $ \v ->
520+
guard $ v ^? key "tag" == Just "HeadIsFinalized"
521+
522+
-- Assert final wallet balance
523+
(balance <$> queryUTxOFor networkId nodeSocket QueryTip walletVk)
524+
`shouldReturn` lovelaceToValue commitAmount
525+
where
526+
RunningNode{networkId, nodeSocket, blockTime} = node
527+
hydraNodeBaseUrl HydraClient{hydraNodeId} = "http://127.0.0.1:" <> show (4000 + hydraNodeId)
528+
529+
-- | Compute the integrity hash of a transaction using a list of plutus languages.
530+
recomputeIntegrityHash ::
531+
(AlonzoEraPParams ppera, AlonzoEraTxWits txera, AlonzoEraTxBody txera, EraTx txera) =>
532+
PParams ppera ->
533+
[Language] ->
534+
Ledger.Tx txera ->
535+
Ledger.Tx txera
536+
recomputeIntegrityHash pp languages tx = do
537+
tx & bodyTxL . scriptIntegrityHashTxBodyL .~ integrityHash
538+
where
539+
integrityHash =
540+
hashScriptIntegrity
541+
(Set.fromList $ getLanguageView pp <$> languages)
542+
(tx ^. witsTxL . rdmrsTxWitsL)
543+
(tx ^. witsTxL . datsTxWitsL)
544+
384545
singlePartyCommitsScriptBlueprint ::
385546
Tracer IO EndToEndLog ->
386547
FilePath ->

hydra-cluster/src/HydraNode.hs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,9 @@ withHydraNode' tracer chainConfig workDir hydraNodeId hydraSKey hydraVKeys allNo
350350
& atKey "txFeePerByte" ?~ toJSON (Number 0)
351351
& key "executionUnitPrices" . atKey "priceMemory" ?~ toJSON (Number 0)
352352
& key "executionUnitPrices" . atKey "priceSteps" ?~ toJSON (Number 0)
353+
& atKey "utxoCostPerByte" ?~ toJSON (Number 0)
354+
& atKey "treasuryCut" ?~ toJSON (Number 0)
355+
& atKey "minFeeRefScriptCostPerByte" ?~ toJSON (Number 0)
353356

354357
let hydraSigningKey = dir </> (show hydraNodeId <> ".sk")
355358
void $ writeFileTextEnvelope (File hydraSigningKey) Nothing hydraSKey

hydra-cluster/test/Test/EndToEndSpec.hs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import Hydra.Cluster.Scenarios (
6767
singlePartyCommitsFromExternalTxBlueprint,
6868
singlePartyCommitsScriptBlueprint,
6969
singlePartyHeadFullLifeCycle,
70+
singlePartyUsesScriptOnL2,
7071
testPreventResumeReconfiguredPeer,
7172
threeNodesNoErrorsOnOpen,
7273
)
@@ -178,6 +179,11 @@ spec = around (showLogsOnFailure "EndToEndSpec") $ do
178179
withCardanoNodeDevnet (contramap FromCardanoNode tracer) tmpDir $ \node ->
179180
publishHydraScriptsAs node Faucet
180181
>>= singlePartyCommitsFromExternal tracer tmpDir node
182+
it "can spend from a script on L2" $ \tracer -> do
183+
withClusterTempDir $ \tmpDir -> do
184+
withCardanoNodeDevnet (contramap FromCardanoNode tracer) tmpDir $ \node ->
185+
publishHydraScriptsAs node Faucet
186+
>>= singlePartyUsesScriptOnL2 tracer tmpDir node
181187
it "can submit a signed user transaction" $ \tracer -> do
182188
withClusterTempDir $ \tmpDir -> do
183189
withCardanoNodeDevnet (contramap FromCardanoNode tracer) tmpDir $ \node ->

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

Lines changed: 56 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -63,27 +63,27 @@ mkCardanoClient networkId nodeSocket =
6363

6464
-- * Tx Construction / Submission
6565

66-
-- | Construct a simple payment consuming some inputs and producing some
67-
-- outputs (no certificates or withdrawals involved).
68-
--
69-
-- On success, the returned transaction is fully balanced. On error, return
70-
-- `TxBodyErrorAutoBalance`.
71-
buildTransaction ::
66+
buildTransactionWithBody ::
67+
-- | Protocol parameters
68+
PParams LedgerEra ->
7269
-- | Current network identifier
7370
NetworkId ->
7471
-- | Filepath to the cardano-node's domain socket
7572
SocketPath ->
7673
-- | Change address to send
7774
AddressInEra ->
75+
-- | Body
76+
TxBodyContent BuildTx ->
7877
-- | Unspent transaction outputs to spend.
7978
UTxO ->
80-
-- | Collateral inputs.
81-
[TxIn] ->
82-
-- | Outputs to create.
83-
[TxOut CtxTx] ->
8479
IO (Either (TxBodyErrorAutoBalance Era) Tx)
85-
buildTransaction networkId socket changeAddress utxoToSpend collateral outs = do
86-
pparams <- queryProtocolParameters networkId socket QueryTip
80+
buildTransactionWithBody pparams networkId socket changeAddress body utxoToSpend = do
81+
-- XXX: Note minor inconsistency here; we are querying the _socket_ (i.e.
82+
-- L1) for this information, but in fact this function may be called for the
83+
-- construction of an L2 transaction. For this reason we take the pparams as
84+
-- an argument, and at some point we can move these other fields to
85+
-- arguments as well; but they are not important for our purposes at
86+
-- present.
8787
systemStart <- querySystemStart networkId socket QueryTip
8888
eraHistory <- queryEraHistory networkId socket QueryTip
8989
stakePools <- queryStakePools networkId socket QueryTip
@@ -98,14 +98,55 @@ buildTransaction networkId socket changeAddress utxoToSpend collateral outs = do
9898
mempty
9999
mempty
100100
(UTxO.toApi utxoToSpend)
101-
(bodyContent pparams)
101+
body
102102
changeAddress
103103
Nothing
104+
105+
buildTransaction ::
106+
-- | Current network identifier
107+
NetworkId ->
108+
-- | Filepath to the cardano-node's domain socket
109+
SocketPath ->
110+
-- | Change address to send
111+
AddressInEra ->
112+
-- | Unspent transaction outputs to spend.
113+
UTxO ->
114+
-- | Collateral inputs.
115+
[TxIn] ->
116+
-- | Outputs to create.
117+
[TxOut CtxTx] ->
118+
IO (Either (TxBodyErrorAutoBalance Era) Tx)
119+
buildTransaction networkId socket changeAddress body utxoToSpend outs = do
120+
pparams <- queryProtocolParameters networkId socket QueryTip
121+
buildTransactionWithPParams pparams networkId socket changeAddress body utxoToSpend outs
122+
123+
-- | Construct a simple payment consuming some inputs and producing some
124+
-- outputs (no certificates or withdrawals involved).
125+
--
126+
-- On success, the returned transaction is fully balanced. On error, return
127+
-- `TxBodyErrorAutoBalance`.
128+
buildTransactionWithPParams ::
129+
-- | Protocol parameters
130+
PParams LedgerEra ->
131+
-- | Current network identifier
132+
NetworkId ->
133+
-- | Filepath to the cardano-node's domain socket
134+
SocketPath ->
135+
-- | Change address to send
136+
AddressInEra ->
137+
-- | Unspent transaction outputs to spend.
138+
UTxO ->
139+
-- | Collateral inputs.
140+
[TxIn] ->
141+
-- | Outputs to create.
142+
[TxOut CtxTx] ->
143+
IO (Either (TxBodyErrorAutoBalance Era) Tx)
144+
buildTransactionWithPParams pparams networkId socket changeAddress utxoToSpend collateral outs = do
145+
buildTransactionWithBody pparams networkId socket changeAddress bodyContent utxoToSpend
104146
where
105147
-- NOTE: 'makeTransactionBodyAutoBalance' overwrites this.
106148
dummyFeeForBalancing = TxFeeExplicit 0
107-
108-
bodyContent pparams =
149+
bodyContent =
109150
TxBodyContent
110151
(withWitness <$> toList (UTxO.inputSet utxoToSpend))
111152
(TxInsCollateral collateral)

0 commit comments

Comments
 (0)