Pattern for non-blocking / nonconsuming parallel invites?

Consider the Expense pool example:

If you have a Pool contract

template Pool 
    with
        owner: Party
        name: Text
        participants: [Party]

and you want to invite participants, the default example is to use a consuming choice which would then allow a single invite and then re-create the pool with the additional participant.

Is it possible to have nonconsuming invites that update the participants of the pool?
Nonconsuming would allow multiple invites to be created while having an active Pool contract. Parties would Accept their invite, and the pool would be updated. But you would be limited to single sequential participant updates and would need to use a key instead of the contract id.

From my tests it seems this pattern is “difficult” at best: where the PoolInvite template would have a Accept choice which would have to find the pool and exercise a participantUpdate choice (which seems difficult to have a proper control logic on the choice.

My design keeps coming back to: The Pool should not hold participants, and Participants should be another contract. This type of contract separation would be used whenever parallel activity is desired.

Thanks

1 Like

I imagine the steps similar to:

Here is another variation of the story:

Here is an example how a club could allow its members to invite other members in a non-racy fashion (using a non-consuming choice). The example also shows how a member can prove its membership to third parties.

module Club where

import Daml.Script


template Club
  with
    owner: Party
    name: Text
    publicParty : Party
  where
    signatory owner
    observer publicParty

    key this : Club
    maintainer key.owner

    nonconsuming choice Invite : ContractId Member
      with
        member : Party
        newMember : Party
      controller member
      do
        isMember <- visibleByKey @Member (this, member)
        assert $ isMember || member == owner
        create Member with club = this; member = newMember

    nonconsuming choice Prove : ContractId MembershipProof
      with
        member : Party
        observers : [Party]
      controller member
      do
        isMember <- visibleByKey @Member (this, member)
        assert $ isMember || member == owner
        create MembershipProof with club = this; member; observers

template Member
  with
    club : Club
    member : Party
  where
    signatory club.owner
    observer member

    key (club, member) : (Club, Party)
    maintainer key._1.owner

template MembershipProof
  with
    club : Club
    member : Party
    observers : [Party]
  where
    signatory club.owner, member
    observer member, observers

    agreement "Here by " <> club.name <> " assures that " <> show member <> " is a member of " <> club.name


test = script do
  -- Setup parties
  [owner, alice, bob, charlie, eve, publicParty] <- mapA allocateParty ["Owner", "Alice", "Bob", "Charlie", "Eve", "PublicParty"]

  -- Setup club
  clubCid <- submit owner do createCmd Club with owner; name = "Club ABC"; publicParty

  -- Club invites Alice
  aliceMembershiCid <- submit owner do exerciseCmd clubCid Invite with member = owner; newMember = alice

  -- Alice invites Bob
  bobMembershiCid <- submitMulti [alice] [publicParty] do exerciseCmd clubCid Invite with member = alice; newMember = bob

  -- Bob proves membership (for Charlie)
  submitMulti [bob] [publicParty] do exerciseCmd clubCid Prove with member = bob; observers = [charlie]

  -- Eve can't prove membership for anyone
  submitMultiMustFail [eve] [publicParty] do exerciseCmd clubCid Prove with member = eve; observers = []

  return ()
2 Likes

@Johan_Sjodin you pass club into the membership and proof: if you had to update the original club (such as change of owner) this would break the relationships? Why pass the entire club contract instead of a club key?

So this basically implies that the pattern for scaling/parallel comes down to breaking everything up into contracts.

@StephenOTT you are right that in order to make the Club unique you would need a key there too. For simplicity, I added the following as key to the Club ^^

 key this : Club
 maintainer key.owner

(Regarding changing owner/name/publicParty, the club could do that as they are the sole party to Club and Member templates. However, the purpose of the above example was only to illustrate how you can avoid raise conditions adding new members.)

In order for the club to be able to change its name you could have a different key, say:

module Test.Club where

import Daml.Script


type ClubKey = (Party, Int)

template Club
  with
    owner : Party
    id : Int
    name: Text
    publicParty : Party
  where
    signatory owner
    observer publicParty

    key (owner, id) : ClubKey
    maintainer key._1

    nonconsuming choice Invite : ContractId Member
      with
        member : Party
        newMember : Party
      controller member
      do
        isMember <- visibleByKey @Member ((owner, id), member)
        assert $ isMember || member == owner
        create Member with clubKey = key this; member = newMember

    nonconsuming choice Prove : ContractId MembershipProof
      with
        member : Party
        observers : [Party]
      controller member
      do
        isMember <- visibleByKey @Member ((owner, id), member)
        assert $ isMember || member == owner
        create MembershipProof with clubKey = key this; member; observers

    choice ChangeName : ContractId Club
      with
        newName : Text
      controller owner
      do
        create this with name = newName

template Member
  with
    clubKey : ClubKey
    member : Party
  where
    let owner = clubKey._1

    signatory owner
    observer member

    key (clubKey, member) : (ClubKey, Party)
    maintainer key._1._1

template MembershipProof
  with
    clubKey : ClubKey
    member : Party
    observers : [Party]
  where
    let owner = clubKey._1

    signatory owner, member
    observer member, observers


test = script do
  -- Setup parties
  [owner, alice, bob, charlie, eve, publicParty] <- mapA allocateParty ["Owner", "Alice", "Bob", "Charlie", "Eve", "PublicParty"]

  -- Setup club
  clubCid <- submit owner do createCmd Club with owner; id = 1; name = "Club ABC"; publicParty

  -- Club invites Alice
  aliceMembershiCid <- submit owner do exerciseCmd clubCid Invite with member = owner; newMember = alice

  -- Alice invites Bob
  bobMembershiCid <- submitMulti [alice] [publicParty] do exerciseCmd clubCid Invite with member = alice; newMember = bob

  -- Bob proves membership (for Charlie)
  submitMulti [bob] [publicParty] do exerciseCmd clubCid Prove with member = bob; observers = [charlie]

  -- Eve can't prove membership for anyone
  submitMultiMustFail [eve] [publicParty] do exerciseCmd clubCid Prove with member = eve; observers = []

  -- Owner changes the name of the club
  clubCid <- submitMulti [owner] [publicParty] do exerciseCmd clubCid ChangeName with newName = "Club 123"

  -- Alice can prove membership (for Charlie)
  submitMulti [alice] [publicParty] do exerciseCmd clubCid Prove with member = alice; observers = [charlie]

  return ()

@Johan_Sjodin if you changed the owner of the club, would this not break your membership contracts? I see it as all part of the same pattern: yes the example shows a limited feature, but is suggests to pass the pool contract into other contracts such as membership: Which would seem to break if the original pool was ever updated (such as change owner). So the pattern would be to pass the key, and always do lookups?

@Johan_Sjodin Pool Example with owner update:

In your example of the Club, if the Club changed owners, you are suggesting that all membership and proof contracts would be re-created with the new club contract?

Alternate pattern: pass the Club Key into Membership and Proof, and those contacts would need to do a lookup/fetch on the key to get the current active club contract ? This would seem to mean you do not need to re-create membership and proof contracts whenever some of the club data changes (such as owner).

As the owner of a club is the sole signatory of the Club and Membership (in the above example), the owner can archive the whole club and all associated memberships. Note that any party can also create its own club, and add arbitrary members. But no member would need to prove a membership of such a club. I haven’t thought more about changing the owner of a club (and it was not the purpose of the “minimal” example).

@Johan_Sjodin thanks for followup. I agree the owner of the club can archive, as my ExpensePool example demonstrates the same concept. And yes i agree the club is a minimal example, but in that example it passes the club into the membership and proof, essentially making the club immutable unless you are willing to re-create contracts for membership and proof whenever the club contract has changes.

This is the pattern I am identifying/questioning in my first comment of this thread. Many of the examples are based on designs of immutable parent contracts that if changed would require re-creation of all child contracts. Of course this story works well for the basic concepts, but I keep seeing the failure when trying to scale the workflow of the contracts to real-world structures/business workflows (as shown in comment).

has there been testing or experience on if it is “better” to re-create contracts vs create loose key based linkages between contracts? Example: If the Club is immutable, then a change to the club would require complete archiving of the club and child contracts followed by the recreation of the club with the new data + re-creation of all child contracts using the new version of the club. At ~scale is this a lot of/burdensome changes against the ledger??

vs if the club is mutable similar to my ExpensePool example above, then the linkages between contracts are based on Key and lookups during choices instead of using a static already passed club within the child templates/contracts. A ~caveat problem is that ensure cannot seem to use fetchByKey, so potential limitations there (as per comment in update-in-ensure-clause thread).

Just to clarify:

The second example ^^ (i.e., where the ClubKey = (Parties, Int)) actually allows the owner of the Club to change its name without having to update any of the other contracts referring to it by key (as the key remains the same). Any field which is not part of the key can obviously be updated in this fashion without breaking a reference by key.

The purpose of the first example ^^ was only to answer your initial question

with a “small” example how members could be added to a club in a non-racy manner (and there I simply put club : Club as key for simplicity reasons). But having such a key (as you point out) does not allow you to modify any fields of the Club (as they all belong to the key, and would hence break the key reference made), so it is better to put the fields you want to be able to modify outside of the key (as in the second example ^^).

Regarding:

I would say that depends very much on your use case what is better. If you know that a contract instance, say of Foo, is not going to be archived and recreated (very often), and is only referenced by a few other contract instances, you might even want to consider referencing by a ContractId Foo (especially if such referring instances also need to be updated in case the ContractId Foo is no longer active). But key references can indeed save you from recreating a lot of contracts.

1 Like

Thanks for the detailed response. Understood.