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:

  1. It seems that one cannot reuse the HasEnsure type 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.

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

  2. 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 Price in this example would lead to another check of the Price’s validity on the creation of the choice’s output. Furthermore, I assume the nesting Ensurable’s would incur a cost that has to be paid at construction. For example, if I were to have a type Inventory = TextMap Price, no way around walking the map?

1 Like

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.

1 Like
  1. Starting with the easy question: Yes it is intentional that hand-written HasEnsure instances don’t work. The same is true for a lot of other Has* 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.

1 Like

Isn’t that validation purely client-side when there’s only one signatory anyway?

1 Like

@Luciano Thanks, you’re right!

1 Like

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

1 Like

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.

2 Likes

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.

1 Like

Thanks for the clarification!

1 Like

For clarification, this would be an entirely different contract?

1 Like

What I mean is this:

  1. I submit create Good with seller = Bernhard; asset = "An original drawing by me"; price = -9000.0 on 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.
  2. Now suppose I’ve got some way of “offering” that Good to you. Eg by divulging the contract as part of the creation of a GoodOffer. Your participant will validate the ensure clause of the Good contract 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 original Good contract, any outputs from the GoodOffer will be unusable since everyone else has rejected it.
2 Likes

Sorry, I miswrote, that Good would an entirely different template so I would be able to tell.

1 Like

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…

2 Likes

How does one create a dishonest participant, create a fork of daml and ignore the ensure clause validation?

1 Like

That’s the obvious way to go about it, yes. Not a supported feature :wink:

3 Likes