Skip to content
Merged
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ changes.

- Introduce an option to publish hydra scripts using blockfrost.

- Remove checks that rely on hydra-node's local state and trust on-chain data when we observe decrement/recover transactions.

- Fix a bug in increment observation where wrong deposited UTxO was picked up.

- Fix a bug where incremental commits / decommits were not correctly observed after restart of `hydra-node`. This was due to incorrect handling of internal chain state [#1894](https://github.com/cardano-scaling/hydra/pull/1894)
Expand Down
6 changes: 3 additions & 3 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

384 changes: 12 additions & 372 deletions hydra-chain-observer/golden/OnChainTx/OnDecrementTx.json

Large diffs are not rendered by default.

27 changes: 25 additions & 2 deletions hydra-chain-observer/golden/OnChainTx/OnRecoverTx.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,31 @@
{
"samples": [
{
"headId": "a42836de904bad82fe8684bee8a92cab",
"recoveredTxId": "8c912bcc3f52bb968cbe13d560ee871b4f11d26fab67a8bc7eb22f3d5b8ff8a8",
"headId": "01010001010101010000010000010100",
"recoveredTxId": "0101010001000101010101010000000100010101000101010000010100000000",
"recoveredUTxO": {
"0000010100000001010001010001000001010000000100010001010100010001#19": {
"address": "addr1x92vfd793n7lrdf25997kcer6e6jw7uhqpe00ey5ankrgdk007z0vphatrd0wnnqtn03xtwm8a047zdfxzx5tdfcx3zqu0sl53",
"datum": null,
"datumhash": "41755210135075cee7ff8d57236c51b4ed7c3c4867d088794ccba361f4752f97",
"inlineDatum": null,
"inlineDatumRaw": null,
"referenceScript": {
"script": {
"cborHex": "830303848202848202828200581c9bbabc12319ea48352a6a835890bd8fad0f14ab9ec0a0f251894184b8200581cd9b1df6c1ac835f8513c8cd0de277abf304a334d6d438e937817b53c830302838200581ce61b4b8251e699e032b4f4f43f4ea0c25b035f7d43f4b20211cbda028200581c21d70ae5ee17527d8e2f82688bcc20bd4d3eed680df390871412f4398200581cdeb1647f384d13c10570fa5c1161e7150afa5cacc8202bd9441de9308200581c2720245cb48418676d67bf8888272011a66d7bd6e39b4f64ce69b2e2830301818200581c33c415f5ab19d59ae612cb5ee119f07dbd7db9ebac6df054adb8f2d78201818201838200581c30faeb1e5fbb28ce466fc4da0cd5981845336a2dd02231441ce54f658200581c87b10fbac658e08c486e1ee81a5f5abdcbf3f136aea3e79bbb1bf1788200581c6d82b551bda55b8949a1d38d6dd4d59d7a0621d518d6bd25a385946c830303848200581ceaf3ebce47ee589cb3883e0bfccb9c6953aa78b4065d10c20df34e2b8202848200581cfbebf90ad162b1cd95ceaabfc25bd28980549fbea8f6dc1d448781178200581c16d184c4e3838078e4f550bd75556b63e8661e61c43c72f859fcddb18200581ce244244c609691dd6b104adfffa5fabb2f22a1b0d6d76bcf4475c7bf8200581cb76f0f9cb9010b48fecfba785686b76864926bce8c0ea474a3f47eac8200581ca3020a1e15cea47d4cbad968f6870e74531b831f5bde8a6d449684438200581cf4d02e30e2a91897ea4ab473f39fac97a9e7072534e49eff5f1da150820280",
"description": "",
"type": "SimpleScript"
},
"scriptLanguage": "SimpleScriptLanguage"
},
"value": {
"245d5a7a06fe18358242e81281cd5ba9e6abe4efc54e7b659f25abae": {
"55": 590494318831573294
},
"lovelace": 878270261755167725
}
}
},
"tag": "OnRecoverTx"
}
],
Expand Down
138 changes: 135 additions & 3 deletions hydra-cluster/src/Hydra/Cluster/Scenarios.hs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import Hydra.Cardano.Api (
LedgerProtocolParameters (..),
PaymentKey,
Tx,
TxId,
TxId (..),
UTxO,
addTxIns,
addTxInsCollateral,
Expand Down Expand Up @@ -150,6 +150,7 @@ import Network.HTTP.Req (
import Network.HTTP.Simple (getResponseBody, httpJSON, setRequestBodyJSON)
import Network.HTTP.Types (urlEncode)
import System.FilePath ((</>))
import System.Process (proc, readCreateProcessWithExitCode)
import Test.Hydra.Tx.Fixture (testNetworkId)
import Test.Hydra.Tx.Gen (genKeyPair)
import Test.QuickCheck (choose, elements, generate)
Expand Down Expand Up @@ -307,6 +308,131 @@ restartedNodeCanAbort tracer workDir cardanoNode hydraScriptsTxId = do
where
RunningNode{nodeSocket, networkId} = cardanoNode

nodeReObservesOnChainTxs :: Tracer IO EndToEndLog -> FilePath -> RunningNode -> [TxId] -> IO ()
nodeReObservesOnChainTxs tracer workDir cardanoNode hydraScriptsTxId = do
refuelIfNeeded tracer cardanoNode Alice 100_000_000
refuelIfNeeded tracer cardanoNode Bob 100_000_000
-- Start hydra-node on chain tip
tip <- queryTip networkId nodeSocket
let contestationPeriod = UnsafeContestationPeriod 2
let deadline = 10
let depositDeadline = UnsafeDepositDeadline deadline
aliceChainConfig <-
chainConfigFor Alice workDir nodeSocket hydraScriptsTxId [Bob] contestationPeriod depositDeadline
<&> modifyConfig (\config -> config{networkId, startChainFrom = Nothing})

bobChainConfig <-
chainConfigFor Bob workDir nodeSocket hydraScriptsTxId [Alice] contestationPeriod depositDeadline
<&> modifyConfig (\config -> config{networkId, startChainFrom = Nothing})

(aliceCardanoVk, aliceCardanoSk) <- keysFor Alice
commitUTxO <- seedFromFaucet cardanoNode aliceCardanoVk 5_000_000 (contramap FromFaucet tracer)

let hydraTracer = contramap FromHydraNode tracer

withHydraNode hydraTracer aliceChainConfig workDir 1 aliceSk [bobVk] [2] $ \n1 -> do
(headId', decrementOuts) <- withHydraNode hydraTracer bobChainConfig workDir 2 bobSk [aliceVk] [1] $ \n2 -> do
send n1 $ input "Init" []

headId <- waitMatch (20 * blockTime) n1 $ headIsInitializingWith (Set.fromList [alice, bob])
_ <- waitMatch (20 * blockTime) n2 $ headIsInitializingWith (Set.fromList [alice, bob])

requestCommitTx n1 mempty >>= submitTx cardanoNode
requestCommitTx n2 mempty >>= submitTx cardanoNode

waitFor hydraTracer (20 * blockTime) [n1, n2] $
output "HeadIsOpen" ["utxo" .= object mempty, "headId" .= headId]

resp <-
parseUrlThrow ("POST " <> hydraNodeBaseUrl n1 <> "/commit")
<&> setRequestBodyJSON commitUTxO
>>= httpJSON

let depositTransaction = getResponseBody resp :: Tx
let tx = signTx aliceCardanoSk depositTransaction

submitTx cardanoNode tx

waitFor hydraTracer 10 [n1, n2] $
output "CommitApproved" ["headId" .= headId, "utxoToCommit" .= commitUTxO]

waitFor hydraTracer 10 [n1, n2] $
output "CommitFinalized" ["headId" .= headId, "depositTxId" .= getTxId (getTxBody tx)]

getSnapshotUTxO n1 `shouldReturn` commitUTxO

let aliceAddress = mkVkAddress networkId aliceCardanoVk

decommitTx <- do
let (i, o) = List.head $ UTxO.pairs commitUTxO
either (failure . show) pure $
mkSimpleTx (i, o) (aliceAddress, txOutValue o) aliceCardanoSk

let decommitUTxO = utxoFromTx decommitTx
decommitTxId = txId decommitTx

-- Sometimes use websocket, sometimes use HTTP
join . generate $
elements
[ send n1 $ input "Decommit" ["decommitTx" .= decommitTx]
, postDecommit n1 decommitTx
]

waitFor hydraTracer 10 [n1, n2] $
output "DecommitRequested" ["headId" .= headId, "decommitTx" .= decommitTx, "utxoToDecommit" .= decommitUTxO]
waitFor hydraTracer 10 [n1, n2] $
output "DecommitApproved" ["headId" .= headId, "decommitTxId" .= decommitTxId, "utxoToDecommit" .= decommitUTxO]

failAfter 10 $ waitForUTxO cardanoNode decommitUTxO

distributedUTxO <- waitForAllMatch 10 [n1, n2] $ \v -> do
guard $ v ^? key "tag" == Just "DecommitFinalized"
guard $ v ^? key "headId" == Just (toJSON headId)
v ^? key "distributedUTxO" . _JSON

guard $ distributedUTxO `UTxO.containsOutputs` utxoFromTx decommitTx

pure (headId, decommitUTxO)

bobChainConfigFromTip <-
chainConfigFor Bob workDir nodeSocket hydraScriptsTxId [Alice] contestationPeriod depositDeadline
<&> modifyConfig (\config -> config{networkId, startChainFrom = Just tip})

withTempDir "blank-state" $ \tmpDir -> do
void $ readCreateProcessWithExitCode (proc "cp" ["-r", workDir </> "state-2", tmpDir]) ""
void $ readCreateProcessWithExitCode (proc "rm" ["-rf", tmpDir </> "state-2" </> "state"]) ""
void $ readCreateProcessWithExitCode (proc "rm" ["-rf", tmpDir </> "state-2" </> "last-known-revision"]) ""
withHydraNode hydraTracer bobChainConfigFromTip tmpDir 2 bobSk [aliceVk] [1] $ \n2 -> do
-- Also expect to see past server outputs replayed
headId2 <- waitMatch 5 n2 $ headIsInitializingWith (Set.fromList [alice, bob])
headId2 `shouldBe` headId'
waitFor hydraTracer 5 [n2] $
output "HeadIsOpen" ["utxo" .= object mempty, "headId" .= headId2]

distributedUTxO <- waitForAllMatch 5 [n2] $ \v -> do
guard $ v ^? key "tag" == Just "DecommitFinalized"
guard $ v ^? key "headId" == Just (toJSON headId2)
v ^? key "distributedUTxO" . _JSON

guard $ distributedUTxO `UTxO.containsOutputs` decrementOuts

send n1 $ input "Close" []

deadline' <- waitMatch (20 * blockTime) n2 $ \v -> do
guard $ v ^? key "tag" == Just "HeadIsClosed"
guard $ v ^? key "headId" == Just (toJSON headId')
v ^? key "contestationDeadline" . _JSON
remainingTime <- diffUTCTime deadline' <$> getCurrentTime
waitFor hydraTracer (remainingTime + 3 * blockTime) [n1, n2] $
output "ReadyToFanout" ["headId" .= headId']
send n1 $ input "Fanout" []

waitForAllMatch (10 * blockTime) [n1, n2] $ checkFanout headId' mempty
where
RunningNode{nodeSocket, networkId, blockTime} = cardanoNode

hydraNodeBaseUrl HydraClient{hydraNodeId} = "http://127.0.0.1:" <> show (4000 + hydraNodeId)

-- | Step through the full life cycle of a Hydra Head with only a single
-- participant. This scenario is also used by the smoke test run via the
-- `hydra-cluster` executable.
Expand Down Expand Up @@ -1165,6 +1291,7 @@ canRecoverDeposit tracer workDir node hydraScriptsTxId =

waitForAllMatch 20 [n1] $ \v -> do
guard $ v ^? key "tag" == Just "CommitRecovered"
guard $ v ^? key "recoveredUTxO" == Just (toJSON commitUTxO)

(balance <$> queryUTxOFor networkId nodeSocket QueryTip walletVk)
`shouldReturn` lovelaceToValue commitAmount
Expand Down Expand Up @@ -1350,8 +1477,13 @@ canDecommit tracer workDir node hydraScriptsTxId =
waitFor hydraTracer 10 [n] $
output "DecommitApproved" ["headId" .= headId, "decommitTxId" .= decommitTxId, "utxoToDecommit" .= decommitUTxO]
failAfter 10 $ waitForUTxO node decommitUTxO
waitFor hydraTracer 10 [n] $
output "DecommitFinalized" ["headId" .= headId, "decommitTxId" .= decommitTxId]

distributedUTxO <- waitForAllMatch 10 [n] $ \v -> do
guard $ v ^? key "tag" == Just "DecommitFinalized"
guard $ v ^? key "headId" == Just (toJSON headId)
v ^? key "distributedUTxO" . _JSON

guard $ distributedUTxO `UTxO.containsOutputs` decommitUTxO

expectFailureOnUnsignedDecommitTx n headId decommitTx = do
let unsignedDecommitTx = makeSignedTransaction [] $ getTxBody decommitTx
Expand Down
13 changes: 8 additions & 5 deletions hydra-cluster/test/Test/ChainObserverSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,16 @@ import CardanoNode (NodeLog, withCardanoNodeDevnet)
import Control.Concurrent.Class.MonadSTM (modifyTVar', newTVarIO, readTVarIO)
import Control.Lens ((^?))
import Data.Aeson as Aeson
import Data.Aeson.Lens (key, _String)
import Data.Aeson.Lens (key, _JSON, _String)
import Data.ByteString (hGetLine)
import Data.List qualified as List
import Data.Text qualified as T
import Hydra.Cardano.Api (NetworkId (..), NetworkMagic (..), lovelaceToValue, mkVkAddress, signTx, unFile)
import Hydra.Cardano.Api (NetworkId (..), NetworkMagic (..), lovelaceToValue, mkVkAddress, signTx, unFile, utxoFromTx)
import Hydra.Cluster.Faucet (FaucetLog, publishHydraScriptsAs, seedFromFaucet, seedFromFaucet_)
import Hydra.Cluster.Fixture (Actor (..))
import Hydra.Cluster.Util (chainConfigFor, keysFor)
import Hydra.Ledger.Cardano (mkSimpleTx)
import Hydra.Logging (showLogsOnFailure)
import Hydra.Tx.IsTx (txId)
import HydraNode (HydraNodeLog, input, output, requestCommitTx, send, waitFor, waitMatch, withHydraNode)
import System.IO.Error (isEOFError, isIllegalOperation)
import System.Process (CreateProcess (std_out), StdStream (..), proc, withCreateProcess)
Expand Down Expand Up @@ -85,8 +84,12 @@ spec = do

chainObserverSees observer "HeadDecrementTx" headId

waitFor hydraTracer 50 [hydraNode] $
output "DecommitFinalized" ["headId" .= headId, "decommitTxId" .= txId decommitTx]
distributedUTxO <- waitMatch 50 hydraNode $ \v -> do
guard $ v ^? key "tag" == Just "DecommitFinalized"
guard $ v ^? key "headId" == Just (toJSON headId)
v ^? key "distributedUTxO" . _JSON

guard $ distributedUTxO `UTxO.containsOutputs` utxoFromTx decommitTx

send hydraNode $ input "Close" []

Expand Down
6 changes: 6 additions & 0 deletions hydra-cluster/test/Test/EndToEndSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import Hydra.Cluster.Scenarios (
checkFanout,
headIsInitializingWith,
initWithWrongKeys,
nodeReObservesOnChainTxs,
oneOfThreeNodesStopsForAWhile,
persistenceCanLoadWithEmptyCommit,
refuelIfNeeded,
Expand Down Expand Up @@ -261,6 +262,11 @@ spec = around (showLogsOnFailure "EndToEndSpec") $ do
withCardanoNodeDevnet (contramap FromCardanoNode tracer) tmpDir $ \node ->
publishHydraScriptsAs node Faucet
>>= persistenceCanLoadWithEmptyCommit tracer tmpDir node
it "node re-observes on-chain txs" $ \tracer -> do
withClusterTempDir $ \tmpDir -> do
withCardanoNodeDevnet (contramap FromCardanoNode tracer) tmpDir $ \node ->
publishHydraScriptsAs node Faucet
>>= nodeReObservesOnChainTxs tracer tmpDir node

describe "three hydra nodes scenario" $ do
it "can survive a bit of downtime of 1 in 3 nodes" $ \tracer -> do
Expand Down
4 changes: 2 additions & 2 deletions hydra-node/golden/ServerOutput/DecommitFinalized.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"samples": [
{
"decommitTxId": "171e664c04a43149bfd8c1cb1e9a9f7646076421bff99b652891cf15db78ec7a",
"headId": "203296a3f8557f12f55a28c25b68c335",
"distributedUTxO": {},
"headId": "01000101010100010100000001010100",
"tag": "DecommitFinalized"
}
],
Expand Down
45 changes: 35 additions & 10 deletions hydra-node/golden/StateChanged/DecommitFinalized.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,36 +3,61 @@
{
"chainState": {
"recordedAt": {
"blockHash": "0100010101010101010100000100010001000100000000010000010000010001",
"blockHash": "0000010000000001000001000101010100010101010100010101000100010001",
"slot": 1,
"tag": "ChainPoint"
},
"spendableUTxO": {
"0000000000010100010101000001010100010100010100010100000001010100#19": {
"address": "addr_test1zpscm90puxtm785w02th7qws0w0v0vphzyh88g2w90e50387lg66ceyc8n04addy7qhqfwgpem2qkkhe3gafa47ltdls8pjj0t",
"0000000000000101000101000000000001000100010101000100000001000000#33": {
"address": "addr1z8x2xz27e7rtnr7sg5srqz3xvw5ent7lk8swq6nhv6cxez2vsky84frr8aun8vkqm7rcsyea5fh7unnl7l98cgqwhrjqgvpy82",
"datum": null,
"datumhash": null,
"datumhash": "27dfefd7b5d62489c2656fa160886efdc5abbf8a3a180049a439c3417db7c16e",
"inlineDatum": null,
"inlineDatumRaw": null,
"referenceScript": {
"script": {
"cborHex": "83030081820180",
"cborHex": "830301828202838200581cff262700384cf402b1dd67c64885bb86daaa4fb001b0dc23993f010f830301818200581cfa7e2656f1c916a670ca9eb1ab960d8bd95c8b54c9589d10f2952b2f8201828200581cff6a39027ffd86c3816475723fc81d4b40935c877ff0249da7829b298200581c7767c6cb10c95c7dee64e1bff8624bca269ff4e4c8646f1418b63936820180",
"description": "",
"type": "SimpleScript"
},
"scriptLanguage": "SimpleScriptLanguage"
},
"value": {
"b0c53e2bf180858da4b64eb5598c5615bba7d723d2b604a83b7f9165": {
"eb0211177e54": 1
}
"38": 1
},
"lovelace": 5089451595132491754
}
}
}
},
"decommitTxId": "0101000101010000000101000001000000000101010001000100010100000101",
"headId": "00000101010100010001000000000100",
"newVersion": 0,
"distributedUTxO": {
"0100000101000101000000010000000100000000010101000001010000000100#77": {
"address": "addr_test1wpwe0u03kaxzzah58su5dg23uza090pr0q9544wn3jgprlgrlennf",
"datum": null,
"inlineDatum": {
"map": []
},
"inlineDatumRaw": "a0",
"inlineDatumhash": "d36a2619a672494604e11bb447cbcf5231e9f2ba25c2169177edc941bd50ad6c",
"referenceScript": {
"script": {
"cborHex": "820180",
"description": "",
"type": "SimpleScript"
},
"scriptLanguage": "SimpleScriptLanguage"
},
"value": {
"d239ea01587fbc7f83f42878838d76b355c9a507ec41ffd1e9a6551e": {
"38": 1
},
"lovelace": 5303846555036031747
}
}
},
"headId": "00000001000000000001000101010100",
"newVersion": 1,
"tag": "DecommitFinalized"
}
],
Expand Down
Loading