Skip to content

Logbook 2022 H1

Sebastian Nagel edited this page Jan 24, 2023 · 3 revisions

June 2022

2022-06-27

Babbage era update

  • 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?

2022-06-24

Babbage era update

  • Continuing updates to Babbage, now lately the outputs of a Babbage.TxBody seemingly turned into Sized 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 the genPointInTime
  • 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!

2022-06-23

Babbage era update

  • 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
  • Seems like cardano-api got some more updates because of reference inputs, script witnesses now can be either PScript or PReferenceScript
  • The error types of Alonzo.Tools.evaluateTransactionExecutionUnits and the monad of EpochInfo 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 from fixedEpochSize. No brain capacity left..

2022-06-20

MBT

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

Fanout Deadline Issue

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.

Using quickcheck-dynamic in Hydra

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.

2022-06-17

Pair Programming

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 faulty ClientInput
  • 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 the TestHydraNode 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 a Text, this still works in the BehaviorSpec: Injecting a fault makes the test dump the node's logs, but no logs appear for the ModelSpec
  • 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 the TestHydraNode which thus prevents getting logs as counterexample in case the property fails, which is annoying. The problem is that the assertion runs in the StateT (Nodes ..) (IOSim s) monad which implies the trace is not available as we are still running the action.

2022-06-10

Pairing Session - #395

Seems like we have a clear idea of what's going on:

  • When we compute the hash for the InitialSnapshot we order them by the TxIn corresponding to the CollectCom transaction, but the UTxO inside in the InitialSnapshot uses the original TxIns
  • 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 the IsTx 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 commits 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 to genStClosed
  • 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/"

AB Solo

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 TxIns.

  • The problem is that I would like to ensure the TxOut lists are the same without taking care of the ordering, but there is no Ord instance for TxOut => 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 the commitOutputs and not the committed 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 in collectCom 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 the Head script.
  • TxOutRef is not an Ord instance... Trying to implement Ord for TxOutRef failed for some mysterious reason: Got a full PLC dump telling me some function was not INLINEABLE which seemingly was related to my Ord 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.

2022-06-09

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 single Command wrapping a ClientInput. This is made possible thanks to actionName which is a method from StateModel 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 generating NewTx command: We select one random party, then lookup a UTxO it can spend and generate a transaction from it, using mkSimpleTx. We consume a UTxO we own, and send the value to some address owned by someone else in the Head
  • 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, as Alice -[ 10 ]-> Bob eg. a simple payment transaction, leaving the details of the construction of the actual Tx to the perform. This highlights the fact the Model 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, implementing IsTx typeclass and updating the generators
  • We need to converet from our Payment UTxO to the standard UTxO which requires a TxIn, but we cannot invoke arbitrary in the ActionMonad
  • 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 utxoHashes 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.

2022-06-08

How we talk about Architecture in Hydra

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 :) )

Test-Driven Development:

  • 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 performing -> 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 we waitNext 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 and tabulate or label 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 the Open 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.

2022-06-07

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 the StateModel
  • Had a remaining issue was the Typeable m constraint on the StateModel instance for WorldState 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 a forever loop 🤦
  • Spawning the nodes using async: This is fine because we run in IOSim 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.

2022-06-03

Random thoughts SN

  • 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?

Pairing Session

#384

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 of MockServer in WalletSpec
  • We simplify the update function of the Wallet 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.

2022-06-02

AB Solo

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 type a on top of chaning the state, like in this example for a thread registry.
  • The perform function produces a value of type a which is stored in a Var a and passed to the nextState function to update the state. This makes the model's actions independent of the details of the values produced
  • The StateModel class requires definition of an ActionMonad which is pinned down for execution of perform 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 variable m, adding the needed constraints. The runActions 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 the HydraNode if stored in the WorldState but there's a way to not store them there but only as the outcome of actions, using the Var 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 a Var n where n represents the step number, incremented at each step
    • This sequence of action is run by
      1. checking the precondition of the actual state is ok
      2. perform the action passing it the variable looked up from the environment (searched by type)
      3. the result is stored in a new environment
      4. the postcondition for the action and the starting state is checked, passing it the result of perform step

