Skip to content

Commit 092f445

Browse files
authored
Merge pull request #1163 from input-output-hk/abailly-iohk/1104/adjust-inconsistencies
Align minor discrepancies between spec and code
2 parents 704b67c + 9aac998 commit 092f445

File tree

9 files changed

+90
-50
lines changed

9 files changed

+90
-50
lines changed

hydra-node/src/Hydra/Ledger/Cardano.hs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ instance IsTx Tx where
109109

110110
txId = getTxId . getTxBody
111111
balance = foldMap txOutValue
112+
113+
-- NOTE: See note from `Head.hashTxOuts`.
112114
hashUTxO = fromBuiltin . Head.hashTxOuts . mapMaybe toPlutusTxOut . toList
113115

114116
instance ToCBOR Tx where

hydra-node/test/Hydra/Chain/Direct/Contract/Abort.hs

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import Hydra.Chain.Direct.Contract.Mutation (
1919
changeMintedTokens,
2020
changeMintedValueQuantityFrom,
2121
isHeadOutput,
22+
removePTFromMintedValue,
2223
replacePolicyIdWith,
2324
)
2425
import Hydra.Chain.Direct.Fixture (testNetworkId, testPolicyId, testSeedInput)
@@ -248,22 +249,3 @@ genAbortMutation (tx, utxo) =
248249
, SomeMutation (Just $ toErrorCode STNotBurned) DoNotBurnSTInitial
249250
<$> changeMintedTokens tx (valueFromList [(AssetId (headPolicyId testSeedInput) hydraHeadV1AssetName, 1)])
250251
]
251-
252-
removePTFromMintedValue :: TxOut CtxUTxO -> Tx -> Value
253-
removePTFromMintedValue output tx =
254-
case txMintValue $ txBodyContent $ txBody tx of
255-
TxMintValueNone -> error "expected minted value"
256-
TxMintValue v _ -> valueFromList $ filter (not . isPT) $ valueToList v
257-
where
258-
outValue = txOutValue output
259-
assetNames =
260-
[ (policyId, pkh) | (AssetId policyId pkh, _) <- valueToList outValue, policyId == testPolicyId
261-
]
262-
(headId, assetName) =
263-
case assetNames of
264-
[assetId] -> assetId
265-
_ -> error "expected one assetId"
266-
isPT = \case
267-
(AssetId pid asset, _) ->
268-
pid == headId && asset == assetName
269-
_ -> False

hydra-node/test/Hydra/Chain/Direct/Contract/FanOut.hs

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import Hydra.Cardano.Api
77
import Hydra.Prelude hiding (label)
88

99
import Cardano.Api.UTxO as UTxO
10-
import Hydra.Chain.Direct.Contract.Mutation (Mutation (..), SomeMutation (..))
10+
import Hydra.Chain.Direct.Contract.Mutation (Mutation (..), SomeMutation (..), changeMintedTokens)
1111
import Hydra.Chain.Direct.Fixture (testNetworkId, testPolicyId, testSeedInput)
1212
import Hydra.Chain.Direct.ScriptRegistry (genScriptRegistry, registryUTxO)
1313
import Hydra.Chain.Direct.Tx (fanoutTx, mkHeadOutput)
@@ -24,11 +24,11 @@ import Hydra.Ledger.Cardano (
2424
genValue,
2525
)
2626
import Hydra.Ledger.Cardano.Evaluate (slotNoFromUTCTime, slotNoToUTCTime)
27-
import Hydra.Party (partyToChain)
27+
import Hydra.Party (Party, partyToChain, vkey)
2828
import Hydra.Plutus.Extras (posixFromUTCTime)
2929
import Hydra.Plutus.Orphans ()
3030
import PlutusTx.Builtins (toBuiltin)
31-
import Test.QuickCheck (choose, elements, oneof, suchThat, vectorOf)
31+
import Test.QuickCheck (choose, elements, oneof, suchThat)
3232
import Test.QuickCheck.Instances ()
3333

3434
healthyFanoutTx :: (Tx, UTxO)
@@ -55,17 +55,15 @@ healthyFanoutTx =
5555

