Contract life cycle¶
After having formulated a contract template, it is time to “instantiate” it. Instantiating a contract template corresponds to running a computer program, where the contract specification corresponds to the program executable (or the source code). It is making operational the relation between events that is accepted by a contract and the contract specification.
A residual contract denotes what is remaining of an instantiated contract, where “remaining” means which events the contract still accepts.
If a contract specifies that some event is currently expected and that event is applied, the contract is evolved to the residual contract that represents the remaining expected events after the occurrence of the just given event.
If a contract no longer accepts any events, i.e. it is reduced to success
or failure
, it is said to be complete.
Instantiated contracts are stored in a contract store, which for now can be considered a container for keeping instantiated contracts and the events that have been applied to them.
The life cycle of a contract can roughly be summarized as follows:
- Instantiate the contract template with required arguments.
- Submit an event to the instantiated contract.
- If the event applies to the contract, compute the residual contract after applying the submitted event.
- Repeat from (2) until the residual contract is complete.
These steps are described in more detail below.
Instantiating contracts¶
When you instantiate a contract template, an artifact is put into the contract store that represents the contract after zero events have been applied. The precise nature of this “artifact” is not important (and is also dependent on the underlying contract store technology), it is just a piece of data that represents a running contract, much like a process in an operating system represents a running computer program. After a contract has been instantiated, a new entity exists in the contract store. We may instantiate contract templates as often as we want -– a desirable property as we can describe different contracts using the agreed common underlying structure, like reusing a general sales contract for different sales.
Deon Digital maintains a prototype GUI that allows users to interact with a contract storage platform by writing and instantiating contract specifications as well as submitting events to instantiated contracts.
Submitting events to contracts¶
Two things happen when you submit an event to a contract:
- First, the system tests whether the event applies to the contract in its current state.
This means that the issuing agent and the event fields must match the predicates given in the contract.
That is, if a contract expects an event from agent
alice
, any event from agentbob
will be rejected. - Secondly, if the event can be accepted by the contract in its current state, the event is applied to the contract, evolving it to a residual version.
We illustrate this using the example contract developed in the preceding section, whose definitions we repeat here:
type Order : Event {
amount : Int, // The amount of euros is offered for the item.
recipient : Agent, // The recipient of the order.
item : String // The item that is ordered
}
type Delivery : Event {
recipient : Agent, // The recipient of the item
item : String // The item that is delivered
}
val bikeShopInventory =
[("Bike", 100),
("Brakes", 20),
("Helmet", 30)] // It's a small bike shop.
val acceptOffer = \inventory -> \(item : String) -> \(price : Int) ->
// If the item is listed in the inventory and the price is right,
// then accept the offer
List::any
(\(name, acceptPrice) -> name = item && price >= acceptPrice)
inventory
template Sale(buyer, seller, amount, item, inventory, maxDays) =
// Some buyer orders an item for some price from a seller
<buyer> order: Order where
order.amount = amount &&
order.recipient = seller &&
order.item = item
then (
// The seller delivers that item
<seller> delivery: Delivery where
acceptOffer inventory order.item order.amount &&
delivery.item = order.item &&
delivery.recipient = buyer &&
// 'DateTime::addDays t d' creates a new timestamp that
// is 'd' days after timestamp 't'.
delivery.timestamp <= DateTime::addDays order.timestamp maxDays
or
// The seller tries to cheat
<seller> delivery: Delivery where
acceptOffer inventory order.item order.amount &&
(not (delivery.item = order.item) ||
not (delivery.recipient = buyer))
then failure
)
template BikeSale(buyer, seller) =
Sale(buyer, seller, 100, "Bike", bikeShopInventory, 3)
In the last line, we instantiated our Sale
contract to specify the sale of one bicycle for 100 euros from the seller to the buyer:
Sale(buyer, seller, 100, "Bike", bikeShopInventory, 3)
Notice that we have passed the bikeShopInventory
list as the inventory and that we set the maximum delay between the delivery and the order to three days.
The buyer
and seller
parameters remain abstract in the new BikeSale
template.
The instantiated contract now corresponds to the following snippet:
<buyer> order: Order where
order.amount = 100 && // 'amount' is set to 100
order.recipient = seller &&
order.item = "Bike" // 'item' is set to "Bike"
then (
<seller> delivery: Delivery where
// 'inventory' is set to 'bikeShopInventory'
acceptOffer bikeShopInventory order.item order.amount &&
delivery.item = order.item &&
delivery.recipient = buyer &&
// 'maxDays' is set to 3
delivery.timestamp <= DateTime::addDays order.timestamp 3
or
<seller> delivery: Delivery where
acceptOffer bikeShopInventory order.item order.amount &&
(not (delivery.item = order.item) ||
not (delivery.recipient = buyer))
then failure
)
The following is an example of an event that would be accepted:
Order {
agent = alice, // The 'buyer' is now set to 'alice'
// This is arbitrary, as there are no constraints on the 'timestamp'
// of an 'Order' event in the contract.
timestamp = #2017-12-24T16:00:00Z#,
amount = 100,
recipient = bob, // The 'seller' is now set to 'bob'
item = "Bike"
}
Note that the timestamp
field is not specified in the first part of the contract, so any value would be accepted.
The value of the timestamp
field in the Order
event does influence the acceptable values of the timestamp
in the Delivery
, however, as we have instantiated the contract such that it requires that no more than three days pass between an Order
and a Delivery
.
When this event is applied to the contract, it evolves into something equivalent to the following:
<bob> delivery: Delivery where // 'seller' is set to Bob
acceptOffer bikeShopInventory "Bike" 100 &&
delivery.item = "Bike" &&
delivery.recipient = alice && // 'buyer' is set to Alice
// 'maxDays' is set to 3
delivery.timestamp <= DateTime::addDays #2017-12-24T16:00:00Z# 3
or
<bob> delivery: Delivery where
acceptOffer bikeShopInventory "Bike" 100 &&
(not (delivery.item = "Bike") ||
not (delivery.recipient = alice)
then failure
The following is an accepted event for the contract in this state:
Delivery {
agent = bob,
// <= 3 days after the 'timestamp' in the 'Order' event
timestamp = #2017-12-25T17:00:00Z#,
item = "Bike",
recipient = alice
}
After this event has been applied, the contract is evolved to a finishing, successful state:
success
The contract store now holds the residual contract, the original specification, and the events that were applied to evolve the original instantiated contract to its current state. As this contract no longer accepts any events, it is complete.
Exercise: Selling a bike
In this and the next exercise, we assume you are using the provided GUI to interact with the system.
First, instantiate the BikeSale
contract in the Composer
tab of the web application.
Next, check that the Viewer
tab shows the correct instantiation of the contract.
In the Actions
tab, try applying an event that should not be accepted, for instance offer too little money for the bike.
Verify that the contract hasn’t changed in the View
tab.
Now apply an event that is expected.
Verify that this time the contract has evolved.
Continue interacting with the contract to get it to be a success
.
Can you get stuck in any way?
Exercise: Make a contract stuck
Wrong instantiation parameters may make it impossible for the contract to ever evolve to a success
.
Try changing the BikeSale
contract in such a way that no chain of events will let it progress.
For instance, see what happens if the price offered for the chosen item is too low in the contract instantiation.
Querying past state¶
Regardless of the state of an instantiated contract, it is possible to query both residual contract and past events from the contract store.
The former is available directly from the contract store.
For the latter we will use the special function getEvents that is provided in the CSL standard library.
The basic idea is that each running contract in the contract store is associated with a unique id.
getEvents
takes this id as its argument and returns a list of events that have been applied to the corresponding contract in the contract store, sorted with older events before newer events.
In order to query the state of the current contract we will need to have the id of the contract available.
Since the contract ids are assigned at instantiation, we need some way to refer to the id in the contract before it is assigned.
To do this we take the current contract id as an instantiation argument.
At instantiation time we can the use the special self
value which will be resolved to the actual contract id.
Using getEvents
we can rewrite the above bike example, this time accepting any Order
event, but only allowing a Delivery
if an Order
arrived for a “Bike” for 100 Euro:
type Order : Event {
amount : Int, // The amount of euros is offered for the item.
recipient : Agent, // The recipient of the order.
item : String // The item that is ordered
}
type Delivery : Event {
recipient : Agent, // The recipient of the item
item : String // The item that is delivered
}
val isCorrectOrder = \contractId -> \buyer -> \seller -> \price -> \item ->
List::any ( \(e:Event) -> // List::any from the standard library
type order = e of {
Order -> // Only consider Order events
order.recipient = seller &&
order.amount = price &&
order.agent = buyer &&
order.item = item;
_ -> False // All other events
}) (getEvents contractId) // Function accessing past events
template Sale(self, buyer, seller, price, item) =
<buyer> Order // Accept any Order event from buyer
then
<seller> delivery: Delivery where
isCorrectOrder self buyer seller price item &&
delivery.item = item &&
delivery.recipient = buyer
template BikeSale(self, buyer, seller) = Sale(self, buyer, seller, 100, "Bike")
Note that this makes it possible to breach the contract by ordering anything other than a bike for exactly 100 Euro.
Ignoring some of the hairier syntax for type casing (see Value Expression Language for details), the isCorrectOrder
is a function that takes the contract id, expected buyer and seller names, price, and item; and checks that there exists a past Order
event on the contract with the given id matching these values.
The List::any
function is defined in the standard library and returns True
if any of the elements of the list match the predicate.
Contracts observing the past of other contracts¶
In the previous section we reference events from the same contract using getEvents.
By using the getEvents
function with other contract ids we can observe events from other contracts in the contract store.
Contract ids may be stored in variables just like other values, so we can provide contract ids to a contract by including them in the events that the contract accepts.
The following artificial example shows how one uses a contract id of a running contract to control the accepted events in another contract:
PromiscuousContract
is a very simple contract that does nothing but accept any events sent to it.
Predicate isMyEvent
is used to indicate whether an event is of the type MyEvent
.
This predicate is used in the contract DependentOnOther
to ensure that an event e
of the type CarriesContractId
only gets accepted, if the contract associated with the id in the field e.contractId
has previously accepted any event of the type MyEvent
:
type MyEvent : Event {}
type CarriesContractId : Event {
contractId : ContractId
}
// The "promiscuous contract" accepts any sequence of events.
template rec PromiscuousContract() = <*> Event then PromiscuousContract()
// This predicate takes any event and returns 'True' if and only if the
// event is a 'MyEvent' event, defined above.
val isMyEvent = \(e: Event) ->
type e = e of {
MyEvent -> True;
_ -> False
}
// This contract accepts a 'CarriesContractId' event only if the contract id
// in the event refers to a running contract in the contract store that has
// previously accepted an event of the type 'MyEvent'.
template DependentOnOther() =
<*> e: CarriesContractId where
List::any isMyEvent (getEvents e.contractId)
Let’s say we instantiate the contracts PromiscuousContract
and DependentOnOther
and that the auto-generated contract id for the instantiated PromiscuousContract
is "42"
.
If we supply the following event to DependentOnOther
, it will only be accepted if the PromiscousContract
has previously accepted an event of type MyEvent
:
CarriesContractId {
contractId = "42",
timestamp = #2017-12-24T16:00:00Z#, // Arbitrary in this example.
agent = alice // Arbitrary in this example.
}
In this example, the observed contract and the observing contract have been specified in the same set of declarations.
In general, this is not necessary; the contract id supplied to getEvents
must identify some instantiated contract in the contract store, but this could be a contract that was instantiated prior to the definition of the observing contract.