Submitted a few ideas from previous days' discussions:

Ensemble Session

#382

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 both Initial, Commit, and Head script Possible solution: Add some index using unstableMakeDataIndexed

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

May 2022

2022-05-31

AB Solo - Model-Based Testing - #375

Need to create a bunch of HydraNodes 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, the withHydraNode 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 and sign but actually, only sign 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 the TinyWallet
  • The coverFee function uses a different UTxO 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 the sign 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.

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.

2022-05-30

AB Solo - Model Testing

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 mapping Party 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 and State modules, from the Direct 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 the IOSim 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

2022-05-24

MB - Feedback from Pi

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.

SN on #363

  • 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 a Contest 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 the Fanout client input. How would the HeadLogic 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 the waitUntil functions with veryLong (TM) timeouts to still get nice error locations.
  • Why are we using multiples of clusterSize in waiting on the bench?

2022-05-23

SN Solo on vasil testnet

  • 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 the hydra-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.

  • 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
  • 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 the remainingContestationPeriod to 0 because we would need to have current time to compute it, and this done in the calling code. Adjusted the relevant StateSpec 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:

  1. In the posixToUTCTime function, I multiplied by 1000 instead of dividing by 1000
  2. 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 🎉

2022-05-20

Notes about Audit & Security Workshop in Barcelona

Auditor Presentations

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.

Auditees Presentations

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 / Contract Model Testing

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.

Side Discussions

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.

2022-05-19

Pair Programming

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:

  1. we pass a wrong time to the close function
  2. thus the contestation dealine is wrong and too much in the future
  3. 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 the ScriptContext -> 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 in DiffTime rather than abstract slots, adding like a Tick DiffTime to OnChainEvent?

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.

2022-05-18

AB Solo Programming

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)]}

2022-05-17

AB Solo Programming

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 a contestationDeadline 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.

2022-05-16

The Mental Model of DevOps

  • 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

SN Solo on babbage-preview

  • 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 the SerializedTxOut. Important here is to use the Plutus.TxOut type and its correct binary format.
  • Off-chain code for observeCommitTx is now failing because it can't deserialize the SerializedTxOut. This now needs a fromPlutusTxOut and we could use some better functions on the usage side of things (tighten up the assumptions around SerializedTxOut).
  • Due to the lack of a FromCBOR Plutus.Data instance, I switched to Serialise Plutus.Data which goes bothways
    • Where was the ToCBOR Plutus.Data instance coming from?
  • 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 the NetworkId 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.

2022-05-12

Contestation

  • 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 the Direct component.

SN Solo on babbage preview

  • 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 with initTx.
    • 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 for head []
  • 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 be const () -> 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
  • 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
  • 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 of serialiseData . toBuiltinData
    • For some reason fromBuiltinData is much more expensive than unsafeFromBuiltinData in wrapValidator -> how expensive is unsafeFromBuiltinData then? We maybe should use partly "from-data-decoded" script contexts?

2022-05-11

SN Solo on babbage preview

  • 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
  • The fact that Hydra.Chain.Direct.Wallet uses cardano-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 :| _))))))))
  • 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 when cabal 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
    

Ensemble Session

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.

2022-05-10

Ensemble Session

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 the hashPreserialisedTxOut function from on-chain code
  • In the close, we use the hashTxOuts function which does not explicitly sort, only use the Map.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 the CollectCom 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, the ClosingSnapshot which we use in the Tx 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

SN Solo on babbage preview

  • Switching hydra-cardano-api to BabbageEra 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 on hydra-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
  • Now we are missing Arbitrary ValidatedTx instances for babbage era txs -> gonna look for them tomorrow

2022-05-09

Pairing session

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

Formalising Head Protocol

  • 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

2022-05-06

