A Daml ledger tells a story–how does the action happen?

György Balázsi
DAML Masterclass
Published in
12 min readJul 17, 2023

--

In this follow-up post I will show you how the action happens on a Daml ledger and how the screenplay is written for that. TL;DR: the key is the Update monad. You’ll also learn how to build a time machine by graph query.

Photo by Jakob Owens on Unsplash

In my previous blog post A Daml ledger tells a story — it’s time to show it to everyone, I explained how a Daml ledger tells a story via the event graph it stores and how the event graph can be visualized and analyzed via a graph database, in our example Neo4J, using the Cypher graph query language.

In this follow-up post I will look more closely into one kind of event out of the two kinds which are recorded on the ledger, namely choice exercises.

Recap: visualizing the event graph

As a recap, I show again the visualization of the event graph, this time using a Daml model a little bit more complex than the Skeleton model used in the previous post (although, of course, this “more complex” model is still oversimplified compared to any real-life business model).

This time, let’s look into a Daml model which implements a ticket offer/purchase business model. The three actors of the model are in the following roles in the legal relationships expressed by the three kinds of contracts, “ticket offer”, “ticket agreement” and “cash”:

  • Actor #1 is the buyer in the “ticket offer” relationship, owner in the “ticket agreement” relationship and original owner in the “cash” relationship.
  • Actor #2 is event organizer in the “ticket offer” and the “ticket agreement” relationships and new owner in the “cash” relationship.
  • Actor #3 is issuer in the two versions of the “cash” relationship.

The structure of the Daml model can be represented, using the model visualizer feature of the Daml platform, in the following way:

The visual representation of the ticket offer/purchase Daml model

The event graph visible to Actor #1 after purchasing a ticket is the following:

Actor #1’s ledger projection

The introduction: In offset 9 a Cash contract was created, signed by Actor #3, the issuer of the Cash contract. In offset 10 a TicketOffer contract was created, signed by Actor #2, the event organizer.

The real action: In offset 11, Actor #1 exercised the Accept choice on the TicketOffer contract, specifying the contract id of the Cash contract created in offset 9 as input, namely the money which is meant to pay for the ticket. Offset 11 now forms a non-trivial transaction tree, unlike the transaction tree formed by the only consequence of the Give choice in my previous blog post:

  • One consequence of accepting the offer by Actor #1 is transferring the cash to the event organizer (itself having a consequence, creating a new Cash contract for the event organizer, and at the same time recording a consuming relationship with the original version of the Cash contract)
  • The other consequence is getting created a TicketAgreement contract for Actor #1.

Aside: contracts are “items on the ledger”?

As an aside, I want to draw your attention to an abstraction in the Daml parlance which is fine to use — as long as you exactly understand what it actually means. Otherwise it can lead to serious misunderstandings.

The Daml docs say that “Contracts are items on a ledger.”

They go on: “When a contract is created on a ledger, it becomes active. But that doesn’t mean it will remain active forever: it can be archived.”

From this formulation the false conclusion can be drawn that being active or archived are properties of isolated “items” or things, namely contracts, “on the ledger”.

From what you’ve learnt about the ledger event graph, you can see that this is actually not the case. A contract, “active” or “archived”, is actually a relationship (or lack of relationship) between events recorded on the ledger.

The expression “active contract” is shorthand for a creation event recorded on the ledger for which no consuming exercise event was recorded on the ledger. The expression “archived contract” is shorthand for the totality of a creation event and a consuming exercise event pointing to that creation event, recorded on the ledger.

Atomic “delivery vs payment”

In general, the above pattern is called “delivery vs payment”, or “DvP” for short. Another name for the pattern is “atomic swap”.

The challenge in this use case is that we want to guarantee for both the seller and the buyer that they cannot remain without compensation: it cannot happen that the buyer pays the money but doesn’t get the goods or the seller delivers the goods and doesn’t get the money.

One way to achieve such a guarantee is to involve an escrow agent trusted by both the seller and the buyer, but Daml knows better: as long as the “money” asset and the goods asset live on the ledger, the atomic swap of these can be guaranteed (if not, promises or IOU’s for both can be swapped).

The way how the Daml ledger model supports atomic swaps is actually simple: a transaction tree is either appended to the ledger as a whole or not, there is no third option. As the transaction tree contains both the creation of new contracts and the consuming exercise on previously created contracts, the swap as a whole either happens or not.

Achieving this “simplicity”, of course, under the hood is not simple. You can see how many Canton system elements are needed for it to work from the section of the docs describing Transaction Processing in Canton.

Need-to-know basis

As an aside, we can demonstrate Daml’s need-to-know-basis subtransaction privacy feature by looking at the ledger projection of Actor #3. As expected, it only contains the events for which Actor #3 is a stakeholder, namely the events around the creation and transfer of Cash contracts — the issuer of the Cash contracts doesn’t need to know anything about what the money buys:

Actor #3’s ledger projection

Time machine

If you wish to see the ledger history in state machine style, as a series of active contract set (ACS) snapshots, the Cypher graph query language comes in handy.

For this still simple ledger you can tell without query, simply by inspection that (in the ledger projection of Actor #1):

  • Before the 11_0 Accept event, the 9_0 Cash and the 10_0 TicketOffer contracts were active. If you imagine the event graph before adding the snapshot 11 transaction tree, these were the creation events for which no consuming exercise event was recorded on the ledger.
  • After recording the 11_0 Accept event, the 11_3 Cash and the 11_4 TicketAgreement contracts constitute the ACS. Now these are the creation events for which no consuming exercise event was recorded on the ledger.

For a larger ledger, simple inspection doesn’t work, so it’s useful to know how to query the ACS snapshot.

The simpler query is the one for the current ACS, expressing in the Cypher graph query language that we are looking for create events for which no consuming exercise was recorded on the ledger:

MATCH (c:created) 
WHERE NOT ()-[:CONSUMING]->(c)
RETURN c

For getting the series of historic ACS sets at a certain snapshot, we have to query for the create events which happened at or before that snapshot, for which no consuming exercise was recorded on the ledger before or at that snapshot. The below example shows the query for the ACS at snapshot 10, and all other snapshots can be queried the same way:

MATCH (c:created WHERE c.offset<=10)
WHERE COUNT{
(e:exercised WHERE e.offset<=10)-[:CONSUMING]->(c)
}=0
RETURN c

How the screenplay is written

Now let’s see how the screenplay is written for the action of our story.

The full code of the underlying Daml model and the Daml script for the purchase is the following:

module Main where

import DA.Assert ((===))
import Daml.Script

template TicketOffer
with
organizer : Party
buyer : Party
price : Decimal
where
signatory organizer
observer buyer
choice Accept : ContractId TicketAgreement
with
cashId : ContractId Cash
controller buyer
do
cash <- fetch cashId
cash.amount === price
exercise cashId Transfer with
newOwner = organizer
create TicketAgreement with
organizer; owner = buyer

template Cash
with
issuer : Party
owner : Party
amount : Decimal
where
signatory issuer
observer owner
choice Transfer : ContractId Cash
with
newOwner : Party
controller owner
do
create this with owner = newOwner

template TicketAgreement
with
organizer : Party
owner : Party
where
signatory organizer, owner

setup : Script (ContractId TicketAgreement)
setup = script do
alice <- allocatePartyWithHint "Alice" (PartyIdHint "Alice")
ticketWizard <- allocatePartyWithHint "TicketWizard" (PartyIdHint "TicketWizard")
scroogeBank <- allocatePartyWithHint "ScroogeBank" (PartyIdHint "ScroogeBank")
aliceId <- validateUserId "alice"
ticketWizardId <- validateUserId "ticketwizard"
scroogeBankId <- validateUserId "scroogebank"
createUser (User aliceId (Some alice)) [CanActAs alice]
createUser (User ticketWizardId (Some ticketWizard)) [CanActAs ticketWizard]
createUser (User scroogeBankId (Some scroogeBank)) [CanActAs scroogeBank]

cashCid <- submit scroogeBank $ createCmd (Cash scroogeBank alice 10.0)
offerCid <- submit ticketWizard $ createCmd (TicketOffer ticketWizard alice 10.0)
submit alice $ exerciseCmd offerCid (Accept cashCid)

The important part for us now is the formulation of the “Accept” choice on the TicketOffer contract, so let’s highlight that:

template TicketOffer
...
choice Accept : ContractId TicketAgreement
with
cashId : ContractId Cash
controller buyer
do
cash <- fetch cashId
cash.amount === price
exercise cashId Transfer with
newOwner = organizer
create TicketAgreement with
organizer; owner = buyer

In order to fully understand what this choice really means, let’s consider that the template choice syntax is a shorthand of something a little bit more complex.

As the Daml docs put it:

[Choices are] permissioned functions that result in an Update. Using choices, authority can be passed around, allowing the construction of complex transactions…

As a do block always returns the value of the last statement within it, the whole do block returns an Update, but the return type on the choice is just a ContractId Contact. This is a convenience. Choices always return an Update so for readability it’s omitted on the type declaration of a choice.

How a choice is an Update function can be fully understood if we convert it back into a real Update function and plug it back into the choice body:

accept: TicketOffer -> Accept -> Update (ContractId TicketAgreement)
accept TicketOffer{..} Accept{..} = do
cash <- fetch cashId
cash.amount === price
exercise cashId Transfer with
newOwner = organizer
create TicketAgreement with
organizer; owner = buyer

Note that the “TicketOffer” record data type stands for the payload of the template with the same name, and the “Accept” record data type stands for the input data type of the choice with the same name.

After having “outsourced” the “Accept” choice into the “accept” function, we can simply plug the function invocation into the template’s choice body like this:

template TicketOffer
...

choice Accept : ContractId TicketAgreement
with
cashId : ContractId Cash
controller buyer
do
accept TicketOffer{..} Accept{..}

After this substitution you will see that the compiler accepts the new code and the script result will be the same as before, so the two pieces of code express the same.

Update is a monad

As you can see from the signature of the “outsourced” choice function above, its return type is “Update (ContractId TicketAgreement)”.

What is this Update?

Update is a parametric type. It describes some kind of “context” which “contains” an instance of some type, in this case “ContractId TicketAgreement”. (In this case, the “contained” type is parametric itself: ContractId is a parametric type which becomes a specific type if we add a record data type which is a template payload type.)

The meaning of Update is a ledger update which may or may not be successful. The meaning of its type parameter is that this is the type of the data which gets returned from a ledger update — if the update is successful.

(The returned data can be “nothing”, e.g. from the archival of a contract, expressed by the unit data type which is written as an empty tuple. But it has to be specified even in this case. Returning this kind of “nothing” from a successful ledger update is fundamentally different from returning “nothing but an error message” from an unsuccessful ledger update.)

Returning a piece of data from a successful ledger update means that this piece of data gets recorded in the “exercise_result” field of the choice exercise events. For creation events the return value is always the contract id of the created contract, recorded in the “contract_id” field of the event. If the update is not successful, an error message gets returned to the caller client application and nothing recorded on the ledger.

The whole list of ledger updates is listed in the Reference: Updates chapter of the Daml docs. The list entails creation, choice exercise (including archive), fetch a contract payload by contract id, fetch contract id and payload by contract key, check contract key visibility, assertion, getting the ledger time, etc.

What does it mean that Update is a monad?

The concept of “monad” is central to functional programming. It may sound strange at first, but once you understand it, you will see that it’s quite useful. There are a lot of explanations out there, I list here some such explanations I found useful:

The Update monad is somewhat analogous to the IO monad in Haskell, so you may want to take a look at how this is explained in the Learn You a Haskell for Great Good! book.

In a nutshell: a monad is a parametric family of types (type class) which has functions for expressing the composition of functions with side effects (like IO in Haskell or ledger update in Daml) in a meaningful way so that return values can be passed over to the next steps of the function chain. “In a meaningful way” means that adhering to certain rules which basically express associativity.

The “do” block is syntactic sugar

You may have noticed that in the Daml choice body the consequences of the choice exercise are nicely listed in a “do” block.

This “do” block notation in Daml choice bodies is convenient because it reads like a list of todo items, just as instructions in non-functional, procedural languages.

This is also something which is fine to use, as long as you exactly understand what it actually means. Otherwise, it can lead to serious misunderstandings.

The fundamental difference between a “do” block and a list of instructions in procedural languages is the following:

  • the “do” block consists of actions which may or may not succeed
  • the “do” block as a whole will fail if any of its elements fail
  • the elements of the “do” block must belong to the same side effect category (that is monad), in our case ledger update
  • the “do” block returns the return value its last element.

The backwards pointing arrow syntax is used for assigning action return values to variables, which can be used in later actions of the block.

It turns out that the “do” block notation in Daml choice bodies is the same as the “do” notation in Haskell for monads, and is syntactic sugar for the native >>= (“bind” operator to pass a monadic value to a pure -> monad function to receive a monadic value) and >> (“then” operator) operators.

The above discussed, outsourced function version of the “Accept” choice can be rewritten without the “do” block sugar in the following way:

accept': TicketOffer -> Accept -> Update (ContractId TicketAgreement)
accept' TicketOffer{..} Accept{..} =
fetch cashId >>=
\cash -> cash.amount === price >>
exercise cashId (Transfer with newOwner = organizer) >>
create TicketAgreement with organizer = organizer, owner = buyer

Just like in Haskell, “do” blocks in Daml are not only used in choice bodies but can be used in any monad. We use “do” blocks in Script all the time. You can even use it, if you wish to show off your functional programming savvy with the list monad e.g. like this (but your PR will be probably rejected):

let baseList = [1,2,3]
let modifiedList = do
x <- baseList
[x,x,x]

-- modifiedList = [1,1,1,2,2,2,3,3,3]

How is it guaranteed that the Update monad adheres monadic laws?

It is fundamental to understand that the monadic laws are not checked by the compiler. If the programmer declares that a certain type is an instance of the monad type class (called Action in Daml), the compiler only uses this information for type checking. The programmer themselves need to make sure that the underlying implementation is correct in that it adheres the monad laws.

This is guaranteed e.g. for the IO monad in Haskell how the language interacts with the operating system. And this is guaranteed for the Update monad in Daml how transactions are processed by Canton.

--

--