5656
headOutput' = mkHeadOutput testNetworkId testPolicyId (toUTxOContext $ mkTxOutDatumInline healthyFanoutDatum)
5757

58-
parties = generateWith (vectorOf 3 (arbitrary @(VerificationKey PaymentKey))) 42
59-
6058
headOutput = modifyTxOutValue (<> participationTokens) headOutput'
6159

6260
participationTokens =
6361
valueFromList $
6462
map
65-
( \vk ->
66-
(AssetId testPolicyId (AssetName . serialiseToRawBytes . verificationKeyHash $ vk), 1)
63+
( \party ->
64+
(AssetId testPolicyId (AssetName . serialiseToRawBytes . verificationKeyHash . vkey $ party), 1)
6765
)
68-
parties
66+
healthyParties
6967

7068
healthyFanoutUTxO :: UTxO
7169
healthyFanoutUTxO =
@@ -84,7 +82,8 @@ healthyFanoutDatum =
8482
Head.Closed
8583
{ snapshotNumber = 1
8684
, utxoHash = toBuiltin $ hashUTxO @Tx healthyFanoutUTxO
87-
, parties = partyToChain <$> arbitrary `generateWith` 42
85+
, parties =
86+
partyToChain <$> healthyParties
8887
, contestationDeadline = posixFromUTCTime healthyContestationDeadline
8988
, contestationPeriod = healthyContestationPeriod
9089
, headId = toPlutusCurrencySymbol testPolicyId
@@ -95,10 +94,17 @@ healthyFanoutDatum =
9594

9695
healthyContestationPeriod = OnChain.contestationPeriodFromDiffTime $ fromInteger healthyContestationPeriodSeconds
9796

97+
healthyParties :: [Party]
98+
healthyParties =
99+
[ generateWith arbitrary i | i <- [1 .. 3]
100+
]
101+
98102
data FanoutMutation
99103
= MutateAddUnexpectedOutput
100104
| MutateChangeOutputValue
101105
| MutateValidityBeforeDeadline
106+
| -- | Meant to test that the minting policy is burning all PTs and ST present in tx
107+
MutateThreadTokenQuantity
102108
deriving stock (Generic, Show, Enum, Bounded)
103109

104110
genFanoutMutation :: (Tx, UTxO) -> Gen SomeMutation
@@ -117,6 +123,14 @@ genFanoutMutation (tx, _utxo) =
117123
, SomeMutation (Just $ toErrorCode LowerBoundBeforeContestationDeadline) MutateValidityBeforeDeadline . ChangeValidityInterval <$> do
118124
lb <- genSlotBefore $ slotNoFromUTCTime healthyContestationDeadline
119125
pure (TxValidityLowerBound lb, TxValidityNoUpperBound)
126+
, SomeMutation (Just $ toErrorCode BurntTokenNumberMismatch) MutateThreadTokenQuantity <$> do
127+
(token, _) <- elements burntTokens
128+
changeMintedTokens tx (valueFromList [(token, 1)])
120129
]
121130
where
131+
burntTokens =
132+
case txMintValue $ txBodyContent $ txBody tx of
133+
TxMintValueNone -> error "expected minted value"
134+
TxMintValue v _ -> valueToList v
135+
122136
genSlotBefore (SlotNo slot) = SlotNo <$> choose (0, slot)

hydra-node/test/Hydra/Chain/Direct/Contract/Mutation.hs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -872,3 +872,22 @@ replaceContesters contesters = \case
872872
, Head.contesters = contesters
873873
}
874874
otherState -> otherState
875+
876+
removePTFromMintedValue :: TxOut CtxUTxO -> Tx -> Value
877+
removePTFromMintedValue output tx =
878+
case txMintValue $ txBodyContent $ txBody tx of
879+
TxMintValueNone -> error "expected minted value"
880+
TxMintValue v _ -> valueFromList $ filter (not . isPT) $ valueToList v
881+
where
882+
outValue = txOutValue output
883+
assetNames =
884+
[ (policyId, pkh) | (AssetId policyId pkh, _) <- valueToList outValue, policyId == testPolicyId
885+
]
886+
(headId, assetName) =
887+
case assetNames of
888+
[assetId] -> assetId
889+
_ -> error "expected one assetId"
890+
isPT = \case
891+
(AssetId pid asset, _) ->
892+
pid == headId && asset == assetName
893+
_ -> False