SN Solo

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 "as PlutusV1" by our hydra-cardano-api is bound to fail.
  • Updating mkScriptWitness is not easy, as I cannot add an instance HasScriptLanguage for BabbageEra because the type is not exposed by the cardano-api :(

2022-05-05

SN Solo

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 remove applyTx's signature as we will instantiate era as LedgerEra right above anyways.
  • Seems like consensus has parameterized blocks by protocol now. And the only two options satisfying the classes are Praos and TPraos -> 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 via hydra-node --script-info
  • Finally some polish of tx-cost output

AB Solo

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.

Pairing Session - Contestation

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 the Contest 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.

2022-05-02

AB Solo

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:
    % 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'
    
    The code depends on the existence of some keys to produce a seed transaction for each node

Merged 2 PRs:

April 2022

2022-04-29

SN solo on updating deps

  • 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 a localTxMonitorProtocol

    • can mock this like the localTxSubmissionProtocol .. but why are we using this data structure and not the subset we need?
  • Mocked both, Hydra.Chain.Direct and Hydra.Chain.Direct.Wallet

  • We see golden sample WARNINGs for Event SimpleTx, ReasonablySized (ServerOutput SimpleTx) and ReasonablySized (ServerOutput (Tx AlonzoEra))

    • was this the case on master already?
    • if not, which generator changed?
  • Got a MuxError (MuxIOException writev: resource vanished (Broken pipe)) "(sendAll errored)" in tests using the MockServer

    • not yet added the localTxMonitorProtocol there
  • 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's index-state with latest haskell.nix (cabal only workflow was likely using latest packages on hackage, while haskell.nix is using "more pinned" versions)
  • 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.

SN solo on babbage preview

  • 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 plutus release/v1.0.0 for vasil <2022-04-29 Fri 15:43>
  • Plutus-apps is not updated to work with plutus release/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 in hydra-test-utils and the plutus-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 using TypedValidator seems promising! <2022-04-29 Fri 18:29>
  • Yes, turns out.. wrapValidator and a vendored scriptValidatorHash is the only things we need from plutus-ledger 🎉! <2022-04-29 Fri 19:24>
  • Continuing the dependency bump for babbage-preview <2022-04-29 Fri 19:25>
  • cardano-crypto-class needs secp256k1 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 has plutus-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 and cardano-ledger use a potentially newer nixpkgs (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 the cardano-node fails with some undefined symbol in cardano-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>

2022-04-28

SN solo

  • After https://github.com/input-output-hk/hydra-poc/pull/329, updating the cardano-node used in tests is very easy -> just update shell.nix and the, then failing, test asserting cardano-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 from plutus -> plutus-apps .. okay, let's add plutus-apps as a source repository package
  • plutus-ledger depends on cardano-wallet-core now.. this seems like a rat's tail! We are using at least TypedValidator from plutus-ledger.. maybe trace what we really need and not try to have all wallet deps in scope?
  • Hydra.OnChain.Util also used checkScriptContext from plutus-ledger.. but the module is now unused -> drop it.
  • Can we avoid using plutus-ledger in some places? let's have a look at inspect-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 in plutus-ledger -> filed a PR: https://github.com/input-output-hk/plutus-apps/pull/437

2022-04-27

Hydra engineering meeting

SN solo

  • 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 a newtype around ByteString, but that would side-step the safety we get from using Cardano.Crypto.Class things?

2022-04-26

Ensemble

  • 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!?

2022-04-25

SN solo

  • 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 now let 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 has generateSigningKey -> 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 on arbitrary
  • 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 any Gen a, using the two different generators genVerificationKey and fst <$> genKeyPair yielded different verification keys.. this is bound to happen again :(

2022-04-20

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 include a 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 to verify -> why are functions having a SignableRepresentation!?
  • Instead of simply "porting" toPlutusSignature, we could define To/FromData instances for MultiSignature 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.

2022-04-15

Ensemble + MB Solo - Twists and turns of rollback testing

  • 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 a ChainPoint, 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).

2022-04-14

MB Solo - ChainSyncHandler in the direct-chain

  • 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.

Pairing Session

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 an OnChainTx event is observed, or a Rollback happens: In the first case, we advance the state and link to the previousState ; in the second case we simply drop a number of links in the chain
  • 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

2022-04-13

Hydra Engineering Meeting

  • 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

  • Even later: we might get pairing support

2022-04-12

MB Solo - CI Workflow Rework

  • Spent some time trying to improve our CI workflow, mainly from two motivating factors:

    1. The full workflow in itself takes about 30 minutes, which is quite a long feedback loop.
    2. 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.

2022-04-08

MB Solo

  • 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 this withHydraNode, 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.

Ensemble Session

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 the HydraNode 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 of Host 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.

2022-04-07

2022-04-06

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

2022-04-05

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

March 2022

2022-03-31

AB Solo

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?

2022-03-30

AB Solo

Mainnet VM restarted automatically this morning and of course cardano-node wasn't.

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?

2022-03-29

AB Solo

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 runs yarn build to generate the docusaurus documentation, then yarn validate which generates and validates the API schemas
  • yarn validate invokes yarn validate:inputs && yarn validate:outputs which points ultimately to validate-api.js which is a JS source file (node) at toplevel of the project that takes some json files and generates some output like
    $ ./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"}
    ...
    
    It's unclear to me why we generate this output here, it would be somewhat more valuable as a page?
  • The haddock files are generated by ci-haddock.sh into a directory docs/static/haddock where they will be picked up by docusaurus as static assets. The links in the welcome page's footer points to index.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

2022-03-28

AB Solo

Working on merging PRs for Hydra

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 💪

2022-03-21

AB Solo

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.

2022-03-18

Post-Hackathon Discussion

  • 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:
      1. Administering the node itself (config IPs, keys, network topology...),
      2. Administering the Head(s) (Head lifecycle),
      3. 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

2022-03-17

Improving Logging

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"
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

Meeting w/ Quviq

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:

  1. Walk Quviq team through our codebase and testing strategy
  2. Get feedback from their studying the code, tests written, what are the main areas of improvement
  3. Work together q/ Quviq to build missing parts, possibly also with support from other teams that would be interested in new approach
  4. Start the formal certification process now as we want to be ready by June

2022-03-16

Hydra Workshop Day 3

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:

Hydraw Hackathon Setup

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!

2022-03-15

Hydra Workshop Day 2

Connecting Hydra Nodes

  • 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

Opening/Closing Head

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 and CollectCom
  • 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

Hydraw Hacking Session

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 paint x/y/r/g/b and transforms that into a transaction sent as NewTx 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) }
    

