Skip to content

Commit

Permalink
Decrement on-chain: Check value in the Head is preserved
Browse files Browse the repository at this point in the history
Add this check in the specfication too.
  • Loading branch information
v0d1ch authored and ch1bo committed Apr 17, 2024
1 parent 48388dc commit 71c4bea
Show file tree
Hide file tree
Showing 9 changed files with 56 additions and 38 deletions.
2 changes: 1 addition & 1 deletion hydra-cluster/test/Test/ChainObserverSpec.hs
Expand Up @@ -61,7 +61,7 @@ spec = do

commitTx <- requestCommitTx hydraNode commitUTxO

pure (signTx walletSk commitTx) >>= submitTx cardanoNode
submitTx cardanoNode (signTx walletSk commitTx)

waitFor hydraTracer 5 [hydraNode] $
output "HeadIsOpen" ["utxo" .= commitUTxO, "headId" .= headId]
Expand Down
19 changes: 18 additions & 1 deletion hydra-node/json-schemas/logs.yaml
Expand Up @@ -1550,7 +1550,10 @@ definitions:
seenSnapshot:
$ref: "logs.yaml#/definitions/SeenSnapshot"
decommitTx:
$ref: "api.yaml#/components/schemas/Transaction"
oneOf:
- type: "null"
- type: object
$ref: "api.yaml#/components/schemas/Transaction"

SeenSnapshot:
oneOf:
Expand Down Expand Up @@ -2164,6 +2167,20 @@ definitions:
type: array
items:
$ref: "api.yaml#/components/schemas/TxId"
- title: WaitOnNotApplicableDecommitTx
description: >-
Somebody requested a Decommit but there is another one in flight already.
type: object
additionalProperties: false
required:
- tag
- waitingOnDecommitTx
properties:
tag:
type: string
enum: ["WaitOnNotApplicableDecommitTx"]
waitingOnDecommitTx:
$ref: "api.yaml#/components/schemas/Transaction"

