-
Notifications
You must be signed in to change notification settings - Fork 87
Logbook 2022 H1
- What is this about?
- Newer entries
- June 2022
- May 2022
- April 2022
- March 2022
- February 2022
- January 2022
- Older entries
-
Wanting to run a babbage-era hydra-node against testnet while it's still Alonzo. Copied scripts from vasil-testnet/ -> testnet/. Should be reusable
-
Synchronizing testnet takes ages.. After synched up, running a babbage hydra-node against it:
[ch1bo][~/code/iog/hydra-poc/testnet][⌥ ch1bo/babbage-era] λ ./hydra-node.sh
hydra-node: HardForkEncoderDisabledEra (SingleEraInfo {singleEraName = "Babbage"})
-
When running the master hydra-node against the testnet, cardano-node was seeming stuck
- restart takes long (replaying blocks)
- For some reason, I can not connect to the hydra-node once it's running (no tui, no ws)
- Receving a
websocket: close 1006 (abnormal closure): unexpected EOF
-> works fine on devnet / demo though!? - On Ctrl+c the node prints:
hydra-node: SeedBytesExhausted {seedBytesSupplied = 8}
-> exception? - When not trying to send history, the first client input (Init) will have the hydra-node crash with:
hydra-node: SeedBytesExhausted {seedBytesSupplied = 8}
- Found it: the hydra keys were not proper / too short and
loadSigningKey
from disk failed when forcing it on first use (lazy IO). Plus, the websocket thread died silently.
-
Did a full smoke test run of a single node head on testnet ~ 10 tADA cost?
-
Continuing updates to Babbage, now lately the
outputs
of aBabbage.TxBody
seemingly turned intoSized TxOut
.. we really should not rely on the ledger but the cardano-api types more. -
We see the
tx-cost
benchmark busy loop on some (??) contest transactions- We narrowed it done to serialization of that txs??
- Turns out.. we cannot even show these transactions!?
- After ours we found the issue: genContestTx was looping forever on
suchThat
predicate for thegenPointInTime
-
For some reason, some generated fanoutTx are a lot smaller than others, that is particularly weird if it's for big number of outputs.
- The resulting # of outputs on the tx was not 84, but 2 in one example
- The reason: sometimes the initial snapshot is used to close the head.. we need to separate benchmark from test generators!
- Starting work on #300 by rebasing babbage-preview on top of master again.. it is becoming a pain.
- When continuing the oddysey of upgrading tests to compile with babbage, I encounter a (now?) missing
Arbitrary Praos.Header
instance?- Asked in #consensus and #ledger, continue with other compile errors
- In the midst of fixing compilation errors there was the need to update cardano-node to latest tag because the old commit is "gone"
- Updated to
1.35.0-rc4
- This led to a barrage of more things changed
- Updated to
- Seems like cardano-api got some more updates because of reference inputs, script witnesses now can be either
PScript
orPReferenceScript
- The error types of
Alonzo.Tools.evaluateTransactionExecutionUnits
and the monad ofEpochInfo
changed- Instead of fixing
evaluateTx
to these new types we could migrate it to the cardano-api's version? - Tried this, but I got stuck in creating a fixture for
eraHistory
fromfixedEpochSize
. No brain capacity left..
- Instead of fixing
Changing the representation for Payment
so that it dumps the addresses instead of the signing keys which will make it easier to relate to UTxO displayed in the logs: We are not using the Show
instance for AddressInEra
but the ToJSON
one which uses serialise-to-bech32 function
Adding some log to Direct
component to display chain time so that we capture some information about the context in which contestation deadline is computed. Looking at fanout deadline issue w/ MLabs team:
- Checking the dates inside each container is correct: :check:
- checking
systemStart
in the cardano-node -> genesis-byron.json and genesis-shelley.json -> :check: - Checking the
cardano-cli query tip
produces something that make sense :check:
It seems the currentPointInTime
function is wrong: it always gives the same answer whenever it's called because it depends on the slotNo
that's retrieved when we invoke the queryTimeHandle
function, which happens only once.
FIXED: Turns out this was caused by the major
field of genesis-shelley.json
being set to 0: It should be 6.
As we are getting closer to "General Availability" of Hydra node and Head protocol, eg. releasing a version (1.0) that is deemed usable for mainnet, we want to increase the confidence in our codebase and verify it correctly implements the Hydra Head protocol. More precisely we want to have a high level of assurance that the Safety Properties stated and proven in the paper indeed hold in our reference implementation. We have been exploring various approaches to, discussing with other teams at IOG and assessing which avenue would benefit us most. This is discussed in the Formalising Hydra page.
As our goal is to provide both on-chain and off-chain validation of our system, we settled on a Model-Based Testing approach using quickcheck-dynamic, the framework initially developed by Quviq for testing Plutus smart contracts. We are aiming at building models and test Hydra Head's properties in at least two flavors:
- One model running in IOSim monad, based only on quickcheck-dynamic's base framework, focusing on the overal correctness of the off-chain part of protocol assuming validity of on-chain code,
- Another model to check the behaviour of the on-chain Plutus code and the
Hydra.Chain.Direct
component, using not only QD but also Plutus' emulator in order to actually build and validate transactions.
We've already started work on the first model and it already helped us pinpoint some issues in the DX while writing the model.
Working on #388, now trying to rebuild images from latest master
in order to spin up a demo environment and check whether or not I can reproduce the behaviour of the bug.
We cannot reproduce the bug with latest master (3c2cca4841a849b0719f97dbebeee39136bf887e):
- We can correctly init/commit/close/fanout a head without any transaction
- We can do the same with one transaction
Working back on #391 picking up where we left and rebasing on master.
We realise the NewTx
command should wait until a non-empty UTxO appears which is not the case:
- => we should filter
NewTx
in the model to make sure there's a UTxO for the party - The transaction is invalid though, because the amount exceeds what's available for spending
Adding precondition to filter on the possibility of a NewTx
leads to the generator looping. Why?
- Adding traces to understand why the model is looping
- It seems the node is looping in the event queue processing, preventing further tx to be processed and outputs to appear
We try resubmitting the same tx several times because from the POV of the model they are different but in the perform
they end up being the same because the previous tx has not been confirmed. This highlights an issue in our Node
, we should either or both:
- Ensure submitted Tx have a TTL so that we don't keep them indefinitely in the event queue
- Filter tx so that already applied tx are not enqueued
- Adding a waitUntilMathc on
SnapshotConfirmed
does not fix the issue, the test times out => The faulty tx is never seen confirmed - Seems like the first tx is ok, but the second one is invalid which "explains" why we loop on the second one -> could it be that leader election is wrong in the case of a single node?
Trying to generate head with more than one party => same issue
Tracing the outputs from the node shows that we have CommandFailed
which is suspicious -> there should not be any.
- Adding some context to
CommandFailed
would be useful -> adding the faultyClientInput
- We probably try to submit a
NewTx
in a state that's not correct?
Problem is that we need to wait for HeadIsOpen
before submitting NewTx
and not after
Seems like checking the opening of the head before submitting solves the infinite looping issue, even though we end up with errors when submitting "similar" tx in a single member head
Other errors we see:
- Negative tx amount in tx: The negative amounts error is caused by shrinking: We don't have any shrinking of the actions themselves but the engine tries to shrink the trace thus potentially discarding intermediate actions. As we don't have a strong precondition on the
NewTx
we can end up in a situation with a non-spendable tx - not matching the UTxo
Adding a precondition removes the negative failure but we still have failure when comparing the states. This is caused by discrepancy b/w model and actual UTxO: we want to have a consisten account-based model using addresses
- Note: cardano-api addresses are not sortable, but ledger's are!
- Seems like our
applyTx
is definitely wrong...
We need some improvements on DX/UX:
- Infinite looping on new tx
- Errors reporting
- State observation/sync API on node
The assertion in ModelSpec
is wrong: We compare the initial UTxO from implementation (observing HeadIsOpen
) with the confirmed UTxO from the model and it's not surprising they are different after a NewTx.
We are still observing timeout failure when a single node runs more than one transaction, we would like to have the node's log as a counterexample
- Adding a
TVar
in theTestHydraNode
works but we have a failure before even the assertion is tested - We need to be able to dump the logs when the
IOSim
evaluation fails
Struggling to find a way to dump logs within the ModelSpec
. I am trying to leverage the printTrace
and shouldRunInSim
functions from Util
module.
The idea is to look at all the traces generated in IOSim and select those about logging, this works in BehaviourSpec but seems to fail in ModelSpec
- Refactored
printTrace
to be pure and return aText
, this still works in theBehaviorSpec
: Injecting a fault makes the test dump the node's logs, but no logs appear for theModelSpec
- Realised that I was not passing the correct tracer -> We now have a dump of logs when the
IOSim
monad fails. This removes storage of logs inside theTestHydraNode
which thus prevents getting logs ascounterexample
in case the property fails, which is annoying. The problem is that the assertion runs in theStateT (Nodes ..) (IOSim s)
monad which implies the trace is not available as we are still running the action.
Seems like we have a clear idea of what's going on:
- When we compute the hash for the
InitialSnapshot
we order them by theTxIn
corresponding to theCollectCom
transaction, but the UTxO inside in theInitialSnapshot
uses the originalTxIn
s - We need an ordering for the initial snapshot's txOut that's stable across the collectcom/close/dance
- We used to sort by
TxOut
but this has been removed in b82f880d1885
Trying to putback the previous version of hashTxOuts
-> still have test failures though...
Proposal:
- Use a single function for computing hash everywhere: There is a
hashUTxO
in theIsTx
typeclass, we can change it to output not only the hash but also the (ordered) list of TxOut - This requires to deserialise the TxOut when building the Collectcom function, from the serialised representation in the Datum
Problem: On-chain, we hash using the "natural" ordering of the inputs to the collectCom
transaction which corresponds to the output of the commit
s txs not the utxo we maintain on the L2 which has the TxIns of the input to the commit
txs.
Solution: We should "rewrite" the off-chain utxo's txins to the ones consumed effectively by the CollectCom
transaction
We modify the observeCommitTx
to replace the original txIn
with the commitIn
corresponding to the txId
of the commit transaction. This entails removing a lot of code related to observation of initial inputs which appear useless, but this breaks quite a few tests along the way.
Also, we notice the commit observation does not check the headId
which means we observe any commit and only verify the party is part of "current" head -> this should lead to failure in ETE tests with 2 open heads
Adding back the observation of the initials in the observeCommit
and erroring on the case where we consume more than one initials yield errors on commit observastion Prop tests -> :thinking_face:
We still have a failing test for the fanout hash: The test fails because of a problem in the tests
- We generate a U0 in
genStOpen
and then another (arbitrary) utxo for passing togenStClosed
- we always use the latter whether or not the (arbitrary) snapshot to fanout is initial or not, but in the former case we should pass U0
Generating initial snapshots as part of genStClosed
breaks the genContest
because now the minimum snapshot number can be 0 so we can post a contest with an initial snapshot
which is incorrect. -> The genConfirmedSnapshot
function is very awkward...
The UTxO we return from the genStOpen
is not the one that will end up off-chain, we need to reconstruct the off-chain UTxO from the actual commits
This highlights the fact the current StateSpec
is hard to modify and understand, as it's probably to fine-grained and closely tied to the implementation, requiring a great deal of external knowledge
Now the ETE tests fail because the TxIn
of the UTXO we expect are incorrect: We need to change the expectations to check the TxOut
matches, without the TxIn
Here are the latest failures:
Failures:
test/Test/DirectChainSpec.hs:292:26:
1) Test.DirectChain can init and abort a 2-parties head after one party has committed
expected: OnCommitTx {party = Party {vkey = HydraVerificationKey (VerKeyEd25519DSIGN "38088e4c2ae82f5c45c6808a61a6490d3c612ce1da235714466fc748fbc4cbbb")}, committed = fromList [(TxIn "80e9db7411365a3fce90cfb6cbba9c7c8a246ec27a67fc1b8ccd1b739bcac763" (TxIx 1),TxOut (AddressInEra (ShelleyAddressInEra ShelleyBasedEraAlonzo) (ShelleyAddress Testnet (KeyHashObj (KeyHash "f8a68cd18e59a6ace848155a0e967af64f4d00cf8acee8adc95a6b0d")) StakeRefNull)) (TxOutValue MultiAssetInAlonzoEra (valueFromList [(AdaAssetId,66000000)])) TxOutDatumNone)]}
but got: OnCommitTx {party = Party {vkey = HydraVerificationKey (VerKeyEd25519DSIGN "38088e4c2ae82f5c45c6808a61a6490d3c612ce1da235714466fc748fbc4cbbb")}, committed = fromList [(TxIn "4d21d53e885c81f3b4227fd6cec2eff09d908ac425381830cf0b4f2a7933bf2a" (TxIx 0),TxOut (AddressInEra (ShelleyAddressInEra ShelleyBasedEraAlonzo) (ShelleyAddress Testnet (KeyHashObj (KeyHash "f8a68cd18e59a6ace848155a0e967af64f4d00cf8acee8adc95a6b0d")) StakeRefNull)) (TxOutValue MultiAssetInAlonzoEra (valueFromList [(AdaAssetId,66000000)])) TxOutDatumNone)]}
To rerun use: --match "/Test.DirectChain/can init and abort a 2-parties head after one party has committed/"
Replacing the shouldBe
assertion in DirectChainSpec
with a observesInTimeMatching
so that I can pass a predicate, then defining a predicate that checks OnCommitTx
irrespective of the TxIn
s.
- The problem is that I would like to ensure the
TxOut
lists are the same without taking care of the ordering, but there is noOrd
instance forTxOut
=> need to check inclusion in both directions, one by one - As expected our change that "rewrites" the UTXO to replace the original
TxIn
severely impacts the ETE tests as we are not able anymore to assume what we commit is put into the head as is, and prebuild transactions from those UTxO. This is really annoying from a test-writing and probably a user perspective
Writing an ETE test to expose the problem with immediately closing a Head from U0, something we should have done from the get go
- What we really want is not replacing the
TxIn
but computing the UTXO hash by ordering the commits according to their original TxIn, not the ones - when we observe the commits we accumulate in the state the commitOutputs which are the things we will spend in the collectComTx, and we compute the utxo Hash on the ordering of these commit outputs extracting the encoded TxOut. The committed outputs are just reported up to the HeaDLogic but not accumulated so that when we create the
CollectComTx
we only have available thecommitOutputs
and not thecommitted
UTxO to compute the hash.
Struggling to fix the tests after adding the committed UTxO as argument to collectCom: We now need to return those generated UTxO which once again highlights how cumbersome it is to work with tuples...
Not all tests are passing! I now see errors in the collectCom
specific tests and the ETE tests fail. Why?
- The failures in the
ContractSpec
are somewhat logical: We reconstruct the hash from the commit outputs using their ordering and it's different from the one used incollectCom
now.
Idea: Include the TxIn
(or TxOutRef
) into the commit datum so that we can sort on it when checking the txOutHash
- I am stuck trying to put
TxOutRef
in the commit datum in order to be able to sort it properly in theHead
script. -
TxOutRef
is not anOrd
instance... Trying to implementOrd
forTxOutRef
failed for some mysterious reason: Got a full PLC dump telling me some function was notINLINEABLE
which seemingly was related to myOrd
declaration but unclear why - Just use a custom sorting function
Adding the TxOutRef
on-chain and using that to sort the commits before hashing solves our issues:
- Consequence is that we can keep the same UTxO from L1 in L2 as U0 which is better DX, at the expense of the sorting and storage cost in the script.
Pairing Session - #375
Did some cleanup on the Model
and ModelSpec
:
- Removed not needed
WrapIOSim
monad - Cleanup unneeded functions for casting and indirection in tests
- Make a single state in
WorldState
- Simplify
Action
to have a singleCommand
wrapping aClientInput
. This is made possible thanks toactionName
which is a method fromStateModel
class that allows fine tuning of actions classification - Also improved
precondition
to reject by default
We want to complete the Model
to address the full lifecycle of the Head protocol.
- Now tackling what happens in
Open
state, starting with generatingNewTx
command: We select one random party, then lookup a UTxO it can spend and generate a transaction from it, usingmkSimpleTx
. We consume a UTxO we own, and send the value to some address owned by someone else in theHead
- Interestingly, while trying to write the
NewTx
command we realise we need to abstract the model away from the implementation details: A new transaction should be expressed, in the model, asAlice -[ 10 ]-> Bob
eg. a simple payment transaction, leaving the details of the construction of the actualTx
to theperform
. This highlights the fact theModel
is really, well, a model and in our case it embeds some assumptions about the "use case".
We introduce a Payment
type that will be used as a parameter for ClientInput
in the Action
datatype.
- Working on a
Payment
type, implementingIsTx
typeclass and updating the generators - We need to converet from our
Payment
UTxO to the standard UTxO which requires aTxIn
, but we cannot invokearbitrary
in theActionMonad
- We can use the vk as the raw source of bytes for the txId because the latter is a hash encoded with
blake2b_256
hence has 32 bytes, whereas the hash for a vk is a blake2b_224 (28 bytes).
Writing a proper model requires some logic to represent the beahviour of the ledger. Using an account based ledger assumes we always consume full UTxO.
We have updated the tests in accordance with the new model which required some simplifications of UTxO to be able to compare the states. But we now get errors because we cannot generate a payment:
test/Hydra/ModelSpec.hs:34:5:
1) Hydra.Model implementation respects model
uncaught exception: ErrorCall
no UTxO available for payment.
CallStack (from HasCallStack):
error, called at src/Relude/Debug.hs:288:11 in relude-1.0.0.1-FnlvBqksJVpBc8Ijn4tdSP:Relude.Debug
error, called at test/Hydra/Model.hs:356:21 in main:Hydra.Model
(after 15 tests and 3 shrinks)
Actions
[Var 1 := Seed {seedKeys = [(HydraSigningKey (SignKeyEd25519DSIGN "4d5572d174ba0000000000000000000000000000000000000000000000000000eadad991ccb48abc739cf6802fdfa52776977d504d4cd8a5dd01d1a9875af5d8"),"0302020103020601080801020500020805060505070500040803080000040205")]},
Var 2 := Command {party = Party {vkey = HydraVerificationKey (VerKeyEd25519DSIGN "eadad991ccb48abc739cf6802fdfa52776977d504d4cd8a5dd01d1a9875af5d8")}, command = Init {contestationPeriod = 12.333369372475s}},
Var 3 := Command {party = Party {vkey = HydraVerificationKey (VerKeyEd25519DSIGN "eadad991ccb48abc739cf6802fdfa52776977d504d4cd8a5dd01d1a9875af5d8")}, command = Commit {utxo = [("0302020103020601080801020500020805060505070500040803080000040205",valueFromList [(AdaAssetId,10607997295420064185)])]}},
Var 6 := Command {party = Party {vkey = HydraVerificationKey (VerKeyEd25519DSIGN "eadad991ccb48abc739cf6802fdfa52776977d504d4cd8a5dd01d1a9875af5d8")}, command = NewTx {transaction = Payment {from = "0302020103020601080801020500020805060505070500040803080000040205", to = "0302020103020601080801020500020805060505070500040803080000040205", value = valueFromList [(AdaAssetId,10607997295420064185)]}}}]
This is caused by the UTxO not being available in the Head, which might come from the fact we have not yet observed the head being opened?
AB Solo - #388
Following discussion with MLabs team on how to setup an ETE test environment, they showed again they were having trouble with posting Fanout: https://github.com/input-output-hk/hydra-poc/issues/388
- Run the demo with locally built docker images and see if I can reproduce the problem
- Add logs related to time observation and deadline
Trying to close -> fanout without creating any transaction gives me this error:
fannedOutUtxoHash /= closedUtxoHash
when posting the fanout transaction
Looking for the remainingContestationPeriod
retrieved from the close tx, I can see it's mostly correct:
remainingContestationPeriod":20.686456384,"tag":"OnCloseTx","snapshotNumber"
The end of the effect processing and the subsequent ShouldFanOut
event are correctly 20s apart:
{"timestamp":"2022-06-09T14:46:41.614095732Z","threadId":87,"namespace":"HydraNode-3","message":{"tag":"Node","node":{"tag":"ProcessedEvent","event":{"tag":"OnChainEvent","chainEvent":{"contents":{"remainingContestationPeriod":20.686456384,"tag":"OnCloseTx","snapshotNumber":0},"tag":"Observation"}},"by":{"vkey":"accac8a5f014fa4a5f012e9fc13f2788f63d2ebccb8b416d496a64a1a3eb61c6"}}}}
{"timestamp":"2022-06-09T14:47:02.301681216Z","threadId":87,"namespace":"HydraNode-3","message":{"tag":"Node","node":{"tag":"ProcessingEvent","event":{"tag":"ShouldPostFanout"},"by":{"vkey":"accac8a5f014fa4a5f012e9fc13f2788f63d2ebccb8b416d496a64a1a3eb61c6"}}}}
The UTxO that's posted by Fanout:
{
"3eeea5c2376b033d5bdeab6fe551950883b04c08a37848c6d648ea03476dce83#1": {
"address": "addr_test1vru2drx33ev6dt8gfq245r5k0tmy7ngqe79va69de9dxkrg09c7d3",
"value": {
"lovelace": 1000000000
}
},
"6db235b8759454654d19baf3ef601a2cb0e4ea3ebdc5e9db466dd07bccf53c7d#1": {
"address": "addr_test1vqg9ywrpx6e50uam03nlu0ewunh3yrscxmjayurmkp52lfskgkq5k",
"value": {
"lovelace": 500000000
}
},
"0ad134cc87cdf66a6863464b4393501eda7632f7c268b068c48b2aaf84f20e51#1": {
"address": "addr_test1vqa25t3aayfmpad20elswmsj94ehmdfjnhc64yz3jg5yl6skf5cck",
"value": {
"lovelace": 250000000
}
}
}
is identical to the initial UTxO reported by HeadIsOpen
so the problem probably lies in the hashing functions?
{
"3eeea5c2376b033d5bdeab6fe551950883b04c08a37848c6d648ea03476dce83#1": {
"address": "addr_test1vru2drx33ev6dt8gfq245r5k0tmy7ngqe79va69de9dxkrg09c7d3",
"value": {
"lovelace": 1000000000
}
},
"6db235b8759454654d19baf3ef601a2cb0e4ea3ebdc5e9db466dd07bccf53c7d#1": {
"address": "addr_test1vqg9ywrpx6e50uam03nlu0ewunh3yrscxmjayurmkp52lfskgkq5k",
"value": {
"lovelace": 500000000
}
},
"0ad134cc87cdf66a6863464b4393501eda7632f7c268b068c48b2aaf84f20e51#1": {
"address": "addr_test1vqa25t3aayfmpad20elswmsj94ehmdfjnhc64yz3jg5yl6skf5cck",
"value": {
"lovelace": 250000000
}
}
}
The outputs of the transaction:
{
"3eeea5c2376b033d5bdeab6fe551950883b04c08a37848c6d648ea03476dce83#1": {
"address": "addr_test1vru2drx33ev6dt8gfq245r5k0tmy7ngqe79va69de9dxkrg09c7d3",
"value": {
"lovelace": 1000000000
}
},
"6db235b8759454654d19baf3ef601a2cb0e4ea3ebdc5e9db466dd07bccf53c7d#1": {
"address": "addr_test1vqg9ywrpx6e50uam03nlu0ewunh3yrscxmjayurmkp52lfskgkq5k",
"value": {
"lovelace": 500000000
}
},
"0ad134cc87cdf66a6863464b4393501eda7632f7c268b068c48b2aaf84f20e51#1": {
"address": "addr_test1vqa25t3aayfmpad20elswmsj94ehmdfjnhc64yz3jg5yl6skf5cck",
"value": {
"lovelace": 250000000
}
}
}
are precisely the expected ones
Trying to run the demo and fanout with one transaction in the head
- Fanout worked on demo with one transaction in the head so most probably the problem I am observing is only present when closing with
initialSnapshot
.
Trying again on demo setup to see if the same problem occurs
- Retrying to close a Head without any transaction yields me the same error: The hashes do not match between the fanout and the close
When we create the closeTx
we compute the utxo hash in 2 different ways:
- For the initial snapshot, we take the hash that's been observed from the
CollectCom
- For other snapshots, we compute it
Hypothesis for #395: The decoding from CollectCom or computing in the collect com is invalid.
Trying to validate hypothesis by not using the openUtxoHash
from teh state but recomputing on the fly from the InitialSnapshot
- Interestingly, changing the way the hash is computed in the initial snapshot cases makes a test fail. There's even a comment suggesting to have better classification for errors 😄
forAllClose action = do -- TODO: label / classify tx and snapshots to understand test failures
This failure seems once again to point to the way we compute the hash in CollectCom
which is somewhat odd:
utxoHash =
Head.hashPreSerializedCommits $ mapMaybe (extractSerialisedTxOut . snd . snd) orderedCommits
orderedCommits =
Map.toList commits
Could it be that Foldable.toList
is not equivalent to Map.toList
? => :no:
Looking at the transaction that's generated it seems the utxoHash
es are inconsistent.
-
The OnChainHeadState has
openUtxoHash = "\240\194\v\241o\219j\FS\191\253\235\157C\190\241 \215m\FS\219\155\230\vQ,\156\241{\150\ETX\228\252"
-
The transaction has a datum with encoded
Open
state of:DataConstr Constr 1 [ Constr 0 [I 0] , List [ B ";j'\188\206\182\164-b\163\168\208*o\rse2\NAKw\GS\226C\166:\192H\161\139Y\218)" , B ";j'\188\206\182\164-b\163\168\208*o\rse2\NAKw\GS\226C\166:\192H\161\139Y\218)" , B ";j'\188\206\182\164-b\163\168\208*o\rse2\NAKw\GS\226C\166:\192H\161\139Y\218)" ] , B "\240\194\v\241o\219j\FS\191\253\235\157C\190\241 \215m\FS\219\155\230\vQ,\156\241{\150\ETX\228\252" ]
-
However the closed state is
DataConstr Constr 2 [ List [ B ";j'\188\206\182\164-b\163\168\208*o\rse2\NAKw\GS\226C\166:\192H\161\139Y\218)" , B ";j'\188\206\182\164-b\163\168\208*o\rse2\NAKw\GS\226C\166:\192H\161\139Y\218)" , B ";j'\188\206\182\164-b\163\168\208*o\rse2\NAKw\GS\226C\166:\192H\161\139Y\218)" ] , I 0 , B "\US\145\166+\232\n\CAN\170\209\213o6\DC2\148\EM\238>\158\240\145\205\251\237\204\GS\167)\ESC\205\204\213\&8" , I 91814000 ]
which shows the hashes are not the same
Side remark: Seems like Babbage has provision for starting up a cardano node with stakes: https://github.com/input-output-hk/cardano-node/blob/95c3692cfbd4cdb82071495d771b23e51840fb0e/scripts/babbage/mkfiles.sh#L111
Interestingly, we do not tests the case of fanoutting an initialSnapshot
:
-- FIXME: We need a ConfirmedSnapshot here because the utxo's in an
-- 'InitialSnapshot' are ignored and we would not be able to fan them out
confirmed <-
arbitrary `suchThat` \case
InitialSnapshot{} -> False
ConfirmedSnapshot{} -> True
I was able to reproduce the failure in a unit test in StateSpec
by specifically removing the filter when generating snapshots to fanout that excluded InitialSnapshot
.
A short presentation to the Architecture & Engineering chapter
Sticking to agile principles:
- Working software over Comprehensive Documentation
- People and Interactions over Process and Tools
- Responding to Change over Following a Plan
(We don't really have customers :) )
- Writing tests first is a great way to expose and make explicit the architecture
- Outside-in TDD, start from ETE tests and "drill down"
- Property-Based testing wherever possible
Collaborative diagramming with Miro
Architectural Decision Records:
- Record major decisions impacting the design & arch of the system
- Discuss possibly contentious choices within the team
Pairing - #375
Enhancing the model to wait for specific events to happen when perform
ing -> We reuse the waitForXXX
logic from BehaviorSpec
tests
Now tackling definition of "interesting" property on our Model:
- The general idea is to check that the model and the real state of nodes are consistent with each other
- We start checking the committed utxo are consistent. This requires the ability to observe the current state of the nodes at the point where we check the property -> We store all the nodes' outputs in the
TestHydraNode
The assertion fails, most probably because the state we observe on the actual nodes is not updated because the events have not been processed
- We need to wait for nodes to reach a quiescent state, eg. to drain the event queue. We use
threadDelay
to wait for event queue to be drained which might not be very robust
Assertion still fails:
- We modify the
outputHistory
at the wrong place, when wewaitNext
so we never get what we expect
Modifying the TestHydraNode
machinery to observe the list of ServerOutput
emitted by the node -> tests are green 🎉
We now want to be sure we are actually covering the states and not only the alphabet (the Action
ctors):
-
This can be achieved by implementing the
monitoring
function andtabulate
orlabel
with pair of states. -
We realise the
WorldState
is the same for all nodes, as it represents the consensus that should be reached by hydra nodes through some exchanges of messages and posting of txs. -> refactor it to be a single state -
Write the
monitoring
function to ensure we cover theOpen
state. Why do we get multiple classification of transitions:Hydra.Model implementation respects model +++ OK, passed 1000 tests: 85.7% Idle -> Initial 33.9% Initial -> Initial 7.9% Initial -> Open 9.8% Initial -> Initial 3.1% Initial -> Open 2.0% Initial -> Initial 0.9% Initial -> Open 0.3% Initial -> Open 0.2% Initial -> Initial
-
This is probalby tied to the number of actions generated? Increasing the number of parties and the max. success increases the number of groups of transitions, confirming the hypothesis that the
monitoring
is somehow linked to the length of traces generated.
Problem: The generators are rather blind to the current state, so a lot of actions are discarded and the probability of discarding increases as the length of the trace increases. It's unclear however how the percentages are computed.
AB Solo Programming - #375
Managed to have a running ModelSpec
thanks to Quviq's team help:
- Created a separate repository to not have to depend on plutus-apps, with latest changes from Quviq removing
Typeable
constraint on theStateModel
- Had a remaining issue was the
Typeable m
constraint on theStateModel
instance forWorldState m
Now trying to extend the Model
to handle more transitions (Eg. commits, off-chain transactions, closing...) but I am still running in an issue: the runIOSim
function throws a FailureDeadlock
error which seems somewhat odd to me.
- It deadlocks because code calls
runHydraNode
which is aforever
loop 🤦 - Spawning the nodes using
async
: This is fine because we run inIOSim
so there's no risk of leaking threads - Now hitting a problem with the shrinker failing on
elements
on an empty list which raises an error
Of course, generator also depends on the state but the precondition
needs an Action
to be generated so it cannot apply as is => there's some duplication in the State
to determine when something can be generated and then validated
Adding Abort
and Commit
to the actions, with some "refactoring". The coverage report from the test run still says only Seed
and Init
transitions are generated and tested so there seems to be a problem in the generator or the precondition?
Got a passing test that generates a whole bunch of sequence of actions, now trying to define some interesting properties to check.
- This requires to compare the model's state with the actual state produced by implementation, but how do I get hold of the latter?
- Also better to have distinct constructors for each
Action
as this is displayed in the coverage report and it makes showing transitions more explicit.
Coverage transitions show only Seed
and Init
transitions: Commit
are generated and performed
but they probably produce some kind of error because they are only meaningful when a head is initialised, so we need to wait for the correct state to be observed by the node executing the action, just like we do in the BehaviourSpec
.
- Inlinable changes and renames in plutus scripts do not change script hashes
- While working on refactoring checkCollectCom I wonder: Is there sharing in Plutus semantics? Strictly evaluated it might happen that an expensive fold is done twice if the value produced is used twice in an expression?
Working on refactoring Wallet
module to simplify and make it "synchronous": It would be updated using the same chain following logic than the Direct
module.
- We introduce a handle to replace the need for a connecting directly from the
Wallet
to the node, which allos us to replace the use ofMockServer
in WalletSpec - We simplify the
update
function of theWallet
to remove the need of having a conditional: We always log the application of block, which removes the need for testing specifically it -
Wallet
module is dependent on ledger types but we would like to migrate to use cardano API types - We introduced some machinery to record and check the point at which the
ChainQuery
function is called - Still have
DirectSpec
test failing but we probably don't need it anymore
There might be interesting property tests to write agains the Wallet
reusing the machinery from StateSpec
: Ensure that given a "chain" and a sequence of rollbacks and roll-forwards, its state is consistent with the chain?
The ChainSyncClient
interface is really an Observer of the chain which is implemented by 2 things: The handlers to propagate events to the HeadLogic
and the Wallet
to handle internal UTxO to spend. We could unify the two in a composite Observer which simplifies the implementation ot just propagate rollbacks and rollforwards.
We finally can ditch the MockServer
and teh DirectSpec
test which was the only remaining use of it: It fails and the tested behaviour is covereted by tests in hydra-cluster
.
Converting Model
code I wrote to use quickcheck-dynamic.
- The
Action state a
type as an additional type variable which seems to be used to represent the fact an action produces a value of typea
on top of chaning thestate
, like in this example for a thread registry. - The
perform
function produces a value of typea
which is stored in aVar a
and passed to thenextState
function to update thestate
. This makes the model's actions independent of the details of the values produced - The
StateModel
class requires definition of anActionMonad
which is pinned down for execution ofperform
funcftion - This type cannot be parameterised (it's a type alias, or an associated type family) so it's not possible to say
type ActionMonad GlobalState = forall s . IOSim s
I wish the model and concrete states were separated instead of conflated in the same typeclass. Ideally, we want to assert there is an homomorphism between the state transition function the model defines and the actual state transition of the code, which requires some way of mapping states between the 2 functions?
Got a compilable StateModel
for a Hydra off-chain network:
- The solution to the
ActionMonad
problem is easy: Leave it as a type variablem
, adding the needed constraints. TherunActions
property is parameterised over that monad anyway, so it's a simple matter of running in IOSim. - Need to provide a suitable
Show
instance for theHydraNode
if stored in theWorldState
but there's a way to not store them there but only as the outcome of actions, using theVar
from the first step - Looking at
runActions
makes it clearer what's intended:- A sequence of actions is generated, using the
nextState
as a transition function and passing aVar n
where n represents the step number, incremented at each step - This sequence of action is run by
- checking the
precondition
of the actual state is ok -
perform
the action passing it the variable looked up from the environment (searched by type) - the result is stored in a new environment
- the
postcondition
for the action and the starting state is checked, passing it the result of perform step
- checking the
- A sequence of actions is generated, using the
Submitted a few ideas from previous days' discussions:
Writing mutation to check we properly count the PT when aborting, so that we ensure the head is properly closed with the correct initials and commits
- It's actually quite involved: We need to forge an initial UTxO with the same asset name buut a different policyid to represent the fact we are consuming a UTxO from a different head
- Negative est is not failing, which means we validator rejects the transaction for the wrong reason
- Turns out the problem came from using the wrong redeemer: We use the name
Abort
as a constructor name for bothInitial
,Commit
, andHead
script Possible solution: Add some index usingunstableMakeDataIndexed
We manage to make the test pass, just checking we burn all head tokens, including PT and ST
We could refactor on-chain contracts to not use initial
adn commit
address and only look at the head id but this is mostly straightforward and code shuffling. We choose instead to have the off-chain observeAbortTx
retrieve the Head id so that State
code can reject observation from aborts which are not from our head.
- Ideally, this decision should be propagated upper up, in the Head Logic which would keep the
HeadId
Write a test for checking an abort tx for another head is not observed, but surprisingly it passes!
- It happens that the
OnChainHeadState
tracks a single head implicitly in the UTxO it keeps around, hence it enforces the behaviour we expect but in a not very obvious and intuitive way. - What we would like is for the observations to just observe any Head relevant tx and have an upper layer do the filtering
AB Solo - Model-Based Testing - #375
Need to create a bunch of HydraNode
s from parameters when starting the World
and maintain a mapping from a Party
identifier to the node to act with the proper actor.
- This is already something we do in the
BehaviorSpec
so there are probably things to refactor and share. In particular, thewithHydraNode
is not at all generic in the type of transactions, and the chain implementation so we should probably refactor that.
I would like to untangle the mini-protocols handling from the actual transactions handling from Direct
and Wallet
so that we can implement a "mock" chain handling cardano Tx.
- The
TinyWallet
should probably disappear or be externalised as a basic wallet that's provided out-of-the-box but can be replaced by any other wallet so that users can plug in their own wallets -> this would enable things like External Commits or non-custodial -
TinyWallet
exposes 3 functions:coverFee
,getUTxO
andsign
but actually, onlysign
is useful outside of the wallet. The other 2 are used only to provide some feedback to the user and throw specific exceptions in case the wallet cannot cover fees, this could be completely encapsulated in theTinyWallet
- The
coverFee
function uses a differentUTxO
than the wallet hence why it's needed there: We pass the "headUTxO" which is the UTxO representing the head state machine output, but this could part of thesign
interface.
Creating nodes for running model against requires:
- Connecting nodes through a network, which can be done reasonably easily with channel
- Connecting nodes to a chain, which should implement the whole logic of actually posting transactions and observing them from the chain
- This is important because it is required to exert the on-chain contracts and check global properties of the protocol
- creating queues to put outputs in, and consume them from the model
The Model
needs to represent the behavior of a full network, which means it needs to maintain the observable state of all the involved parties.
- This makes the
interpret
part trickier: The model expresses expected changes according to some inputs
Reading more of Contract testing article, to understand how it deals with the problem of generating sequence of actions: You actually cannot generate a sequence blindly, with each actor emiting actions without taking care of what's happening with other actors.
- Reading https://github.com/input-output-hk/plutus-apps/blob/main/quickcheck-dynamic/src/Test/QuickCheck/DynamicLogic.hs code which contains the core logic for
quickcheck-dynamic
tests - Peeking at Quviq's work on Hydra from a branch in their clone. As expected, they are testing at the level of the contracts interface, not at the level of the Hydra nodes which would be probalby more interesting to us
We should be able to deduce the expected "quiescent" state from the client's input only, shouldn't we? Up to some possible interleaving of actions and assuming we always wait for events to propagate fully, we should be able to know what the resulting state is.
Extracted PR #376 as a baby step towards being able to run a full (off- + on-chain) model of Hydra Head.
Start implementing a model for the Hydra protocol, taking into both on- and off-chain behaviour, following https://plutus-apps.readthedocs.io/en/latest/plutus/tutorials/contract-models.html#
- Worked on a 'Model' module tying together various parts of the system. The problem, as always, is how to not duplicate what's already implemented, eg. how to work at a level which is abstract enough that we can interpret the actions differently, in a simpler model, than the actual code.
- It's better to model the system abstractly, somewhat like the
HeadLogic
does, and maintain a translation layer that's just concerend with mappingParty
to cardano keys and handle the details of interacting with the chain - What would be nice is to be able to decouple the construction and observation of transactions in our code, located in the
Tx
andState
modules, from theDirect
module, eg. to be able to emulate the chain using an internal ledger rather than the actual chain. This would make it possible to run our tests in theIOSim
monad which would make them way faster.
Modelling the Hydra Head makes more sense at the toplevel, through the Node
's API. If we want to do this efficiently, eg. so that we can run the model in IOSim
we would need to emulate both the network layer and the chain layer.
- The network layer can be easily modelled using something similar to what's been implemented for hydra-sim a while ago: A set of channels connected to a central dispatcher, as it's also implemented in the
BehaviorSpec
- The chain layer require a bit of work because we want to ensure we exercise the actual transactions and contracts, which means we want to use most of the
Hydra.Chain.Direct
components but without the networking and real interaction with a node part, which is not very complicated but requires a bit of work esp. as the coupling with networking is quite intricate in those components
Some interesting feedback from Pi@SundaeSwap:
- Things they wish they had (when integrating with cardano-node, somewhat also applicable to Hydra):
- Dry-run transaction submission with comprehensive errors;
- Better error messages / pretty-printer for errors;
- More comprehensive health metrics;
- Ability to inspect the mempool / pending transactions;
- Ways to build transaction more easily, especially in the presence of native assets;
- More comprehensive specifications of intermediate representations used by cardano-cli (e.g. unsigned transactions);
- Have tools work 'offline' when they can, and not require a connection to a running node for every single command (in cardano-cli, this is mostly due to the need to request protocol parameters for various commands, but parameters could be user-provided on the cli instead);
And for Hydra more specifically:
- Gives way to query L2 protocol parameters from the hydra-node, mostly for off-chain code to use and not worry about carrying around configuration files.
- Add a ReadyToFanout server output
- Roundtrip time of tui spec not good, so diving down two layers -> BehaviorSpec
- Extending the existing fanout behavior test should be enough
- Could fix the HUnit failure not correctly rendered from within shouldRunInSim along the way
- Printing the traces in shouldRunInSim was actually the problematic bit, we need to catch exceptions when trying to process the trace events!
- When adding a
Fanout
client input I realize there is even aContest
client input which we are not handling that right now! - After only adding the client input all tests pass.. which was slightly surprising, but I guess having a property enumerating correct handling of all inputs in the right state is probably a bit too hard to do (right now).
- Let's start outside in by updating the TUISpec
- While it is feels weird to just wait and issue a command in the tui (without visual feedback), it becomes quite obvious that we need an additional
HeadState
when it comes to handling theFanout
client input. How would theHeadLogic
know otherwise when to handle / not to handle a fanout? As I'm typing this out, we could just route it to the chain layer and return an error if too early. Let's start with that. - After hacking the TUISpec to expect the right thing at the right time, it lacks granularity to express that fanout is only done by clients and not the head logic. Dive down to BehaviorSpec level
- Adding a test that fanout does not happen automatically feels weird, but is somewhat possible by waiting for 1M secs in io-sim
- When doing some business in BehaviorSpec I realized
waitFor
was misleading as it was only waiting for a second. I ended up removing it and keeping thewaitUntil
functions with veryLong (TM) timeouts to still get nice error locations. - Why are we using multiples of clusterSize in waiting on the bench?
-
Updating cardano-node + deps to vasil testnet tagged version. Goal: connect to and open/close heads on the vasil testnet
-
Only some minor additional changes to
debugPlutus
and thehydra-node
compiles. Let's try to connect to the testnet. If it fails somehow, probably worthwile to update test suites and check them first. -
Using setup instructions from Sam / environments from new cardano-world repo: https://github.com/input-output-hk/cardano-world/tree/master/docs/environments/vasil-qa
-
When usign the 'rev = vasil-testnet-v1' for the cardano-node in our shell.nix, I get a too old cardano-node which cannot parse the PlutusV2 cost model?
- Using the commit SHA as rev it works.. weird.
-
Need the
hydra-tui
to do evaluation of vasil-testnet. So let's see how we can get that compiled.- Making it not depend on
hydra-cluster
by factoringCardanoClient
: https://github.com/input-output-hk/hydra-poc/pull/360
- Making it not depend on
-
Updating
hydra-test-utils
and other test packages on the side between meetings. We are still directly using cardano-ledger in hydra-test-utils!- When updating
Hydra.Chain.Direct.Fixture
I merged many things with Hydra.Ledger.Cardano.Evaluate defaults - Generating blocks at some given SlotNo is a bit tricky as we now use Praos, not TPraos
- When updating
-
Setting up some instructions on how to connect to the testnet and focus on the main goal again
-
Initializing a Head works on vasil testnet! 🎉
- Init tx id: 65b8d0a9a325e8e54c5dea0f9b4a26dacb429959290f6d2914fb824f2db8e8a1
-
Commit/abort fails with
NonOutputSupplimentaryDatums
-> Maybe because of inline datums vs. datum witnesses?- Suspect: Including datum witnesses when the input has an inline datum is not liked by the ledger.
- After reverting back to
TxOutDatumInTx
commit and open works 🎉
-
After succesfully opening a head on vasil-testnet, I realized the protocol parameters include fees and our TUI is not accounting for them. So I updated the protocol parameters to 0 fees. When starting the hydra-node again, I see closed/fanout transactions being observed & tried? I did never close the head (maybe accidentially because of Ctrl-c / not hitting Ctrl correctly?). Anyway. Now the hydra-node sees a closed head.
-
They hydra node is now trying to post a fanout tx, which fails to validate because of
fannedOutUtxoHash /= closedUtxoHash
. -
On a side note: when replaying the chain, the hydra-node tries to post a collect and errors with
InvalidStateToPost {txTried = CollectComTx..
-
The hydra-tui then also fails to decode the error message because some
$.postTxError.tx.witnesses.scripts.f2dc3a3b50082d1fb34e250be1bf93bb684aa552a956b8e16e33a9aa: DecoderErrorDeserialiseFailure \"Script\" (DeserialiseFailure 0 \"expected list len or indef\")
.. seems to be coming from "inside" the failed tx decoding- Indeed the scripts field looks odd, it's an identical key/value object
"scripts": { "f2dc3a3b50082d1fb34e250be1bf93bb684aa552a956b8e16e33a9aa": "f2dc3a3b50082d1fb34e250be1bf93bb684aa552a956b8e16e33a9aa", "f48c11e7b724932eccc5685b16ee1181bae3030e0bb9b4e820ce1e1c": "f48c11e7b724932eccc5685b16ee1181bae3030e0bb9b4e820ce1e1c" }
- This is likely our json instance. Need to look into this. Probably good point to get
babbage-preview
back into green test territory first.
AB Solo - #359
Trying to get back on track with this time tracking in the HeadLogic, seems like we left a failing test for conversion property and few other issues in the code
- Added some coverage measurement to tests for contestation period, struggling to get the tests to terminate when adding
checkCoverage
. Even lowering the Confidence values does not help
Got an error in this test: Seems like we don't correctly extract the time?
- The test actually fails for a good reason! We have a "fake"
remainingContestationPeriod
value set to 0 when observing the close tx. - We have a special case for observing the
OnCloseTx
whereby we always set theremainingContestationPeriod
to 0 because we would need to have current time to compute it, and this done in the calling code. Adjusted the relevantStateSpec
test to deal with that special case too.
ETE tests are still failing, we fail to observe the fanout
tx in time, probably because it's rejected
Replacing the use of DiffTime
with NominalDiffTime
everywhere: According to the time package docs DiffTime
is less commonly needed than NominalDiffTime
.
When one computes the difference of 2 UTCTime one gets a NominalDiffTime
and not a DiffTime
. The latter requires handling of leap seconds which requires some table...
Got a failure in DirectSpec
which stems from the fact I changed the conversion to/from ContestationPeriod
and there wasn't any test checking that! -> add a property to ensure we can roundtrip
tracing observed TX in DirectChainSpec
gives me this :
Observation (OnCloseTx {snapshotNumber = 1, remainingContestationPeriod = -1653315436482929.887860059s})
Looks like computation of remainingContestationPeriod is wrong... 🤦
There was no less than 2 more errors:
- In the
posixToUTCTime
function, I multiplied by 1000 instead of dividing by 1000 - in
Direct
I subtracted in the wrong direction to compute the remaining time
ETE tests are still failing, the Fanout tx fails to be posted because it's lower bound is too low -> adding some buffer to the remainingContestationPeriod
and tweaking wait time in test to account for close grace period
Now all green 🎉
- *date: 2022-05-16 → 2022-05-19
- *event link: https://iogmeetups2022.co.uk/
Several representatives from auditing companies: Runtime Verification, FYEO, Certik, Tweag. Key takeaways:
- Runtime verification also offers audits of projects' tokenomics and layer 2 protocol designs.
- FYEO seemed keen on agile, with auditors embedded as part of the development team. Seems like a good fit for teams that needs guidance.
- Tweag presented two of their open-source tools: cooked-validators and pirouette.
- Certik provides Haskell-to-Coq (somewhat automated) translations and then works on proofs of parts of the code through Coq.
Side presentation from another Tweag guy working on a separate project called Hachi which aims to provide some automated contract vulnerabilities discovery. Seemed somewhat similar to snyk.
Both ADAX and SundaeSwap told their stories about being audited. Both felt value and were positive about the process. ADAX mentioned they'd love to see more IDE support and automated testing tools in the Haskell/Plutus ecosystem. SundaeSwap stressed that getting an audit early on in the process was a tremendous help/success factor to them.
Quviq presented property testing and in particular contract-model testing. They showcased their latest tools which are built on top of the Plutus emulator. The presentation was more or less a live presentation of a guide they've recently published. Unclear whether the underlying tools are already available or still under development. They also mentioned how they have crafted a few generic properties that apply to most contracts (e.g. 'no funds remain locked') and how they now have tooling around to test that more or less automatically for any project. Overall, QuviQ's approach helps get coverage of the visible API surface of a contract but does not help much when it comes to pen-testing adversarial behaviours.
Had a few good chats with various folks, but in particular:
-
With Max (Quviq) regarding contract model-testing and the work they're doing for Hydra. Hydra is indeed not using the PAB, and thus not relying on the contract monad & simulator so we've been working with them to adapt their current tooling to work also with setups like Hydra. Seemed like they were pretty close to being able to tie the knot and show us something.
-
With Duncan (IOG), discussed input endorsers and how the way the design is headed should not much impact existing client applications which could still digest the chain by looking at the consensus block only. Possibly, existing interfaces (i.e. chain-sync mini-protocol) could be adapted in a way that they abstract away the indirections between transaction blocks and consensus blocks coming with Ouroboros Leios / input endorsers. We also discussed the pros and cons of the Cardano-api vs the ledger api.
-
With Pi (SundaeSwap) about ideas for Hydra and how SundaeSwap could work on Hydra with a few additions/modifications. Namely, the ideas of "fantom tokens" and "constrained hydra heads". We also discussed an idea of a "mainnet on-demand short-lived simulations" as a potential tool/project to help people build trust from interacting with dapps. The idea would be to have a node which replicates the mainnet traffic but also allows the execution of arbitrary transactions on that parallel network as if they were executed on the mainnet. So, a kind of testnet but fueled with actual mainnet traffic.
ETE test failure on fanout with contestation deadline. There's clearly something wrong in the way we compute time from slot, two different slots give the same POSIXTime
:
fromPostChainTx: (SlotNo 216,POSIXTime {getPOSIXTime = 1000000000000})
fromPostChainTx: (SlotNo 222,POSIXTime {getPOSIXTime = 1000000000000})
Tracing the execution of queryTimeHandle
to understand on which basis the time is derived from slotNo
. Perhaps there is a miscalculation on-chain when translating slot to POSIXTime?
Problem is that:
- we pass a wrong time to the close function
- thus the contestation dealine is wrong and too much in the future
- when the fanout tx is checked, the validity lower bound is always below contestation deadline.
The contestation deadline is definitely wrong:
head state: Closed {parties = ["gZ\EM\"[\206Y\RSN-{\195\241m\192\242\161\132*\142\221\v\172\DC1\208{\149\243q\245u\188","W\169\255\&2yF\210J\SYN\157\206\168\194\194=\243l\215q*\247\248$\US\150h\"\229\234\186\&4\f","2\132L\DEL\222\132\151Z\226\128\CAN\235\244}`\250\229}\165Y:\244*cy\SYN\171\&1$y\142\201"], snapshotNumber = 1, utxoHash = "\ACK\192@\DC1#\141\249'}{\160\194;\246\146\170n\254\134\155\163\175\204\160~gP\131\133\139f\227", contestationDeadline = POSIXTime {getPOSIXTime = 1000000010000}}
So it seems the internal relative time computation from EpochInfo
is correct:
rel time: RelativeTime 19.5s, slotno: (SlotNo 195,POSIXTime {getPOSIXTime = 1000000000000})
and computing directly epochInfoSlotToUTCTime
works fine.
translateTimeForPlutusScripts
checks the version is greater than 5 but our protocolVersion
in cardano-node.json
is (0,0)
-> Changing to protocol version 6 shows time is changing
- Posting FanoutTx still fails -> Are we waiting long enough before trying to post it?
- Doubling the wait time in the
HeadLogic
makes the test pass 🎉
We are "racing" against the chain, so we should probably find a way to observe the passing of time from the chain to head logic.
- We should request from Plutus team to include
SlotNo
as validity boudns in theScriptContext
-> All computations would be done in slots thus removing the need to translate back and forth - If we observe time in the
HeadLogic
it should be expressed inDiffTime
rather than abstract slots, adding like aTick DiffTime
toOnChainEvent
?
We need to keep the wait time before posting the fanout tx in sync with the gracePeriod
from Direct
which shifts the start of the deadline by 100 slots, but time is handled using DiffTime
in HeadLogic
hence this is really dependent on the slotLength
parameter of the underlying chain.
It's hard to define grace periods that work for chains with different slotLength
, but changing our slotLength
in tests to 1 to be consistent with testnet and mainnet makes it very slow and does not really fix the problem.
ETE test passes with some fixed constant time added to wait but we should discuss a better solution.
Trying to implement a Contest
mutation that requires checking the Tx validity interval against the contestationDeadline
.
- Problem is to generate a validity upper bound in slots that is guaranteed to be greater than the contestationDeadline in milliseconds.
- The way we compute contestationDeadline on-chain for verification purpose is incorrect. How do I write a failing test?
Completed work on Contest transactions and handling of contestation deadline for Close
and Contest
logic -> #356.
Now tackling #357: The Fanout should only be posted and valid after the contestation deadline.
Added a lower bound on Fanout
tx validity, but the problem is that now it fails to be posted in our ETE tests because we don't wait enough -> increasing the wait time in ETE tests.
- But even if we wait enough, we'll need to handle resubmission of the tx because we are adjusting the lower bound by some factor.
- Perhaps we can get away with it right now by removing grace period from the fanout transaction and wait long enough?
ETE tests are now failing because of fanout tx being posted too soon:
uncaught exception: PostTxError
CannotCoverFees {walletUTxO = fromList [(TxIn "d048d456e9635308eb97d9ae719e2c279f4931963b4d6088506317524ffa3a46" (TxIx 1),TxOut (AddressInEra (ShelleyAddressInEra ShelleyBasedEraAlonzo) (ShelleyAddress Testnet (KeyHashObj (KeyHash "f8a68cd18e59a6ace848155a0e967af64f4d00cf8acee8adc95a6b0d")) StakeRefNull)) (TxOutValue MultiAssetInAlonzoEra (valueFromList [(AdaAssetId,80500000)])) (TxOutDatumHash ScriptDataInAlonzoEra "a654fb60d21c1fed48db2c320aa6df9737ec0204c0ba53b9b94a09fb40e757f3"))], headUTxO = fromList [(TxIn "d048d456e9635308eb97d9ae719e2c279f4931963b4d6088506317524ffa3a46" (TxIx 0),TxOut (AddressInEra (ShelleyAddressInEra ShelleyBasedEraAlonzo) (ShelleyAddress Testnet (ScriptHashObj (ScriptHash "5703a5ce6ba53c760b93e0d90816f8cb5b41e23d8da2d2a3c8a7a257")) StakeRefNull)) (TxOutValue MultiAssetInAlonzoEra (valueFromList [(AdaAssetId,5000000),(AssetId "417dcfd87cbcc10b0f2dceac9ec6e54f1f36cc28b3845c5c17491b99" "HydraHeadV1",1),(AssetId "417dcfd87cbcc10b0f2dceac9ec6e54f1f36cc28b3845c5c17491b99" "\248\166\140\209\142Y\166\172\232H\NAKZ\SO\150z\246OM\NUL\207\138\206\232\173\201Zk\r",1)])) (TxOutDatumHash ScriptDataInAlonzoEra "61a28685e734137a0d156dc1093de8613bc822d6a60c2edaa8378fe302460b06"))], reason = "ErrScriptExecutionFailed (RdmrPtr Spend 0,ValidationFailedV1 (CekError An error has occurred: User error:\nThe provided Plutus code called 'error'.) [\"lower bound validity before contestation deadline\",\"PT5\"])", tx = ShelleyTx ShelleyBasedEraAlonzo (ValidatedTx {body = TxBodyConstr TxBodyRaw {_inputs = fromList [TxInCompact (TxId {_unTxId = SafeHash "d048d456e9635308eb97d9ae719e2c279f4931963b4d6088506317524ffa3a46"}) 0], _collateral = fromList [], _outputs = StrictSeq {fromStrict = fromList [(Addr Testnet (KeyHashObj (KeyHash "f8a68cd18e59a6ace848155a0e967af64f4d00cf8acee8adc95a6b0d")) StakeRefNull,Value 1000000 (fromList []),SNothing)]}, _certs = StrictSeq {fromStrict = fromList []}, _wdrls = Wdrl {unWdrl = fromList []}, _txfee = Coin 0, _vldt = ValidityInterval {invalidBefore = SJust (SlotNo 51), invalidHereafter = SNothing}, _update = SNothing, _reqSignerHashes = fromList [], _mint = Value 0 (fromList [(PolicyID {policyID = ScriptHash "417dcfd87cbcc10b0f2dceac9ec6e54f1f36cc28b3845c5c17491b99"},fromList [("HydraHeadV1",-1),("\248\166\140\209\142Y\166\172\232H\NAKZ\SO\150z\246OM\NUL\207\138\206\232\173\201Zk\r",-1)])]), _scriptIntegrityHash = SJust (SafeHash "bc91a43e1495688a05260b969357c57d11b6f83a082b451e901f107840c0ad9e"), _adHash = SNothing, _txnetworkid = SNothing}, wits = TxWitnessRaw {_txwitsVKey = fromList [], _txwitsBoot = fromList [], _txscripts = fromList [(ScriptHash "417dcfd87cbcc10b0f2dceac9ec6e54f1f36cc28b3845c5c17491b99",PlutusScript PlutusV1 ScriptHash "417dcfd87cbcc10b0f2dceac9ec6e54f1f36cc28b3845c5c17491b99"),(ScriptHash "5703a5ce6ba53c760b93e0d90816f8cb5b41e23d8da2d2a3c8a7a257",PlutusScript PlutusV1 ScriptHash "5703a5ce6ba53c760b93e0d90816f8cb5b41e23d8da2d2a3c8a7a257")], _txdats = TxDatsRaw (fromList [(SafeHash "61a28685e734137a0d156dc1093de8613bc822d6a60c2edaa8378fe302460b06",DataConstr Constr 2 [List [B "8\b\142L*\232/\\E\198\128\138a\166I\r<a,\225\218#W\DC4Fo\199H\251\196\203\187"],I 1,B ",\"\220s\159\251H\249\221M\174\249l\NUL\166\182j\186\144\195\&7F\133\&4Fm9\245HrX\192",I 1000000100000])]), _txrdmrs = RedeemersRaw (fromList [(RdmrPtr Spend 0,(DataConstr Constr 4 [I 1],WrapExUnits {unWrapExUnits = ExUnits' {exUnitsMem' = 0, exUnitsSteps' = 0}})),(RdmrPtr Mint 0,(DataConstr Constr 1 [],WrapExUnits {unWrapExUnits = ExUnits' {exUnitsMem' = 0, exUnitsSteps' = 0}}))])}, isValid = IsValid True, auxiliaryData = SNothing})}
Trying to augment the time we wait within the HeadLogic
and increase accordingly the time waiting in the ETE test. The node's log ends exactly when the direct chain tries to post them, or rather when the Direct
chain component enqueues the transaction for posting, which seems odd, as if the nodes were blocked there:
{"message":{"tag":"Node","node":{"tag":"ProcessingEvent","event":{"tag":"ShouldPostFanout"},"by":{"vkey":"675a19225bce591e4e2d7bc3f16dc0f2a1842a8edd0bac11d07b95f371f575bc"}}},"threadId":35,"timestamp":"2022-05-18T16:31:38.815438965Z","namespace":"HydraNode-3"}
{"message":{"tag":"Node","node":{"tag":"ProcessingEffect","effect":{"tag":"OnChainEffect","onChainTx":{"tag":"FanoutTx","utxo":{"d93a887ada43ba4f5ddc39ccbcdda95ed8e2558e0b960edb87190f8e7a7190bd#1":{"address":"addr_test1vzfjrvg8w3wcqsr0s7t9xu9csz9t9g520yfugkwl8lyh2ys2pjz8a","value":{"lovelace":5000000}},"56602b5268991a51c16d93e3a3f23e8288b07f19c19c63e57189d39156609956#0":{"address":"addr_test1vzfjrvg8w3wcqsr0s7t9xu9csz9t9g520yfugkwl8lyh2ys2pjz8a","value":{"lovelace":1000000}},"56602b5268991a51c16d93e3a3f23e8288b07f19c19c63e57189d39156609956#1":{"address":"addr_test1vpemzng7e5nvp2ynwpstydvkdrsevmhwtswxa8zt0dda2rcrwkrvp","value":{"lovelace":19000000}}}}},"by":{"vkey":"675a19225bce591e4e2d7bc3f16dc0f2a1842a8edd0bac11d07b95f371f575bc"}}},"threadId":35,"timestamp":"2022-05-18T16:31:38.815444838Z","namespace":"HydraNode-3"}
{"message":{"tag":"DirectChain","directChain":{"tag":"ToPost","toPost":{"tag":"FanoutTx","utxo":{"d93a887ada43ba4f5ddc39ccbcdda95ed8e2558e0b960edb87190f8e7a7190bd#1":{"address":"addr_test1vzfjrvg8w3wcqsr0s7t9xu9csz9t9g520yfugkwl8lyh2ys2pjz8a","value":{"lovelace":5000000}},"56602b5268991a51c16d93e3a3f23e8288b07f19c19c63e57189d39156609956#0":{"address":"addr_test1vzfjrvg8w3wcqsr0s7t9xu9csz9t9g520yfugkwl8lyh2ys2pjz8a","value":{"lovelace":1000000}},"56602b5268991a51c16d93e3a3f23e8288b07f19c19c63e57189d39156609956#1":{"address":"addr_test1vpemzng7e5nvp2ynwpstydvkdrsevmhwtswxa8zt0dda2rcrwkrvp","value":{"lovelace":19000000}}}}}},"threadId":35,"timestamp":"2022-05-18T16:31:38.815447104Z","namespace":"HydraNode-3"}
Yet, the transactions from each node are correctly constructed, however the time we compute does not make sense:
fromPostChainTx: (SlotNo 726,POSIXTime {getPOSIXTime = 1000000000000}), tx: FanoutTx {utxo = fromList [(TxIn "14d3799365e384ed57cac9392521c0c78f8c62195c707ddbdb9236582556d5f2" (TxIx 1),TxOut (AddressInEra (ShelleyAddressInEra ShelleyBasedEraAlonzo) (ShelleyAddress Testnet (KeyHashObj (KeyHash "764c16ddcf7f559225399184098ee132c33eca4c80d1def5fe0beb23")) StakeRefNull)) (TxOutValue MultiAssetInAlonzoEra (valueFromList [(AdaAssetId,5000000)])) TxOutDatumNone),(TxIn "7c655025efb8e089b14672a7085594c91e3cb56b3b549fd798bcc1c8ee60dfb8" (TxIx 0),TxOut (AddressInEra (ShelleyAddressInEra ShelleyBasedEraAlonzo) (ShelleyAddress Testnet (KeyHashObj (KeyHash "764c16ddcf7f559225399184098ee132c33eca4c80d1def5fe0beb23")) StakeRefNull)) (TxOutValue MultiAssetInAlonzoEra (valueFromList [(AdaAssetId,1000000)])) TxOutDatumNone),(TxIn "7c655025efb8e089b14672a7085594c91e3cb56b3b549fd798bcc1c8ee60dfb8" (TxIx 1),TxOut (AddressInEra (ShelleyAddressInEra ShelleyBasedEraAlonzo) (ShelleyAddress Testnet (KeyHashObj (KeyHash "f27ab2a3f2ac48c0727c4fc2982e41caedbc27d5356427d7b86cfb50")) StakeRefNull)) (TxOutValue MultiAssetInAlonzoEra (valueFromList [(AdaAssetId,19000000)])) TxOutDatumNone)]}
Running hoogle server --local
turns all file://
links into http://
links which makes it straightforward to browse remotely!
Working on contestation period:
- replace
closedAt
state field with acontestationDeadline
field taking into account the contestation period - check Contest transaction respects contestation period
Migrating code from State
to stop using tuples for storing on-chain state information and start using proper records
- Annoyingly it's not possible to have GADT fields with same name because the constructores have different types
Passed contestationPeriod
to Open
state on-chain, and also stored it in the ThreadOutputXXX
so that it's available all along the chain of transactions as we need it to check the contest transactions' time against the deadline
Introduce a MutateCloseContestationDeadline
mutation for the Close transactions to ensure the computation is done correctly, but it "fails" to kill the mutant
ContestationPeriod
is an integer representing a number of picoseconds, which can be translated to a DiffTime
. We probably don't need that resolution level and should stick to just a number of seconds -> Change to use Plutus' DiffTimeMillis
.
- I had an interesting meeting with David Arnold about his proposed approach to DevOps based on nix
- He showed us what he've done, a quick and dirty experiment, to pack a cardano node into the proposed 4 layers:
- source & binary packages, expressed using nix dependencies down to the lowest possible level (eg. glibc)
- entrypoints defining how to run the software, what configuration is needed, what runtime dependencies are required
- OCI image packaging which relies on entrypoints to produce a "runnable" thing
- scheduler which describes the execution infrastructure
- We plan to schedule a working session to explore how this approach could apply to Mithril
- After switching to inline datums, I forgot to update the minting policy & validators
- After upgrading initial validator to PlutusV2 +
serialiseData
, I also needed to update the off-chain code creating theSerializedTxOut
. Important here is to use thePlutus.TxOut
type and its correct binary format. - Off-chain code for
observeCommitTx
is now failing because it can't deserialize theSerializedTxOut
. This now needs afromPlutusTxOut
and we could use some better functions on the usage side of things (tighten up the assumptions aroundSerializedTxOut
). - Due to the lack of a
FromCBOR Plutus.Data
instance, I switched toSerialise Plutus.Data
which goes bothways- Where was the
ToCBOR Plutus.Data
instance coming from?
- Where was the
-
hashPreserializedCommits
seems not to do the right thing now.. off-chain code does not produce the expected/same hash in collectcom. - Implementing
fromPlutusTxOut
is a bit a churn.. not sure if I should do it now. The current version is good enough for tx-cost evaluation and we only need to fix this when we want to properly observe commits off-chain.- Nope.. we need it to construct abort transactions :/
- I realize we can (and should) define only a single
hashTxOuts
function to be used off- and on-chain. - When implementing
fromPlutusAddress
I realize that theNetworkId
is lost from Ledger -> Plutus and we would have no way to retrieve it back. In the past we have been serializing to cardano-api/ledger format, which includes these and observing that back.
- Finish basic
checkContest
- Handle contestation period -> on-chain
- Also handling of failure propagation from Direct
- Need to handle the case of ContestTx failing and resubmission
- BehaviorSpec does not model tx failure -> Add logic to simulated chain? or craft scenario by hand
- Ask researcher about contesting only once => probably not a security risk as we guarantee monotically increasing snapshot number
Reviewing #349 to check we have covered all basic cases for contest contract:
- Notice there's duplication in the
Close
mutations probably causaed by wrongly resolved merge conflicts - Most mutations are not interesting, the only one we are interested in is the
MutateParties
Contestation period #351
- Store the time/slot at which close happens on-chain in the datum + add it as time interval
- when constructing the tx, read the current slot + add a buffer as upper bound for validity
- bounds are checked at level 1 validation + contract validation to check consistency of datum
- Contest tx needs to be within closed datum slot + contestation period
- Fanout tx needs to be after closed datum slot
We introduce mutations for changing the validity range of the transaction which leads to some changes in the hydra-cardano-api
:
- Trying to use
TxBody
pattern to easily deconstruct the validity interval but we realise it's unidirectional! => need to work with ledger API - We realise we need to handle
POSIXTime
-based interval on-chain but it seems plutus V1's time management is broken
We add closedAt
field to the Closed
on-chain state in order to record the moment in time the closed happen, to be able to check the contestation period
- Fixing code everywhere following introduction of
closedAt
in the on-chain state - The function to construct a
POSIXTime
is somewhat involved but we should have everything available in theDirect
component.
- Error calls in
makeShelleyTransactionBody
for BabbageEra.- Can try to fill in the gaps with what we get from cardano-ledger.
- Seems like jordan has done so upstream already, nice.
- None of the
tx-cost
transaction validates.. let's start debugging withinitTx
.- The minting script of initTx fails with:
(ValidationFailedV2 (CekError An error has occurred: User error:
The provided Plutus code called 'error'.
Caused by: (force headList [])) [])
- This seems to be a pretty print of an
ErrorWithCause
-
force headList []
may be plutus-core forhead []
- Trying to work it backwards with a
const True
minting policy - Same error, this suggests something is wrong in the invocation of the plutus code and not "in our code"
- not invoking
wrapMonetaryPolicy
and define our script to beconst ()
-> works! - Using explicit
fromBuiltinData
to debug decoding errors (this is a trap.. see further below)
- Yeah.. forgot to switch to PlutusV2 ScriptContext.. this error was not very descriptive :/
- Added some traces when converting to untyped validators / minting policies (this is a trap, neatly implemented .. see further below)
- Get a somewhat realistic limit of initTx now. Need to backport the full evaluation of initTx (incl. execution budgets) + all redeemers (54e9e007a1673a70bf8cbb698bce3f9b7834de9e)
- Next: cost of commit fails when generating the starting state in
unsafeObserveTx
- Likely
observeInitTx
just returned Nothing, let's trace it - Seems like cardano-api reports only a TxOutDatumHash and not a TxOutDatumInTx in observeInitTx.. even though we do create the transaction with TxOutDatumInTx
- This seems to be the culprit: https://github.com/input-output-hk/cardano-node/blob/a1e947e6e281f1b3739d34c74356f1b93ecc1d50/cardano-api/src/Cardano/Api/TxBody.hs#L2241-L2252 -> TxOutDatums are not resolved in babbage era?
- Seeing the code it might just work if we pivot to using inline datums. I also asked the node-api team about it, but let's try..
- Seems like
ReferenceTxInsScriptsInlineDatumsInBabbageEra
is also not re-exported properly - After using
TxOutDatumInline
, observation seems to work again
- Seems like
- Likely
- Initial and Commit validators need migration to V2, let's try to validate a close transaction first
- Getting closer.. now
serialiseData
seems to be "forbidden". Maybe missing a cost model?
ValidationFailedV2 (CodecError (DeserialiseFailure 8861 "BadEncoding (0x00000042000452ad,S {currPtr = 0x000000420004309c, usedBits = 6}) \"Forbidden builtin function: (builtin serialiseData)\"")) []))]
- serialiseData is the only builtin requiring protocol version 6 -> let's update it in evaluateTx / our fixtures
- Working to get fanout validate in tx-cost:
hashTxOuts
is not consistent off-/on-chain.- Debugging by vendoring the
prop_consistentOnAndOffChainHashOfTxOuts
into tx-cost
- Debugging by vendoring the
- After aligning the hashTxOuts it turns out that the "new" fanout transaction is WAY more expensive. Revert only encoding to non-serialiseData to double check
- Okay.. the cost is as high with plutus-cbor, verified by using
serialiseTxOuts
instad ofserialiseData . toBuiltinData
- For some reason
fromBuiltinData
is much more expensive thanunsafeFromBuiltinData
in wrapValidator -> how expensive isunsafeFromBuiltinData
then? We maybe should use partly "from-data-decoded" script contexts?
- Okay.. the cost is as high with plutus-cbor, verified by using
- When fixing arbitrary instances, I realize there are hedgehog generators in
cardano-api
which we could start using?- One note though: these generators are fixed range, e.g. 0-10 assets in a txout
- No
cardano-ledger
generators for "traces" of transactions, only arbitrary ones from serialization- Need to disable
genFixedSizeSequenceOfValidTransactions
for now -> it's a spike after all
- Need to disable
- The fact that
Hydra.Chain.Direct.Wallet
usescardano-ledger
types directly is a major PITA - Predicate failure wrapping is getting out of hand with babbage "inheriting" alonzo errors
- it's now a pattern match on
UtxowFailure (FromAlonzoUtxowFail (WrappedShelleyEraFailure (UtxoFailure (FromAlonzoUtxoFail (UtxosFailure (ValidationTagMismatch _ (FailedUnexpectedly (PlutusFailure plutusFailure debug :| _))))))))
- it's now a pattern match on
- Some last remaining consensus wrangling in
Hydra.Chain.Direct
.. now the tx-cost should compile. - After fixing everything it seems like
cardano-api
is not ready yet to handle babbage scripts -> I get this error whencabal run tx-cost
:cabal run tx-cost Up to date tx-cost: TODO: Babbage scripts - depends on consensus exposing a babbage era CallStack (from HasCallStack): error, called at src/Cardano/Api/TxBody.hs:3300:10 in cardano-api-1.33.0-6a6e2f7c2ab86f979fe10873d25f3d5dd6b48d29a51d0a0c31aaa30d1adbdd8c:Cardano.Api.TxBody
What's to do on contestation:
- Contract code is
const True
- Ensuring a party can only contest once?
- contested snapshot number should be monotically increasing
- We don't "handle" the contestation period
- Burning PTs is optional from a user perspective
- What to do in case of observing a
OnContestTx
off-chain?- We should be able to contest on observing a contest
- Unifying Close and Contest
Started working on Contest
transition on-chain validator, scaffolding mutation testing framework
We realise we need the parties
in the on-chain Closed
state in order to verify the signatures, which means we need to add them when creating the CloseTx
and the contest tx
Interesting idea for improving our mutation:
- Use the error traces from plutus evaluation as labels for failures -> this would pinpoint the actual errors produced and would help troubleshooting the (common) case of the mutation that passes the test nbut shouldn't
- We could get coverage of code by enumerating all the errors possible in the on-chain code into a property ADT, then use that as coverage check for the mutations.
Our mutation fails on signatures verification which means we sign with wrong signatures/snapshot number.
-
Contest
code is unclear about what is starting state, what's input and what's the expected end state -> We need a better way to express those in the tests
Finally got a red test for NonNewerSnapshot
mutation, implementing it is straightforward.
We finish the session on a failing test for StateSpec
because our snapshot generators are now too broad given the contract.
Working on issue with computing close utxo hash leading to contract error when comparing initial utxo hash with snapshot hash. Turns out the problem comes from the way we create the hash:
- In the
collectCom
, we create the hash by first sorting on the input references of the commit transactions' outputs then calling thehashPreserialisedTxOut
function from on-chain code - In the close, we use the
hashTxOuts
function which does not explicitly sort, only use theMap.toList
function which sorts the elements according to their keys which are input references of the committed UTxO
We solve the issue by replicating the ordering, absed on the serialised TxOut form, both on-chain and off-chain, resting on the equivalence tests we have in place, but this leads to another issue wtih State/CollectCom
test failing.
IT fails because, once again, of the ordering of the serialised TxOut:
- In the
checkCollectCom
on-chain function, we rely on the ordering of the serialised TxOut as they appear in the inputs to theCollectCom
transaction, which depends on the TxIn ordering of this transaction - When we compute off-chain the hash, we (now) order the list of serialised TxOut according to their natural ordering (eg. as bytestrings)
Goal: Reduce the number of places we compute UTxO hash and make sure the ordering is always consisten on- and off-chain.
We introduce a type for use by closeTx
to represent the 2 different type of snapshots we want to close:
- either an
InitialSnapshot
containing just a hash - or a confirmed snapshot with a number, a UTxO and a signature
We extract the utxoHash from the head datum when we observe the CollectCom
transaction and store it in the state to be used in the case of Initial snapshot cloes
We fix the issue by observing the utxoHash from the CollectCom
transaction so that, should we close with initial snapshot, we don't recompute the hash in the close tx but only passes it from the OnChainHeadState
The fanout tx tests are now failing because one again of the ordering: When we construct the outputs of the fanout transaction, we need to have consistent order of TxOut -> We revert the explicit sorting we did earlier for solving the Close
issue
We are back to green 🎉
Now tackling the issue of standardising the handling of confirmed snapshot in the closing situation:
- We have potentially 3 types: The
ConfirmedSnapshot
which we use internally, theClosingSnapshot
which we use in theTx
context representing the low-level data we need to create the transcation and keep on-chain contracts happy, and another (missing one) representing the information we pass from HeadLogic to Direct chain component to initiate a close
- Switching
hydra-cardano-api
toBabbageEra
required not only a change in a single place :( - Seems like I need to touch most of
hydra-cardano-api
to update to babbage era - I realize that
tx-cost
is depending onhydra-test-utils
-> why? dropped the dependency - Maybe we should move Hydra.Ledger.Cardano.Builder to
hydra-cardano-api
as Hydra.Cardano.Api.Builder?- Reason: It directly depends on the
Era
in use and needed update when switching to babbage
- Reason: It directly depends on the
- Now we are missing
Arbitrary ValidatedTx
instances for babbage era txs -> gonna look for them tomorrow
Looking at this draft PR failure on checkCloseDatumHash
.
The problem occurs in the case of closing the initial snapshot: The hash of the initialUtxo
as appearing in the Initial
state on chain is different from the one we pass as redeemer and set as datum in the close
transaction.
- One issue is that we generate an arbitrary snaphot in the generator for the close tx, even in the case of the initial one which does not make sense
- Trying to ensure we pass the correct snapshot by returning the snapshot generated from the
genStOpen
generator does not work - We let the problem to rest for solving tomorrow morning
- Discussing w/ ledger and formal methods experts what could be a good approach to formalising the Head protocol in order to prove security and safety properties
- Embedding LaTeX notation in the Haddock for the code is cool and will probably help researchers
- Ledger team has the issue that because the notation is very close to Haskell code, it's hard for researchers to be autonomous with it
- Of course, maintaining code and formal notation together is fraught with perils as there's nothing that guarantees they stay in sync
- Ott language: https://github.com/ott-lang/ott and https://www.cl.cam.ac.uk/~pes20/ott/ott-jfp.pdf
- Tim used the approach of embedding properties inside type classes describing the interface of the protocol, and ensure the quickcheck is run on each build against implementations
- There's been some work using model checking in Djed, and it seems the properites we want to express are temporal so we would need some form of temporal logic
On preparing a babbage-preview
branch:
- There is a new
BasicFailure
:BadTranslation
- The
Hydra.Chain.Direct.Wallet.estimateScriptCost
function is throwing exceptions in pure code -> this is discouraged by our style guide (albeit not fully accepted?) - First run of tx-cost possible -> Very similar results to master! Let's make the bench a bit more deterministic before diving into
PlutusV2
- We have been measuring only the
Right
results from the redeemer report in tx-cost! If we require ALL redeemers to not exceed total tx budget AND check against maxMem/maxCPU we get more conservative, but also more accurate numbers. - It's not clear to me what amounts to most fluctuation, but the filtering of byron addresses makes utxos / transactions of varying sizes / complexity.
-
PlutusV2
uses the same Value/AssetClass types but does not re-export functions operating on them -> import of V1 next to V2 necessary!? - After using
Builtin.serialiseData
instead of our on-chain encoder, Head script size did not decrease noticably! - tx-cost compiles, but transactions don't validate as expected.
PlutusV2
scripts wrapped up "asPlutusV1
" by ourhydra-cardano-api
is bound to fail. - Updating
mkScriptWitness
is not easy, as I cannot add aninstance HasScriptLanguage
forBabbageEra
because the type is not exposed by thecardano-api
:(
On preparing a babbage-preview
branch:
- Ledger mempool interface seemingly changed. Trying to migrate to using the "new" applyTx
-
LedgerState
is a record now, some changes required because of that - Some type signature wrangling as (e.g. the constraint
Ledger.Crypto.Crypto (Ledger.Era.Crypto era)
) had me removeapplyTx
's signature as we will instantiate era asLedgerEra
right above anyways. - Seems like consensus has parameterized blocks by
protocol
now. And the only two options satisfying the classes arePraos
andTPraos
-> which one do we use? -
TPraos
seems to be the right one (it's requested by the local state query protocol) -> need to add a new package dependency though as I cannot find a suitable re-export - I struggle in deciphering a view pattern and which direction of hashes are now unwrapped/wrapped.. we should avoid these.
On preparing the 0.5.0
release:
- While writing/grooming the changelog I realized we forgot
RolledBack
handling in the TUI -> PR that - While doing that I realize that the API still has
UTxO
as server output.. we wanted to rename that, better do it now than just after the release -> PR that - Also created PRs to have a improved
hydra-node --version
and some output to see changes in script hashes viahydra-node --script-info
- Finally some polish of
tx-cost
output
Experimenting with embedded LaTeX in Haddock documentation for HeadLogic
module, seems like it's rendered quite nicely in HTML so I guess it would make sense to do that to ease researcher's verification work. The plan is to put everything (at least off-chain part) in this module, with the module's header describing the "semantics" of the notation and have each message/event type being handled by a separate function ripped out of the big update
state machine function.
Working on https://github.com/input-output-hk/hydra-poc/pull/340 to move the MT benchmarks ot plutus-merkle-tree
package which completes the PR -> waiting for CI
- On-chain contestation is more complex than other transactions because it's perfectly possible the posting fails for a valid reason: Several parties are contesting the same
CloseTx
but only one can succeed. This implies we need some specific logic in theContest
to deal with this case, which we don't for other transactions- Possibly, we want to check why the transaction posting fails to distinguish between "technical errors" which should be reported, and logical errors which should not
- Going for simple case to scaffold the whole stack (and make ETE pass)
We currently stay in the same Closed
state when contesting. Later on, we should burn PTs when contesting to ensure a party only ever contests once
Close
and Contest
tx are very similar to one another, so we should probably later on merge them and have a single Close
transaction type that burn the PT of the poster.
We were missing the waitForNodesConnected
at the beginning of the test so hit some race conditions in the observations.
With this addition, the test still fails but at the end, when we wait for HeadContested
-> The transaction is not observed on-chain
We add a crude implementation of observeContestTx
copy-pasted and adapted from observeCloseTx
but tied to the StClosed -> StClosed
state transition, which fixes the test. This emphasizes once again how similar close and contest are so we should factor them in the same logic.
Starting work on integrating load tester in CI from Ryan Williams' work.
- While testing
README
instructions in load tester, ran into a python3 configuration problem: Dependencies are not installed!. Need to use https://note.nkmk.me/en/python-pip-install-requirements/ for example to add requirements when needed and install with pip3 - Managed to install deps but got following error:
The code depends on the existence of some keys to produce a seed transaction for each node
% python3 prepare-instance-data.py === Instance 0 === Creating directories Importing data directories Importing signing keys Traceback (most recent call last): File "/Users/arnaud/projects/hydra-load-testing/prepare-instance-data.py", line 61, in <module> shutil.copyfile(SEED_KEYS_SOURCE+'seed-cardano.sk', credentials_dir+'seed-cardano.sk') File "/usr/local/Cellar/[email protected]/3.9.10/Frameworks/Python.framework/Versions/3.9/lib/python3.9/shutil.py", line 264, in copyfile with open(src, 'rb') as fsrc: FileNotFoundError: [Errno 2] No such file or directory: './hydra-infrastructure/seed-keys/seed-cardano.sk'
Merged 2 PRs:
- https://github.com/input-output-hk/hydra-poc/pull/326 which finalises replacement of mock crypto with concrete (Ed25519) cryptograph
-
https://github.com/input-output-hk/hydra-poc/pull/329 which also closes https://github.com/input-output-hk/hydra-poc/pull/218, adding dependency on
cardano-node
in nix rather than letting the ETE tests depend on it using cabal'sbuild-depends
stanza
-
Completing plutus-apps PR with some import fixes
- seems like they will accept it
- plutus-ledger is a bit of a kitchen-sink
- we shoud try to start avoiding it
-
Back to updating deps:
NodeToClientProtocols
now has alocalTxMonitorProtocol
- can mock this like the
localTxSubmissionProtocol
.. but why are we using this data structure and not the subset we need?
- can mock this like the
-
Mocked both, Hydra.Chain.Direct and Hydra.Chain.Direct.Wallet
-
We see golden sample WARNINGs for
Event SimpleTx
,ReasonablySized (ServerOutput SimpleTx)
andReasonablySized (ServerOutput (Tx AlonzoEra))
- was this the case on
master
already? - if not, which generator changed?
- was this the case on
-
Got a
MuxError (MuxIOException writev: resource vanished (Broken pipe)) "(sendAll errored)"
in tests using the MockServer- not yet added the
localTxMonitorProtocol
there
- not yet added the
-
tx-cost was still using old ledger-based code for
computeFanOutCost
-> updated it to use the stateful tx generators -
After everything builds / tests ran, I had errors when re-entering the nix-shell (and on CI) -> haskell.nix could not resolve some haskell dependencies!
- trying to bump to latest haskellNix in default.nix -> nothing changed
- adding freer-extra (the missing dep) back to plutus-apps source-repository-package in cabal.project -> works
- seems like it remained installed before and a proper rebuild via nix was highlighting this
-
More build errors with haskell.nix (something from Beam / Database?)
- using
plutus-apps
'sindex-state
with latesthaskell.nix
(cabal
only workflow was likely using latest packages on hackage, while haskell.nix is using "more pinned" versions)
- using
-
Stuck on nix having a build error on
cardano-ledger-byron-test-lib-cardano-ledger-byron-test
.. not sure why this is a problem now? -
After an oddyssey of confusion why it works in cabal, not in nix (but in cardano-ledger both modes work), it turns out cabal was using a not-so-up-to-date version of hedgehog and thus succeeded in compiling. Adding a constraint to cabal.project solves the problem.
-
For some reason our cabal.project was selecting
servant-0.2
(which is ancient), added a constraint -
I also discovered
nix-tree
which is handy to explore nix dependencies, e.g.nix-shell -p nix-tree --run "nix-tree $(nix-instantiate shell.nix)"
shows all dependencies of our shell environment and we could for example search for "hedgehog" to see which version we are using.
- Start next dependency update to form a
babbage-preview
branch <2022-04-29 Fri 14:34> - Using https://github.com/input-output-hk/cardano-node/pull/3818 and https://github.com/input-output-hk/ouroboros-network/pull/3595 as they seem to track integration <2022-04-29 Fri 14:46>
- While chasing some, now missing, re-exports of plutus-ledger-api in plutus-ledger.. I realize that
serialiseData
is not included in the referred plutus version (57cebe002021af75d56a35977b517b377ea7c9bd)? -> Checked with #ledger/#plutus and they will track plutusrelease/v1.0.0
for vasil <2022-04-29 Fri 15:43> -
Plutus-apps
is not updated to work with plutusrelease/1.0.0
branch (which is the one to be released) and plutus-ledger now misses dropped stuff from plutus now. <2022-04-29 Fri 16:01> - I'll invest some time to get rid of
plutus-ledger
.. probably avoids many problems and at least to find out how difficult it is <2022-04-29 Fri 16:05> -
TypedValidator
definitely being the "big issue".. let's see whether I can factor out the basics <2022-04-29 Fri 16:26> - We still use a lot of
cardano-ledger
code inhydra-test-utils
and theplutus-xxx
packages.. could use (hydra-)cardano-api there, not changing it now. <2022-04-29 Fri 16:33> - Turns out
wrapValidator
is quite handy to the type conversion -> vendor it <2022-04-29 Fri 16:46> - Running into template-haskell stage restriction when using
wrapValidator
directly in the compile splice <2022-04-29 Fri 17:20> - The stage-restriction is actually coming from applying types to
wrapValidator
, when doing that in a where clause it's fine!? Well..template haskell. <2022-04-29 Fri 17:47> - Getting access to the script (bytes) is fine and we never really used the types from
TypedValidator
as it seems? How to get the validatorHash now? In general, the rewrite of not usingTypedValidator
seems promising! <2022-04-29 Fri 18:29> - Yes, turns out..
wrapValidator
and a vendoredscriptValidatorHash
is the only things we need fromplutus-ledger
🎉! <2022-04-29 Fri 19:24> - Continuing the dependency bump for
babbage-preview
<2022-04-29 Fri 19:25> -
cardano-crypto-class
needssecp256k1
library, so I set off to adding it to the nix build / shell <2022-04-29 Fri 19:56> - The overridden secp library seems not to be picked up by haskell.nix build? <2022-04-29 Fri 20:02>
- Not changing the name makes it even more confusing <2022-04-29 Fri 20:05>
- Found a reference to
pkgs.secp26k1
in cardano-ledger, but no specific overlay or so.. maybe it's from iohk-nix -> bumping this in hydra-poc as well <2022-04-29 Fri 20:23> - No.. seems not to pick up a more recent
secp256k1
<2022-04-29 Fri 20:26> - Trying to turn off
secp256k1-support
hasplutus-core
choke and no way to disable it there. Maybe only thing left is to ditch haskell.nix for now until somebody helps me? <2022-04-29 Fri 20:30> - I realize that
cardano-base
andcardano-ledger
use a potentially newernixpkgs
(unstable) version. Let's try that one in our repo. <2022-04-29 Fri 20:39> - That seems to work for
cardano-crypto-class
, but now thecardano-node
fails with some undefined symbol incardano-crypto-praos
? <2022-04-29 Fri 21:09> - Removing
cardano-node
from our .cabal files allow us to use haskell.nix shell.. why are we even depending on the cardano-node package? <2022-04-29 Fri 22:09>
- After https://github.com/input-output-hk/hydra-poc/pull/329, updating the
cardano-node
used in tests is very easy -> just updateshell.nix
and the, then failing, test assertingcardano-node --version
- Updating ledger, plutus, ouroboros-network, et al dependencies is likely trickier.. starting with updating git commit hashes in
cabal.project
-
cardano-api
upstreamed some things (we proposed)? Easy fix -
plutus-ledger
got moved fromplutus
->plutus-apps
.. okay, let's addplutus-apps
as a source repository package -
plutus-ledger
depends oncardano-wallet-core
now.. this seems like a rat's tail! We are using at leastTypedValidator
fromplutus-ledger
.. maybe trace what we really need and not try to have all wallet deps in scope? -
Hydra.OnChain.Util
also usedcheckScriptContext
fromplutus-ledger
.. but the module is now unused -> drop it. - Can we avoid using
plutus-ledger
in some places? let's have a look atinspect-script
which is quite narrow in scope - Indeed.. some use of
Ledger.Scripts
are just convenience conversions a we have them also in(hydra-)cardano-api
- Have noticed
cardano-wallet-core
usage is very minimal inplutus-ledger
-> filed a PR: https://github.com/input-output-hk/plutus-apps/pull/437
- We discussed how we would approach auditing the Hydra implementation against the paper and it's security proofs
- Some observation and next steps persisted in this ticket: https://github.com/input-output-hk/hydra-poc/issues/194#issuecomment-1110980313
- Trying to avoid the party conversion by trying to use our Hydra.Party in plutus
- Seems like we cannot use the high-level types as is in plutus (getting a "not supported" error)
- Asked on StackExchange (and #plutus) whether we could re-use VerKeyDSIGN-based types in plutus: https://cardano.stackexchange.com/questions/8035/is-it-possible-to-not-compile-data-constructors-with-plutus-tx
- no
- So we are stuck with the conversion. We could however try to use a simpler type for
Hydra.VerificationKey
, being just anewtype
aroundByteString
, but that would side-step the safety we get from using Cardano.Crypto.Class things?
- Working on finalizing #318
- We start the ensemble by discussing
genForParty
and agreeing to move it to test code (fixture), also dropping generateVerificationKey and generateParty should help in avoiding problems (make dropping signing keys explicit) - The HydraLog was having issues as we derived newtypes the MultiSignature and missed a "multiSignature" key in the object
- When we wrote
serialiseSigningKeyToRawBytes
it really showed we might rather want to use cardano-api type classes and maybe even the text envelope!?
- Continue on #318 crypto branch after ensemble last week
- First step: Follow compilation errors
- Many fixture become less ad-hoc now (we used Num instances a lot)
let alice = 1
is nowlet alice = generateParty "alice"
-> create a fixture module? - Is
generateParty
/generateVerificationKey
ever a good idea? It could be quite hard to find out if one forgets that its not derived, but generated when keys don't align. -
cardano-api
also hasgenerateSigningKey
-> can we maybe re-use cardano-api types and add a Hydra keyrole!? - I stumble (only now) over the mismatch of
Snapshot
/SnapshotNumber
being multi-signed!? I'm confused.. - Ah nevermind.. before signatures were just
ByteString
, so that's why it's now asking for a type variable onarbitrary
- Stumbled over
instance SignableRepresentation Snapshot
and we are not properly signing snapshots still and need to also incorporate the utxo hash!- This is what I vaguely remembered and why I was confused above ..
- Resolved quite a number of golden/faulty files as parties have now way longer keys -> spotted even a list with a duplicate not having a duplicate anymore (generators less likely to collide)
- Our mutation tests are quite fragile. They depend on a deterministic generation of Cardano verification keys from Hydra parties. When refactoring that bit into the
genForParty
form which allows to use anyGen a
, using the two different generatorsgenVerificationKey
andfst <$> genKeyPair
yielded different verification keys.. this is bound to happen again :(
SN solo on #318
- Starting work on Use non-mocked Ed25519 crypto
- First step: a dedicated module with types for Hydra credentials -> Hydra.Crypto
- Using
newtype
wrappers to avoid accidential re-use, now that we are using the same keys and signature scheme as cardano - Renaming
Signed a -> Signature a
, because it does not includea
as the old name would suggest - Implementing
sign
/verify
in a test-driven way using properties - I realize there is a
SignedDSIGN
wrapper also in the cardano-base library. Maybe we should use it? - Test failures in roundtrip have been puzzling me.. until I realize I didn't pass
a
toverify
-> why are functions having aSignableRepresentation
!? - Instead of simply "porting"
toPlutusSignature
, we could defineTo/FromData
instances forMultiSignature
and make it's use isomorphic? - Next step is less clear: I could rewrite MultiSignature right away to be not sensitive to order, but then the plutus cost of that implementation is unclear. Also we might want to see the cost of switching from Mock to Ed25519, so likely a good step forward would be to add closeTx tests / benchmarks first.
-
Continued on the rollback work and started drafting a test scenario to force us to implement a correct rollback logic. Our goal has been to generate an arbitrary observable sequence of blocks (i.e. blocks containing transactions 'observable' from the Hydra protocol standpoint); play that scenario once using the RollForward handler, and then, inject a rollback to anywhere within this sequence, assert that the rollback is reported correctly and then, replay the rolledback blocks on top of the rolled back state. If everything went well, we should be able to end up in the same state as if no rolled back happened.
-
To cope with rollbacks in the direct-chain component, we first started introducing some form of linked state directly in the OnChainHeadState / SomeOnChainHeadState; turned out to ripple quite over many parts of the application which we judged to be a red flag. We stopped, stepped back and figured out that we could handle the on-chain head state management almost fully in the chain-sync handler, without touching any of the underlying state inner logic.
-
Thus, we now keep track of, not-only
SomeOnChainHeadState
, but aChainPoint
, the point at which this state was recorded on-chain as well as the previous state before that. Similar to what we did in the Headlogic, this allows to easily crawl back the history of on-chain head states on demand upon rolling back. -
On the test side, it is quite involved, because in order to generate a valid sequence of events (which are in practice, the result of multiple actors on multiple nodes), we need to generate and maintain states from the point of view of all participants, and record transactions visible to everyone on our chain ersatz. Perhaps an easier way would be to start our rollback spec directly in the open-state, and only consider the close and fanout steps (which require less synchronization than participants' commits).
-
I didn't really got time to get to the bottom of the story regarding the necessary modifications in the direct-chain component as I ended up shaving some yak 🐄. BUT, I did prepare the ground by isolating the relevant piece of code and setting up a little "framework" for testing the chain sync handler with a first test to illustrate it with the RollForward.
-
Now remains for tomorrow pairing to test that the behavior of the chain-sync handler regarding rollbacks, which should force us to implement the missing pieces.
Discussing and starting implementation of Rollback I
- MB gave us an intro on what's going on with rollbacks actually, and how this impacts the Head
- The basic strategy for dealing with rollbacks at this stage is to also rollback the Head's state to a "safe" point, which mostly means to the point corresponding to the last known state of the chain before the point we need to rollback
- This lead us to realise we need to maintain some form of index within the
HeadState
but not much more information for now.- Direct chain notifies rollbacks providing a relative index, like
Rollback 2
representing the number of observed transactions the head needs to discard - We maintain a chain of
HeadState
which is updated every time anOnChainTx
event is observed, or aRollback
happens: In the first case, we advance the state and link to thepreviousState
; in the second case we simply drop a number of links in the chain
- Direct chain notifies rollbacks providing a relative index, like
- In essence, we structure the
HeadState
like a "blockchain" where blocks are just the changes in state induced by the underlying layer 1.
The HeadLogic
module and more precisely the update
function have become really unwieldy by now
Now going to tackle the harder part, eg. handling of Rollback
notification from the Direct
chain component
-
Musig2 situation
- We did a trick which is not audited, we did reach out to cryptographers, but to no avail yet
- Code audit will likely be not the problem
-
We do not NEED aggregated multi-signatures right now, but might become an issue later
-
Plutus is evolving: Schnorr signatures
- Interaction is the same
- It's just using a different keypair -> we would not be able to re-use Cardano Ed25519 keys
- Merged in cardano-base https://github.com/input-output-hk/cardano-base/pull/258
- Plutus integration still on-going
-
Even later: we might get pairing support
- This allows for non-interactive signature aggregation!
- Pairing evaluation is more expensive though!
- https://github.com/cardano-foundation/CIPs/pull/220
-
Spent some time trying to improve our CI workflow, mainly from two motivating factors:
- The full workflow in itself takes about 30 minutes, which is quite a long feedback loop.
- The TUI tests are quite flaky, and sometimes don't start. This, combined with long execution times makes it extremely frustrating.
-
One first oddity seems to be the time needed to prepare the nix-shell. This currently takes ~6m30s though, almost nothing is built. And it's mostly fetching dependencies from a cache. This doesn't happen in parallel yet, but there's some hope: https://github.com/NixOS/nix/issues/5118. So at the moment, I've trimmed down a bit the nix-shell from all the tools that we use for local development but that aren't of any use in the CI workflow. This saves about 2m30s on the overall nix-shell build.
-
I also split the one CI job building everything and running all tests sequentially, into multiple jobs, one for each package. This reduces the overall build time by another 12minutes, since most test executions happen in parallel. On the plus side also, jobs can be restarted independently.
-
Finally, I added some retry logic to the TUI job, so that it retries at least once the test in case of failure. This should gives us a better experience and reduce the friction when the test runners fail to start in xterm for some reason.
- I continued on top of our pairing session and Arnaud's solo work on #304; refactored further the
withHydraNode
to remove more stringly-typed arguments; also addressed a TODO. More refactor could be done I guess for thiswithHydraNode
, but otherwise, the only thing remaining now I believe is to write the end-to-end test showing that a node can be started from a point in the past, which will force to us to wire the options with the internal direct chain component.
Working on passing additional option for starting chain at arbitrary point to Hydra node.
- Added the needed parametr to
withDirectChain
component - Need to add the option to
hydra-node
executable, which is straightforward but we realise we have quite some technical debt in theHydraNode
module when buildind and launching the process for the node, which makes it harder and uglier than necessary to pass specific options - We want to write a proper end-to-end test to ensure we can actually pass the option to the CLI and it gets used
Writing generator for Options is relatively straightforward, found a couple interesting issues doing it:
- Why use
IP
type + port argument instead ofHost
which takes name and port? -> The point is the former is used to bind listening sockets to specific interface, but this could probably be done also with names?
Struggled to write a generator for BlockPoint
, the HeaderHash
part was a bit of a PITA to work with
Completed generator for Options and then started using the type in the HydraNode
module, replacing manual command-line building with toArgs
. Then started removing arguments to the withHydraNode
function and replace with an Options
instance that will be updated.
- Implementing tx-cost for abort transaction so that it appears in the benchmarks page
- Started work on https://github.com/input-output-hk/hydra-poc/issues/300, added integration test + some skeletal functions to retrieve tip from chain
Fixing and merging https://github.com/input-output-hk/hydra-poc/pull/295 to close https://github.com/input-output-hk/hydra-poc/issues/243
Discussing about issue https://github.com/input-output-hk/hydra-poc/issues/243
- Do we really need to check on-chain the matching of parties' keys with the PTs Token Names? By definition, the parties of a Head are identified by the PKH of the token they are given, which is part of a UTxO sent to the Initial script.
- The problem that can happen is that one observes a
Init
tx defining parties which I am unaware of (eg. not part of the vks given to me as parameters) => verification is necessarily done off-chain - Should we do the verification in the
observeInitTx
, given some list of cardano keys?
Test to write:
- Pass the cardano vkeys to the observeInitTx and check the minted tokens match those keys -> otherwise it's an error and we don't care about this transaction
- The only way for such a "flawed" transaction to progress is for the initiator to post the abort
We could reuse the mutation framework to inject mutations that do not fail valitation of contracts but fail observation of the tx off-chain
Wrapped-up work on #246
- Separated cost computation logic from markdown formatting in the tx-cost benchmark, extracting the former to own module.
- Might make sense later to factor common code for tx generation with
StateSpec
: The 2 things serve different purpose but share common logic
Working on Mithril ETE tests I thought it could be useful to publish some of our packages related to the cardano API and the management of a local network. But perhaps there are existing packages I am unaware of to handle that machinery?
Mainnet VM restarted automatically this morning and of course cardano-node wasn't.
- Proper configuration would need to add the node to systemd: https://gist.github.com/r0l1/4039684a76c508aa097a89ae5d752ca4
Continuing work on 246, goal is to be able to generate a markdown page containing transaction costs and sizes for all types of transactions we have in Hydra
- Currently working on generating the page and linking it to documentation
Added "cost" computation for init and commit transaction, using generators from StateSpec
and the provided HydraContext
which handily packs some useful parameters for generating transaction depending on the number of parties.
Regarding commitTx
, the check is rather naive and assumes UTxO are "simple", eg. they are just ADA values. We should add check for the case where values contain arbitrarily large assets.
Regarding collectCom, I wasted time because of 2 mistakes:
- I was generating the "lookup UTxO" from the wrong
OnChainHeadState
thus transaction validation was failing because inputs could not be resolved - The other tx types expect the redeemers report to be a singleton as there's a single script to evaluate (in the case of initTx this is the minting policy script), but of course in the
collectCom
case there can be multiple scripts evaluated.
Puzzle: The outcome of evaluateTx
can result in a RedeemerReport
where the sum of all individual execution costs is greater than the maximum from the PParams
which seems odd. I would have expected the evaluation to fail in this case, or does the ledger checks the bound later on?
Plan for providing tx size benchmark (PR, Issue) :
- turn tx-cost executable into a benchmark
- generate proper markdown tables with data
- Output tables to file in the
public/
directory for consumption by documentation generator - add more tx types than just fanout
Making sense of how documentation is produced:
- the
ci.yaml
file runsyarn build
to generate the docusaurus documentation, thenyarn validate
which generates and validates the API schemas -
yarn validate
invokesyarn validate:inputs && yarn validate:outputs
which points ultimately tovalidate-api.js
which is a JS source file (node) at toplevel of the project that takes some json files and generates some output likeIt's unclear to me why we generate this output here, it would be somewhat more valuable as a page?$ ./validate-api.js publish '/' '../hydra-node/golden/ReasonablySized (ClientInput (Tx AlonzoEra)).json' ✓ Commit → {"tag":"Commit","utxo":{"03170a2e7597b7b7e3d84c05391d139a62... ✓ Init → {"contestationPeriod":-1.943790302836,"tag":"Init"} ✓ Abort → {"tag":"Abort"} ✓ GetUTxO → {"tag":"GetUTxO"} ...
- The haddock files are generated by
ci-haddock.sh
into a directorydocs/static/haddock
where they will be picked up by docusaurus as static assets. The links in the welcome page's footer points toindex.html
underneath that directory.
It's not clear to me what the pages build action is now doing: Seems like it publishes some artifact package somewhere?
Managed to have benchmarks "published" to stdout: https://github.com/input-output-hk/hydra-poc/runs/5739729903?check_suite_focus=true
Working on merging PRs for Hydra
- rebased https://github.com/input-output-hk/hydra-poc/pull/292 on master and merged it, removing failing tests assertion
- Merged https://github.com/input-output-hk/hydra-poc/pull/291 and closed associated issue
- completed https://github.com/input-output-hk/hydra-poc/pull/293
- Merged https://github.com/input-output-hk/hydra-poc/pull/282
cabal test hydra-cluster
fails when demo is running on the same machine due to ports conflicting -> we should use random ports for everything the hydra-node is listening on and use in tests to reduce the risk of misinterpreting tests error
Rebased and merged a couple hopefully uncontroversial PRs, we now only have 2 PRs in flight 💪
Facilitated first meeting with Quviq's team, giving them a whirlwind tour of our codebase and what kind of tests we had put in place.
- Started from toplevel End-to-end test, illustrating the architecture of the Hydra node,
- Went through the on-chain transactions state machine, using Miro board to show the structure of transactions and UTxO,
- Then explained our Mutation-based Properties
- Team as good sense of what are the priorities and where to go from now: We agreed the critical part is to implement a model that would test the on-chain state machine, exerting not only single transactions but various sequences
- A follow-up meeting to discuss details of implementation plan has been scheduled for April 11th.
- Expose Cardano-compatible API:
- Write ADR/Requirement about exposing Cardano-compatible API
- Make it possible to develop in parallel => delegate to other people
- Probably a High priority item given what we've heard from other people and to be community-friendly
- Provide CBOR-only Tx sub/pub interface for client
- For people using standard Cardano tools, having the Tx in a custom JSON format does not make much sense -> expect CBOR
- We could provide a tool for debugging purpose using JSON format
- Start documenting Hydra node operations drawing from our workshop experience
- enhance user manual
- configuration logs/monitoring
- provide basic docker-compose/documentation about arguments
- bootstrap testnet from snapshot?
- Having a single script?
- Could increase load on Close/FanOut but make CollectCom simpler
- Would be worthwhile to timebox
- Admin API?
- A minima have a Pre-flight check about keys, ports, IPs...
- 3 levels of API:
- Administering the node itself (config IPs, keys, network topology...),
- Administering the Head(s) (Head lifecycle),
- Posting/Chain sync inside an opened Head
- Have different channels in the WS doc/API?
- First step: Have the primitives Command/Queries in the existing API to do Admin stuff (eg. configure network), even though it seems Req/Rep interfafce would make more sense here
- Fuel?
- Creating fuel is a PITA =>
- External wallet would handle that
- Need to add money to the Hydra node for handling tx
- Provide an API for "constructing" the config of a Head
- Why do we have 2 set of keys?
- Multisig keys are not supposed to be usable for on-chain signing (that's what in the paper)
- But actually they are Ed25519 keys so could be used for both on-chain and off-chain signing
- Need to check with Inigo whether or not this makes sense in the long run? What is the level of security of a single Hydra key? How does it play with multisig key generation?
- Could greatly simplify configuration of a node
- Audit/Certification
- Testing our Cardano API wrapper functions? => best effort, not really needed
- Wait and see
Looking at feeding hydra nodes logs to Grafana which seems the solution of choice at IOG, got a free plan account up and running and a simple configuration for promtail
to push logs to it:
server:
http_listen_port: 0
grpc_listen_port: 0
positions:
filename: /tmp/positions.yaml
client:
url: <redacted>
scrape_configs:
- job_name: system
static_configs:
- targets:
- localhost
labels:
job: varlogs
__path__: /var/log/*.log
Need to add log rotation to docker containers in order to ensure we don't fill the filesystem:
version: '3.7'
services:
app:
image: ...
logging:
options:
max-size: "10m"
max-file: "3"
- Article on using loki/promtail with docker: https://itnext.io/monitoring-your-docker-containers-logs-the-loki-way-e9fdbae6bafd
- We actually only need promtail which will ship the logs to grafana cloud instance running loki
docker run --name promtail --volume "$PWD/promtail:/etc/promtail" --volume "/var/log:/var/log" grafana/promtail:master -config.file=/etc/promtail/config.yaml
There is a GCP logs driver for docker that one can use to ship logs to GCP Logs
Smart Contracts Certification:
- There are 3 different levels: testing, audit, and full formal spec
Level 1 leverages Quviq tooling, is run by IOG and publishes results:
- Let people write quickcheck-dynamic props plus pass some default properties ensuring basic safety properties of the contracts (no funds locked...)
- There's nothing in the framework that relies heavily on the
Contract
monad - But rebuilding an emulator could take a lot of work, looks like a deep 🐰 hole
- Good first step for Hydra would be to build a state machine model for the platform we already have
- Certification boils down to: "How well did you test?" so it could be assessed in current state of code and later on improved, with CI pushing generated documentation about the checks passing
We should also start thinking about providing tooling to help Hydra users test their applications:
- This is part of an ongoing documentation, tooling, and ops effort to ease use of Hydra
- We could expose our internal testing framework quite easily
Next steps:
- Walk Quviq team through our codebase and testing strategy
- Get feedback from their studying the code, tests written, what are the main areas of improvement
- Work together q/ Quviq to build missing parts, possibly also with support from other teams that would be interested in new approach
- Start the formal certification process now as we want to be ready by June
Plan for day 3:
-
Product Meeting at 3pm
-
MRR at 5:15pm
-
Hacking until noon -> wrap the pixel painting example Hydra-enabled DApp
- decoding of the CBOR metadata of the client
- pack static assets in single integreated server
- expose a websocket proxy/relay to hide the API server
- (optional) disable painting while head is closed/closing
- Stage 2: Put something in the tx that ends up in th?
- Use Plutus MT to provide a root hash of a MT representing the painted canvas?
-
What is MT? -> https://en.wikipedia.org/wiki/Merkle_tree
- Is it binary or not? 20 minutes later... -> it does not matter
-
Bonues point: Paint a 32x32 bison: https://wdrfree.com/stock-vector/download/pixel-art-ox-character-isolated-273483802
-
Step 1:
- fix CBOR encoding
- reproduce painting demo
Implement the Hydraw application and run full end-to-end demo of it:
Looking at serving static files from the pixel-painter
app...
- Modified
pixel-painting
server app to serve assets locally, from the directory - Also remove the
NETWORK_ID
env variable
We realise our previous day Head is closed but not fanned-out! Which means we don't have money anymore
- MB needs to send us money again from the faucet...
- We should provide a command to enab le anyone to do the fanout should the node of the closer crash or disappears => This is the 🔫
Random Idea on top of BIP-39: Given the 32 bytes of a key, generate a family of word mappings such tthat the 24 words can be broken in 4 sentences of 6 words and each word can either be a noun, an adjective or a verb thus generating a poem one can memorize
Integrated websockets proxy inside pixel-painting
server so there's no need to connect from the client to the API Server directly, it's all proxied => We still have access to the full API
some inspiration https://medium.com/@pvh/pixelpusher-real-time-peer-to-peer-collaboration-with-react-7c7bc8ecbf74: like the local-first nature of this kind of app
Ironing out creases on the Hydraxel or better Hydraw app!
- SN starting over node configuration from
docker-compose.yml
to use containers - Rebooting my VM and restarting node, forgot to define
NETWORK_MAGIC
Got connected to MB's box and vice-versa 🎉
- We cannot connect to SN's box, seems like some local firewalling rules are in place preventing access
- Struggling to get 5001 opened on SN's machine -> had to disable firewall from NixOS
Got connection working but the reporting of peers connected is not precise enough, seems like when one is the last node connecting it does not get the PeerConnected
event reported. https://github.com/input-output-hk/hydra-poc/issues/265
- Trying to fix a problem in the network stack which seems to run out of
TChan
if the node crashes or the connections are dropped? - Actually, it works well once we get over firewall configurations :)
Initialising the head fails with minted wrong
message -> there is only one token minted
Looks like we are configuring the keys wrongly:
- MB had right number of cardano keys but with wrong content (from the demo), and not the hydra vkeys
- SN and AB had the right hydra keys but no cardano vkyes => mint wrong number of tokens
Managed to get the Head opened on the testnet: d36a9936ae7a07f5f4bdc9ad0b23761cb7b14f35007e54947e27a1510f897f04
- Aborting the head failed with Max TX Size exceeded 😭 because there are a lot of scripts
- The script sizes are the problem, probably because of compiler issue with large
force/delay
. We had a full Head/Commit x 2 / CollectCom / Close tx -> We were not able to create a tx inside the head probalby because of Hydra network connecting issues - Discussing over the tx size limitation occuring on aborts => Changing back to actual testnet parmaeters (16KB) breaks tests for
Abort
andCollectCom
- Trying to apply shrinker tool on our scripts but it's a dead-end: it does not compile with our version of Plutus
- Looking at the PLC code, we see some patterns that appear redundant like nested
force
call or(delay (force ...
calls. Seems like optimising contract is necessary but could be done later as we don't really care about aborting a head:
TODO: Upgrade plutus dependency to latest version (possibly with serialiseData
update?)
Some ideas for improvement:
- Use Hydra network to broadcast
Init
- Display HeadID
We were able to open a head and make transactions in it:
Hydra TUI 0.4.0 connected to hydra-node:4001 | Head status: Open | [N]ew Transaction
Connected peers: +----------------------------------------------------------------------+ [C]lose
- 0.0.0.0:5031 | Head UTXO, total: 229000000 lovelace | [Q]uit
- 0.0.0.0:5014 | |
--------------------------------------------------+ addr_test1vq7xjxzc835p25vf6tun8upamzuy.. |
Party 000000000000002a | 395c03974b#0 ↦ 1 lovelace |
Address addr_test1vq7xjxzc835p25vf6tun8upamzuy.. | 4dad7ea40a#0 ↦ 1000000 lovelace |
Head participants: | 4dad7ea40a#1 ↦ 99000000 lovelace |
- 0000000000000033 | 99f8780bfe#0 ↦ 42 lovelace |
- 0000000000000042 | |
- 000000000000002a | addr_test1vp2l229athdj05l20ggnqz24p4lt.. |
| b1d7be10c2#0 ↦ 8000000 lovelace |
| b1d7be10c2#1 ↦ 79000000 lovelace |
| |
| addr_test1vzhntgxl8c33a2talf47l00p0gm4.. |
| 7e165151a7#1 ↦ 41999957 lovelace |
| |
| |
The problem is that this state cannot be closed because of the minimum UTxO value on L1. This can be fixed tactically by allowing merging UTXO in the TUI so it will be the responsibility of users to not keep so small UTXO at closing time.
We managed to do the full dance: Open -> Commit -> CollectCom -> NewTx* -> Close -> FanOut
🍾
This is the first fanout tx on the testnet: https://testnet.cardanoscan.io/transaction/99fcc8143ef147767716729b2cf62b51a5fef266c43b0ef87abefa9f12db134c
Intense hacking session working on getting Stage 1 pixel painter up and running ensues. What we have:
-
A
bundle.js
javascript client that connects to the server WS API on a hardcoded path, reads the messages from the server and allows user to select pixel to paints then call a local server on 1337 to post a tx -
A thin HTTP wrapper
pixel-painting
to receive calls to paintx/y/r/g/b
and transforms that into a transaction sent asNewTx
message to the API WS server. The transaction contains metadata representing the pixel to paint -
The bundle reads those metadata and represents the pixel in the canvas
-
Looking at how to build auxiliary Data for Tx. TxMetadata is a map from "metadata types" to arbitrary structured values. In our case, we could do something as simple as packing the pixel we paint as a JSON string in a
ByteString
metadata.data Pixel = { x :: Int, y :: Int, color :: (Word8, Word8, Word8) }
- "external wallet" integration: makes sense for most external people
- lightweight node? What does it mean really, how is it different from the managed heads?
- if it owns the keys the it's some kind of node
- connect to the testnet? => still needs to be done
- plutus limitations are pretty artificial? tied to the whole validation chain? or afraid of opening Pandora's jar?
- timebox 1/2 day, attach metadata to the tx as a way to keep track of it
Goal: A pixel painting application running on a Hydra head => Each Head is a single painting session
-
We (SN + MB + AB) operate 3 hydra nodes interconnected
-
Hydra nodes are connected to the testnet
-
We are using direct chain -> keys are owned by the hydra nodes (== ETE setup)
-
Client (for off-chain tx) has the same (cardano payment key) to post tx
-
GUI:
- a browser client that can connect to a hydra node, reads the Tx metadata and paints the pixel on a JS canvas
- client takes user input to build the tx for each post to send to API server
- displays remaining funds to spend (~ UTXO provided from the API)
- (dirt-road?): TUI display
-
Level 1:
- a simple tx paying yourself with metadata containing pixel info
- commit is just there to start the ball rolling
- it does no rules, no enforcement but has (low) fees => you cannot go on forever
- where to the fees go? => the fanout poster would get them as balancing output (change)
- Head closes => we commit back the funds you have
-
Level 2: Do more logic in the head to enforce some ppty of the head
- does not change game logic, only adds more rules inside the head
- representing the state:
- state in datum: canvas controlled by a script => checks the painted pixel is not already bought and the tx pays enough for it
- state in value: mint 1 token for each pixel => need a minting policy and script anyway => burn each token when painted
- when head is closed, result of the painting is represented on L1 => script needs to know whether or not it is in a Head or not to have different behaviour (you cannot control when a head is closed)
- outcome is a single output (+ remainder of commits) containing as datum the hash of the painting
- if we mint/burn in the head, this means we cannot close until all the tokens have been burnt (eg. all the pixels have been painted) which makes a case for non-closeable heads
Plan for the afternoon:
- Setup a VM running a hydra-node connected to a cardano-node runing on testnet
Configuring testnet enabled node:
- Download snapshot from https://storage.googleapis.com/cardano-testnet/testnet.tar.gz
- Get configuration from https://github.com/input-output-hk/cardano-configurations/tree/master/network/testnet
Trying to merge all pending PRs using a single merge branch
- Dropping PR that removes the
Party
because it leads to failures we don't understand - Adding some commit to remove a non-useful property test about generators (used to be there because we wanted to check soundness of benchmarks)
Generating signature for testnet archive:
gpg --detach-sign -o testnet.tar.gz.gpg testnet.tar.gz
Configuring testnet machine:
- Download testnet archive from GCP + verify signature
- unpack the archive
- retrieve network configuration files from https://github.com/input-output-hk/cardano-configurations
we are only interested in the
testnet/
directory but that's ok... - define a docker-compose.yaml file that spins up:
- One hydra-node
- One cardano-node
- One prometheus server for monitoring
- Cardano-node needs to use the DB from the testnet archive + configuration from the repo
Managed to get cardano-node up and running inside docker on testnet!
- Need to mount the
testnet
directory completely becauseconfig.json
refers to parent directory Got blocks synced on the testnet...
Now need to configure hydra-node:
- Need to generate cardano keys from
cardano-cli
cardano-cli address key-gen --normal-key --verification-key-file arnaud.vk --signing-key-file arnaud.sk
cardano-cli address build --payment-verification-key-file arnaud.vk --testnet-magic 42
Got a running docker-compose file with cardano-node + hydra-node + prometheus 🎉
Had to upload various files:
- Signing keys for hydra + cardano
- genesis + protocol params for in-head ledger
- prometheus.yml file
TODO: use secrets for the docker-compose: https://stackoverflow.com/questions/42139605/how-do-you-manage-secret-values-with-docker-compose-v3-1
The hydra-node container needs to be restart: always
to wait for the socket file
Also need to make sure the network id matches:
- We want to export the network magic in an environment variable:
jq .networkMagic cardano-configurations/network/testnet/cardano-node/config.json
Added Terraform rule to allow 5001 port connection on hydra machines => Which inadvertendly destroyed the hydra-testnet
machine... 😭
Trying to connect to the remote hydra-node's API
-
Lost time trying to connect to port 5001 which worked but obviously does not speak WS because it's the Hydra network port!
-
Forwarding with ssh 4001 locally works though:
% websocat ws://localhost:4001 {"me":{"vkey":"000000000000002a"},"tag":"Greetings"}
Tried to use the hydra-tui
pulling the image and running container, but then we run into a problem: The TUI needs a connection to the cardano-node to be able to get the commit signed!
-
Connected TUI on the remote host:
$ export NETWORK_MAGIC=$(jq .networkMagic cardano-configurations/network/testnet/genesis/shelley.json $ docker run -ti --network curry_default \ -v $(pwd)/arnaud.sk:/arnaud.sk:ro \ -v $(pwd)/ipc:/ipc ghcr.io/input-output-hk/hydra-tui:latest \ --connect hydra-node:4001 --node-socket /ipc/node.socket -k /arnaud.sk -n $NETWORK_MAGIC
Trying to connect SN/AB/MB nodes together, having troubles... Everyone can starts his node, but we don't see each other
- Spent some time integrating AsyncApi into Docusaurus.
- I did find an official react component for AsyncAPI which sounded exactly like what I needed.
- However, integrating the component turned out to be everything but a smooth experience. Behind the scene, Docusaurus handles dependencies using WebPack (as many JavaScript frontend project) which can be tricky (read: a hell) to configure correctly; especially because Docusaurus doesn't give explicit access to the webpack configuration so one has to write Plugins to manipulate those.
- Another difficulty comes with how Docusaurus is actually architectured. It is indeed both a client and server application, though the server application is "statically compiled" using server side rendering and then served as static files. This adds on top of the setup complexity as some components have to work on both frontend and backend contexts.
- I did spent about 3h wrangling with Webpack and Docusaurus and got close to something but never really into a fully working solution. Along the way, I also found: redocusaurus which is a plugin for Redoc - a tool for showing OpenAPI documentation from which AsyncApi is greatly inspired. So maybe a good inspiration to do this properly later on.
- "Fun" (for some definitions of fun) fact: I did find a project successfully using the AsyncApi's React component within a Docusaurus setup; cloning the project and trying out does work. But interestingly, they also use
redocusaurus
as their documentation is a hybrid of OpenAPI and AsyncAPI. Yet, removing theredocusaurus
plugin (which is arguably unrelated to AsyncAPI) breaks the setup! Indeed, the plugin does mutate parts of the docusaurus configuration and mounts webpack loaders that are necessary for the AsyncAPI React component to work! - Eventually I gave up fiddling with React and WebPack and grabbed a
useEffect
sledge-hammer where I **shamelessly- create a script DOM element client-side to run plain vanilla JavaScript! (what a bold thing to do right! 🤦). - Outcome of the story: (a) we now have an integrated AsyncAPI doc in our user manual and, (b) stay away from frontend JavaScript, seriously, just use Elm.
- Fixing the build of our ensemble branch on #241
- Seems like to be a flaky test
- After 1h debugging it turns out that the
genAbortableOutputs
was generating overflappingTxIn
forinitials
andcommits
, which collapsed when put in the sameUTxO
- fixed this directly in
genAbortableOutputs
- fixed this directly in
- Also changed the
Arbitrary TxIn
instance to use a "more random"genTxIn
which we already have defined and added regression tests to check for reasonable (> 100k) collision resistance onTxIn
,TxId
,VerificationKey
andHash
generators.
- Address a failing
collectCom
test which was missing PTs in the commit outputs - Now the budget is exceeded and we need to reduce to even
3
participants in theTxSpec
test ofcollectCom
. - The end-to-end tests also fail now, but they are using
3
party heads already! - Turns out that
validateTxScriptsUnlimited
et al is running each script with the maximum budget individually. While this may fit for 3 parties, adding / removing the 3commit
script executions from budget it's not enough anymore.- created a follow-up task to still fail if we exceed cumulative budget -> https://github.com/input-output-hk/hydra-poc/issues/255
- *IMPORTANT Increased the memory budget on
hydra-cluster
config to20M
(from14M
)- need to optimize
collectCom
to be able to run on testnet (currently max memory16M
) -> https://github.com/input-output-hk/hydra-poc/issues/254
- need to optimize
- Question of the week: what about minting / burning inside a head?
- No deep thoughts on the topic from research
- Generally speaking, you can't de-commit something that wasn't committed.
- Must re-mint or re-burn values during the fanout
- Discussing maybe "wrapping" policies on-chain
- The wrapped script becomes the actual script used in head and outside of the head (same policy id), and contains branching logic which runs differently depending on the context.
- In practice, this only works for new tokens. Indeed, any token that exists prior to a head and that has a minting policy which does not allow minting inside a head will generally not be "mintable" inside a head.
- Secondary question / discussions about Plutus changes from Orbis
- Zk-Rollups solutions are still very early
- People shouldn't be writing plutus-core and should write PIR instead
- TinyRAM -> performance concerns from researchers
Working on https://github.com/input-output-hk/hydra-poc/issues/241
- We want to get our
ownInput
, extract the policyId from the state token and check the PTs match
Got errors on both healtyTx and mutated ones after introducing MutateHeadId
:
- Healthy Tx not being valid is just fine because it's lacking the PTs in the commit outputs consumed
- Mutated Tx error is weirder: Seems like it's still counting commits, perhaps because it's mixing up ids?
Adding the PTs to the commits makes healthyTx test pass \o/ but mutation still fails mysteriously
- Problem comes from
ChangeInput
: It removes redeemer corresponding to the input's script which probably means that the validator is never executed
Adjusting redeemers map depending on how we change the input makes mutation tests all pass but validates
test for collectCom
fails with an overspent budget.
When reducing the number of commits to 5, it fails with the following error:
Script Evaluation(s):
- RdmrPtr Spend 0: WrapExUnits {unWrapExUnits = ExUnits' {exUnitsMem' = 1284850, exUnitsSteps' = 531545530}}
- RdmrPtr Spend 1: WrapExUnits {unWrapExUnits = ExUnits' {exUnitsMem' = 1284850, exUnitsSteps' = 531545530}}
- RdmrPtr Spend 2: WrapExUnits {unWrapExUnits = ExUnits' {exUnitsMem' = 1284850, exUnitsSteps' = 531545530}}
- RdmrPtr Spend 3: WrapExUnits {unWrapExUnits = ExUnits' {exUnitsMem' = 1284850, exUnitsSteps' = 531545530}}
- RdmrPtr Spend 4: WrapExUnits {unWrapExUnits = ExUnits' {exUnitsMem' = 1288174, exUnitsSteps' = 533518493}}
- RdmrPtr Spend 5: ValidationFailedV1 (CekError An error has occurred: User error: The provided Plutus code called 'error'.) ["PT5"]
- Start work with goal "tests should speak for themselves" in terms of our limitations
- Assessing the inventory: tx size tests are now in
StateSpec
and use32kB
tx size - Lowering
Fixture.maxTxSize
to16kB
and make it configurable - Printing tx size limit (as it varies now) in the hspec test description
-
TxSpec
still contains a fanoutTx size test which is stillexpectedFailure
? why is this not working? - It seems like
genUTxO
is generating Ada-only TxOuts and ourArbitrary UTxO
is generating multi-asset TxOuts .. is this intended?
(Start of derailing into configuring generators)
- Seems like only
genUTxOWithSimplifiedAddresses
is using thearbitrary
instance of UTxO -> changing it to use the Alonzo ledger and thus (seemingly) only producing adaOnly values is fine - No, of course it's used in more places.. still, we might not care about assets too much?
- After a quick discussion within the team we agree that the
arbitrary
instance should sample ALL the cases and filters/more specific generators shall be used in situations where we care - Why were we not using the
Arbitrary UTxO
instance ofcardano-ledger
? TheAlonzo
era instance seems to be sampling everything relevant (multi-assets, ada-only, shelley/byron addresses and even datum hashes) - Using the
Mary
eraarbitrary
instance seems not to have any benefit over theAlonzo
one. So we might be really be ending up withgenUTxO = fromLedgerUTxO <$> arbitrary
!? - Maybe the reason of using the
genUTxo0
was the ability to generate a sequence of valid transactions? i.e. if we usearbitrary
, we might end up with outputs which we do not know how to spend (did this work before "by accident" as we used the samegenEnv
ingenUTxO
andgenTx
before?)
Configuring hydra-nodes deployed on VMs:
- Start from a docker-compose template based in jinja
- Process templates for each target node passing in the information about each peers
- This yield one directory per target node containing:
docker-compose.yaml
+ signing keys for this node + all verification keys to the peers - Actually, probably better to parse the Yaml and get back a python dictionary that's manipulated in code
- This yield one directory per target node containing:
- Upload the directory/files to the corresponding nodes (scp?)
-
ssh ... docker-compose up -d
to start the hydra-node on each VM, can be done "manually" at first then we can automate that into a (python) script
Ryan's work on setting up a distributed infrastructure for testing Hydra nodes highlights some operational issue with the way hydra-node is configured and started: The node is configured by passing it the IP:port of its peers, as well as all the vkeys matching the peers, and of course its own signing keys.
Doing this for a cluster of machine is somewhat cumbersome as one needs to:
- Spin up the machines
- Know the right IP addresses for each machine s.t. the machines can communicate with each other
- Use that information to build the command-line parameters for each node and then write the corresponding
docker-compose.yaml
file to ship to each machine (or run the node directly on the machine?), where each command-line is obviously different
It would be easier to be able to start a node with only its "local" information, eg. own signing keys and perhaps connectivity to L1 chain, and then configure its peers at runtime. We already have connectivity detection in the network layer so a node can know whether or not all its peers are connected, subject to usual limitations about failure detectors in distributed systems. I had this PR #222 which used a file-based configuration but it really sucks, we should probably do the same thing but through a REST API, providing:
- Incremental configuration ability, eg. add peers one or a bunch at a time,
- Go/reset function to let the node know when it can start trying to connect to its peers,
-
GET
routes to provide information about the state of the config.
Writing ADR 15 to propose exposition of an Admin API for Hydra node.
Discussing how to handle backlog of work items (in contrast to features
which are already in GitHub for roadmapping).
- Conclusion: Let's try to be more disciplined in Miro backlog before embarking into more structured tool.
We need to extract ownInitialToken
from the PT from the Initial, which is relateively straightforward
CollectCom
validator does not currently do anything with the parties
, whereas it should check that all parties have committed.
- We have a mutation for removing an input, but it fails (eh, succeeds!) because this introduces imbalances in values in the output, so we need to change the head output value to remove what's committed.
*This requires to introduce 2
Mutation
s: One to remove the input, one to change the (only) output's value
We realise there's no helper function to "resolve" a TxOut
from a TxIn
given a UTxO
, seems like we always unpack the UTxO
type -> Add resolveUTxO
function to cardano API
The problem is that we want the CollectCom
to ensure it collects all commits from all parties.
- Right now, we can just count the parties and check commits are there => mutate the numner parties in the input and check it fails.
- Adding mutation to add a party to the head input -> fails because we also need to change the output's datum
- Problem with changing the output datum is that we also need to change the hash which is referenced in the
txDats
field of the tx's witnesses. But this is kind of odd: Why do we need to provide the actualDatum
for the outputs of a tx we are constructing?
We realise that to check all the parties have committed in the CollectCom
we need the policy Id to find the PTs, which means we need to parameterise all the scripts with it... :(
- Right now we simply check the number of commits matches the number of parties but of course this is weak.
- We could add the policy id in the datum of the head output which makes it easy to check 2 things:
- That all the legitimate parties have committed (or aborted)
- That the poster of the Head SM transaction has the right to do so, because it is in the participants
MB observes that we can actually use the ST which is part of the Head output as this already contains the CurrencySymbol
and it's a valid ST by construction.
- But what if the head output contains more than one ST? => we can't enforce that because the
InitTx
transaction does not run any validator
Next steps:
- Check the PTs in the CollectCom instead of only checking the number of parties matches the number of commits
- Improve the robustness of
Abort
mutation by also checking the PTs there
For the distributed benchmark:
- figure out the visible IP addresses of all nodes (through VPC or public IPs?)
- generate Hydra sks for all parties
- generate Cardano sks + derive vks for all parties
- expose 5001 port for hydra networking
- set
--peer
parameter for each peer with the assigned IP:5001 - connect the cardano-nodes into a proper cardano network... => that's trickier even
Discussing issues related to minting and burning inside the head.
- We should by default disallow it, as not "doing it right" could make the Head not closeable at all time which is a desired security property. Should users have use cases that need minting/burning inside a Head, we could add a flag to allow it.
- This should be handled in the wire format of the transactions we can pass to the
newTx
=> Parse, don't validate
Handling verification of TxOutRef
passed to minting policy: We need to verify the input is actually consumed by the transaction
- Adding a mutation to remove inputs from a transaction, which surprisingly was never implemented before -> Could be useful for other kind of transactions (CollectCom, Commit)
- Added mutation to check removing inputs fail CollectCom and Abort transactions
Next step: Ensure the Initial
validator checks the signatory of the transaction is the corresponding party from the PT
- We can mutate the
extraKeyWitnesses
field of thetxBody
which contains, obviously, required keys that must sign the tx without needing to actually sign it. The ledger does not know about the signer for the commit tx because it is consuming from a script, hence the need to add arequiredSigner
We first add "naive" validator that checks there are signatories -> thsi should fail the healthyTx
Then we'll add a mutation to change signatories
-
MutateRequiredSigner
changes the pkh of the signer in order to trip our naive validator and force us to actually check identity of signer - In the
Initial
validator we need to verify the PT'sTokenName
is included in required signatories Which means we need to lookup the PT, thus requiring to pass thePolicyId
to theInitial
validator - We got tripped by the
PlutusTx.AssocMap
being usually imported qualified asMap
=> UseAssocMap
to make sure it's distinguished from Haskell'sMap
? OTOH, we must not use Haskell libraries in Plutus code so calling itMap
should be fine 🤔
Goal for Initial validator:
- We expect value attached to txOut to be well-formed: Some ADAs + a single PT => We don't check the
currencySymbol
in this validator - We only need to extract the
TokenName
from the one and only PT and check it's in the required signers
Removing the datum from the Initial
makes validator code simpler but its side-effect on off-chain code is that we can't retrieve our PT/Own key anymore from it, so observation of Init fails.
Adding mutation in Abort
transaction to check we are correctly burning all the PTs.
Adding a check for the number of PT/STs burnt to the minting policy makes the Abort
tx fail with budget exhausted:
1) Hydra.Chain.Direct.Contract.Abort is healthy
Falsified (after 1 test):
Phase-2 validation failed
Redeemer report: fromList [(RdmrPtr Spend 0,Right (WrapExUnits {unWrapExUnits = ExUnits' {exUnitsMem' = 1658994, exUnitsSteps' = 690025855}})),(RdmrPtr Spend 1,Right (WrapExUnits {unWrapExUnits = ExUnits' {exUnitsMem' = 1
348266, exUnitsSteps' = 589829979}})),(RdmrPtr Spend 2,Right (WrapExUnits {unWrapExUnits = ExUnits' {exUnitsMem' = 1749677, exUnitsSteps' = 726641323}})),(RdmrPtr Spend 3,Right (WrapExUnits {unWrapExUnits = ExUnits' {exUnitsMem' =
1348266, exUnitsSteps' = 589829979}})),(RdmrPtr Mint 0,Left (ValidationFailedV1 (CekError An error has occurred: User error:
The budget was overspent. Final negative state: ({ cpu: 6019809806
| mem: -1656
- Tried to bump execution units in
pparams
but turned out I changed the wrongpparams
as there are 2 of them -> unifying those for testing purpose inEvaluate
module, adding some documentation in the process
Tests are now failing because the abortTx
and fanoutTx
do not burn all the tokens!
- The
BurnOneMoreToken
mutation is not powerful enough: It passes but the abortTx does not burn the commits, and this is because the healthy abort tx does not have PTs either. - Should add the PTs to the healthy tx and then tests will fail for the right reason.
Healthy abortTx
now fails: The PTs are part of the initials and commits but they are not burnt properly.
- Implementing burning PT in
Abort
tx: We use the same strategy than in the test, just burn everything - Budget is overspent for burning because our code is inefficient: we
foldMap
over all values in inputs, which includes unrelated tokens, then filter again on the full list
It's hard to distinguish Initial
txs from Commit
txs: Fixing the generated initials so that we only have 2ADAs value and no more values
Got a tricky error in mutation for RemoveOutput
: We don't understand why the removed output makes the transaction validated
-
removeAt
crashes if index is out of bounds to make sure we are using properly - Turns out we needed to:
- Ensure there's at least 1 commit in the generated
healthyTx
- Take care of the fact an commit can be empty thus does not generate an output in the abort tx
- The only outputs of an abort tx are the reimbursed commits
- Ensure there's at least 1 commit in the generated
Fix burning of tokens in the FanoutTX: => All tests pass 🎉
- Actually, a
DirectChainSpec
test fails because we don't pass the cardano keys so we don't correctly generate the PTs in theInitTx
Mostly completed addition of ST and PT to the transitions, next steps:
- Use the
TxOutRef
in the minting script to ensure we mint the right thing - Use the PT as authentication mechanism for hte close/contest
- Extract pkh from the observing the PTs
- Use pkh to validate commits
Managed to have a 40 nodes benchmark running by bumping slotLength
to 5s and tweaking timeouts inside the EndToEnd
module for benchmarks
Discussing the issue of scripts time execution:
- We should benchmark the
collectCom
transaction both for CPU and Ex Units
Merged MB's branch on parameterised ledger
Working on minting PTs
We can't check the correspondance between minted token's token name and parties verification key so for now we just count the number of tokens -> this would require either passing the vkeys as parameter to the minting policy, or rather enhancing Party
which is payload for Head state's datum to contain the vkeys.
Note; (-)
symbol... does not work in Plutus, one has to use negate
:rolling_eyes:
We can easily mint PTs because we have the Cardano keys -> transform mintTokens function to take a list of AssetNames
- Burning all tokens is hearder because we don't have the list of VKs handy
-
HeadParameters
in healthy tx was arbitrary, but we need it to be consistent with number of vkeys generated -> Now healthy tx passes but mutation fails because we change the quantities of tokens
Need to update FanoutTx
mutator to handle burning of PTs and ST
- FanOutTx is not healthy, so we need to fix it now. But this implies we need to pass the cardano vkeys to create the fanout tx => we need to keep those around in the State => we need them in the
Party
The overall plan to complete issue:
- Add PT to the initial output in the Init tx and corresponding mutation
- right now, we only add the ST to the head output, but not the PT to each initial output
- Add cardano key hashes to Abort and Fanout tx so that they can burn the PTs
- Observe them from the init tx, looking at the PTs in each initial output and pass them around to the transaction constructors as needed
Changing the output value of the Init tx's Initial
outputs, not the head output, in order to trigger validation in minting policy that enforces the PTs are distributed to the right parties.
While adding a check that PTs are distributed, I got an "interesting" error from Plutus compiler:
1) Hydra.Chain.Direct.Contract.Init is healthy
uncaught exception: ErrorCall
Error: Unsupported feature: Use of Haskell Integer equality, possibly via the Haskell Eq typeclass
Context: Compiling definition of: GHC.Integer.Type.eqInteger
Context: Compiling definition of: Hydra.Contract.HeadTokens.participationTokensAreDistributed
Context: Compiling definition of: Hydra.Contract.HeadTokens.validate
Context: Compiling expr at "hydra-plutus-0.3.0-inplace:Hydra.Contract.HeadTokens:(91,8)-(91,101)"
(after 1 test)
It's because I pattern match on a constant number 1
to check tokens
We need to check the number and values of initial outputs is correct too
- Adding PTs to Initial outputs requires changing the
Commit
mutator too because we need thePolicyId
to build the input for the commit transaction tested - Checke the number of distributed PTs equals the number of parties in the transaction. I'd rather separate the 2 checks (quantity of PT and number of PT) for clarity's sake but there's room for some optimisation there
- Mutation healthy test for Commits pass when I had the value from initial output to the commit.
- It seems there's a lot of redundant work we are doing now because we extract the key hash to pay to from the datum instead of the PT
Now have 9 tests failing, for various reasons related to the transport of the PTs of course.
- The fanout and abort needs to burn them, so that could be next mutation test to add
The error in the healthy fanout test is puzzling:
1) Hydra.Chain.Direct.Contract.Fanout is healthy
Falsified (after 1 test):
Phase-2 validation failed
Redeemer report: fromList [(RdmrPtr Spend 0,Right (WrapExUnits {unWrapExUnits = ExUnits' {exUnitsMem' = 1688218, exUnitsSteps' = 683082436}})),(RdmrPtr Mint 0,Left (ValidationFailedV1 (CekError An error has occurred: Use
r error:
The provided Plutus code called 'error'.) ["expected single head output"]))]
...
Tx: "729ad8c68c25f64a31b797d8963d4f05eba9fa2464d02f870e8faa3b62bb827f"
Input set (1)
- 31237cdb79ae1dfa7ffb87cde7ea8a80352d300ee5ac758a6cddd19d671925ec#455
Outputs (7)
total number of assets: 0
- 3716473 lovelace
- 12783723 lovelace
Minted: TxMintValue MultiAssetInAlonzoEra (valueFromList [(AssetId "53178c5f27dd526942cf4624b0d7edfad46078102c4b24b37d98740a" "HydraHeadV1",-1)]) ViewTx
When we Burn
we use the same logic as Mint
, trying to locate Head script to pay to but the fanout transaction has no head output!
Added a check we burn state token in fanout tx, now need to check we burn all the PTs which is not so obvious => In the head state at the point of fanout we don't have the parties anymore, and the number of outputs can be different from it anyhow so it's not clear what's the way to do that
Looking at the Abort
mutations trying to see how to ensure we burn the ST/PTs correctly
-
Mutating the value burnt in the
Abort
mutation script to ensure we correctly burn everything -
Added burning of tokens to
AbortTx
but now thevalidates
test is failing:test/Hydra/Chain/Direct/TxSpec.hs:157:7: 1) Hydra.Chain.Direct.Tx.abortTx validates Falsified (after 1 test): TxIn "03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c111314" (TxIx 393) 0s [] ([],[]) Input utxo: { ... Redeemer report: fromList [(RdmrPtr Spend 0,Right (WrapExUnits {unWrapExUnits = ExUnits' {exUnitsMem' = 535086, exUnitsSteps' = 215724901}})),(RdmrPtr Mint 0,Right (WrapExUnits {unWrapExUnits = ExUnits' {exUnitsMem' = 427658, exUnitsSteps' = 177590399}}))]
-
The property fails because we expect less successful scripts in
Redeemer report
: The mint script accrues to this, so adding 1 more makes it pass -
All unit tests now pass
-
It's annoying the input utxo shows the token name in hex whereas the minted values are shown as text
- Working on the
HeadTokens
minting policy, namedμ_head
in the diagrams - After checking that the right quantity of state thread tokens is minted, we realize that they can't be burned anymore (negative quantity)
- A simple
|| quantity == -1
suffices, but this makes it hard to distinguish policy failure - Introduced
MintAction = Mint | Burn
to distinguish minting and burning - Next, we thought added a mutation which tries to add another PT to the
init
tx - To address this we first tried to
[PubKeyHash]
as a parameter toμ_head
to ensure the right PTs are minted.. but this ripples everywhere and we can achieve the same result if we would look at theinit
tx token whereHeadParameters
would be available - This means, that the
μ_head
needs theValidatorHash
ofν_head
, which is currently parameterized by aMintingPolicyHash
-> that one is unused and we want to get rid of it anyways. - Removing policyID to identify heads leads to ETE and other integration tests failures, although the policyID is not used
- Seems like the failure comes from the fact we only validate minting but not burning of state token, so fanout transaction fails to validate => changing condition to check -1 or 1 succeeds
- Now, it's unclear when the validator fails, a redeemer could be passed to minting policy to tell it which phases it is in? How do you tell Plutus which type the
Redeemer
is for a minting policy script? => Just type annotate thewrapMintingPolicy
function, but this requires the type to be defined in another module because of the phase restrictino in TH.
Trying to understand or find a workaround over our problem with 25+ nodes benchmarks not working
- Posted question to the ledger team for insights on why this would happen
- Seems like what's happening is actually simple: The validation time is greater than the slot duration, so the consensus probably keeps trying to validate repeatedly the same transaction over and over
- Trying to increase timeout and slot time in our benchmark with a 40 nodes benchmark to see if at least it can start
Sync-up with Ryan:
- Managed to get several VMs up and running, with number configure
- discussion about distinguishing VM images and docker images
- We want to run docker on the VMs in order to be able to pull the docker images for hydra-node and cardano-node
- Idea: Use podman to remove the need for docker daemon?
- Tricky part is networking: We need to know the names/IPs of all hydra-nodes beforehand hence having a VPC with well-known subnetwork and address space would be helpful, but we could also query the IPs of all the spin up images
- Next goal: Have one VM with a Hydra node up and running, even unconnected to anything -> would prove we can run docker and docker-compose
Discussing what to do next and what's the current state of master
- We observe there's an irregularity in the
observeXXX
functions whereby theobserveInitTx
andobserveCommitTx
take more information than the others -> we could make them agnostics of the particular head and have the upper layer decide (eg.State
module?). This would pave the way to implement a global Hydra Heads Observer.
How to drive dev of PTs?
- Could implement monetary policy which is currently
const True
, create the PTs and thread them around - Or use the PT in the observation/verifier?
Start from the Monetary policy and check it enforces creation of ST + PTs + burning
- How do we test that?
- We don't have a
Init
mutation based test and the only thing we can check there, the only script that runs would be the monetary policy => AddInit
module for mutation + relevant mutations and tests - Add a
healthyInitTx
and relevant property inContractSpec
, got a valid healthy initTx test passing, now tackling some mutation
Implementing mutation:
- We don't want to change the output but the
TxMintValue
- We cannot easily alter the
PolicyId
because we need the witneesses to be correct otherwise the failure will surface at stage 1 validaqtion - While implementing
ChangeMintedValue
we realise the cardano api does not provide a way to reconstruct the body, hence the need to go down toLedger
layer. The latter is actually easier to work with in this case because themint
field is just aValue
- Our mutation succeeds (eh... fails) because we generate an arbitrary
Value
hence we have a validation failure because the monetary policy script does not exist
Q. for researchers: What happens if we have multiple thread tokens? => it's not a Head so we ignore it but there migt be a use?
- We need a proper user guide so that other people can start using it, so that we can have users!
- Seems like some people are trying to use Hydra node -> Want to use Hydra heads in a specific manner
Having to switch between branches when there are dependencies chanvge is a PITA. Context switch always takes time but in this case it's heavily materialised in the time it takes for nix and cabal to get up to speed.
Decided to drop stalled PRs:
- 230: No consensus there is a need for such a feature now so no need to waste time discussing it
- 229: Adding more CLI parameters just for the sake of running a mock-chain for testing purpose only is not conducive to simpler and more "habitable" code. This would be more useful in relationship with the above PR, but all in all we would rather have our benchmarks run using a real chain anyway.
- 222: We don't really that feature and it's cumbersome and awkward. What would make more sense would be an "Admin API" of sort.
Working on identifying heads and being able to have multiple concurrent heads for different nodes
We have 2 slightly different situations to cope with:
- A party (identified by a key) is involved in 2 heads: The problem is within the wallet, we are using the same address in 2 wallets/nodes
- depends on whether we allow reusing the same address for 2 heads? 1 node = 1 key that's generated when the head is started
- alternative: The user passes the cardano key for the head and can therefore reuse the same address across different (possibly concurrent) heads => need to take care of change/fuel in the Wallet
- 2 heads with disjoint parties are running concurrently => We want to pass the ETE test first, so ensuring keys are disjoint should be fine
Problem is that we are reusing the party identifiers for 2 different heads
- We need to do the same for hydra keys as we do for cardano keys: Generate them for each different Head (at the moment)
Test w/ 2 heads passes once we ensure the parties are different
Next steps:
- Separation of wallets: Separating fuel and commit wallet, separating keys for commits Define the interface with the commit wallet -> implement it with our tiny wallet but leave room for other wallets
Benchmarks are now not running properly: They start once we pass the correct SigningKey
but they don't make any progress.
- Adding some reason to
Wait
outcome and logging it to understand why a transaction is not validated inside the head - Problem was caused by a filter having been removed during a merge/rebase from the list of node ids defining the "other" parties when startging
HydraNode
Lessons (re)learnt:
- Branches/PRs should be short-lived: That branch has been opened for 3 weeks, with lot of inactivity in between
- Having flaky CI/tests leads us to not paying attention to them failing when pushing, thus delaying fixing pbs
Trying to fixup branch identify-heads
still having troubles with ETE test:
test/Test/EndToEndSpec.hs:228:7:
1) Test.EndToEnd, End-to-end test using a single cardano-node, two hydra heads scenario, two heads on the same network do not conflict
waitForAll timed out after 5s
nodeId: 0
expected: {"utxo":{"9eee9f18d44c2817d6e3b781e2b99d8163bebeabf8c6a75dbb8260c7a3c00159#1":{"address":"addr_test1vzck20v4k8twswud7e75httptdwr7avx2n50zmy0hw7hu9cksa7eh","value":{"lovelace":5000000}},"de3425a8127a66730dc8cdf49bc1f503de6e8930deff69eec69951de1fcccb3c#1":{"address":"addr_test1vpa80s0qhurt2xzd8qf936rtz3d8vrcg05nx7mqr55kkd2grtkluh","value":{"lovelace":20000000}}},"tag":"HeadIsOpen"}
seen messages: {"party":{"vkey":"0000000000000014"},"utxo":{"9eee9f18d44c2817d6e3b781e2b99d8163bebeabf8c6a75dbb8260c7a3c00159#1":{"address":"addr_test1vzck20v4k8twswud7e75httptdwr7avx2n50zmy0hw7hu9cksa7eh","value":{"lovelace":5000000}}},"tag":"Committed"}
{"party":{"vkey":"000000000000001e"},"utxo":{},"tag":"Committed"}
{"postChainTx":{"committed":{"de3425a8127a66730dc8cdf49bc1f503de6e8930deff69eec69951de1fcccb3c#1":{"address":"addr_test1vpa80s0qhurt2xzd8qf936rtz3d8vrcg05nx7mqr55kkd2grtkluh","value":{"lovelace":20000000}}},"party":{"vkey":"000000000000000a"},"tag":"CommitTx"},"postTxError":{"input":"TxInCompact (TxId {_unTxId = SafeHash \"de3425a8127a66730dc8cdf49bc1f503de6e8930deff69eec69951de1fcccb3c\"}) 1","walletUTxO":{"def20363c3543928880bbe5de002c16780a4e0248f98c5c651369521ffb06daa#1":{"address":"addr_test1vqpgwgasgcex2xv6ll2nmmz49e3p4v7nns8h9xjfzf5r9vg07mwqu","value":{"lovelace":5000000}},"059b79e443921cd0c915a29bc7441ceab2286f841b7ecf2233a9ded8d5acbfd5#4":{"address":"addr_test1vqpgwgasgcex2xv6ll2nmmz49e3p4v7nns8h9xjfzf5r9vg07mwqu","datumhash":"a654fb60d21c1fed48db2c320aa6df9737ec0204c0ba53b9b94a09fb40e757f3","value":{"lovelace":88557750}}},"headUTxO":{"059b79e443921cd0c915a29bc7441ceab2286f841b7ecf2233a9ded8d5acbfd5#2":{"address":"addr_test1wqjr7zxq85sr9yp0hcwkx0fu82mdcatekt2hvwm2hnlxfzss42u4q","datumhash":"076e9093310212f402871720788d90cc426b888f2b20df5072820d4d87a55783","value":{"lovelace":2000000}},"059b79e443921cd0c915a29bc7441ceab2286f841b7ecf2233a9ded8d5acbfd5#3":{"address":"addr_test1wqjr7zxq85sr9yp0hcwkx0fu82mdcatekt2hvwm2hnlxfzss42u4q","datumhash":"41ca0d6ad487f46acfc4d62422436b7b99062f0f5db96919da12f360c3fcc7eb","value":{"lovelace":2000000}},"059b79e443921cd0c915a29bc7441ceab2286f841b7ecf2233a9ded8d5acbfd5#0":{"address":"addr_test1wpvkdkyl5qh4fhqg84sgdew5gt2zfzjtsypgfxsttllfs6gwtuezs","datumhash":"c2f7589a052854c8877e74b7ec3de892981766ef819fc03bc8c893daf66dd72e","value":{"bc3fb393410948029c8c61601c16a630e230e80b15df3cf8adf87061":{"4879647261486561645631":1},"lovelace":2000000}},"059b79e443921cd0c915a29bc7441ceab2286f841b7ecf2233a9ded8d5acbfd5#1":{"address":"addr_test1wqjr7zxq85sr9yp0hcwkx0fu82mdcatekt2hvwm2hnlxfzss42u4q","datumhash":"8a4d4a889dc047d35958d4d31f0942764826f34400b9da3806cf958de23ffe92","value":{"lovelace":2000000}}},"tag":"CannotSpendInput"},"tag":"PostTxOnChainFailed"}
Increasing timeout to 10s on each action fixed it.
Plan for scaling up Hydra node benchmarks:
- Describe needed infrastructure using Terraform over GCloud IOG-Hydra project.
- We need to be flexible in the number of VMs we deploy, so this should be a parameter of the deployment
- VMs should be lightweight, possibly single core
- They should be interconnected by VPC network
- of course, we should also be able to tear it down/recreate it at will
- VM images should be able to run docker
- Deploy Hydra nodes over the given VM
- One node per VM at first (we know each node needs 1 CPU to be effective, there's possibly more bottlenecks with RAM we should explore later)
- Could be interesting later to be able to have multiple Hydra nodes per VM
- We should use docker images for that => need to have docker installed on the VMs
- Deploy mock-chain to own or shared VM
- mock chain is not CPU intensive as its role is confined to relay the init/commit/collectCom/close/fanout tx dance
- it just need to be accessible from all hydra-nodes
- also should be run from the docker image
- Remove infrastructure setup code from hdyra-cluster' benchmark
- Should split the chain/node spinup part from actually running the clients
- Benchmark can be run assuming infrastructure/nodes exists
- nice side-effect is that it would allow running load tester against real infrastructure, something possibly useful for users
- Load tester should be able to run for a definite duration instead of running with some predefined number of txs
- => tx should be generated on the flight
- Run several different loads
- dimensions:
- number of nodes
- distribution (different DCs, different regions, different continents/zones)
- duration/load
- dimensions:
- Automate daily load tester to run in CI
- goal is to have a daily report available for some interesting (target) parameters
Working on distributing funds through the faucet
- For the benchmark, we cannot use mkGenesisTx anymore as the faucet would need to distribute funds to each dataset/actor.
- One thought: use a genesis tx which does distribute to all datasets/actors in one transaction
- A bit more flexible: Just denote the starting utxo/value/lovelace and use
seedFromFaucet
to kick off execution of the dataset. - Unfortunately, this is not possible as we would not know the TxIn a priori
- So the first idea is the only alternative? Use a generalized
mkGenesisTx
and distribute funds in one go to all datasets/actors - After generalizing
mkGenesisTx
a refactor of theDataset
type is overdue to keep some sanity in generation and execution of these datasets - After splitting things into a
ClientDataset
and generators are fine, I realize it would also be good to know the distributed individual UTxO (needed in the bench to commit) - Benchmark compiles, but the the funding transaction fails: https://github.com/input-output-hk/hydra-poc/pull/227#discussion_r813131200
- It says unbalanced value, but why is there only one output in the tx of the error?
Working on https://github.com/input-output-hk/hydra-poc/pull/229
Odd compilation error appearing on CI run:
<command line>: /home/runner/.cabal/store/ghc-8.10.7/cardano-crypto-praos-2.0.0-8ff763aa71b349aa3d64f01fa06094c245c2408b75c7dbea00d4bc4327ab5207/lib/libHScardano-crypto-praos-2.0.0-8ff763aa71b349aa3d64f01fa06094c245c2408b75c7dbea00d4bc4327ab5207-ghc8.10.7.so: undefined symbol: crypto_vrf_seedbytes
cabal: Failed to build hydra-node-0.3.0 (which is required by test:tests from
hydra-tui-0.4.0, exe:hydra-tui from hydra-tui-0.4.0 and others).
Trying to troubleshoot issue with CI, I already had it before (https://sensei.app.pankzsoft.com/#/notes/2021-10-09) but it's not clear how the issue was solved.
- Seems like some dependencies are messed up but I don't understand why this would be the case as I have not changed them, except to add
zmq4haskell
- Trying
cabal clean && cabal build all --enable-tests
=> I can reproduce the error locally after acabal clean
- Trying
nix-shell -A cabalOnly
and thencabal build all
as it seems this was relevant to the issue we faced last time? => does not work either - Trying again after a
cabal update
and it works -> I always forget tocabal update
, why does it not do it automatically like every other tool out there? Updating should be opt-out
- Modified
Tx
et al. to take care of transporting the state token across the various transations - With the
fanoutTx
we hit a small snag: We need the actualPlutusScript
to be able to burn the token(s) which requires carrying it in theOnChainHeadState
as it's not observable from the transactions other thaninitTx
which mint them.
Options for 50 nodes network:
-
Ditch real cardano network and use mock chain: We don't really care about the chain behaviour
-
We need to change the benchmark runner to generate things on the go to be able to run for extended period time
-
Keep PR about dynamic network configuration around, in case?
-
Use faucets in the tests, TUI -> need to use in the benchmarks too = last missing piece
Discussion about refactoring Direct chain:
- Keep the state as a list of events in the
HeadState
- The
OnChainTx
event could either hold aggregated state, or just event data and state is reconstructed in theDirect
component by folding over the list of events - Constraints-like DSL to simplify/abstract the details of Tx construction
Programming:
- We pick up the identify heads branch again and orient ourselves
- Start by promoting the
HeadId
into theHydra.Chain
layer and pick a chain-agnosticByteString
a its content.. for that we introduce UsingRawBytesHex to get JSON instances - Now we add
headId :: HeadId
to all theOnChainTx
as we need to be able to tell them apart .. and realize this has a rat's tail so only do theOnInitTx
for now.
Started work on resurrecting mock-chain, in orde to be able to scale the benchmarks until direct chain catches up.
- Reverted SN's commit removing its support and will have to enable rewiring it for benchmarks, probably with an option.
- Got a branch compiling and with all tests working, now trying to run a benchmark with mock chain
Need to change the EndToEnd
code as we used to create a UTXO for committing, and also adapt HydraCluster
- Introduced an ADT to handle different types of connections to the chain, works and compiles fine but benchmark fails:
Invalid option `--mock-chain-host'
Usage: hydra-node [-q|--quiet] [-n|--node-id INTEGER] [-h|--host IP]
[-p|--port PORT] [-P|--peer ARG] [--api-host IP]
[--api-port PORT] [--monitoring-port PORT]
[--hydra-signing-key FILE] [--hydra-verification-key FILE]
[[--mock-chain-host HOSTNAME] [--mock-chain-ports [PORT]] |
[--network-magic MAGIC] --node-socket FILE
--cardano-signing-key FILE
[--cardano-verification-key FILE]] [--version]
which is odd as the option is obviously available? => There were options for direct chain mixed up with options for mock-chain
Managed to have benchmarks running with the nodes connecting to mock chain, however it is failing because I am not sure to build the initial UTXOs to commit. Need to have a look at how things were done before...
- I was able to run a 50 nodes benchmark, or at least to start it, using mock-chain based transactions. Of course, on a 4 cores machine it just chokes and does progress very slowly but at least it proves we can spin up a large Hydra network if we take care of the nodes' resources.
- Need now to boot several VMs and spread the Hydra nodes over them to not starve the CPU.
- Created https://github.com/input-output-hk/hydra-poc/pull/229 to merge this work more quikcly and then pave the way towards actual distribution
- Investigating bug https://github.com/input-output-hk/hydra-poc/issues/224
- When increasing the shown number by +1, 10x smaller value is sent to the recipient
- Logging the
amount
provided to thesubmit
function in the dialog, it seems that the wrong value is passed tomkSimpleTx
- How is the validation limiting the field?
- It uses editField, which supports Maybe a -> not an issue as it seems
- What if the validator is true after hitting backspace and false later? the value without 1 / 10x lower would be the latest valid!
- Enforcing dialogs to have
null . invalidFields
works and rendering invalid form fields red did the trick -> opened PR #225.
Trying to reproduce issue https://github.com/input-output-hk/hydra-poc/issues/223 using a fresh VM but without hydra-node cachix instance, which seems to be the only difference with the OP's setting (Assuming nix ensures consistency of everything of course...).
Adapted demo to use file-based network configuration. This required generating JSON files in the prepare-devnet.sh
script and making sure the Hydra nodes are started in the right directory as the filepath to the network-topology.json
file is currently hardcoded. This is not very user friendly and should be exposed as an option but I am not sure I want to go down that route: Configuring a possibly changing network topology using files seems a bit wrong and cumbersome in the long run.
I also had a look at https://github.com/input-output-hk/hydra-poc/issues/223 and thanks to MPJ and SN it seems we have a fix: There was a misconfiguration in the packages
attribute of shellFor
in shell.nix
file, or rather we were explicitly listing local packages which implied the other packages had their cabal dependencies managed by cabal hence the conflict that arose. Not having to list manually the packages is a great improvement to haskell.nix.
Revisiting https://github.com/tweag/plutus-libs/issues/73 as I got pinged by Victor Miraldo. Trying to rebuild it on a fresh VM and see if I still have the same issues.
- plutus-libs seems to build fine even though I have to download a lot of GHC8.10.4 related stuff when entering the shell, not sure why?
Just thought I could write a cheap "benchmark" for collectCom execution time by scaling it in the TxSpec
tests and tweaking execution parameters to give it enough budget to run
Validating a collectCom
transaction with 50 commits takes a while, about 1s
Hydra.Chain.Direct.Tx
collectComTx
validates
+++ OK, passed 10 tests.
Finished in 12.0590 seconds
1 example, 0 failures
How do I start BFT cardano cluster? Our code says withBFTNode
but is this node really working in BFT mode?
Topic: Do we even need the ST if we have the PTs?
-
After walking through the new diagrams we started the discussion on above's question.
-
In the paper with the "Constant depth close" we were re-using the PTs
-
The simplified protocol was not using the PTs and could be burnt in the collectCom
-
No point in keeping them around as it makes the UTXOs bigger
-
Having constant addresses for all Heads simplifies things -> not up for discussion
-
The ST identifies the head output
- What are the consequences / simplifications of this?
- We could get away with only having access to UTXO map, no need to track all transactions
- We don't do that at the moment though, but observe all transactions and do statefully track
- ST encodes "provenance", where it is coming from
-
This is convenient to observe/identify heads (partly) even after submitting the tx
- Blockchain explorers and querying by address also come to mind
-
Removing it now seems like "premature optimization"
-
There seems to be no strict security requirements
- The more complex protocol extension works similar to commit (mirrored) situation
-
Having the ST would simplify the "specification" of what a Hydra protocol transaction / utxo is
-
Current situation:
- We are using ouroboros with a naïve protocol and mesh network topology
- We've had discussions in the past with Duncan to implement some routing/relaying mechanism instead of mesh network
- We need basically broadcast to all parties
- We observe that past a handful of interconnected nodes, establishing connections takes a while
-
Future:
- We need nodes to connect to a dynamically set of other nodes
- We need reliable broadcast among ~100 nodes
- Head networks might be short lived and possibly ad hoc
- We need encrypted transport between nodes for each head
-
Some figures:
- World circumference: 600ms
- One continent: 50-100ms
- DC: 2-3ms
- subsecond roundtrip should be fine
-
basic reliability decrease w/ distance
- w/in DC connection can last forever
- outside DC: hard to keep a single TCP cnx alive for a long time
- if a reroute occurs => 90s to resettle
- With 100^2 connections, it's very likely to have a bunch of connections down at all time but much less likely to have nodes fully disconnected from the network (eg. without a path to each other node even indireclty)
-
we need to define a logical framework/abstraction for our network layer with some expected semantics:
- we don't require not strongly sequential ordering of messages
- associate CDF with time for a message to appear at all nodes with timeouts
- closing of the head is dissociated from network connections => a TCP cnx disappearing =/=> closing the head
-
Using current ouroboros network:
- disseminating an empty block to 10K nodes takes 400ms
- ouroboros network should withstand 1000s of connections (some system-level limits)
- implement relaying/routing
-
Wireguard could be an interesting alternative in the long run, with some caveats:
- ➖ we need a "global" addressing scheme
- ➖ there's one eth interface / connection
- ➕ manages IP address changes and routing
- ➕ transparent handling of encryption
- ➖ does not help w/ Firewalls
-
Next Steps:
- AB: Investigate what's going on with our current setting as there's not reason it should not work
- Define expected semantics for network layer (over the current simple
broadcast
function)- Address/naming/identification of nodes
- Setting up a Head dynamically among a set of known nodes
- Expected limits on messages diffusion
Trying to revive the hydra benchmark and scale it up to check waht's going on with ouroboros connections. We are probably doing something wrong in detecting our peers.
Looking at logs for each node in a 10-nodes network,
for i in $(seq 1 10); do tail -50 /tmp/bench-5e84ba427887f317/$i | jq -c .message.network.data.trace[2][1].contents.port | sort | uniq > $i.pings ; done
Nodes 8,9, and 10 have incomplete pings, looking at node 10 logs, I can see:
- It succeeds to connect only to nodes 1 to 7, and there's no trace for node 8 and 9
- I have this message
"Starting Subscription Worker, valency 7"
which would seem to indicate node is limited to 7 connections?
In the complete logs we also see ConnectSuccessLast
message appearing which is documented as:
| ConnectSuccessLast
-- ^ Successfully connection, reached the valency target. Other ongoing
-- connection attempts will be killed.
So this probably means the valency is incorrectly configured and needs to be scaled up to match the expected number of nodes.
In the Ouroboros module source code we force the valency to hardcoded value 7!
- Fixing the valency to be exactly the number of remote peers works fine, but now we run into another problem: With more than 9 nodes we cannot open the head because the
collectCom
transaction is too large... - Actually, benchmark fails to run with more than 4 nodes
- Start work on making demo use a faucet to distribute funds
- Which version to put in demo/docker-compose.yaml? Latest released?
- When rebasing the identify-heads branch, had some problems:
- txOutLovelace not exists -> where to put it? into TxOut or Lovelace module?
- TxOut pattern cannot be imported explicitly?
- Workarounds: import qualified or import everything?
- Seems that patterns are not compatible with explicit imports :(
- "Cardano.Api.UTxO" being part of
hydra-cardano-api
confused me; The naming suggested it's from cardano-api and I don't buy the "drop-in" replacement argument -> it is annoying if one wants to know from which package this module is coming from!
- Replacing
postSeedPayment
withseedFromFaucet
in all the tests now - Forgot to update the initial funds distribution to be
[faucetVk]
-> move that into withBFTNode to simplify? - Last usage of
postSeedPayment
: seed-network executable -> update or remove this one as we also have a shell script (that also needs updating) for the demo
- Today we discussed our previous request for a
serializeData :: BuiltinData -> BuiltinByteString
plutus builtin - We started to draft for a CIP with collecting motivation, specification and rationale on HackMD: https://hackmd.io/NknVIIHwTh-W7ecYgrDsxw?both
- A quick benchmark in which we compare Haskell-performance of
plutus-cbor
vs.cborg
on serializing variousTxOut
values resulted good motivating measurements: https://github.com/input-output-hk/hydra-poc/pull/212 - It is odd though, that the
plutus-cbor
benchmark indicates non-linear performance despite the on-chain evaluation reports O(n) behavior of our on-chain encoding: https://github.com/input-output-hk/hydra-poc/issues/161#issuecomment-1013010028
- Our early benchmark of
plutus-cbor
vs.cborg
was a good motivation for the serialiseData CIP, but it is weird that plutus-cbor would not scale (badly, but still) proportional to the input data. Especially as we saw the plutus execution cost scale linearly with the number of assets as also reported by theexecution-cost
exe inplutus-cbor
. - Hypothesis: the lazy evaluation semantics of haskell (when run through cabal bench) are biting us -> make plutus-cbor more strict
- Starting point for 100 assets on my machine: 645.8 μs (cborg takes 24.73 μs)
-
encodeXXX
internal functions take the continuation bytestring, let's bang them -> Result: 592.8 μs - Force continuation on semigroup
<>
-> Result: 464.1 μs - Encoding just wraps functions: try to introduce a smart constructor to force continuations -> Result: no difference, dropped
- The counter in
encodeList
andencodeMap
is not forced -> Result: no difference, keep it still because of potential space leak? - Also force the bytestring in inner encodeMap loop -> Result: 459.4 μs
- Force integers on
encodeUnsigned
to have subtractions peformed right away -> Result: 479.9 μs (no difference besides system load noise) - Sledgehammer: Force remaining arguments to
encodeXXX
functions -> Result: 453.1 μs (no difference)- This did not achieve a noticable improvement, but made all functions consistent in their strictness behavior with internal functions
- Opened a PR covering this work stream: https://github.com/input-output-hk/hydra-poc/pull/213
-
We discussed more in depth the role of the TinyWallet within the wallet and in particular, how we currently artificially mark some outputs for internal UTXO management. We made the observation that we do current use the TinyWallet for two distinct purposes:
- As a operational device, to fuel the Hydra heads and pay for transactions necessary for the protocol on-chain transitions;
- As a straw man user wallet for commits.
While we agreed that the first point is probably fine to keep internal and relatively hidden of the end user, the second point is something we would very much prefer done via an external wallet. An idea could be to build a small browser bridge to leverage wallets implementing CIP-0030 for that matter or, to directly talk to Ledger/Trezor device from a Hydra node. A typical flow for a Hydra node would be to:
- Query the external wallet for its UTXO set (note: not possible directly for hardware devices);
- Offer the user to commit some entries of this UTXO set
- Construct and balance the appropriate transaction using the Hydra node internal wallet
- Prompt the external wallet for signature of all committed UTXO entries
- Post the transaction.
-
Wrapped up work on the hydra-cardano-api.
-
Made some extra effort on the documentation to look good on Haddock. In particular, I tried to structure it a bit more around the exported types but hit a wall: we cannot currently implicitly export all *record fields of a pattern synonym. While we can export them explicitly, I wanted to avoid having to list the 50+ fields in the export list, as well as having to maintain that list in the future. So, I met half-way and made use of inline haddock to get into something acceptable.
-
Made all "internal" modules era-agnostic with the rationale that they could all be pushed upstream eventually. This really makes the internal modules look like a wishlist now. Only the final top-level
Hydra.Cardano.Api
does specialize types, constructors and functions to one specific era and script language.
-
-
Had an extended chat with John about CIPs.
-
Discussed with Teddy regarding hydra-node's internal wallet and how the cardano-wallet team could provide useful building blocks in the form of Haskell libraries.
-
Make
plutus-cbor
package ready-for-release:- Added proper haddock documentation and examples
- Added
encodeBool
,encodeString
andencodeTag
(with corresponding tests) to make the module (almost) complete. The only constructor missing is major type 5, which is for encoding floating numbers which is arguably sound to NOT have in a library at the heart of automated scripts for a financial system.
- Preparing the ensemble session such that the end-to-end test scenario in parallel
- First step: Switch the fund distribution to use a well-known faucet instead of modifying initial funds in the
genesis-shelley.json
- Make
keysFor
use a proper Actor ADT and introduce a newFaucet
actor - Next, draft a
seedFromFaucet
function which should be used instead ofpostSeedPayment
to get funds from the faucet instead of consuming some Actor's genesis funds -
withBFTNode
is still overwriting initialFunds in the genesis json -> add the faucet to those, for now - After implementing
seedFromFaucet
tests failed -> forgot to add the 'markerDatumHash' - Getting confused on which outputs need to be marked now and which not -> should document with a drawing? do we still need this mechanism it?
- Make
seedFromFaucet
only mark outputs when requested - Had some errors when shuffling the amounts in the
EndToEndSpec
-> improvedwaitMatch
error message
I have a problem when trying to encode CompiledCode
to Flat
format: Our plutus version does not have a Flat
instance for CompiledCode
:(
- I can't use the
SerializedCode
constructor directly, it's only used in themkCompiledCode
function which is internal and what I get back is always deserialzed. So I need to actuallyflat
encode the output ofgetPlc
Tried to evaluate
the MT Builder script but got failure with cryptic message:
$ ../plutus/dist-newstyle/build/x86_64-linux/ghc-8.10.4.20210212/plutus-core-0.1.0.0/x/uplc/build/uplc/uplc evaluate -i mtBuilderScript.flat --if flat-named
An error has occurred: error:
Cannot evaluate an open term
Caused by: fix1_4
Spent some time discussing what's next from our roadmap. The next important milestone would be to be able to connect Hydra nodes to Alonzo testnet and run Heads there.
To connect to Alonzo testnet we need to be able to have multiple heads on the same network
- Could be done in various ways, we need a Head identifier of sort -> we don't need to change the Node to handle multiple heads in one node, but need to identify the head we care for
- Identifier must be unique with an obvious source of uniqueness being a
TxIn
- The only thing we really need to observe scripts/heads is the Value because it's governed by a
MonetaryPolicy
. This would make the scripts address generic, unparameterised, which could pave the way to some optimisation once script references are available on the network - Testnet is even harsher than Mainnet because everyone has money so it's more stressful for applications there -> we need to ensure tx are signed by the right parties
What about the Collecting metrics feature?:
- Metrics are all the more important while we are still developing the product and not sure what use cases would suit it best. Analysing "real world" data (eg. from Testnet) would also help product understand how to steer Hydra development
- What do we want to collect? It's hard to tell in advance, ask product!
- Can be done at the chain level and the node level. Doing it at the chain level
Started work on identifying Heads:
- Added ETE test manifesting simple interaction between 2 heads: Two nodes create 2 heads with one party overlap
Discussing waht happens when opening a head? Which credentials are used to identify you're part of a Head?
Dropping at the BehaviourSpec
level to introduce head identifier: When we see an OnInitTx
while we are already in a Head, we compare identifiers. This implies the HeadState
would carry the head identifier.
When an OnInitTx
happens with same head identifier it signals some rollback from the chain
- Would a map from
HeadId -> HeadState
not be as simple to do now as trying to shove aHeadId
inside various data structures? - We agree that right now it would be simpler to keep the head identifier logic in the
Direct
component, even though we also agree it's annoying to have this component be stateful
We end up case-splitting on the current state when we observe an InitTx
: It is enough to make the test pass and we don't need to inject a head identifier.
- To really get to the meat of it, we want to run the ETE test with 2 parallel heads using the same keys/parties: One node runs a single head, but one set of cardano credentials could be used for multiples heads over multiples nodes
Trying to generate Haddock documentation for tests module, seems like the Cabal documentation is inaccurate. Official doc says the haddock-tests
configuration flag can be used to generate it, but it does not work when cabal haddock all
. Adding --tests
flag also does not work as it's not recognized by cabal.
The only thing that worked was to generate it directly with
cabal haddock hydra-node:test:tests
Doing
cabal haddock all --haddock-options --tests
entails rebuilding all of dependencies!
The right option is
cabal haddock --haddock-tests tall
🤷
- We started a session to complete the
abortTx
and involved validators - We created a
healthyAbortTx
unit test in the same style as for other transactions inContractSpec
- We realized that
abortTx
is not actually reimburse already commited Utxo - Thus we added a high-level test to
DirectChainSpec
which covers this: i.e. alice gets reimbursed after committing and aborting a Head - Making that test pass is necessary before we dive into adversarial testing of the abort and implementing validators
Having another look at Plutus script profiling, still no luck, having the same error about attempt to apply a non-function
and then an expression containing the list of "hashes":
Issues with profiling MT builder:
- Implement Merkle-Tree builder in Plutus: https://github.com/input-output-hk/hydra-poc/blob/master/plutus-merkle-tree/src/Plutus/MerkleTree.hs#L90
- We know it works because we are using the script to provide metrics on how much execution units it costs to build a MT on-chain: https://github.com/input-output-hk/hydra-poc/blob/master/hydra-node/exe/tx-cost/Main.hs#L123
- We have an
inspect-script
program that dumps scripts and datums in various formats for inspection. In particular this program dumps a bunch of scripts in PLC format.- Among those scripts we dump fully-applied MT builder function, doing the following:
where
appliedMTBuilderScript = $$( PlutusTx.compile [|| MT.fromList ||] ) `PlutusTx.applyCode` PlutusTx.liftCode utxos
utxos = generateWith (vectorOf 20 (toBuiltin . BS.pack <$> vectorOf 60 arbitrary)) 12
- Among those scripts we dump fully-applied MT builder function, doing the following:
Working on Initial validators, taking it from where SN got it
Note: We realise that we are writing tests per transaction type (Commit, Abort, CollectCom...) while implementing contracts per output script type (Initial, Commit, Head).
We don't need to pack the TxOutRef
in the datum of the Commit
validator, this can be inferred from the redeemer of the Initial
validator.
- => Refactoring
Commit
datums and propagating changes
Different approaches for testing Plutus contracts and apps:
-
Model-based testing of complete Plutus apps, tested at the level of the
Contract
monad, eg. off-chain code.- Based on defining a state machine model of the system
- Uses QuickCheck and quickcheck-dynamic framework to explore state machine, generate traces and check correctness of implementation
- Tests are run within an
Emulator
that's supposed to reproduce the behaviour of the blockchain
-
plutus-libs is another model-based testing approach also based on QuickCheck from Tweag, called
cooked-validators
:- Based on their own
MonadBlockChain
abstraction which ultimately is based on Plutus' representation of the ledger - Tests are written as properties over trace expressions written in a
GenT
monad allowing interleaving generators and chain interactions (Eg. posting transactions) -
modalities
somewhere
andeverywhere
provide a way to modify generated traces to produce more traces representing some arbitrary change over the set of traces. See example for use ofsomewhere
- Based on their own
-
tasty-plutus provides a unit and property testing framework integrated with Tasty
- Provides a DSL to build a
ScriptContext
that can then be used to run the validators directly - Uses Plutus'
Scripts.runScript
function to run the script - The scripts are run in compiled form and passed to the CEK interpreter
- Provides a DSL to build a
- Hydra's contracts Mutation-based testing
- Define tests at the transaction level, e.g without the filter of a DSL to build those
- Tries to express various possible "attacks" or mutations of the transaction than can affect its validity
- Used in combination with TDD approach: Write mutators, see validation fails, fix the validator to ensure mutants are killed
- Uses actual cardano-api data structures and functions which removes an extra layer of indirection and possible "impedance mismatch" between abstraction layers
Looking at Tweag's plutus-libs again, see if I can build it
-
Can't seem to be able to enter nix-shell in plutus-libs repository without compiling GHC, even though it's present in the nix store
$ ls -l /nix/store/ldh1r8ny0914fn8931a26ki4p3mky9bw-ghc-8.10.4.20210212 total 24 dr-xr-xr-x 2 root root 4096 Jan 1 1970 bin dr-xr-xr-x 2 root root 4096 Jan 1 1970 evalDeps dr-xr-xr-x 36 root root 4096 Jan 1 1970 exactDeps dr-xr-xr-x 3 root root 4096 Jan 1 1970 lib dr-xr-xr-x 2 root root 4096 Jan 1 1970 nix-support dr-xr-xr-x 4 root root 4096 Jan 1 1970 share
-
Managed to build plutus-libs and run the tests in about 3 hours, now looking at adapting it to our usage Might be interesting to provide a
DirectChain
implementation foir the MonadBlockChain ?
Trying to wrap my head around this somewhere/everywhere combinators they are providing. I understand they are modalities expressing the fact we want some predicate to be true for some trace, or for all possible traces. What's unclear is how the branching of traces happen, it seems the somewhere
combinator tries to applies its predicate to all the possible combinations of the monadic operations it is working on?
plutus-libs
uses the same technique of an existentially wrapped Show
able field to classify/display attacks: https://github.com/tweag/plutus-libs/blob/main/examples/tests/PMultiSigStatefulSpec.hs#L327
What's annoying with the plutus-libs framework is that the whole code for interacting with the chain is in tests. => risk of duplication of logic w/ inconsistencies. However, it might be simpler than Contract
code?
Fixing mutator for Head input's redeemer in the ContractSpec
:
- I need to locate the correct redeemer using the
RmdrPtr
- The
RdmrPtr
is an index into the sortedTxIn
s of the transaction - To locate the correct
TxIn
I need theTxOut
that pays to the Head script address - Which can only be found in the
Utxo
map
Doing some refactoring to simplify the ContractSpec
module which is becoming a bit unwieldy.
- Extracted modules from ContractSpec to separate what's generic from each different contract's specific code. I think it makes things clearer and might help identifying gaps in each contract and auditing.
I am not sure I understand why the cost we set in the tx before submitting would be taken into account by the execution engine.
- I suspect the node assumes it's an upper bound yet computes the actual cost, so that if actual cost is actaully fine with builting limits, it's ok and the set costs are ignored. Trying to increase Wallet costs tenfold to prove my hypothesis.
- OK, seems like I am wrong:
hydra-node: Some other ledger failure: UtxowFailure (WrappedShelleyEraFailure (UtxoFailure (ExUnitsTooBigUTxO (WrapExUnits {unWrapExUnits = ExUnits' {exUnitsMem' = 12500000, exUn itsSteps' = 10000000000}}) (WrapExUnits {unWrapExUnits = ExUnits' {exUnitsMem' = 125000000, exUnitsSteps' = 100000000000}}))))
- Testing with 0 ExUnits does not work either, we have to compute the costs I guess... What we should do is basically what is done in cardano-cli's makeTransactionAutoBalance
Writing a test to generate empty commits in CollectCom
in order to check we correctly handle this case in Head
commit. We need to refine the case in Head
contract: we can have or not a committed output, but we always have a value that should be added to the head output
Checking that increasing the genesis fees make the ETE test pass
- ETE test still failing, looking at the logs does not reveal anything, looks like the commits are posted but they never end up in a block => We also need to increase the budget for blocks!
- NOTE: Could that be an attack vector on a node? The txs sit in the mempool because budget is overspent for the whole block, although there's no message saying anything about this
Splitting work among the three of us:
- SN: Initial validator
- MB: execution cost computation in
Wallet
- AB: Commit validator
How would I make commit fail? It needs to check its serialised committed txOut
is present in the utxoHash
of the collectCom
output, if the transaction is a collectCom.
- From a full tx testing perspective, the commit validator is redundant as it is checking the same thing the collectCom validator is checking, more or less.
I can write a test for it by disabling the collectCom
validator, which would represent the case where an attacker tries to consume a commit's output.
- Test is passing which means my mutation is not doing its job properly... => Adding a unit test with only the mutation of interest checked against a valid tx to debug what's going on
- The transaction validates but not the redeemers evaluation, need to make sure the changed input is not a script address
Struggling to remove the redeemer for head input from the redeemers map. Working with Cardano api and Ledger aPI is a major PITA and I always waste time moving from one to the other.
🎉 Got a properly failing test (it should fail to validate tx but does not), going to wire it in mutations, Mutators expectedly fail:
test/Hydra/Chain/Direct/ContractSpec.hs:143:5:
1) Hydra.Chain.Direct.Contract.CollectCom does not survive random adversarial mutations
Falsified (after 1 test):
SomeMutation {label = MutateUtxoHashAndUnwireCollectCom, mutations = [ChangeInput (TxIn "31237cdb79ae1dfa7ffb87cde7ea8a80352d300ee5ac758a6cddd19d671925ec" (TxIx 455)) (TxOut (AddressInEra (ShelleyAddressInEra ShelleyBasedEraAlonzo) (ShelleyAddress Testnet (KeyHashObj (KeyHash "769e4459bc4e030f6bb0189dd4b196d6c226990eebb3e461834c2458")) StakeRefNull)) (TxOutValue MultiAssetInAlonzoEra (valueFromList [])) TxOutDatumNone),ChangeOutput 0 (TxOut (AddressInEra (ShelleyAddressInEra ShelleyBasedEraAlonzo) (ShelleyAddress Testnet (ScriptHashObj (ScriptHash "db03ce27e537dabd213d55ae0dcf50572f75e32b43dc350ccf2b4ea1")) StakeRefNull)) (TxOutValue MultiAssetInAlonzoEra (valueFromList [(AdaAssetId,34056295)])) (TxOutDatum' ScriptDataInAlonzoEra "697c2f4b5fc1a408a85391f76945d803152d71479c68fae50a118648d471e7ac" (ScriptDataConstructor 1 [ScriptDataList [ScriptDataNumber 18446744073709551597,ScriptDataNumber 4,ScriptDataNumber 21],ScriptDataBytes "\SOH\SOH\NUL\SOH\SOH\NUL\SOH\SOH\SOH\SOH\NUL\SOH\SOH\SOH\NUL\NUL\NUL\SOH\SOH\SOH\NUL\SOH\SOH\NUL\SOH\SOH\NUL\SOH\NUL\SOH\NUL\NUL"])))]}
Should have not validated
Redeemer report: fromList [(RdmrPtr Spend 1,Right (WrapExUnits {unWrapExUnits = ExUnits' {exUnitsMem' = 738026, exUnitsSteps' = 329465749}})),(RdmrPtr Spend 2,Right (WrapExUnits {unWrapExUnits = ExUnits' {exUnitsMem' = 738026, exUnitsSteps' = 329465749}})),(RdmrPtr Spend 3,Right (WrapExUnits {unWrapExUnits = ExUnits' {exUnitsMem' = 738026, exUnitsSteps' = 329465749}}))]
Waht the ν_commit
really wants is a redeemer that proves the committed output is part of the UTXO set in the opened head. This requires something like a MT or else the commit script needs to compute the complete hash of everything and do the same dance than what the collectCom
does twice.
💡 Turns out we don't want to do that: The ν_commit
validator only needs to check the head validator is the correct one (if the address is correct, then the source is known to be "correct") and it runs in the correct state (Initial
) with the correct redeemer (CollectCom
or Abort
).
I need to paramaterise the Commit
script validator with the policyId or validatorHash to infer the head script's adddress but there is a mutually recursive dependency between both now: The commit script now needs the Head script's address or hash, and the Head script needs the commit script's address or hash...
We can get away with this circular dependency by passing the validator hash in the datum, instead of as an argument to the script (that's what MB did with his Dependencies
type a short while ago).
Now trying to force the commit script to check the redeemer is indeed a collectCom
redeemer thus ensuring the commit is consumed in the correct transition:
- To this purpose I need to keep the head input and output in the mutations, but change them to something that validates the
Head
script but is not aCollectCom
transition. Good candidate is having anOpen -> Close
transition but test "fails" => Of course, it's not as easy because the the datum hashes do not match so the collectCom script fails! - I think all our usages of
ChangeHeadDatum
suffer from the same issue: The datum should be injected into the Tx context
Struggling once again with Cardano and Ledger APIs. This time, I need to inject a new datum in the transaction, which means adding a pair (DatumHash, Data)
to the Ledger.TxDats
structure in the TxBody
.
- The problem is that I start from an API's
TxOutDatum
which needs to be passed to the ledger'sTxDats
map hence transformed all along
Injecting a different redeemer/datum pair in the head input for the collectcom transaction "works": The tx is structurally correct and the validator passes (which is expected) but then the commit
validators now fail with a PT1
error:
PT1: TH Generation of Indexed Data Error
I suspect it might comes from the problem we had before: Triples are not correctly supported? => :no:
Try to look into the PLC code to see where this is raised, so I need to compile the code => Extending inspect-script
to dump PLC files
- Actually no
PT1
in the compiled script :( Need to troubleshoot the issue in another way, problem certainly comes from deserialisation of data
Trying to revert my changes to the Commit
script fails, I still have the same PT1
error when executing the mutated transaction with a changed datum and redeemer.
- Not sure what would cause this problem...
- The healthy transaction and the happy path tests in
TxSpec
are ok.
Trying to dump the transaction to inspect why it could fail on the commit scripts
- It's not the datum that's wrong but the redeemer that's passed to each commit: Each is passed the
Close
redeemer and not the()
it expects!
💡 I know what's going on: When we apply the ChangeHeadRedeemer
in the mutation, we do the following:
changeHeadRedeemer newRedeemer redeemer@(dat, units) =
case fromData (Ledger.getPlutusData dat) of
Just (_ :: Head.Input) ->
(Ledger.Data (toData newRedeemer), units)
Nothing ->
redeemer
Which means that any Data
that decodes to something that looks like a Head.Input
will be changed to the given redeemer. This is not what we want, we actually need to change only the redeemer associated with the Head script input which requires associating it correclty with the head's TxIn
.
- A solution is to use a
mapWithKey
here instead of afmap
so that we can know which txIn we are talking about. - Another solution would be to make none of the datum and redeemer types in our scripts are isomorphic so that we can safely decode them as this is not the first time we are hitting this problem.
Discussing CI slowness and how to alleviate it: Seems like caching nix closure does not help much
Settling down on a plan for implementing the collectCom validator:
- We put a
(Party, Maybe (TxIn, TxOut))
in the datum of the commit, with theTxIn
andTxOut
actually beingBuiltingByteString
serialised off-chain from the actual ones - The collectCom validator will check that the
headDatum
contains a Hahs of the concatenation ofTxOut
s ordered by Party number
Had some troubles compiling triples in Plutus which lead to weird E019
error -> resorted to nested pairs!!
- We need a property checking on- and off-chain serialisation of txouts
Utxo
should have its own dedicated module which would simplify naming: Just import qualified Utxo
and then use Utxo.fromPairs
or Utxo.singleton
(re)discovered the trick for providing "dynamic" strings to traceIfFalse
: Plutus validator outputs a hex-encoded UTF-8 string which can be decoded with soimething like Buffer.fromString('1234...', 'hex').toString()
from nodejs.
One can enter nodejs from nix-shell:
nix-shell -p nodejs
Debugging contracts code is cumbersome, hence we should take smaller steps to ensure we don't trip ourselves at any step
Managed to get collectCom
validator working!
- ETE tests are failing, but it's not clear why. It does not generate a
CollectComTx
tx - Seems like we are unable to decode an empty commit from
carol
in the ETE test => In ourcommitTx
tests we don't check observation of empty commit datums
Our CollectCom transaction is overspending the budget in the ETE test which is odd because it doesn't in our unit tests
- Why is our property check for validation not failing for 10 parties/utxos where it's failing in ETE test for 3 parties and 2 utxos?
- Are the protocol parameters used in the wallet for the hydra nodes correct? Actually, they are the same as the ones from
genesis-alonzo
, it's all consistent so far - Increasing memory limit for a transaction fails the test too, but this time I don't even see the commit txs => Need to increase memory limits for blocks too as it now is lower than per tx consumption
Got another error this time:
DebugInfo: DebugInfo ["could not decode commit"] "CekError An error has occurred: User error:\nThe provided Plutus code called 'error'."
This happens in the case of a party not committing anything: We return Nothing
when trying to decode the commit from the datum (on-chain) but then we wrap that into a maybe (error...) ...
which is not what we want.
- Increasing mem limit to 12.5M makes everything pass
Trying to trim down nix shell run in CI to speed things up
Agenda: Meet & discuss with the marlowe team their protocol and common challenges
- We gave a quick intro in Hydra + on-chain parts and the plutus challenges we face
- What is marlowe?
- DSL for financial smart contracts
-
Contract
type,When
construct is the most complex - Storing the interpreter state as datum
- When construct is typically the top-level value when a contract "pauses" -> in a datum
-
Input
is used as a redeemer of the script validator
- Merkleization
- Store only the
Hash
, a serialized continuation - Redeemer needs to provide the
Contract
and needs to prove - Merkleized-abstrct-syntax-tree -> https://blockstream.com/simplicity.pdf
- Store only the
- Serialization/Deserialization must be bijective on the domain!
- No two ByteString shall end up in the same
Contract
(the type at hand) - Deserialization is quick? even constant?
- No two ByteString shall end up in the same
- The extraneous datum idea
- Currently not supported, we considered this as well
- Serializing CBOR
- if both big projects at IOG need serializing, we should be able to add that to Plutus
- even putting Hydra primitives to Plutus should even be a possibility
- Marlowe would also need Deserialization
- Way forward: Marlowe also talks to Plutus and we make sure it's addressed in June HF
- How is it going?
- Barely fits and currently using a private testnet with doubled budgets
- Testing:
- test suite with example contracts -> with user interface end-to-end
- some tooling left & right
- Time is a problem / annoying:
- Slot <-> Time conversion bypassed right now
- Maybe related to private testnets
- They use the time from the ScriptContext
- They are using the PAB
- three contracts, 5-6 endpoints total
- had to fork the PAB and upstreaming fixes
- chain-index DB building took a week for mainnet
- centralized deployment for Marlowe pioneers -> infrastructure hosted by team
- Querying the blockchain discussion
- PAB chain index
- external services
- querying untrusted services and then use the cardano-node for actual data retrieval
- Mithril might be also improving this situation
-
Looked into the cost of creating merkle trees on-chain as this O(n^2) cost has been bothering me for a bit. The current measures show that constructing a tree of 10 elements takes about 111% of the memory budget and 40% of the cpu one. So, looking at the implementation of our
Plutus.MerkleTree.fromList
I tried to understand where the cost comes from and what we could do so reduce it. I was able to reduce the cost for 10 elements down to 44% / 19% with some simple "tricks".-
First thing: the length. The implementation currently calculates the length of the list and sublist on every iteration. However, we can instead calculate the length at the beginning once, and pass it as argument to the recursive calls which only need to keep dividing it by two. This saves us quite a few list traversals.
-
Second, like for CBOR encoders, we can write our own variation of length using plain functions to saves a few more memory and CPU costs. Although, because of the first point, we only use it once and thus its impacts is only visible on larger lists.
fastLength :: [BuiltinByteString] -> Integer fastLength = \case [] -> 0 _ : q -> 1 + fastLength q {-# INLINEABLE fastLength #-}
-
Then, we can also avoid traversing the list (and sublist) twice when splitting the tree by writing a
split
function which does both at the same time. This again, saves a few memory bytes and CPU cycles, especially on larger lists.splitAt :: Integer -> [a] -> ([a], [a]) splitAt n rear | n <= 0 = ([], rear) splitAt n (x : rear) = (\(front', rear') -> (x : front', rear')) (splitAt (subtractInteger n 1) rear) splitAt _ [] = ([], []) {-# INLINEABLE splitAt #-}
However, the real cost comes from combining hashes into one. The bigger tree, the more hashes need to be combined. For example, if we remove the hashes concatenation, we fall into a sub-linear model for the tree construction where 10 elements requires ~17% of the budget. This is also the reason why encoding large chunks becomes rapidly expensive in Plutus. So I think there's definitely a case for either (a) a better / cheaper cost model for bytestring concatenation and (b) more optimized primitives for bytestring manipulation (in particular concatenation). Alternatively (and/or complimentarily), we could also use shorter digests (e.g. blake2b-160) to saves a few bytes on every combinations; Short hash digest may still be acceptable given the size of the UTXO set.
-
Trying to fix again the issue with CI and haddock builds
- As expected, removing offending
emptyValidator
does not change the problem: https://github.com/input-output-hk/hydra-poc/runs/4864469056?check_suite_focus=true#step:7:2916
Trying SN's solution of adding
package hydra-plutus
tests: True
haddock-options: "--optghc=-fplugin-opt PlutusTx.Plugin:defer-errors"
to the relevant sections of cabal.project
which seems more robust. It's actually documented here and I don't know why I overlooked it
- Changing
cabal.project
to add haddock options does not solve CI problem: https://github.com/input-output-hk/hydra-poc/runs/4864629276?check_suite_focus=true#step:7:2704
Sketched in more details the structure of the transactions and outputs for the commit/collectCom sequence, trying to highlight what's going on off-chain, on-chain and in Plutus validators: https://miro.com/app/board/o9J_lRyWcSY=/?moveToWidget=3458764516911251699&cot=14
- The commit transaction has committed UTXO consumed
- The creator of the transaction puts the bytes representing the consumed UTXO as datum
- an observer of the transaction can extract the UTXO from the inputs
- the
ν_initial
validator can check the bytestring in the datum is correct by serialising the input that should be consumed - it can also check the PT and other stuff
- The collectCom transaction:
- pass a redeemer containing a MT inclusion proof to each
ν_commit
validator - puts a MT root hash has as datum of head "continuation" output
- the poster can construct the MT from the observed commit txs
- the
ν_commit
validator can check the proof is valid - the observer can reconstruct the committed UTXO (U0) from the
ν_commit
's datums by deserialising them from bytestrings
- pass a redeemer containing a MT inclusion proof to each
-
Discussed roadmap & all the steps still ahead of us
-
Discussed
collectCom
transaction + validators, whether complexity is reallyO(n^2)
withn = # UTXO
- We could can make it scale with
n = # of commits == # of parties
if we use Merkle trees - Constructing a Merkle Tree on-chain in
O(n^n)
seems too expensive if we think of checking proofs isO(n*log n)
- We could can make it scale with
-
Continue implementation of
collectCom
transaction validators -
First step: ensure it correctly "collects value" by adding a mutator for the Head's output value
-
Making the validator survive mutation by checking the script outputs' value -> that was easy (we collected value before)
-
Ensuring the
utxoHash
included in the resultingOpen
datum, is a bit more tricky -
We started with a mutator again
- Getting our hands on the healthy
collectCom
output is easy - Changing the datum to some mutated
utxoHash
required some verbose case switching & decoding -
NOTE: We mistakenly refactored this into the
ChangeHeadOutput
Mutation
and reverted it.ChangeHeadOutput
is mutating the datum to be consumed by the transaction in the "lookup" UTXO.
- Getting our hands on the healthy
-
To make
collectCom
validator correct, we use the fact that thecommit
transaction should validate (in the initial validator) that theDatumType Commit
holds a correctly CBOR-encodedUtxo
(or singleTxOut
). Then, creating theutxoHash
is concatenating the pre-encodedTxOut
of all commits and hashing this -
We added some makeshift on-chain debugging code which seems to output
BuiltinByteString
via adecodeUtf8
output into the property failure
test/Hydra/Chain/Direct/ContractSpec.hs:153:5:
1) Hydra.Chain.Direct.Contract.CollectCom is healthy
Falsified (after 1 test):
Should have validated
Redeemer report: fromList [(RdmrPtr Spend 0,Left (ValidationFailedV1 (CekError An error has occurred: User error:
The provided Plutus code called 'error'.
Caused by: (decodeUtf8 #756e6578706563746564206f757470757420646174756d20696e20636f6c6c656374436f6d2c206578706563746564207574786f20686173683a209f82582750e0a714319812c3f773ba04ec5d6b3ffcd5aad85006805b047b08254185f67cf3bd6a878cb24b1a0035cdbdff2c2061637475616c207574786f20686173683a205e84d9c82fd110116d5a3d5e88665d36b46052fc506f82ac751c72c80b48dc14)) [])),(RdmrPtr Spend 1,Right (WrapExUnits {unWrapExUnits = ExUnits' {exUnitsMem' = 523338, exUnitsSteps' = 230652180}}))]
Which we could introspect using nodejs
for example:
> Buffer.from('756e6578706563746564206f757470757420646174756d20696e20636f6c6c656374436f6d2c206578706563746564207574786f20686173683a209f82582750e0a714319812c3f773ba04ec5d6b3ffcd5aad85006805b047b08254185f67cf3bd6a878cb24b1a0035cdbdff2c2061637475616c207574786f20686173683a205e84d9c82fd110116d5a3d5e88665d36b46052fc506f82ac751c72c80b48dc14', 'hex').toString()
"unexpected output datum in collectCom, expected utxo hash: ��X'P�\x141�\x12��s�\x04�]k?�ժ�P\x06�[\x04{\b%A��|�j���K\x1A\x005ͽ�, actual utxo hash: ^���/�\x10\x11mZ=^�f]6�`R�Po��u\x1Cr�\x0BH�\x14"
-
We had two errors:
- forgot to hash in the
hashPreSerializedCommits
- decoded the Commit datum assuming it's just an on-chain
Utxo
, but it was(Party, Utxo)
- forgot to hash in the
-
Of course, this approach also requires the committed
Utxo
datums be *in the same order as the commit inputs.
Still trying to have documentation compiled in CI, still having weird plutus compilation error:
Haddock coverage:
7% ( 1 / 14) in 'Plutus.MerkleTree'
Missing documentation for:
Module header
Hash (src/Plutus/MerkleTree.hs:14)
hash (src/Plutus/MerkleTree.hs:25)
combineHash (src/Plutus/MerkleTree.hs:29)
MerkleTree (src/Plutus/MerkleTree.hs:33)
size (src/Plutus/MerkleTree.hs:45)
null (src/Plutus/MerkleTree.hs:52)
Proof (src/Plutus/MerkleTree.hs:58)
mkProof (src/Plutus/MerkleTree.hs:60)
member (src/Plutus/MerkleTree.hs:74)
rootHash (src/Plutus/MerkleTree.hs:83)
fromList (src/Plutus/MerkleTree.hs:90)
toList (src/Plutus/MerkleTree.hs:113)
GHC Core to PLC plugin: E043:Error: Reference to a name which is not a local, a builtin, or an external INLINABLE function: Variable Ledger.Typed.Scripts.Validators.wrapValidator
No unfolding
Context: Compiling expr: Ledger.Typed.Scripts.Validators.wrapValidator
Context: Compiling expr: Ledger.Typed.Scripts.Validators.wrapValidator @ ()
Context: Compiling expr: Ledger.Typed.Scripts.Validators.wrapValidator
@ () @ ()
Context: Compiling expr: Ledger.Typed.Scripts.Validators.wrapValidator
@ () @ () PlutusTx.IsData.Instances.$fUnsafeFromData()
Context: Compiling expr: Ledger.Typed.Scripts.Validators.wrapValidator
@ ()
@ ()
PlutusTx.IsData.Instances.$fUnsafeFromData()
PlutusTx.IsData.Instances.$fUnsafeFromData()
Context: Compiling expr at "plutus-merkle-tree-0.2.0-JVagLoIr6VV9ExRI6nWG6Q:Plutus.MerkleTreeValidator:(26,8)-(26,33)"
This error happens when nix enters the shell.
Reading: https://iohk.io/en/blog/posts/2022/01/14/how-we-re-scaling-cardano-in-2022/
Reading https://tailscale.com/blog/how-tailscale-works/
- Maybe there are interesting ideas about networking to be harvested from it?
- Looking at https://www.wireguard.com/
Trying to serialise a fully applied Plutus script so that I can run uplc evaluate --profile...
on it and get some data
- Trying to
evaluate
MT builder usinguplc
gives me a "weird" error:Could it be that I am using a GHC list and not a Plutus one?$ ../plutus/dist-newstyle/build/x86_64-linux/ghc-8.10.4.20210212/plutus-core-0.1.0.0/x/uplc/build/uplc/uplc evaluate -r -i mtBuilderProfile.plc An error has occurred: error: Attempted to apply a non-function. Caused by: (delay (\case_GHC_Types_Nil_69 -> \case_GHC_Types_Cons_70 -> case_GHC_Types_Cons_70 #913afe731ffea84a50b0bc82195a76f90363704c597dac21de262452f744b6ac1ad5f98b5060d19b6e87a21ffe58df78aadc532fb588f850251801e6 (delay (\case_GHC_Types_Nil_69 -> \case_GHC_Types_Cons_70 -> case_GHC_Types_Cons_70 #22f321fc4663cae6ae2ca6afeedfac4f0941807a4168c2dd1e7d7adaecfba4c5bfc8976ef7f1f1520d846be10f38b87d9b6a0a4ac33095b677d11d3e (delay (\case_GHC_Types_Nil_69 -> \case_GHC_Types_Cons_70 -> case_GHC_Types_Cons_70 #8e3af18f1e5b87e36f76c3e69 .... ))))))))))))))))))))))))))))))) )))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
Turns out removing the plutus-merkle-tree
package and moving everything into hydra-plutus
passes the CI: https://github.com/input-output-hk/hydra-poc/actions/runs/1708177032
Going through Plutus code to try to understand what's the relationship between UPLC and PLC, and how to get from the latter to the former
- There's a
getPlc
function that, if I believe its name, returns a PLC program.getPlc
has signature:CompiledCodeIn uni fun a -> UPLC.Program UPLC.NamedDeBruijn uni fun ()
-
erase
function moves from a typed term to an untyped one. so it's already Untyped Plutus core, no need for further transformation
Running evaluator on membership
script works:
$ ../plutus/dist-newstyle/build/x86_64-linux/ghc-8.10.4.20210212/plutus-core-0.1.0.0/x/uplc/build/uplc/uplc evaluate -r -i mtMemberProfile.plc
(delay (lam case_True_20 (lam case_False_21 case_True_20)))
CPU budget: 9223372036821889920
Memory budget: 9223372036854726941
Putting the GHC plugin options into the MerkleTreeValidator
module did the trick: I now have a "profiling report" but it's quite odd as it seems it checks an empty proof.
$ ../plutus/dist-newstyle/build/x86_64-linux/ghc-8.10.4.20210212/plutus-core-0.1.0.0/x/uplc/build/uplc/uplc evaluate --trace-mode LogsWithBudgets -r -i mtMemberProfile.plc
(delay (lam case_True_21 (lam case_False_22 case_True_21)))
CPU budget: 9223372036820515822
Memory budget: 9223372036854724213
entering member,150100,132
exiting member,3011236,168
entering equalsByteString,30632111,40260
exiting equalsByteString,30895636,40294
Restructured code to have a single executable (in hydra-node
) to run and dump performance data from plutus validators and other stuff
- Note this fails on CI probably because of mising plutus flags, which is annoying and hints at the fact this exe is perhaps in the wrong place?
- Trying to run
build-ci.sh
to check we can indeed generate Haddock, CI is failing because of PLC compilation failing: https://github.com/input-output-hk/hydra-poc/runs/4817861824?check_suite_focus=true#step:7:2706. I already had those errors initially, when I first setup the CI.
Having a look at uplc
which is the plutus untyped language compiler which seems to have some profiling capabilties: https://plutus.readthedocs.io/en/latest/plutus/howtos/profiling-scripts.html
- Will try to run it on the MerkleTree implementation to understand where the time is spent but I can make a prediction: We keep traversing the list to split it in 2 so no surprise building a MT is quadratic
Goal:
- Pretty print errors when script evaluation fails
- More contracts
Managed to display relevant information from redeemers report:
( TxInCompact (TxId{_unTxId = SafeHash "c190ad992ae264008253235c98e3ccdcd78d5d95f74f317f9c8af0482cb04b0b"}) 527
,
(
( Addr Testnet (ScriptHashObj (ScriptHash "576874cda4be3ca7c778405b10f527ff635cc2ebd5620bea56ba6c5e")) StakeRefNull
, Value
2000028
( fromList
[ (PolicyID{policyID = ScriptHash "0d94e174732ef9aae73f395ab44507bfa983d65023c11a951f0c32e4"}, fromList [("\242\185\250\&5\201\DC3\156", 27)])
, (PolicyID{policyID = ScriptHash "1920e782f048e9ef52acd89c4341a89c3908c6e046ad87c474fffb48"}, fromList [("`\238\DLE>nVr", 58)])
, (PolicyID{policyID = ScriptHash "b16b56f5ec064be6ac3cab6035efae86b366cc3dc4a0d571603d70e5"}, fromList [("\211\252", 12)])
, (PolicyID{policyID = ScriptHash "b5ae663aaea8e500157bdf4baafd6f5ba0ce5759f7cd4101fc132f54"}, fromList [("\241P\252", 18)])
, (PolicyID{policyID = ScriptHash "bd039f956f4b302f3ab6fc7c4bac3350a540f44af81a8492194dd2c2"}, fromList [("\161", 8)])
, (PolicyID{policyID = ScriptHash "e0a714319812c3f773ba04ec5d6b3ffcd5aad85006805b047b082541"}, fromList [("\243Qy", 2)])
]
)
, SJust (SafeHash "cbc7a61e1b1a3bf8997d1c0287e266c755e8eff7ffb340ca29d713edd748f7a4")
)
, DataConstr Constr 1 []
, Right (WrapExUnits{unWrapExUnits = ExUnits'{exUnitsMem' = 2210222, exUnitsSteps' = 983688824}})
)
)
Want something more like:
c190ad992ae264008253235c98e3ccdcd78d5d95f74f317f9c8af0482cb04b0b#527:
datum: DataConstr Constr 0 [I 18446744073709551595, B "{\"fb3d635c7cb573d1b9e9bff4a64ab4f25190d29b6fd8db94c605a218a23fa9ad#104\":{\"address\":\"addr1xy659t9n5e\xcps5nqgnq6ckrhpa8g2k3f2lc2h4uvuess8h383rgrv0hy399s7snvpq3mqha5ast98r564rmdd4zyezqh52rky\",\"value\":{\"1920e782f048e9ef52acd89c4341a89c3908c6e046ad87c474fffb48\":{\"60ee103e6e5672\":58},\"b5ae663aaea8e500157bdf4baafd6f5ba0ce5759f7cd4101fc132f54\":{\"f\150fc\":18},\"e0a714319812c3f773ba04ec5d6b3ffcd5aad85006805b047b082541\":{\"f35179\":2},\"b16b56f5ec064be6ac3cab6035efae86b366cc3dc4a0d571603d70e5\":{\"d3fc\":12},\"lovelace\":28,\"bd039f956f4b302f3ab6fc7c4bac3350a540f44af81a8492194dd2c2\":{\"a1\":8},\\"0d94e174732ef9aae73f395ab44507bfa983d65023c11a951f0c32e4\":{\"f2b9fa35c9139c\":27}}}}"]
redeemer: DataConstr Constr 1 []
cost: (2210222, 983688824)
... in the case of a success, and the detailed failure otherwise.
CollectComTx
validator is failing with overspent budget
- Note that our
Fixture
has wrong values for mem and cpu budget: Memory limit is larger than the actual budget on chain by a factor of 1000! - Remember that bytestring appending is quadratic in cost
- Display of overspent error is somewhat misleading, but we have a situation where the memory is exhausted before CPU
Possible hints: We can work on the serialisation of the commit's txout:
- first removing the txin in what's get serialised in the commit
- see if CBOR encoding could help to reduce BS size
Removing the TxIn
from the datum of the commit tx is fine, but we need it down the stream to recreate the state of the ledger:
- We can use any TxIn for that purpose so we try to reuse the one corresponding to the commit tx's
ν_commit
output
Tests for commitTx do not pass now that we change the output's Utxo
- In the test we can just compare the txOuts and ignore the txIns
- Friendly reminder that
toList
onMap
retrieves a list of elements
Note: Removing the Txin
from the commit datum does not change anything in the cost of the commit script execution
Checking if having an untyped validator (eg. one using raw Data
arguments) would not be less expensive becaue it wouldn't have to pay for the cost of building ScriptContext
- Cost of bare commit, eg. one which takes raw
BuiltinData
as arguments and does not need to pay the cost of building theScriptContext
is 1000 times less than the typed one - Cost of deserialising
ScriptContext
is heavy, isn't it somewhat artificial cost that should be shared across all scripts?
For the moment we want to limit the number of UTXO committed to anything that passes the validators.
- It turns out to be surprisingly painful to generate just some number of UTXO and turn them into proper commit outputs...
- Also we want to liaise with Plutus teams and report our findings about the cost of building context. Perhaps it would make sense to accrue it onlye once per tx?
Fixing commit tests and output construction which is currently quite clunky
CollectCom with 8 commits fail, exhausting execution budget
- It also fails with 4 parties...
-
collectCom
Head validator breaks with 2 commits, trying to change its implementation to not usefilter
andfoldMap
Tweag is also working on tool for generating TLA+ model from SM smart contracts: https://github.com/tweag/pirouette
- Problem is no SM-based Plutus contract has been deployed yet
Failure reports are much better:
Redeemer report: 03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c111314#89:
addr: 70cbe60b
datum: DataConstr Constr 0 [Constr 0 [I 0],List [I 0,I 0,I 0]]
redeemer: (DataConstr Constr 0 [B "\144%@\243\151\128'\146\168\177\134\ENQ\145\249\208B!\182\234\NAK\190\&9Z\232j\150\NULs7)\SUB\165"],WrapExUnits {unWrapExUnits = ExUnits' {exUnitsMem' = 0, exUnitsSteps' = 0}})
result: ValidationFailedV1 (CekError An error has occurred: User error:
The budget was overspent. Final negative state: ({ cpu: 6788529847
| mem: -7320
})) []
03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c111314#387:
addr: 70576874
datum: DataConstr Constr 0 [I 0,B "{\"03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c111314#661\":{\"address\":\"addr_test1vzusm27rxmvqc3l62np9fwmkfwc8u0k7fuehc34fu0nl5ls5zguv3\",\"value\":{\"3542acb3a64d80c29302260d62c3b87a742ad14abf855ebc6733081e\":{\"99dff19514\":4},\"4b279a388de46bb4d33c4df64da357640be900b04625a43651b7b059\":{\"\":52},\"65fc709a5e019b8aba76f6977c1c8770e4b36fa76f434efc588747b7\":{\"99ee3bb52091\":22},\"lovelace\":50,\"0d94e174732ef9aae73f395ab44507bfa983d65023c11a951f0c32e4\":{\"c925df\":40}}}}"]
redeemer: (DataConstr Constr 1 [],WrapExUnits {unWrapExUnits = ExUnits' {exUnitsMem' = 0, exUnitsSteps' = 0}})
cost: mem = 1209394 , cpu = 534302280
03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c111314#971:
addr: 70576874
datum: DataConstr Constr 0 [I 0,B "{\"03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c111314#878\":{\"address\":\"addr_test1vrpgptp0ahf5qnfh8y54fku6k0vklamw58ehsqepkz3pk7q95x36v\",\"value\":{\"23f44e7e83a1cd7624805a7d6d16c5fca5317aaa1da99fc05e8cdf26\":{\"32e8ae\":30},\"76e607db2a31c9a2c32761d2431a186a550cc321f79cd8d6a82b29b8\":{\"f392da31cc\":57},\"b5ae663aaea8e500157bdf4baafd6f5ba0ce5759f7cd4101fc132f54\":{\"e1aa9c75b5e304\":50},\"65fc709a5e019b8aba76f6977c1c8770e4b36fa76f434efc588747b7\":{\"\":7,\"e91ad034e3d2\":52},\"lovelace\":3,\"bd039f956f4b302f3ab6fc7c4bac3350a540f44af81a8492194dd2c2\":{\"02bf\":20}}}}"]
redeemer: (DataConstr Constr 1 [],WrapExUnits {unWrapExUnits = ExUnits' {exUnitsMem' = 0, exUnitsSteps' = 0}})
cost: mem = 1209394 , cpu = 534302280
03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c111314#998:
addr: 70576874
datum: DataConstr Constr 0 [I 0,B "{\"03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c111314#458\":{\"address\":\"addr_test1vzdgf4plkxs2u9jt46gajzrggaelm5gz7ccehdszr8pfwrshkm5r4\",\"value\":{\"76e607db2a31c9a2c32761d2431a186a550cc321f79cd8d6a82b29b8\":{\"2ba933\":15},\"1920e782f048e9ef52acd89c4341a89c3908c6e046ad87c474fffb48\":{\"\":24},\"3542acb3a64d80c29302260d62c3b87a742ad14abf855ebc6733081e\":{\"34bd0d8b\":23},\"b5ae663aaea8e500157bdf4baafd6f5ba0ce5759f7cd4101fc132f54\":{\"e8\":6},\"4acf2773917c7b547c576a7ff110d2ba5733c1f1ca9cdc659aea3a56\":{\"ea5ead\":60},\"58e1b65718531b42494610c506cef10ff031fa817a8ff75c0ab180e7\":{\"81030d02\":11},\"lovelace\":46}}}"]
redeemer: (DataConstr Constr 1 [],WrapExUnits {unWrapExUnits = ExUnits' {exUnitsMem' = 0, exUnitsSteps' = 0}})
cost: mem = 1209394 , cpu = 534302280
Our generator for commit UTXO to be consumed by collectCom is wrong, or misleading: We generate a list of (Party, Utxo)
pair to be committed, but there's no guarantee the Party
will be unique, hence the result Map Party Utxo
can end up being smaller than the initially generated list. This does not lead to the test being wrong but the results are misleading.
- Agenda: Ask-Michael-Peyton-Jones-Anything (about plutus)
- Use "address = scriptHashAddress $ Scripts.validatorHash typedValidator" on chain?
Error: Unsupported feature: Type constructor: GHC.Prim.ByteArray# Context: Compiling definition of: Hydra.Contract.Commit.typedValidator Context: Compiling definition of: Hydra.Contract.Commit.address2 Context: Compiling definition of: Hydra.Contract.Commit.address1 Context: Compiling definition of: Hydra.Contract.Commit.address Context: Compiling definition of: Hydra.Contract.Head.hydraTransition
- we want to use it to have the address of another validator
- theoretically we could use compile the address to a literal bytestring, but there are none (right now)
- we use =liftCode= and there is no other way (right now)
- make a github issue of this -> especially the address case is likely common
-
foldMap
andlength
implementations are slower than hand-rolled? https://input-output-rnd.slack.com/archives/G01PKL5P6PJ/p1641914632019800- this is not surprising
- plutus tx are focusing on correctness, not performance
- upstream contributions possible (and had been accepted already?)
- Why memory limits are so low?
- maybe miscalibrated and they are maybe not honest
- the transaction memory budget "feels" very low
- time / cpu should be fine
- memory calibration was way harder to measure
- Why are there two budgets if they are correlated?
- mostly because of builtins
- some of them may be using lot of memory and not much cpu
- in order to represent this there are two budgets
- What's the plan with (memory) limits?
- maybe by june the situation is better
- protocol updates with higher memory limits over the next months etc.
- currently not looking at changing the cost model
- pipelining will also help with aligning validation and thus makes room for higher budgets
- We need to calculate the hash of things, for which we have no serialized representation. What are the options?
- on-chain CBOR encoder
- is what we do now
- it's expensive because of two things:
- integer division: shift
Integer
is realistic (HF event, but not PlutusV2 necessary) - appending bytestrings: Bytestring builders on chain is not realistic
- integer division: shift
- some builtin to encode any plutus =Data= to =BuiltinByteString=
- hard to cost (maybe only deserialize is hard?)
- is appealing (instead of bytestring manipulation)
- this going to be CBOR
- use extra datums and have the ledger check that it
- double size of transaction
- not tried yet https://input-output.atlassian.net/browse/SCP-2417
Relative cost of computing Merkle-Tree membership of (small) bytestrings on-chain.
Raw data for both membership and MT building:
# UTXO | Mem (member) | CPU (member) | Mem (build) | CPU (build) |
---|---|---|---|---|
1 | 3.79926 | 1.66382747 | 3.92862 | 1.69953237 |
2 | 4.00523 | 1.78074127 | 6.41958 | 2.61146129 |
3 | 4.14665 | 1.86060451 | 11.8103 | 4.55927679 |
4 | 4.2112 | 1.89765508 | 17.63118 | 6.64887027 |
5 | 4.29852 | 1.94672544 | 28.82442 | 10.66935212 |
6 | 4.35262 | 1.97751831 | 40.44782 | 14.83161195 |
7 | 4.39126 | 1.99951322 | 52.50138 | 19.13564976 |
8 | 4.41717 | 2.01456888 | 64.9851 | 23.58146555 |
9 | 4.46705 | 2.04247043 | 87.78338 | 31.7472801 |
10 | 4.50449 | 2.06363924 | 111.01182 | 40.05487263 |
20 | 4.71046 | 2.18055305 | 0 | 0 |
30 | 4.81784 | 2.24175466 | 0 | 0 |
40 | 4.91643 | 2.29746685 | 0 | 0 |
50 | 4.98234 | 2.33487927 | 0 | 0 |
60 | 5.02381 | 2.35866846 | 0 | 0 |
70 | 5.07426 | 2.38716361 | 0 | 0 |
80 | 5.1224 | 2.41438066 | 0 | 0 |
90 | 5.15983 | 2.43554948 | 0 | 0 |
100 | 5.18831 | 2.45179308 | 0 | 0 |
120 | 5.22978 | 2.47558227 | 0 | 0 |
140 | 5.28023 | 2.50407741 | 0 | 0 |
160 | 5.32837 | 2.53129446 | 0 | 0 |
180 | 5.3658 | 2.55246328 | 0 | 0 |
200 | 5.39428 | 2.56870688 | 0 | 0 |
220 | 5.41757 | 2.5819971 | 0 | 0 |
240 | 5.43575 | 2.59249607 | 0 | 0 |
260 | 5.45449 | 2.60326714 | 0 | 0 |
280 | 5.4862 | 2.62099122 | 0 | 0 |
300 | 5.51269 | 2.63589112 | 0 | 0 |
320 | 5.53434 | 2.64820827 | 0 | 0 |
340 | 5.55488 | 2.65975424 | 0 | 0 |
360 | 5.57177 | 2.66937709 | 0 | 0 |
380 | 5.58611 | 2.67762308 | 0 | 0 |
400 | 5.60025 | 2.68562069 | 0 | 0 |
420 | 5.6128 | 2.69274686 | 0 | 0 |
440 | 5.62354 | 2.69891091 | 0 | 0 |
460 | 5.63335 | 2.70453895 | 0 | 0 |
480 | 5.64172 | 2.70940988 | 0 | 0 |
500 | 5.64942 | 2.71389113 | 0 | 0 |
I was wondering why the memory cost was so high on our contracts and in particular, on the encoding side... So I went to check a few things to get a better understanding. I still don't quite understand how things work behind the scene, but I discovered a few interesting things along the way...
I started with simply measuring the memory cost of encoding some values. And gradually making it it more complex to see the difference. Starting with a bytestring and integer as a baseline. About ~3M units. Then, I defined a newtype as:
data MyValue = MyValue Integer
Encoding this (as an unwrapped integer) is still about 3M units (about 40.000 units more than a mere integer actually, fair enough). Then, I turned this into a data with two arguments:
data MyValue = MyValue ByteString Integer
And interestingly, it takes around 4.5M memory units to encode. Not 6M; probably because part of the 3M units to encode an integer or a bytestring, some of it is "boilerplate" that is common to both. Then, I changed the type to:
data MyValue = MyValue ByteString [Integer]
BUT, I only generated values with a single integer, always. The cost jumped to 9M units. So I thought: there's something wrong with the list. I'd expect an overhead, but not THAT much overhead. The list encoder is fairly straightforward and look like:
encodeList encodeElem es =
encodeListLen (length es) <> foldMap encodeElem es
I tried to remove the encoding of the length itself, and the cost reduced drastically. Hmmm... The length of the list is calculated from a "builtin" which I'd have thought is the fastest implementation of a length function we could get. Nope. Out of curiosity, I wrote two other implementation for length, using purely recursive functions, one tail-recursive and another one which would likely explode in Haskell but does fine in Plutus.
--builtin length
length
-- hand-written
length2 [] = 0
length2 (_ : q) = 1 + length2 q
-- hand-written tail rec
length3 acc [] = acc
length3 acc (_ : q) = length3 (acc + 1) q
The results? About 25% less memory usage than the builtin length
(for lists of 100 elements)... In doubt, I also compared the CPU units -- thinking that we may be trading memory cost for execution cost (which given our situation would still be fine) but nope... CPU cost is also smaller!
Method | Mem | Δ Mem | CPU | Δ CPU |
---|---|---|---|---|
length | 9_002_207 | - | 3_255_382_379 | - |
length2 | 6_535_228 | -27.5% | 2_508_121_652 | -23.0% |
length3 | 6_697_171 | -25.6% | 2_561_791_297 | -21.3% |
From here, I went to re-write the encodeList
, encodeListIndef
, encodeMap
and encodeMapIndef
functions to use this approach. The explicit ones are more suited to the tail-rec approach, whereas the indef one are fine with the "naive" plain recursion. So, something like:
encodeList encodeElem es = encodeList encodeElem =
encodeListLen (length es) step 0 mempty
<> foldMap encodeElem es where
step n bs = \case
[] -> encodeListLen n <> bs
(e : q) -> step (n+1) (bs <> encodeElem e) q
encodeListIndef encodeElem es = encodeListIndef encodeElem es =
encodeBeginList encodeBeginList <> step es
<> foldMap encodeElem es where
<> encodeBreak step = \case
[] -> encodeBreak
(e : q) -> encodeElem e <> step q
And, I compared the two implementation to encode TxOuts...
with builtin foldMap and builtin length:
Number of assets | Memory | CPU |
---|---|---|
1 | 171103 | 62451845 |
10 | 1106651 | 395660752 |
20 | 2162843 | 775249101 |
with hand-written recursive functions:
Number of assets | Memory | Δ Memory | CPU | Δ CPU |
---|---|---|---|---|
1 | 90191 | -48% | 34674416 | -45% |
10 | 588969 | -47% | 244557904 | -39% |
20 | 1104421 | -49% | 456085834 | -42% |
So.... moral of the story.... do not use built-ins 😬 ?
MB has completed on-chain serialisation to CBOR to include BS and Maps so we should be able to properly hash the value of a TxOut to get same results than off-chain serialisation.
- Adding depedency to plutus-cbor to hydra-plutus so that we can use the CBOR encoding module
- Trying to use CBOR on-chain encoding fails for values because the representation in Plutus is not the same. We need to separate showing ada values from other values
Goals for today:
- Merge PR for CBOR encoding
- Merge branch for MT
- Consolidate ex units cost measurements into a single executable
Seems like the cost of encoding toData
for a ()
is greater than the cost for an integrer, which kind of makes sense as the latter is defined as a data structure. Could be interesting to optimise this case by adding a dedicated U
constructor for the builtin types?
Would be also interesting to measure the cost of building a (small) MT on-chain?
#elems | Mem cost | Cpu Cost |
---|---|---|
1 | 3.93162 | 1.70042556 |
2 | 6.78662 | 2.72467238 |
3 | 13.01544 | 4.93184607 |
4 | 19.67442 | 7.28079774 |
5 | 32.65688 | 11.85561155 |
6 | 46.0695 | 16.57220334 |
7 | 59.91228 | 21.43057311 |
8 | 74.18522 | 26.43072086 |
9 | 100.67496 | 35.74081491 |
10 | 127.59486 | 45.19268694 |
Changing the way the tree is balanced:
- We divide the length by 2 instead of trying to find the smallest power of 2
- This reduces the execution cost by a factor of 2 or 3!
Also, we provide a comparative cost in the contract-cost
execution, as a percentage of the maximum execution unit for both mem and cpu
What we want is the cost of a full fanout tx, which includes:
- Cost of size of ScriptContext (depends on number of outputs) + Redeemer (contains inclusion proof for each UTXO) + Datum (root hash of latest snapshot)
- Cost of MT inclusion verification when we'll use MT
- cost of serialising each UTXO
- cost of verifying proof for each given UTXO
This cost should be computed for varying UTXO size and varying TxOut value size
- we have 2 dimensions: num of UTXO + size of each UTXO (avg. number of assets | ada-only UTxo)
To compute cost of a Fanout Tx we need to ensure we can actually validate the tx on-chain which means we must properly serialise a UTXO and compute its hash: => Reusing txOut encoding from CBOR tests to write a proper CBOR encoding of Values
We need to take into account proper encoding of addresses from https://github.com/cardano-foundation/CIPs/blob/master/CIP-0019/CIP-0019-cardano-addresses.abnf. Our encoding is still wrong:
- We need to take care of stake address as they are generated
- The CBOR encoding from ledger uses variable length encoding of lists
Got dragged into the weeds of fixing the encoding of txOut on-chain.
- There are a lot of details to get right as the representations differ and the encoding is not straightforward.
- We decided to filter out some legacy things in the generator, most notably byron addresses whiuch are not handled on-chain and stake pointers which are an obscure feature with annoyingly complicated encoding.
MB fixed the value serialisation representation, now still having an error on addresses which have a random network id.
- We transform addresses to make sure they are all on
TestNet
- Adding more cases to handle stake addresses
- We need to take care of variable lenght lists and maps because that's what the ledger uses to encode
Value
We still have an error:
Ledger encoding:
9F # array(*)
82 # array(2)
58 39 # bytes(57)
2076E607DB2A31C9A2C32761D2431A186A550CC321F79CD8D6A82B29B8E0A714319812C3F773BA04EC5D6B3FFCD5AAD85006805B047B082541 # " v\xE6\a\xDB*1\xC9\xA2\xC3'a\xD2C\x1A\x18jU\f\xC3!\xF7\x9C\xD8\xD6\xA8+)\xB8\xE0\xA7\x141\x98\x12\xC3\xF7s\xBA\x04\xEC]k?\xFC\xD5\xAA\xD8P\x06\x80[\x04{\b%A"
82 # array(2)
00 # unsigned(0)
A1 # map(1)
58 1C # bytes(28)
3542ACB3A64D80C29302260D62C3B87A742AD14ABF855EBC6733081E # "5B\xAC\xB3\xA6M\x80\xC2\x93\x02&\rb\xC3\xB8zt*\xD1J\xBF\x85^\xBCg3\b\x1E"
A1 # map(1)
42 # bytes(2)
9CA4 # "\x9C\xA4"
0A # unsigned(10)
FF # primitive(*)
Plutus encoding:
9F # array(*)
82 # array(2)
58 39 # bytes(57)
2076E607DB2A31C9A2C32761D2431A186A550CC321F79CD8D6A82B29B8E0A714319812C3F773BA04EC5D6B3FFCD5AAD85006805B047B082541 # " v\xE6\a\xDB*1\xC9\xA2\xC3'a\xD2C\x1A\x18jU\f\xC3!\xF7\x9C\xD8\xD6\xA8+)\xB8\xE0\xA7\x141\x98\x12\xC3\xF7s\xBA\x04\xEC]k?\xFC\xD5\xAA\xD8P\x06\x80[\x04{\b%A"
82 # array(2)
00 # unsigned(0)
BF # map(*)
58 1C # bytes(28)
3542ACB3A64D80C29302260D62C3B87A742AD14ABF855EBC6733081E # "5B\xAC\xB3\xA6M\x80\xC2\x93\x02&\rb\xC3\xB8zt*\xD1J\xBF\x85^\xBCg3\b\x1E"
BF # map(*)
42 # bytes(2)
9CA4 # "\x9C\xA4"
0A # unsigned(10)
FF # primitive(*)
FF # primitive(*)
FF # primitive(*)
We got bitten by the fact the ledger encodes values conditionally:
- If length < 23, then it uses a fixed length map
- Otherwise, it uses a variable length map! This saves about 1 byte...
We're still having the error on validating the healthy transaction
- Looks probable the tx outs are not encoded in the same order off and on-chain
- Dumping the TX itself shows the outputs are in the order they are on-chain, but the CBOR encoding off-chain does not show the same order
- => We were incorrectly
reversing
the list of outputs in the constructiong of thefanoutTx
.foldr
with(:)
actually preserves the order (and laziness) of a list
Trying to complete handling of values in TxOut for computing fanout's hash
- Instead of comparing hashes, running tests comparing serialisation results in order to understand where it's wrong
- Encoding the values such that they have the same on and off-chain representation requires matching CBOR encoding, which requires MB's work to be completed to handle various kind of structures. For now, going to fake it off-chain
Plan for today:
- Implement Merkle-tree on-chain
Start implementing a MT in Plutus, using properties
- We can use
deriving Haskell.Eq
andHaskell.Show
in a Plutus module as we don't have typeclasses inside Plutus
To build a MT from a list we create an insert
function to inject the bytestring (element of the MT) in the right place in the tree.
- got a successful test with a partial implementation: We don't insert more than 2 nodes which is annoying, but we don't care when
toList
ing the tree :) - We don't care having unbalanced MT right now, we want to have a correct construction first and then later balance the tree to make it more efficient
Completed implementation of (unbalanced) MT with membership proof.
- Before balancing the tree, it would be interesting to have some baseline metrics of the cost of checking membership on-chain, in a validator.
- Moving the
evaluateScriptExecutionUnits
function to test prelude so that we can reuse it from various packages.
Got a baseline execution cost for unbalanced MT membership check. We want to assess the size of the tx to verify membership.
- What's the number of tx needed to fan out UTXO from a head?
- Could measure the cost of building a MT on chain in order
Working on balancing the MT:
- Writing property expressing that tree is balanced: All proofs' length should be lower than the log base 2 of the tree size + 1
- Messed up ordering of argument sin
logBase
: First argument is the base, second argument is the number we are looking log of
Studying the QC library for stateful testing that's been written by John Hughes for Plutus stuff: https://github.com/input-output-hk/plutus-apps/tree/main/quickcheck-dynamic
Agenda:
- Discuss the "discovery" that Byron addresses are not supported by Plutus script validators
- What are the consequences of this?
- Is there anything we need or what the hydra-node needs to do as a result?
Notes:
-
Quick Plutus Q: Why does it cost (much) more to run validators on larger UTXO (eg. UTXO with more than ADAs) than on plain ADA ones given we only ever look at the address, which is constant?
- because static evaluation of Plutus likely
- accessing / decoding the script context (from a general, unstructured representation) is accounted to the script already
- looking at address vs. address + value should NOT make a difference
-
Need to prevent byron addresses in two places:
- When committing to a Head
- When submitting transactions to a Head
-
Not supporting byron makes Head less isomorphic
- Also requires Hydra to do additional checks
- Different to not supporting certificates for example, where we can just leave out some ledger rules (BTW: we need to align on that API with ledger team!)
- In a way it's cardano ledger rules + hydra filters on top
- We need to document what is different "In-Head" than on Alonzo, Babbage, etc.
- In general: plutus validators cannot make the assumption that the validated transaction are balanced!
- This would be natural assumption and we need to inform people
- Deposits already make it unbalanced (what does that mean?)
- This is sad
-
General sentiment: we want to get rid of byron addresses, but likely can't just do it.
- For Hydra: we don't want to introduce a HF dependency because of this and would (need to) check byron addresses at the two places (see above)
-
Adding Byron Address support to Plutus? Is this what we want in V2?
- Byron addresses contain a lot of crap
- This is likely not what we want. Deprecating means not encouraging producing new byron addresses
- A phase 1 validation failure when byron addresses would be helpful for the future -> less surprises in PlutusV2
-
Soft deprecation of Byron Addresses idea: add amore and more expensive fee when byron addresses are used.
-
We also discussed that we might want additional witnesses to leverage the ledger hash checking
- Would need to go into babbage
- We will check whether we really need it
- We could propose it here: https://github.com/input-output-hk/cardano-ledger/issues/2479
Continuing work on implementing fanout on-chain validator. We start with hashing the addresses only, both on-chain and off-chain.
Going down at the level of unit testing serialisation functions. We generate a cardano-api Utxo
which we need to translate to a Plutus [TxOut]
as provided by the ScriptContext
.
This is rather straightforward using txOutInfo
function that does the conversion.
We hit an issue because we generate all kind of TxOut
, including ByronAddress
es which are not handled in Plutus.
- We need to generate UTXO without Byron addresses to make the property pass
- We raised the issue with Plutus and Ledger teams as this could be a problem for maintaining the isomorphism property between L2 and L1: If a Byron address is ever used and accepted as a valid part of a Snapshot in the Head, we would not be able to close the head as we would not be able to match the off-chain and on-chain snapshots.
- Either we make sure we filter snapshots in the head that contain byron addresses, eg. they should not be admitted and signed because an attacker could use this as a way to prevent closing the head, or Plutus handles Byron addresses.
We finally make the validator pass on a fanout tx by limiting the size of generated UTXO
- After our ensemble session, we worked on several individual spikes to "get a feeling" for O(n) complexity and whether we will face practical issues / require one or the other Plutus builtins.
Plans for the afternoon:
- Add a correct datum to close state so that ETE tests pass
- Compute various execution costs for different sizes of UTXO
Trying to implement properly the close Tx so that the Closed
on-chain state contains proper hash of the UTXO of the snapshot, which can then be verifed against in the fanout tx validation phase.
- ETE test is passing but not TUISpec, the main difference being the latter does not produce any fanout
- Made TUISpec test pass by using the
closedUtxoHash
instead of theopenUtxoHash
in theClosed
state. This is not correct: We want to use theopenUtxoHash
because that's what's provided by theCollectCom
transition but this is easier to change for now,
Writing a test that dumps the redeemer's cost and tx size for various sizes of UTXO set.
First with arbitrary values. The maximum number of UTXO before the execution budget is exhausted is around 60. Of course, the maximum tx size is way too large so it would be rejected before even the scripts got executed.
# UTXO | Tx Size | Ex Mem Cost | Ex CPU Cost |
---|---|---|---|
1 | 9439 | 1035219 | 403801953 |
2 | 9800 | 1234463 | 487947101 |
3 | 11588 | 1915878 | 782263197 |
4 | 12157 | 2118261 | 867485655 |
5 | 10612 | 1748127 | 703618093 |
6 | 13754 | 2818237 | 1167997644 |
7 | 13146 | 2728106 | 1126549032 |
8 | 13850 | 3045455 | 1261597306 |
9 | 14626 | 3409767 | 1418384014 |
10 | 15240 | 3716835 | 1549109335 |
20 | 23205 | 7260457 | 3069160382 |
30 | 28236 | 9707261 | 4108389519 |
40 | 45365 | 16407143 | 7004547692 |
50 | 49018 | 18267795 | 7794194748 |
60 | 50137 | 19585833 | 8337837953 |
With Ada-only UTXO values, limit grows to 240 UTXO and the maximum Tx Size stays around reasonable limits.
# UTXO | Tx Size | Mem Cost | CPU Cost |
---|---|---|---|
1 | 8821 | 804423 | 303059298 |
2 | 8858 | 899003 | 341714624 |
3 | 8895 | 993586 | 380400988 |
4 | 8932 | 1088173 | 419060661 |
5 | 8969 | 1182763 | 457751372 |
6 | 9006 | 1277357 | 496415392 |
7 | 9043 | 1371954 | 535110450 |
8 | 9080 | 1466555 | 573778817 |
9 | 9117 | 1561159 | 612478222 |
10 | 9154 | 1655767 | 651150936 |
20 | 9524 | 2602037 | 1038141941 |
30 | 9896 | 3548657 | 1425350296 |
40 | 10266 | 4495627 | 1812776001 |
50 | 10636 | 5442947 | 2200419056 |
60 | 11006 | 6390617 | 2588279461 |
70 | 11376 | 7338637 | 2976357216 |
80 | 11744 | 8287007 | 3364652321 |
90 | 12114 | 9235727 | 3753164776 |
100 | 12482 | 10184797 | 4141894581 |
100 | 12484 | 10184797 | 4141894581 |
120 | 13226 | 12083987 | 4920006241 |
140 | 13966 | 13984577 | 5698987301 |
160 | 14706 | 15886567 | 6478837761 |
180 | 15446 | 17789957 | 7259557621 |
200 | 16147 | 19599474 | 8002061202 |
220 | 16926 | 21600937 | 8823605541 |
240 | 17666 | 23508527 | 9606933601 |
What's kind of surprising to me is the execution cost difference between the two types of UTXO: In our current contract, we only hash and verify the address part of each TxOut so the size of the value should be irrelevant as it's not used to compute nor verify anything.
If we replace the UTXO hash verification contract by a simpler one which does not verify anything, we do not get much different numbers.
For ADA-only values:
# UTXO | Tx Size | Mem Cost | CPU Cost |
---|---|---|---|
1 | 8557 | 743306 | 281270203 |
2 | 8594 | 811374 | 311203108 |
3 | 8631 | 879442 | 341136013 |
4 | 8668 | 947510 | 371068918 |
5 | 8705 | 1015578 | 401001823 |
6 | 8742 | 1083646 | 430934728 |
7 | 8779 | 1151714 | 460867633 |
8 | 8816 | 1219782 | 490800538 |
9 | 8851 | 1287850 | 520733443 |
10 | 8890 | 1355918 | 550666348 |
20 | 9260 | 2036598 | 849995398 |
30 | 9632 | 2717278 | 1149324448 |
40 | 10002 | 3397958 | 1448653498 |
50 | 10372 | 4078638 | 1747982548 |
60 | 10742 | 4759318 | 2047311598 |
70 | 11108 | 5439998 | 2346640648 |
80 | 11482 | 6120678 | 2645969698 |
90 | 11848 | 6801358 | 2945298748 |
100 | 12222 | 7482038 | 3244627798 |
100 | 12222 | 7482038 | 3244627798 |
120 | 12962 | 8843398 | 3843285898 |
140 | 13702 | 10204758 | 4441943998 |
160 | 14442 | 11566118 | 5040602098 |
180 | 15143 | 12859410 | 5609327293 |
200 | 15885 | 14220770 | 6207985393 |
220 | 16551 | 15445994 | 6746777683 |
240 | 17398 | 17011558 | 7435234498 |
260 | 18141 | 18372918 | 8033892598 |
280 | 18846 | 19666210 | 8602617793 |
300 | 19584 | 21027570 | 9201275893 |
For arbitrary values:
# UTXO | Tx Size | Mem Cost | CPU Cost |
---|---|---|---|
1 | 9863 | 1180298 | 471919539 |
2 | 10075 | 1328430 | 536506292 |
3 | 11438 | 1826334 | 753834196 |
4 | 11532 | 1884114 | 779439801 |
5 | 12909 | 2438374 | 1021693990 |
6 | 11902 | 2104338 | 876140833 |
7 | 16607 | 3783062 | 1607982538 |
8 | 14829 | 3241938 | 1371414289 |
9 | 14463 | 3203994 | 1357001231 |
10 | 15809 | 3701006 | 1573220724 |
20 | 26334 | 7806954 | 3366309503 |
30 | 34277 | 10995982 | 4758573588 |
40 | 39714 | 13528402 | 5864881393 |
50 | 40322 | 14094586 | 6112141051 |
60 | 55886 | 20080006 | 8724117522 |
70 | 58821 | 21320678 | 9268275778 |
the limit is slightly higher but not significantly so, hence it seems that whether or not the outputs are used in the validators, the execution cost grows linearly with the size of the outputs.