2022-03-14

Hydra Workshop Day 1

Planning

  • "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

The IDEA

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:

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 because config.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

2022-03-10

MB Solo

  • 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 the redocusaurus 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.

SN Solo

  • 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 overflapping TxIn for initials and commits, which collapsed when put in the same UTxO
    • fixed this directly in genAbortableOutputs
  • 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 on TxIn, TxId, VerificationKey and Hash generators.

Pairing Session

  • 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 the TxSpec test of collectCom.
  • 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 3 commit script executions from budget it's not enough anymore.
  • *IMPORTANT Increased the memory budget on hydra-cluster config to 20M (from 14M)

2022-03-09

Weekly Engineering Meeting

  • 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

Pairing Session

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"]

2022-03-07

Notes on test-suite grooming

  • 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 use 32kB tx size
  • Lowering Fixture.maxTxSize to 16kB 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 still expectedFailure? why is this not working?
  • It seems like genUTxO is generating Ada-only TxOuts and our Arbitrary UTxO is generating multi-asset TxOuts .. is this intended?

(Start of derailing into configuring generators)

  • Seems like only genUTxOWithSimplifiedAddresses is using the arbitrary 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 of cardano-ledger? The Alonzo era instance seems to be sampling everything relevant (multi-assets, ada-only, shelley/byron addresses and even datum hashes)
  • Using the Mary era arbitrary instance seems not to have any benefit over the Alonzo one. So we might be really be ending up with genUTxO = fromLedgerUTxO <$> arbitrary!?
  • Maybe the reason of using the genUTxo0 was the ability to generate a sequence of valid transactions? i.e. if we use arbitrary, we might end up with outputs which we do not know how to spend (did this work before "by accident" as we used the same genEnv in genUTxO and genTx before?)

