diff --git a/hydra-node/src/Hydra/HeadLogic.hs b/hydra-node/src/Hydra/HeadLogic.hs index 9051fd5b6e4..9ce07614698 100644 --- a/hydra-node/src/Hydra/HeadLogic.hs +++ b/hydra-node/src/Hydra/HeadLogic.hs @@ -474,16 +474,35 @@ onOpenNetworkReqSn env ledger st otherParty sv sn requestedTxIds mDecommitTx = requireApplicableDecommitTx cont = case mDecommitTx of Nothing -> cont (confirmedUTxO, Nothing) - Just decommitTx -> - -- Spec: require ̅S.𝑈 ◦ txω /= ⊥ - case applyTransactions ledger currentSlot confirmedUTxO [decommitTx] of - Left (_, err) -> - Error $ RequireFailed $ SnapshotDoesNotApply sn (txId decommitTx) err - Right newConfirmedUTxO -> do - -- Spec: 𝑈_active ← ̅S.𝑈 ◦ txω \ outputs(txω) - let utxoToDecommit = utxoFromTx decommitTx - let activeUTxO = newConfirmedUTxO `withoutUTxO` utxoToDecommit - cont (activeUTxO, Just utxoToDecommit) + Just decommitTx + -- Spec: if v = S̄.v + | sv == confVersion -> + case confUTxOToDecommit of + Nothing -> + -- Spec: require ̅S.𝑈 ◦ txω /= ⊥ + case applyTransactions ledger currentSlot (spy confirmedUTxO) [decommitTx] of + Left (_, err) -> + Error $ RequireFailed $ SnapshotDoesNotApply sn (txId (spy decommitTx)) err + Right newConfirmedUTxO -> do + -- Spec: 𝑈_active ← ̅S.𝑈 ◦ txω \ outputs(txω) + let utxoToDecommit = utxoFromTx decommitTx + let activeUTxO = newConfirmedUTxO `withoutUTxO` utxoToDecommit + cont (activeUTxO, Just utxoToDecommit) + Just pendingUtxOToDecommit + | pendingUtxOToDecommit /= utxoFromTx decommitTx -> + Error $ RequireFailed ReqSnDecommitNotSettled + | otherwise -> + cont (confirmedUTxO, Just $ utxoFromTx decommitTx) + | otherwise -> + -- Spec: require ̅S.𝑈 ◦ txω /= ⊥ + case applyTransactions ledger currentSlot (spy confirmedUTxO) [decommitTx] of + Left (_, err) -> + Error $ RequireFailed $ SnapshotDoesNotApply sn (txId (spy decommitTx)) err + Right newConfirmedUTxO -> do + -- Spec: 𝑈_active ← ̅S.𝑈 ◦ txω \ outputs(txω) + let utxoToDecommit = utxoFromTx decommitTx + let activeUTxO = newConfirmedUTxO `withoutUTxO` utxoToDecommit + cont (activeUTxO, Just utxoToDecommit) -- NOTE: at this point we know those transactions apply on the localUTxO because they -- are part of the localTxs. The snapshot can contain less transactions than the ones @@ -512,6 +531,12 @@ onOpenNetworkReqSn env ledger st otherParty sv sn requestedTxIds mDecommitTx = InitialSnapshot{} -> 0 ConfirmedSnapshot{snapshot = Snapshot{number}} -> number + Snapshot{version = confVersion} = getSnapshot confirmedSnapshot + + confUTxOToDecommit = case confirmedSnapshot of + InitialSnapshot{} -> Nothing + ConfirmedSnapshot{snapshot = Snapshot{utxoToDecommit}} -> utxoToDecommit + seenSn = seenSnapshotNumber seenSnapshot confirmedUTxO = case confirmedSnapshot of diff --git a/hydra-node/src/Hydra/HeadLogic/Error.hs b/hydra-node/src/Hydra/HeadLogic/Error.hs index 9c56c40f5c6..1c55df5f8fc 100644 --- a/hydra-node/src/Hydra/HeadLogic/Error.hs +++ b/hydra-node/src/Hydra/HeadLogic/Error.hs @@ -35,6 +35,7 @@ data RequirementFailure tx = ReqSnNumberInvalid {requestedSn :: SnapshotNumber, lastSeenSn :: SnapshotNumber} | ReqSvNumberInvalid {requestedSv :: SnapshotVersion, lastSeenSv :: SnapshotVersion} | ReqSnNotLeader {requestedSn :: SnapshotNumber, leader :: Party} + | ReqSnDecommitNotSettled | InvalidMultisignature {multisig :: Text, vkeys :: [VerificationKey HydraKey]} | SnapshotAlreadySigned {knownSignatures :: [Party], receivedSignature :: Party} | AckSnNumberInvalid {requestedSn :: SnapshotNumber, lastSeenSn :: SnapshotNumber} diff --git a/hydra-node/test/Hydra/BehaviorSpec.hs b/hydra-node/test/Hydra/BehaviorSpec.hs index a14e5c1f05a..875c626feed 100644 --- a/hydra-node/test/Hydra/BehaviorSpec.hs +++ b/hydra-node/test/Hydra/BehaviorSpec.hs @@ -437,6 +437,29 @@ spec = parallel $ do waitUntil [n1, n2] $ DecommitApproved{headId = testHeadId, decommitTxId = txId decommitTx2, utxoToDecommit = utxoRefs [22]} waitUntil [n1, n2] $ DecommitFinalized{headId = testHeadId, decommitTxId = txId decommitTx2} + it "can process transactions while decommit pending" $ + shouldRunInSim $ do + -- NOTE: The simulated network has a block time of 20 (simulated) seconds. + withSimulatedChainAndNetwork $ \chain -> + withHydraNode aliceSk [bob] chain $ \n1 -> + withHydraNode bobSk [alice] chain $ \n2 -> do + openHead chain n1 n2 + + let decommitTx = SimpleTx 1 (utxoRef 1) (utxoRef 42) + send n2 (Decommit{decommitTx}) + waitUntil [n1, n2] $ + DecommitRequested{headId = testHeadId, decommitTx, utxoToDecommit = utxoRefs [42]} + waitUntil [n1, n2] $ + DecommitApproved{headId = testHeadId, decommitTxId = 1, utxoToDecommit = utxoRefs [42]} + + let normalTx = SimpleTx 2 (utxoRef 2) (utxoRef 3) + send n2 (NewTx normalTx) + waitUntilMatch [n1, n2] $ \case + SnapshotConfirmed{snapshot = Snapshot{confirmed}} -> 2 `elem` confirmed + _ -> False + + waitUntil [n1, n2] $ DecommitFinalized{headId = testHeadId, decommitTxId = 1} + it "can close with decommit in flight" $ shouldRunInSim $ do withSimulatedChainAndNetwork $ \chain -> @@ -535,12 +558,11 @@ spec = parallel $ do withHydraNode aliceSk [] chain $ \n1 -> do send n1 Init waitUntil [n1] $ HeadIsInitializing testHeadId (fromList [alice]) - simulateCommit chain (alice, utxoRef 1) logs = selectTraceEventsDynamic @_ @(HydraNodeLog SimpleTx) result - logs `shouldContain` [BeginEffect alice 1 0 (ClientEffect $ HeadIsInitializing testHeadId $ fromList [alice])] - logs `shouldContain` [EndEffect alice 1 0] + logs `shouldContain` [BeginEffect alice 2 0 (ClientEffect $ HeadIsInitializing testHeadId $ fromList [alice])] + logs `shouldContain` [EndEffect alice 2 0] describe "rolling back & forward does not make the node crash" $ do it "does work for rollbacks past init" $ @@ -600,7 +622,7 @@ waitUntilMatch nodes predicate = do failure $ toString $ unlines - [ "waitUntilMatch did not match a message within " <> show oneMonth + [ "waitUntilMatch did not match a message within " <> show oneMonth <> ", seen messages:" , unlines (show <$> msgs) ] where @@ -710,7 +732,10 @@ simulatedChainAndNetwork initialChainState = do Chain { postTx = \tx -> do now <- getCurrentTime - createAndYieldEvent nodes history localChainState $ toOnChainTx now tx + -- Only observe "after one block" + void . async $ do + threadDelay blockTime + createAndYieldEvent nodes history localChainState $ toOnChainTx now tx , draftCommitTx = \_ -> error "unexpected call to draftCommitTx" , submitTx = \_ -> error "unexpected call to submitTx" } @@ -811,7 +836,7 @@ toOnChainTx now = \case DecrementTx{headId, decrementingSnapshot} -> OnDecrementTx { headId - , newVersion = version + , newVersion = version + 1 , distributedOutputs = maybe mempty outputsOfUTxO utxoToDecommit } where diff --git a/hydra-node/test/Hydra/HeadLogicSpec.hs b/hydra-node/test/Hydra/HeadLogicSpec.hs index 58196426440..c7ccc60a1ca 100644 --- a/hydra-node/test/Hydra/HeadLogicSpec.hs +++ b/hydra-node/test/Hydra/HeadLogicSpec.hs @@ -51,7 +51,7 @@ import Hydra.HeadLogic ( defaultTTL, update, ) -import Hydra.HeadLogic.State (getHeadParameters) +import Hydra.HeadLogic.State (SeenSnapshot (..), getHeadParameters) import Hydra.Ledger (ChainSlot (..), IsTx (..), Ledger (..), ValidationError (..)) import Hydra.Ledger.Cardano (cardanoLedger, genKeyPair, genOutput, mkRangedTx) import Hydra.Ledger.Simple (SimpleChainState (..), SimpleTx (..), aValidTx, simpleLedger, utxoRef, utxoRefs) @@ -474,6 +474,35 @@ spec = Error RequireFailed{} -> True _ -> False + it "rejects same version snapshot requests with differring decommit txs" $ do + let decommitTx = SimpleTx 2 (utxoRef 2) (utxoRef 4) + activeUTxO = utxoRefs [2] + snapshot = + Snapshot + { headId = testHeadId + , version = 0 + , number = 1 + , confirmed = [] + , utxo = activeUTxO + , utxoToDecommit = Just $ utxoRefs [3] + } + -- NOTE: Signatures are not relevant here + s0 = + inOpenState' threeParties $ + coordinatedHeadState + { confirmedSnapshot = ConfirmedSnapshot snapshot (Crypto.aggregate []) + , seenSnapshot = LastSeenSnapshot 1 + , localUTxO = activeUTxO + , decommitTx = Just $ SimpleTx 1 (utxoRef 1) (utxoRef 3) + } + reqSn = receiveMessageFrom bob $ ReqSn 0 2 [] (Just decommitTx) + + outcome <- runHeadLogic bobEnv ledger s0 $ do + step reqSn + outcome `shouldSatisfy` \case + Error RequireFailed{} -> True + _ -> False + it "ignores in-flight ReqTx when closed" $ do let s0 = inClosedState threeParties input = receiveMessage $ ReqTx (aValidTx 42) diff --git a/spec/fig_offchain_prot.tex b/spec/fig_offchain_prot.tex index 84fd98bfc04..ea2d6fa6747 100644 --- a/spec/fig_offchain_prot.tex +++ b/spec/fig_offchain_prot.tex @@ -84,19 +84,29 @@ %%% REQ SN \On{$(\hpRS,v,s,\mT_{\mathsf{req}} \red{, \tx_\alpha} , \tx_\omega)$ from $\party_j$}{ \red{\Req{$\tx_\omega = \bot ~ \lor ~ \tx_\alpha = \bot$}} \; - \Req{$v = \hatv ~ \land ~ s = \hats + 1 ~ \land ~ \hpLdr(s) = j$} \; - \Wait{$\hats = \bar{\mc S}.s$}{ + \Req{$s = \hats + 1 ~ \land ~ \hpLdr(s) = j$} \; + \Wait{$ v = \hatv ~ \land ~ \hats = \bar{\mc S}.s$}{ + % TODO: waiting for version observed is way longer than for no snapshot in flight! \blue{ - \Req{$\bar{\mc S}.U \applytx \tx_\omega \not= \bot$} \; - $U_{\mathsf{active}} \gets \bar{\mc S}.U \applytx \tx_\omega \setminus \mathsf{outputs(\tx_\omega)}$ \; - } + \If{$v = \bar{\mc S}.v ~ \land ~ \bar{\mc S}.\tx_{\omega} \neq \bot$ }{ + \Req{$\bar{\mc S}.\tx_{\omega} = \tx_{\omega}$} \; + $U_{\mathsf{active}} \gets \bar{\mc S}.U$ \; + $U_{\omega} \gets \bar{\mc S}.U_{\omega}$ + } + \Else{ + \Req{$\bar{\mc S}.U \applytx \tx_{\omega} \not= \bot$} \; + $U_{\mathsf{active}} \gets \bar{\mc S}.U \applytx \tx_{\omega} \setminus \mathsf{outputs(\tx_{\omega})}$ \; + $U_{\omega} \gets \mathsf{outputs}(\tx_{\omega})$ + } + } \Req{$U_{\mathsf{active}} \applytx \mT_{\mathsf{req}} \not= \bot$} \; $U \gets U_{\mathsf{active}} \applytx \mT_{\mathsf{req}}$ \; $\hats \gets s$ \; % TODO: DRY message creation $\eta \gets \combine(U)$ \; + % TODO: handwavy combine/outputs here \red{$\eta_\alpha \gets \mathsf{combine}(\mathsf{inputs}(\tx_\alpha))$ \;} - $\eta_\omega \gets \mathsf{combine}(\mathsf{outputs}(\tx_\omega))$ \; + $\eta_{\omega} \gets \combine(U_{\omega})$ \; % NOTE: WE could make included transactions auditable by adding % a merkle tree root to the (signed) snapshot data \eta $\msSig_i \gets \msSign(\hydraSigningKey, (\cid || v || \hats || \eta \red{ || \eta_\alpha} || \eta_\omega))$ \;