hydra-plutus/scripts/mHead.plutus

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

hydra-plutus/scripts/vHead.plutus

Lines changed: 2 additions & 2 deletions
Large diffs are not rendered by default.

hydra-plutus/src/Hydra/Contract/Head.hs

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import Hydra.Contract.Commit (Commit (..))
1717
import Hydra.Contract.Commit qualified as Commit
1818
import Hydra.Contract.HeadError (HeadError (..), errorCode)
1919
import Hydra.Contract.HeadState (Input (..), Signature, SnapshotNumber, State (..))
20-
import Hydra.Contract.Util (hasST, mustNotMintOrBurn, (===))
20+
import Hydra.Contract.Util (hasST, mustBurnAllHeadTokens, mustNotMintOrBurn, (===))
2121
import Hydra.Data.ContestationPeriod (ContestationPeriod, addContestationPeriod, milliseconds)
2222
import Hydra.Data.Party (Party (vkey))
2323
import Hydra.Plutus.Extras (ValidatorType, scriptValidatorHash, wrapValidator)
@@ -44,7 +44,7 @@ import PlutusLedgerApi.V2 (
4444
TxOut (..),
4545
TxOutRef (..),
4646
UpperBound (..),
47-
Value (Value, getValue),
47+
Value (Value),
4848
adaSymbol,
4949
adaToken,
5050
)
@@ -77,8 +77,8 @@ headValidator oldState input ctx =
7777
checkClose ctx parties initialUtxoHash signature contestationPeriod headId
7878
(Closed{parties, snapshotNumber = closedSnapshotNumber, contestationDeadline, contestationPeriod, headId, contesters}, Contest{signature}) ->
7979
checkContest ctx contestationDeadline contestationPeriod parties closedSnapshotNumber signature contesters headId
80-
(Closed{utxoHash, contestationDeadline}, Fanout{numberOfFanoutOutputs}) ->
81-
checkFanout utxoHash contestationDeadline numberOfFanoutOutputs ctx
80+
(Closed{parties, utxoHash, contestationDeadline, headId}, Fanout{numberOfFanoutOutputs}) ->
81+
checkFanout utxoHash contestationDeadline numberOfFanoutOutputs ctx headId parties
8282
_ ->
8383
traceError $(errorCode InvalidHeadStateTransition)
8484

@@ -96,20 +96,11 @@ checkAbort ::
9696
[Party] ->
9797
Bool
9898
checkAbort ctx@ScriptContext{scriptContextTxInfo = txInfo} headCurrencySymbol parties =
99-
mustBurnAllHeadTokens
99+
mustBurnAllHeadTokens minted headCurrencySymbol parties
100100
&& mustBeSignedByParticipant ctx headCurrencySymbol
101101
&& mustReimburseCommittedUTxO
102102
where
103-
mustBurnAllHeadTokens =
104-
traceIfFalse $(errorCode BurntTokenNumberMismatch) $
105-
burntTokens == length parties + 1
106-
107-
minted = getValue $ txInfoMint txInfo
108-
109-
burntTokens =
110-
case AssocMap.lookup headCurrencySymbol minted of
111-
Nothing -> 0
112-
Just tokenMap -> negate $ sum tokenMap
103+
minted = txInfoMint txInfo
113104

114105
mustReimburseCommittedUTxO =
115106
traceIfFalse $(errorCode ReimbursedOutputsDontMatch) $
@@ -451,10 +442,16 @@ checkFanout ::
451442
POSIXTime ->
452443
Integer ->
453444
ScriptContext ->
445+
CurrencySymbol ->
446+
[Party] ->
454447
Bool
455-
checkFanout utxoHash contestationDeadline numberOfFanoutOutputs ScriptContext{scriptContextTxInfo = txInfo} =
456-
hasSameUTxOHash && afterContestationDeadline
448+
checkFanout utxoHash contestationDeadline numberOfFanoutOutputs ScriptContext{scriptContextTxInfo = txInfo} currencySymbol parties =
449+
mustBurnAllHeadTokens minted currencySymbol parties
450+
&& hasSameUTxOHash
451+
&& afterContestationDeadline
457452
where
453+
minted = txInfoMint txInfo
454+
458455
hasSameUTxOHash =
459456
traceIfFalse $(errorCode FannedOutUtxoHashNotEqualToClosedUtxoHash) $
460457
fannedOutUtxoHash == utxoHash
@@ -556,6 +553,8 @@ getTxOutDatum o =
556553