2022-03-04

AB Solo

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
  • 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.

Ensemble Session

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 Mutations: 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 actual Datum for the outputs of a tx we are constructing?

11:59 ()

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:
    1. That all the legitimate parties have committed (or aborted)
    2. 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

2022-03-03

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

Ensemble Session

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 the txBody 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 a requiredSigner

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's TokenName is included in required signatories Which means we need to lookup the PT, thus requiring to pass the PolicyId to the Initial validator
  • We got tripped by the PlutusTx.AssocMap being usually imported qualified as Map => Use AssocMap to make sure it's distinguished from Haskell's Map? OTOH, we must not use Haskell libraries in Plutus code so calling it Map 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.

2022-03-02

Ensemble Session

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 wrong pparams as there are 2 of them -> unifying those for testing purpose in Evaluate 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

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 the InitTx

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

2022-03-01

AB Solo Programming

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

Pairing Session

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 the PolicyId 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 the validates 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

February 2022

2022-02-28

Ensemble session

  • 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 the init tx token where HeadParameters would be available
  • This means, that the μ_head needs the ValidatorHash of ν_head, which is currently parameterized by a MintingPolicyHash -> 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 the wrapMintingPolicy function, but this requires the type to be defined in another module because of the phase restrictino in TH.

AB Solo

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

2022-02-25

Ensemble Session

Discussing what to do next and what's the current state of master

  • We observe there's an irregularity in the observeXXX functions whereby the observeInitTx and observeCommitTx 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 => Add Init module for mutation + relevant mutations and tests
  • Add a healthyInitTx and relevant property in ContractSpec, 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 to Ledger layer. The latter is actually easier to work with in this case because the mint field is just a Value
  • 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

AB Solo Programming

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.

2022-02-24

Ensemble Session

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

AB Solo Programming

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:

  1. 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
  2. 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
  3. 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
  4. 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
  5. 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
  6. Run several different loads
    • dimensions:
      • number of nodes
      • distribution (different DCs, different regions, different continents/zones)
      • duration/load
  7. Automate daily load tester to run in CI
    • goal is to have a daily report available for some interesting (target) parameters

2022-02-23

SN Solo

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 the Dataset 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?

AB Solo

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 a cabal clean
  • Trying nix-shell -A cabalOnly and then cabal 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 to cabal update, why does it not do it automatically like every other tool out there? Updating should be opt-out

Pairing

  • 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 actual PlutusScript to be able to burn the token(s) which requires carrying it in the OnChainHeadState as it's not observable from the transactions other than initTx which mint them.

2022-02-22

Ensemble Session

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 the Direct 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 the Hydra.Chain layer and pick a chain-agnostic ByteString a its content.. for that we introduce UsingRawBytesHex to get JSON instances
  • Now we add headId :: HeadId to all the OnChainTx as we need to be able to tell them apart .. and realize this has a rat's tail so only do the OnInitTx for now.

AB Solo Programming

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

2022-02-21

SN bug hunt

  • 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 the submit function in the dialog, it seems that the wrong value is passed to mkSimpleTx
  • 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.

AB Solo Programming

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?

2022-02-16

Hydra Engineering Meeting

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

2022-02-14

Network Team Meeting

  • 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

AB Solo Programming

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

2022-02-11

SN Solo

  • 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 with seedFromFaucet 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

2022-02-09

Ensemble session

SN Solo

  • 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 the execution-cost exe in plutus-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 and encodeMap 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

2022-02-08

Planning Sessions

  • 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:

    1. As a operational device, to fuel the Hydra heads and pay for transactions necessary for the protocol on-chain transitions;
    2. 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:

    1. Query the external wallet for its UTXO set (note: not possible directly for hardware devices);
    2. Offer the user to commit some entries of this UTXO set
    3. Construct and balance the appropriate transaction using the Hydra node internal wallet
    4. Prompt the external wallet for signature of all committed UTXO entries
    5. Post the transaction.

