Extending Ensure for non Templates
There have been previous questions on the forum around smart constructors and the use of newtype’s to have fine grain control over types without the extra boxing burden. One way to verify that one’s on ledger data complies is wrap the check for validity into the precondition ensure clause of a Template.
That is easy enough with a custom type class, ex
newtype Price = Price { p : Decimal }
deriving (Eq, Ord, Show)
class Ensurable a where
valid : a -> Bool
instance Ensurable Price where
valid Price { p } = p >= 0.0
and then
template Good
with
seller : Party
asset : Text
price : Price
where
signatory seller
ensure valid price
Three related questions:
- It seems that one cannot reuse the
HasEnsuretype class for this purpose as:
instance HasEnsure Price where
ensure Price { p } = p >= 0.0
leads to
$ daml build
Compiling training to a DAR.
File: daml/Ensureable.daml
Hidden: no
Range: 1:1-2:1
Source: Core to DAML-LF
Severity: DsError
Message:
Failure to process DAML program, this feature is not currently supported.
Missing required instances in template definition. with (:).
[T, y, p, e, C, o, n, N, a, m, e, , {, u, n, T, y, p, e, C, o, n,
N, a, m, e, , =, , [, ", P, r, i, c, e, ", ], }]
ERROR: Creation of DAR file failed.
Is this intentional? I could imagine that if we were to reuse it for non Template, data that it could be confusing as the default semantics would not execute the ensure check on price (assuming it has a HasEnsure instance) unless specifically asked.
-
On the flip side, if the check was automatic it could extend the usefulness of the type class. And the broad notion of a precondition check would not need to be separated into different type classes. This automatic behavior could be particularly useful as one would not have to nest all of your data in
Ensurable, just the “leaf” element where one defines custom types. But at the surprise of performing computation that is not explicitly defined in a template. -
Are there other considerations to adopting these precondition checks to data types? I can see that it could hinder performance (or at least lead to surprising behavior), as that check gets called on every creation. For example, a consuming choice that updates another field but does not modify a
Pricein this example would lead to another check of the Price’s validity on the creation of the choice’s output. Furthermore, I assume the nestingEnsurable’s would incur a cost that has to be paid at construction. For example, if I were to have atype Inventory = TextMap Price, no way around walking the map?
There have been previous questions on the forum around smart constructors and the use of newtype’s to have fine grain control over types without the extra boxing burden.
An aside here: to the best of my knowledge, newtype just de-sugars into plain old data, at the time of writing (meaning types are still ‘boxed’). I don’t know if there’s any plan to change that.
- Starting with the easy question: Yes it is intentional that hand-written
HasEnsureinstances don’t work. The same is true for a lot of otherHas*classes. We use those internally in the compiler to translate templates.
For 2 and 3, see Should runtime/API respect namespace scopes? for some related discussion. I think you can make this work but it needs a whole bunch of changes across the stack.
template Good with seller : Party asset : Text price : Price where signatory seller ensure valid price
Isn’t that validation purely client-side when there’s only one signatory anyway?
@Luciano Thanks, you’re right!
@Gary_Verhaegen I don’t fully understand. The ensure is validated by the Daml driver, how would it happen on the client-side? Or by “client-side” did you mean exclusively visible to the seller?
I created a single signatory template for ease of demonstration.
Cursory testing shows this is indeed validated server-side upon submission of a create command, so it’s not as bad as I thought. I believe there can still be issues with dishonest participants in a distributed context, but that’s way to far outside my comfort zone for me to make any specific pronouncement.
ensure clauses are fully validated by all Daml integrations. The only thing a dishonest participant can do is to “create” a contract instance violating the ensure clause locally, involving only parties hosted exclusively on that participant, and then use the contract in that same context.
As soon as another participant sees a transaction involving a contract with an invalid ensure clause, they will reject that transaction as invalid.
Thanks for the clarification!
with an invalid
ensureclause,
For clarification, this would be an entirely different contract?
What I mean is this:
- I submit
create Good with seller = Bernhard; asset = "An original drawing by me"; price = -9000.0on my dishonest participant. Depending on the driver I’m running on, this may not get validated by anyone else because in fact nobody else even learns of this. - Now suppose I’ve got some way of “offering” that
Goodto you. Eg by divulging the contract as part of the creation of aGoodOffer. Your participant will validate the ensure clause of theGoodcontract it receives and reject the transaction as invalid. Except in circumstances where my participant is a trusted “VIP”, that means the consensus will be to reject the transaction. If you participant decides to record it anyway, it can do so, but what good is that… Like the originalGoodcontract, any outputs from theGoodOfferwill be unusable since everyone else has rejected it.
Sorry, I miswrote, that Good would an entirely different template so I would be able to tell.
With a dishonest participant I can ignore ensure clauses on existing templates… So you write Good, I can create invalid instances on my participant. But again, I gain nothing from it…
How does one create a dishonest participant, create a fork of daml and ignore the ensure clause validation?
That’s the obvious way to go about it, yes. Not a supported feature 