557554
-- | Hash a potentially unordered list of commits by sorting them, concatenating
558555
-- their 'preSerializedOutput' bytes and creating a SHA2_256 digest over that.
556+
--
557+
-- NOTE: See note from `hashTxOuts`.
559558
hashPreSerializedCommits :: [Commit] -> BuiltinByteString
560559
hashPreSerializedCommits commits =
561560
sha2_256 . foldMap preSerializedOutput $
@@ -565,6 +564,13 @@ hashPreSerializedCommits commits =
565564
-- | Hash a pre-ordered list of transaction outputs by serializing each
566565
-- individual 'TxOut', concatenating all bytes together and creating a SHA2_256
567566
-- digest over that.
567+
--
568+
-- NOTE: In general, from asserting that `hash(x || y) = hash (x' || y')` it is
569+
-- not safe to conclude that `(x,y) = (x', y')` as the same hash could be
570+
-- obtained by moving one or more bytes from the end of `x` to the beginning of
571+
-- `y`, but in the context of Hydra validators it seems impossible to exploit
572+
-- this property without breaking other logic or verification (eg. producing a
573+
-- valid and meaningful `TxOut`).
568574
hashTxOuts :: [TxOut] -> BuiltinByteString
569575
hashTxOuts =
570576
sha2_256 . foldMap (Builtins.serialiseData . toBuiltinData)

hydra-plutus/src/Hydra/Contract/Util.hs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
{-# LANGUAGE TemplateHaskell #-}
12
{-# OPTIONS_GHC -fno-specialize #-}
23

34
module Hydra.Contract.Util where
45

56
import Hydra.Contract.Error (ToErrorCode (..))
7+
import Hydra.Contract.HeadError (HeadError (..), errorCode)
8+
import Hydra.Data.Party (Party)
69
import Hydra.Prelude (Show)
710
import PlutusLedgerApi.V1.Value (isZero)
811
import PlutusLedgerApi.V2 (
@@ -29,6 +32,20 @@ hasST headPolicyId v =
2932
pure $ quantity == 1
3033
{-# INLINEABLE hasST #-}
3134

35+
-- | Checks all tokens related to some specific `CurrencySymbol`.
36+
--
37+
-- This checks both PTs and ST are burnt.
38+
mustBurnAllHeadTokens :: Value -> CurrencySymbol -> [Party] -> Bool
39+
mustBurnAllHeadTokens minted headCurrencySymbol parties =
40+
traceIfFalse $(errorCode BurntTokenNumberMismatch) $
41+
burntTokens == length parties + 1
42+
where
43+
burntTokens =
44+
case AssocMap.lookup headCurrencySymbol (getValue minted) of
45+
Nothing -> 0
46+
Just tokenMap -> negate $ sum tokenMap
47+
{-# INLINEABLE mustBurnAllHeadTokens #-}
48+
3249
-- | Checks if the state token (ST) for list of parties containing specific
3350
-- 'CurrencySymbol' are burnt.
3451
mustBurnST :: Value -> CurrencySymbol -> Bool

spec/onchain.tex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ \subsection{Init transaction}\label{sec:init-tx}
108108
\end{menumerate}
109109
\item When evaluated with the $\mathsf{burn}$ redeemer,
110110
\begin{menumerate}
111-
\item All tokens in $\txMint$ need to be of negative quantity
111+
\item All tokens for this policy in $\txMint$ need to be of negative quantity
112112
$\forall \{\cid \mapsto \cdot \mapsto q\} \in \txMint : q < 0$.
113113
\end{menumerate}
114114
\end{itemize}

0 commit comments

Comments
 (0)