MB Solo

  • 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 and encodeTag (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.

2022-02-03

SN Solo

  • 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 new Faucet actor
  • Next, draft a seedFromFaucet function which should be used instead of postSeedPayment 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 -> improved waitMatch error message

2022-02-02

AB Solo Programming

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 the mkCompiledCode function which is internal and what I get back is always deserialzed. So I need to actually flat encode the output of getPlc

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

Ensemble Programming

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 a HeadId 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

2022-02-01

AB Solo Programming

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

🤷

January 2022

2022-01-27

Ensemble

  • 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 in ContractSpec
  • 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

AB Solo Programming

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:

2022-01-26

Ensemble Programming

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

Testing Plutus contracts

Different approaches for testing Plutus contracts and apps:

  1. 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
  2. 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 and everywhere provide a way to modify generated traces to produce more traces representing some arbitrary change over the set of traces. See example for use of somewhere
  3. 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
  4. 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

2022-01-25

AB Solo Programming

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 Showable 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?

2022-01-24

AB Solo Programming

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 sorted TxIns of the transaction
  • To locate the correct TxIn I need the TxOut 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.

2022-01-21

Completing CollectCom

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

AB Solo Programming

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 a CollectCom transition. Good candidate is having an Open -> 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's TxDats 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 a fmap 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.

2022-01-20

Ensemble Session

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 the TxIn and TxOut actually being BuiltingByteString serialised off-chain from the actual ones
  • The collectCom validator will check that the headDatum contains a Hahs of the concatenation of TxOuts 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 our commitTx 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

2022-01-19

Hydra engineering meeting

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
  • 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?
  • 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

MB Solo

  • 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.

CI, Haddock and Plutus are in a Boat

Trying to fix again the issue with CI and haddock builds

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

Ensemble Session

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

2022-01-18

Ensemble session

  • Discussed roadmap & all the steps still ahead of us

  • Discussed collectCom transaction + validators, whether complexity is really O(n^2) with n = # 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 is O(n*log n)
  • 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 resulting Open 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.
  • To make collectCom validator correct, we use the fact that the commit transaction should validate (in the initial validator) that the DatumType Commit holds a correctly CBOR-encoded Utxo (or single TxOut). Then, creating the utxoHash is concatenating the pre-encoded TxOut of all commits and hashing this

  • We added some makeshift on-chain debugging code which seems to output BuiltinByteString via a decodeUtf8 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)
  • Of course, this approach also requires the committed Utxo datums be *in the same order as the commit inputs.

2022-01-17

AB Solo Programming

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/

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 using uplc gives me a "weird" error:
    $ ../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
    ....
    )))))))))))))))))))))))))))))))
    )))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
    
    Could it be that I am using a GHC list and not a Plutus one?

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

2022-01-14

Restructured code to have a single executable (in hydra-node) to run and dump performance data from plutus validators and other stuff

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

2022-01-13

Pair programming

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:

  1. first removing the txin in what's get serialised in the commit
  2. 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 on Map 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 the ScriptContext 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 use filter and foldMap

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.

2022-01-12

Hydra Engineering Meeting

  • 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 and length 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?
    1. 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
    1. 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
    1. use extra datums and have the ledger check that it

2022-01-11

Cost of On-Chain MT

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

Cost of encoding

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 😬 ?

2022-01-07

Pairing Session

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 the fanoutTx. foldr with (:) actually preserves the order (and laziness) of a list

2022-01-06

AB Solo

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

Pair Programming

Plan for today:

  • Implement Merkle-tree on-chain

Start implementing a MT in Plutus, using properties

  • We can use deriving Haskell.Eq and Haskell.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 toListing 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

AB Solo

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

2022-01-05

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

Hydra Engineering Meeting

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

2022-01-04

Ensemble

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 ByronAddresses 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

AB Solo Programming

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 the openUtxoHash in the Closed state. This is not correct: We want to use the openUtxoHash because that's what's provided by the CollectCom 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.

Older entries

See Logbook-2021-H2

Clone this wiki locally