Internal templates? (templates/contracts that can only be created by other template choices?)
I have not been able to find reference to this in the docs, so sorry if I missed something:
Is there a concept of something like internal templates? Where a template can only be used by another template (such as part of a choice)? If this does not exist, I am not saying it should exist, but i was considering the list of templates in the the navigator UI and in Daml Hub: where we list all templates, but not all are “public”: such as the concept of Public APIs / Interfaces vs the internal code.
Thanks
There is no such concept at the moment. It comes up occasionally and I do think something along those lines is sensible but so far it hasn’t been prioritized.
Great thanks.
@cocreature how do users restrict the usage of a template if there is no concept of “internal”? If you had a flow such as Invite > Participant: and your invite consumed into a Participant, how do you prevent the Participant from being created directly (and only created through a choice in an invite) ?
You cannot prevent contracts from being created directly. However, you can prevent them from being created with your signature (by not providing your authority for that). Generally, I recommend to switch from “all contracts of template T” to “all contracts of template T signed by parties X”. A Daml ledger is an open system. Parties can add contracts that only affect them as they want to.
@cocreature so based on that design, the signatory becomes a rippling change in the ledge:
An Expense Pool + Invites to join the pool. The original invites are created/signatory by a choice in the pool by the owner of the pool (the signatory of the invite). Now the owner of the pool changes, but there are still 100 invites that are active/open for that pool. From the description you provided, my understanding is: A change of the owner would have to propagate updates of the 100 invite contracts to recreate new versions of the contract with the new owner/signatory. Is that correct? Is that the recommended design?
Yes, in that design (and that’s the one I recommend) ownership changes become a bit trickier because you need to update other contracts as well. The details of how to best handle that depend on your specific design. In some cases it may make sense that as part of the ownership transfer you don’t directly modify the other contracts but you delegate authority to the new owner to change the owners of the other contracts.
But in either case, it means all 100 contracts must be updated.
If it was done with delegation it would 100 delegation wrappers + 100 invite contract updates?
No, you could create one contract with a non-consuming choice for the delegation. And you don’t need to do the invite upgrades immediately. You can often find designs where you update them lazily as they are used.
Is there a working example of this delegation?
Would this not be simpler to have secondary parties that were like “super owners” that the IAM gives actAs permissions to the actual “Pool” owner? Such as a party that represented the “pool”
I don’t have an example repo but something like this (untested)
template Invite
with
owner : Party
invitee : Party
where
signatory owner
template DelegateOwnershipChange
with
previousOwner : Party
newOwner : Party
where
signatory previousOwner, newOwner
nonconsuming choice ChangeOwner : ContractId Invite
with
cid : ContractId Invite
controller newOwner
do invite <- fetch cid
invite.owner == oldOwner
create invite with owner = newOwner
Would this not be simpler to have secondary parties that were like “super owners” that the IAM gives actAs permissions to the actual “Pool” owner? Such as a party that represented the “pool”
That’s another option. Keep in mind thought that the IAM is per participant so to make this work you need to potentially move a party across participants (there is functionality for that in Canton in early access).
You can also have a delegation design where you have one party as the owner and then you delegate via the Daml model the rights to act as that owner to another party.
I feel like i am missing a detail in your example:
Based on what you wrote, what i see is: If there were 100 invites, your delegatingOwnership would be used to generate 100 delegating invites, which then generate 100 new invites / replacing the old invites. Which would still be 200+ transactions for a ownership change, no?
You can also have a delegation design where you have one party as the owner and then you delegate via the Daml model the rights to act as that owner to another party.
Is there a working example of this? Where delegation does not mean placing the original contract in “limbo” / wrapped in the delegating contract?
I am looking at this: https://github.com/StephenOTT/daml-quick-start/blob/1206d7cd0a683c0a3f633cce318f018d7f54084a/ExpensePool/daml/ExpensePoolWithState.daml
There is nonconsuming invites, and the invite need would need to still be active after a change to the pool (such changing the name of the pool)
Even with delegation is seems to just generate more and more contract updates
Here is a detailed example with Expense Pool + Invites.
The only way i have thought of to restrict creation is using signatory (“works in the tests”/does not throw errors):
This checks if the invite being processed is from the same pool and if the invite was created by the same pool owner.
But this example would still require Invite contract updates with a new pool owner issuing new invites/replacement invites.
in either case, it seems like unless there is another functionality: the design pattern becomes: live with the potential of mass contract updates or use the IAM and “role-parties”/“group-parties” (or whatever you want to call them) which would act as a super owner that real parties could actAs for admin-like functions on a ~“master-contract”.
module ExpensePoolWithState where
import Daml.Script
import DA.Set (Set)
import DA.Set qualified as Set
type PoolKey = (Party, Text)
type InviteKey = (PoolKey, Party)
template Pool
with
poolId: Text
owner: Party
name: Text
participants: Set Party
invitedParticipants: Set Party
where
signatory owner
observer participants, invitedParticipants
ensure do
-- Cannot have overlap between the two lists:
Set.null(Set.intersection participants invitedParticipants)
key(owner, poolId): PoolKey
maintainer key._1
choice InviteNewParticipant : (ContractId Pool, ContractId PoolInvite)
with
newParticipant: Party
controller owner
do
newPool <- create this -- recreate the pool with the invited participant : This is required because of Observers being able to Accept the Invite.
with
owner
name
participants
invitedParticipants = Set.insert newParticipant invitedParticipants
invite <- create PoolInvite with
poolKey = key(this)
newParticipant
return (newPool, invite)
choice InviteAccept : ContractId Pool
with
invite: ContractId PoolInvite
controller invitedParticipants
do
-- SEE HERE:
i <- fetch @PoolInvite invite
assertMsg ("Creator of invite must be current pool owner") (signatory(i) == [owner])
assertMsg "Pool Key does not match" (i.poolKey == key(this)) -- Assumes that if the owner changes than existing invites become invalid. This line could be removed if you dont care about owner changes.
assertMsg "New Participant is not part of invited participants." (Set.member i.newParticipant invitedParticipants)
create this
with
poolId
owner
name
participants = Set.insert i.newParticipant participants
invitedParticipants = Set.delete i.newParticipant invitedParticipants
choice RemoveParticipant : ContractId Pool
with
participantToRemove: Party
controller owner
do
assertMsg "Participant you want to remove is not in the list of participants for this pool"
(Set.member participantToRemove participants)
create this with
poolId
owner
name
participants = Set.delete participantToRemove participants
invitedParticipants
choice RemoveInvitedParticipant : ContractId Pool
with
participantToRemove: Party
controller owner
do
assertMsg "Participant you want to remove is not in the list of participants for this pool"
(Set.member participantToRemove invitedParticipants)
-- Generates the invite Key + archives the invite: essentially revoking the invite
let inviteKey = (key(this), participantToRemove):InviteKey
invite <- fetchByKey @PoolInvite inviteKey
exerciseByKey @PoolInvite inviteKey Archive
-- recreates the Pool with the invitee removed
create this with
poolId
owner
name
participants
invitedParticipants = Set.delete participantToRemove invitedParticipants
template PoolInvite
with
poolKey: PoolKey -- Use the pool key to find the pool in the future (such as if changes to the pool occur)
newParticipant: Party
where
signatory poolKey._1 -- The pool owner is the creator of the invite
observer newParticipant
key(poolKey, newParticipant): InviteKey
maintainer key._1._1 -- Pool owner
postconsuming choice AcceptInvite : ContractId Pool
controller newParticipant
do
-- This works because the invitee is part of the invitedParticiants Set in the Pool, which is an observer of the pool.
exerciseByKey @Pool poolKey InviteAccept with
invite = self
normalFlow = script do
alice <- allocateParty "Alice"
bob <- allocateParty "Bob"
-- Create a pool:
emptyPool1Cid <- submit alice do
createCmd Pool with
poolId = "pool-1234"
owner = alice
name = "Pool1"
participants = Set.empty
invitedParticipants = Set.empty
-- Invite Bob to the Pool:
(updatedPool1Cid, invite1Cid) <- submit alice do
exerciseCmd emptyPool1Cid InviteNewParticipant with
newParticipant = bob
Some _updatedPool <- queryContractId alice updatedPool1Cid
assert(Set.member bob _updatedPool.invitedParticipants)
-- Revoke Bob's invite:
emptyPool2Cid <- submit alice do
exerciseCmd updatedPool1Cid RemoveInvitedParticipant with
participantToRemove = bob
Some _updatedPool <- queryContractId alice emptyPool2Cid
assert(Set.notMember bob _updatedPool.invitedParticipants)
-- Re-Invite Bob to the Pool:
(updatedPool2Cid, invite2Cid) <- submit alice do
exerciseCmd emptyPool2Cid InviteNewParticipant with
newParticipant = bob
Some _updatedPool <- queryContractId alice updatedPool2Cid
assert(Set.member bob _updatedPool.invitedParticipants)
-- Bob accepts invite:
updatedPool3Cid <- submit bob do
exerciseCmd invite2Cid AcceptInvite
Some _updatedPool <- queryContractId alice updatedPool3Cid
assert(Set.member bob _updatedPool.participants)
assert(Set.notMember bob _updatedPool.invitedParticipants)
-- Remove Bob as a participant:
emptyPool3Cid <- submit alice do
exerciseCmd updatedPool3Cid RemoveParticipant with
participantToRemove = bob
Some _updatedPool <- queryContractId alice emptyPool3Cid
assert(Set.notMember bob _updatedPool.participants)
-- Re-Invite Bob to the Pool:
(updatedPool4Cid, invite3Cid) <- submit alice do
exerciseCmd emptyPool3Cid InviteNewParticipant with
newParticipant = bob
Some _updatedPool <- queryContractId alice updatedPool4Cid
assert(Set.member bob _updatedPool.invitedParticipants)
-- Bob accepts invite:
updatedPool5Cid <- submit bob do
exerciseCmd invite3Cid AcceptInvite
-- Final pool should have bob as a participant
Some finalPool <- queryContractId bob updatedPool5Cid
assert(Set.member bob finalPool.participants)
assert(Set.notMember bob finalPool.invitedParticipants)
assert(Set.null finalPool.invitedParticipants)
Let’s take a step back and look at the different options and the tradeoffs:
- Don’t make the owner a signatory. As you noticed yourself, this allows anyone to create invites. In some cases, that may be acceptable but usually this is not a workable model.
- Make the owner a signatory and solve ownership transfer by changing the owner party. There is no way around archiving & recreating the contracts in that case if you want to promptly change the owner in all contracts. However, at least in some cases you can do something more clever: In your example, one option would be that you keep the invites untouched but on
InviteAcceptyou accept invites that still have the old owners. - Make the owner a signatory and solve ownership transfer at the IAM level. This works if you have a relatively closed system where both parties are hosted on the same participant so they share the IAM. It becomes fairly difficult to manage if parties are hosted on different participants though.
- Make the owner a signatory and solve ownership by delegation. In that model, you have a super owner that never changes. That party delegates rights to the current owner to act on their behalf, e.g., to accept invites. There you don’t need to rewrite anything and it works regardless of how parties are distributed across participants.
A simple sketch for delegating would look something like this (only modelling one choice but the other ones follow the same pattern)
template OwnerDelegation
with
poolOwner : Party
delegate : Party
where
signatory poolOwner, delegate
nonconsuming choice InviteNewParticipant : (ContractId Pool, ContractId PoolInvite)
with
cid : ContractId Pool
newParticipant : Party
controller delegate
do exercise cid (InviteNewParticipant newParticipant)
4 is the most flexible option, 2 and 3 may be a bit simpler in some cases if you’re happy to accept the limitations.
@cocreature thank you very much for the details! In you delegation draft, if the delegate is not a stakeholder of the cid, what is giving them the right to run ‘exercise cid (InviteNewParticipant newParticipant)’ ? Is it the double signatory?
(I am assuming the controller of the choice in the pool is ‘controller newParticupant’, but they would not have visibility of the contract so cannot exercise)
So there are two parts here:
Authorization & visibility.
The delegate has the authorization because within the choice body you have authorization from signatories & controllers.
Visibility is trickier. Ignoring divulgence, the reading parties (the union of actAs & readAs) must be a stakeholder on each contract used in the submision. One option would be that the owner provides readAs rights to the delegate. The other option ofc is that they are an observer although if you want to do that on each invitation as well you’re just back to having to modify all contracts. We are currently working on a new feature which would allow the owner to disclose the contracts to the delegate without requiring modification of the contracts to make this type of stuff easier.
So my invite:
template PoolInvite
with
poolKey: PoolKey
newParticipant: Party
where
signatory poolKey._1 -- The pool owner is the creator of the invite
observer newParticipant
key(poolKey, newParticipant): InviteKey
maintainer key._1._1 -- Pool owner
postconsuming choice AcceptInvite : ContractId Pool
controller newParticipant
do
exerciseByKey @Pool poolKey InviteAccept with
invite = self
(in the above snippet, assume newParticipant is not an observer/stakeholder of the Pool)
should already “technically” allow the newParticipant to exercise InviteAccept because the owner is the signatory of the Invite? BUT because of the visibility issue you describe, newParticipant is not able to “find” the contract and thus the exercise fails due to “cannot find contract”?
Building on this a bit further:
given the details in Roles in Daml - Introducing Multi-party submissions
The first of the blog speaks directly to the use case I was describing.
give that example, my understanding of the pattern is:
click here for full code
module ExpensePoolChain where
{-
Example of using authorization chains from Pool > PoolInvite > PoolMember > Expense.
Where the "org" signatory of the pool is passed into each ~sub-contract (from a business process perspective).
The org (or equivalent) would be a stable/unchanging party.
-}
import Daml.Script
type PoolKey = (Party, Text)
type MemberKey = (Party, Text, Party)
template Pool
with
org: Party
poolMemberRole: Party
owner: Party
poolId: Text -- Never changes
name: Text
where
signatory org
observer owner, poolMemberRole
key(org, poolId): PoolKey
maintainer key._1
nonconsuming choice InviteNewMember : ContractId PoolInvite
with
newMember: Party
controller owner
do
create PoolInvite with
org
poolId
newMember
nonconsuming choice AddExpense : ContractId Expense
with
member: Party
expense: Int
controller member
do
membership <- fetchByKey @PoolMember (org, poolId, member)
create Expense with
poolKey = key(this)
poolMemberRole = poolMemberRole
member
expense
-- Expected to be created through a choice in Pool
template Expense
with
poolKey: PoolKey
poolMemberRole: Party
member: Party
expense: Int
where
signatory poolKey._1, member -- Org and Member
-- Expected to be created through a choice in Pool
template PoolInvite
with
org: Party
poolId: Text
newMember: Party
where
signatory org
observer newMember
choice AcceptInvite : ContractId PoolMember
controller newMember
do
create PoolMember with
org
poolId
member = newMember
-- Expected to be created through a choice in PoolInvite (where pool invite was created through the pool)
template PoolMember
with
org: Party
poolId: Text
member: Party
where
signatory org, member
key(org, poolId, member): MemberKey
maintainer key._1
normalFlow = script do
[alice, bob, org, pool_member] <- mapA allocateParty ["Alice", "Bob", "Org", "POOL_MEMBER"]
let poolId = "pool-1234"
-- Create a pool:
poolCid <- submitMulti [org] [] do
createCmd Pool with
org
poolMemberRole = pool_member
owner = alice
poolId = poolId
name = "Pool1"
-- Invite Bob to the Pool:
invite1Cid <- submitMulti [alice] [] do
exerciseCmd poolCid InviteNewMember with
newMember = bob
-- Bob accepts invite:
poolMemberCid <- submitMulti [bob] [] do
exerciseCmd invite1Cid AcceptInvite
-- Test comparing signatories:
Some (bobsMemCid, bobsMem) <- queryContractKey @PoolMember bob (org, poolId, bob)
Some (bobsPoolCid, bobsPool) <- queryContractKey @Pool [org] (org, poolId)
let sigsMatch: Bool = signatory(bobsMem) == signatory(bobsPool) -- A pool and membership have same signatories
-- Querying for the pool as a "pool_member"
Some (bobsPoolAsMemberCid, bobsPoolAsMember) <- queryContractKey @Pool [bob, pool_member] (org, poolId)
-- Because the IAM would provide bob with the pool_member readAs pool_member permission.
-- Permission would have been determined by bob having an active PoolMember contract signed by the Pool org.
bobsExpense <- submitMulti [bob] [pool_member] do
exerciseByKeyCmd @Pool (org, poolId) AddExpense with
member = bob
expense = 22
return ()
StephenOTT/daml-quick-start/blob/239f5f25f0b6d385777908acfe0e8058a851bcb0/ExpensePool/daml/ExpensePoolChain.daml
module ExpensePoolChain where
{-
Example of using authorization chains from Pool > PoolInvite > PoolMember > Expense.
Where the "org" signatory of the pool is passed into each ~sub-contract (from a business process perspective).
The org (or equivalent) would be a stable/unchanging party.
-}
import Daml.Script
type PoolKey = (Party, Text)
type MemberKey = (Party, Text, Party)
template Pool
with
org: Party
poolMemberRole: Party
owner: Party
poolId: Text -- Never changes
This file has been truncated. show original
If this example, the org is passed as a signatory into each “sub-contract” in the business process: Pool > Pool Invite > PoolMember > Expense.
The choices are allowing the re-use and passing of the signatories into the sub-contracts.
The IAM could/would check if the user/party has an active contract for PoolMember which was signed by Org and if yes then grant the readAs pool_member permission/party in the JWT.
Validation of each sub-contract would always be based on the org signatory which can only come from the parent contract choices (unless created directly by org as some sort of admin function).
Assuming this flow makes sense and is correct @cocreature ?
I understood the authorization patterns as described in the docs, but i feel this pattern / power/implications of how you can re-use signatories to sign sub contracts is something that is not well articulated in the docs and tutorials.
That flow looks sensible from a quick look.
@cocreature thanks for the followup!
@cocreature building on this a bit further:
I am looking at triggers as a way to inspect the PoolMember Contract and generate rights based on that contract: My understanding is:
- Have to inspect the PoolMember
- lookup the Pool using the Key, derived by the data in the PoolMember (the signatory of the member is the “pool”)
- BUT how would you restrict the ability of other actors to crreate a pool and memberships but reference the same role-party? if they generated an equiv tree of contracts, the result would be adding the same party-role. If you create pool-specific party-roles this would potentially lead to massive number of parties? (down side to this?)
Thanks
Please ask new questions in separate threads, that makes it easier for us to keep track of what has already been answered and easier for others to discover if there is already an answer to an issue they’re having.