IP:
type: object
Expand Down
5 changes: 2 additions & 3 deletions hydra-node/src/Hydra/Chain/Direct/Tx.hs
Expand Up @@ -370,7 +370,7 @@ decrementTx scriptRegistry vk headId headParameters (headInput, headOutput) snap
& addInputs [(headInput, headWitness)]
& addReferenceInputs [headScriptRef]
-- NOTE: at this point 'utxoToDecommit' is populated
& addOutputs (headOutput' : map toTxContext (maybe [] (fmap snd . UTxO.pairs) utxoToDecommit))
& addOutputs (headOutput' : map toTxContext (maybe [] toList utxoToDecommit))
& addExtraRequiredSigners [verificationKeyHash vk]
where
headRedeemer = toScriptData $ Head.Decrement (toPlutusSignatures signatures)
Expand Down Expand Up @@ -481,8 +481,7 @@ closeTx scriptRegistry vk closing startSlotNo (endSlotNo, utcTime) openThreadOut
Head.Closed
{ snapshotNumber
, utxoHash = toBuiltin utxoHashBytes
, -- TODO: find a way to introduce this value
utxoToDecommitHash = toBuiltin decommitUTxOHashBytes
, utxoToDecommitHash = toBuiltin decommitUTxOHashBytes
, parties = openParties
, contestationDeadline
, contestationPeriod = openContestationPeriod
Expand Down
33 changes: 13 additions & 20 deletions hydra-node/src/Hydra/HeadLogic.hs
Expand Up @@ -613,27 +613,25 @@ onOpenClientDecommit ::
onOpenClientDecommit env headId ledger currentSlot coordinatedHeadState decommitTx =
checkNoDecommitInFlight $
checkValidDecommitTx $
Effects
[ NetworkEffect ReqDec{transaction = decommitTx, decommitRequester = party}
]
cause (NetworkEffect ReqDec{transaction = decommitTx, decommitRequester = party})
where
checkNoDecommitInFlight continue =
case mExistingDecommitTx of
Just existingDecommitTx ->
Effects
[ ClientEffect
cause
( ClientEffect
ServerOutput.DecommitInvalid
{ headId
, decommitInvalidReason = ServerOutput.DecommitAlreadyInFlight{decommitTx = existingDecommitTx}
}
]
)
Nothing -> continue

checkValidDecommitTx cont =
case applyTransactions ledger currentSlot confirmedUTxO [decommitTx] of
Left (_, err) ->
Effects
[ ClientEffect
cause
( ClientEffect
ServerOutput.DecommitInvalid
{ headId
, decommitInvalidReason =
Expand All @@ -643,7 +641,7 @@ onOpenClientDecommit env headId ledger currentSlot coordinatedHeadState decommit
, validationError = err
}
}
]
)
Right _ -> cont

confirmedUTxO = (getSnapshot confirmedSnapshot).utxo
Expand Down Expand Up @@ -684,22 +682,18 @@ onOpenNetworkReqDec ::
onOpenNetworkReqDec env ttl openState decommitTx =
waitOnApplicableDecommit $
let decommitUTxO = utxoFromTx decommitTx
in StateChanged (DecommitRecorded decommitTx)
<> Effects
[ ClientEffect $ ServerOutput.DecommitRequested headId decommitUTxO
]
in newState (DecommitRecorded decommitTx)
<> cause (ClientEffect $ ServerOutput.DecommitRequested headId decommitUTxO)
<> if isLeader parameters party nextSn
then
Effects
[NetworkEffect (ReqSn nextSn (txId <$> localTxs) (Just decommitTx))]
else Error $ RequireFailed $ ReqSnNotLeader{requestedSn = nextSn, leader = party}
then cause (NetworkEffect (ReqSn nextSn (txId <$> localTxs) (Just decommitTx)))
else noop
where
waitOnApplicableDecommit cont =
case mExistingDecommitTx of
Nothing -> cont
Just existingDecommitTx
| ttl > 0 ->
Wait $ WaitOnNotApplicableDecommitTx decommitTx
wait $ WaitOnNotApplicableDecommitTx decommitTx
| otherwise ->
Error $ RequireFailed $ DecommitTxInFlight{decommitTx = existingDecommitTx}
Environment{party} = env
Expand Down Expand Up @@ -909,8 +903,7 @@ update env ledger st ev = case (st, ev) of
)
-- TODO: What happens if observed decrement tx get's rolled back?
| ourHeadId == headId ->
causes
[ClientEffect $ ServerOutput.DecommitFinalized{headId}]
cause (ClientEffect $ ServerOutput.DecommitFinalized{headId})
<> newState DecommitFinalized
| otherwise ->
Error NotOurHead{ourHeadId, otherHeadId = headId}
Expand Down
2 changes: 1 addition & 1 deletion hydra-node/src/Hydra/HeadLogic/Outcome.hs
Expand Up @@ -158,7 +158,7 @@ data WaitReason tx
| WaitOnSeenSnapshot
| WaitOnTxs {waitingForTxIds :: [TxIdType tx]}
| WaitOnContestationDeadline
| WaitOnNotApplicableDecommitTx { waitingOnDecommitTx :: tx}
| WaitOnNotApplicableDecommitTx {waitingOnDecommitTx :: tx}
deriving stock (Generic)

deriving stock instance IsTx tx => Eq (WaitReason tx)
Expand Down
10 changes: 4 additions & 6 deletions hydra-node/test/Hydra/Chain/Direct/ContractSpec.hs
Expand Up @@ -215,9 +215,8 @@ prop_verifyOffChainSignatures =
onChainSig = List.head . toPlutusSignatures $ aggregate [offChainSig]
onChainParty = partyToChain $ deriveParty sk
snapshotNumber = toInteger number
utxoHash =
(toBuiltin $ hashUTxO @SimpleTx utxo)
utxoToDecommitHash = maybe (toBuiltin $ hashUTxO @SimpleTx mempty) (toBuiltin . hashUTxO @SimpleTx) utxoToDecommit
utxoHash = (toBuiltin $ hashUTxO @SimpleTx utxo)
utxoToDecommitHash = (toBuiltin . hashUTxO @SimpleTx $ fromMaybe mempty utxoToDecommit)
in verifyPartySignature (headIdToCurrencySymbol headId) snapshotNumber utxoHash utxoToDecommitHash onChainParty onChainSig
& counterexample ("headId: " <> show headId)
& counterexample ("signed: " <> show onChainSig)
Expand All @@ -233,7 +232,6 @@ prop_verifySnapshotSignatures =
onChainParties = partyToChain <$> parties
signatures = toPlutusSignatures $ aggregate [sign sk snapshot | sk <- sks]
snapshotNumber = toInteger number
utxoHash =
toBuiltin (hashUTxO @SimpleTx utxo)
utxoToDecommitHash = maybe (toBuiltin $ hashUTxO @SimpleTx mempty) (toBuiltin . hashUTxO @SimpleTx) utxoToDecommit
utxoHash = toBuiltin (hashUTxO @SimpleTx utxo)
utxoToDecommitHash = (toBuiltin . hashUTxO @SimpleTx $ fromMaybe mempty utxoToDecommit)
in verifySnapshotSignature onChainParties (headIdToCurrencySymbol headId) snapshotNumber utxoHash utxoToDecommitHash signatures
9 changes: 4 additions & 5 deletions hydra-node/test/Hydra/HeadLogicSpec.hs
Expand Up @@ -46,6 +46,7 @@ import Hydra.HeadLogic (
aggregateState,
defaultTTL,
update,
cause,
)
import Hydra.HeadLogic.State (getHeadParameters)
import Hydra.Ledger (ChainSlot (..), IsTx (..), Ledger (..), ValidationError (..))
Expand Down Expand Up @@ -152,10 +153,8 @@ spec =
let input = NetworkInput defaultTTL alice reqDec
st <- pickBlind $ oneof $ pure <$> [inInitialState threeParties, inIdleState, inClosedState threeParties]
pure $
update aliceEnv ledger st input
`hasEffectSatisfying` \case
NetworkEffect reqDec' -> reqDec' == reqDec
_ -> False
update aliceEnv ledger st event
`shouldNotBe` cause (NetworkEffect reqDec)

it "wait for second decommit when another one is in flight" $ do
let decommitTx1 = SimpleTx 1 mempty (utxoRef 1)
Expand All @@ -173,7 +172,7 @@ spec =
let outcome = update bobEnv ledger s1 reqDecEvent2

outcome `shouldSatisfy` \case
Wait (WaitOnNotApplicableDecommitTx{waitingOnDecommitTx = decommitTx''}) ->
Wait (WaitOnNotApplicableDecommitTx{waitingOnDecommitTx = decommitTx''}) _ ->
decommitTx2 == decommitTx''
_ -> False

Expand Down
13 changes: 12 additions & 1 deletion hydra-plutus/src/Hydra/Contract/Head.hs
Expand Up @@ -241,10 +241,21 @@ checkDecrement ctx@ScriptContext{scriptContextTxInfo = txInfo} prevParties prevS
&& checkSnapshot
&& checkSnapshotSignature
&& mustBeSignedByParticipant ctx prevHeadId
&& mustPreserveValue
where
mustPreserveValue =
traceIfFalse $(errorCode HeadValueIsNotPreserved) $
headInValue === headOutValue
-- NOTE: head output + whatever is decommitted needs to be equal to the head input.
headOutValue = txOutValue $ head $ txInfoOutputs txInfo <> decommitOutputs

headInValue = maybe mempty (txOutValue . txInInfoResolved) $ findOwnInput ctx

decommitOutputs = tail (txInfoOutputs txInfo)

-- NOTE: we always assume Head output is the first one so we pick all other
-- outputs of a decommit tx to calculate the expected hash.
decommitUtxoHash = hashTxOuts $ tail (txInfoOutputs txInfo)
decommitUtxoHash = hashTxOuts decommitOutputs
(nextUtxoHash, nextParties, nextSnapshotNumber, nextCperiod, nextHeadId) =
case fromBuiltinData @DatumType $ getDatum (headOutputDatum ctx) of
Just
Expand Down
1 change: 1 addition & 0 deletions spec/onchain.tex
Expand Up @@ -314,6 +314,7 @@ \subsection{Decrement Transaction}\label{sec:increment-tx}
\]
\item Transaction is signed by a participant $\exists \{\cid \mapsto \keyHash_{i} \mapsto 1\} \in \valHead' \Rightarrow \keyHash_{i} \in \txKeys$.
\todo{Need a constraint on the value in the head?}
\item Value in the head is preserved $\valHead' = \valHead$.
\end{menumerate}

\begin{figure}[h] \centering
Expand Down

0 comments on commit 71c4bea

Please sign in to comment.