Hi, I’m attempting to create a choice that has just one controller, and upon exercising the choice it will create 3 contracts, all of which the controller will not be a signatory on. From my understanding this is not possible because we don’t have the auth of the other parties that will be signatories on the created contract(s). My question is if we plan on having a JWT that enables the controller party to act on behalf of the other parties, is there a way to simulate that in daml script without adding the parties as controllers to the choice and using submitMulti?
- In the scenario you describe it’s possible to create 3 contracts with the controller of the choice not being a signatory on any of them. This is possible as long as the signatories on the contracts being created are among the signatories on the contract that creates them, because the consequences of a choice are authorized by the choice controllers and by the contract signatories.
template MyContract
with
sig: Party
where
signatory sig
template ContractCreator
with
sigs: [Party]
ctrlr: Party
where
signatory sigs
observer ctrlr
nonconsuming choice CreateMyContracts : ()
controller ctrlr
do
mapA_ (\x -> create MyContract with sig = x) sigs
- In Daml script you can create a Daml user with a set of actAs and readAs claims and use
submitUser
function with the user you created. The transaction submission will carry the authorization of all actAs parties. The example below tests the above templates by creating a user with the actAs claims of 3 parties. The user then submits a transaction that creates ContractCreator contract with these 3 parties as signatories. Then the ctrlr party exercises the choice on ContractCreator contract, which creates 3 MyContract contracts each signed by one of the signatories on ContractCreator.
testContractCreator = script do
sigs <- mapA allocateParty ["p1","p2","p3"]
ctrlr <- allocateParty "controller"
u1 <- validateUserId "user1"
createUser (User u1 None) (map CanActAs sigs)
contractCreatorCid <- submitUser u1 do createCmd ContractCreator with sigs, ctrlr
submit ctrlr do exerciseCmd contractCreatorCid CreateMyContracts
Alex is 100% correct given the single controller constraint you specified in the question. I want to extend his answer with a couple of observations:
A JWT can’t enable a “Party” to act on behalf of another “Party”. What you can do, and what I believe you intend in your question is:
- You can construct a JWT that allows an off-ledger service or application to
ActAs
multiple parties - You can construct a JWT that allows an off-ledger service or application to
ActAs
a user that is configured with the right toActAs
multiple parties.
The only way for a “Party” to be delegated the authority to act on behalf of another is by the delegating party to sign a contract on the ledger on which the delegate is the controller of a choice.
Once you have setup one of the two JWT options the restriction on a single controller becomes an unnecessary complication. You can require the full set of parties required as a joint controllers of the choice.
module MultiCtrlr where
import Daml.Script
import DA.Foldable (forA_)
import DA.List (delete)
template MyContract
with
operator: Party
sig: Party
where
signatory sig
observer operator
template ContractCreator
with
operator: Party
where
signatory operator
nonconsuming choice CreateMyContracts: ()
with
sigs: [Party]
controller operator, sigs
do
forA_ sigs $ create . MyContract operator
testContractCreator = script do
sigs <- mapA allocateParty ["p1","p2","p3"]
operator : Party <- allocateParty "operator"
u1 <- validateUserId "user1"
createUser (User u1 None) . map CanActAs $ operator :: sigs
contractCreatorCid <- submitUser u1 do createCmd ContractCreator with operator
submitUser u1 do exerciseCmd contractCreatorCid CreateMyContracts with sigs = sigs
let acc : [Party] -> MyContract -> [Party]
acc ps mc = if mc.sig `elem` ps
then delete mc.sig ps
else error $ show mc <> " not found in " <> show ps <> " from " <> show sigs
rem <- foldl acc sigs . map (._2) <$> query operator
assertMsg ("Insufficient contracts created. Not found for " <> show rem) $ null rem
Alternatively, if there is no extra logic required in the CreateMyContracts
choice, then there’s no point in making this a choice at all — bundle the creates directly in a single command and use the JWT to authorize it.
module MultiCmd where
import Daml.Script
import DA.List (delete)
template MyContract
with
operator: Party
sig: Party
where
signatory sig
observer operator
testContractCreator = script do
sigs <- mapA allocateParty ["p1","p2","p3"]
operator : Party <- allocateParty "operator"
u1 <- validateUserId "user1"
createUser (User u1 None) . map CanActAs $ operator :: sigs
submitUser u1 $ mapA (createCmd . MyContract operator) sigs
let acc : [Party] -> MyContract -> [Party]
acc ps mc = if mc.sig `elem` ps
then delete mc.sig ps
else error $ show mc <> " not found in " <> show ps <> " from " <> show sigs
rem <- foldl acc sigs . map (._2) <$> query operator
assertMsg ("Insufficient contracts created. Not found for " <> show rem) $ null rem
However Alex’s solution is generally preferred. The advantage of his approach is that you don’t need the complexity and security implications of juggling multi-party JWTs. Also, both JWT approaches require that ALL the parties involved be hosted on the SAME participant node, which severely impacts your ability to take advantage of the distributed nature of Canton and Daml.
The final consideration that applies to any approach that uses a single choice is that the resulting exercise ends up being authorized by ALL the parties involved, which means they ALL get to see the results of the ENTIRE transaction — in most cases you probably only want the non-operator parties to see their specific contract.
The compound-create command approach (MultiCmd
) does avoid this, but it is also trivially avoided when using the on-ledger by adding an intermediating contract that allows Daml’s sub-transaction privacy to do its job. Generally this contract represents a user registration or onboarding, and therefore some form of client or master-services agreement.
The critical thing to observe in this example is that every single command submitted to the ledger is a single-party unilateral command. So there is zero off-ledger inter-party coordination required. Also, because each command is authorized by only a single party, you are no longer restricted to a single participant node — this can be distributed arbitrarily, either for horizontal scalability, or to allow the application to participate in the wider Canton Network ecosystem.
module Intermediated where
import Daml.Script
import DA.Foldable (forA_)
import DA.List (delete)
template MyContract
with
operator: Party
sig: Party
where
signatory sig
observer operator
template MyContractCreator
with
operator: Party
sig: Party
where
signatory sig
observer operator
nonconsuming choice CreateMyContract: ContractId MyContract
controller operator
do
create MyContract with operator, sig
template ContractCreator
with
operator: Party
where
signatory operator
nonconsuming choice CreateMyContracts: ()
with
mccs: [ContractId MyContractCreator]
controller operator
do
-- Extra logic goes here
forA_ mccs $ \mcc -> exercise mcc CreateMyContract
testContractCreator = script do
sigs <- mapA allocateParty ["p1","p2","p3"]
operator : Party <- allocateParty "operator"
u1 <- validateUserId "user1"
createUser (User u1 None) [CanActAs operator]
mccs <- mapA (\p -> submit p $ createCmd MyContractCreator with operator, sig = p) sigs
contractCreatorCid <- submitUser u1 do createCmd ContractCreator with operator
submitUser u1 do exerciseCmd contractCreatorCid CreateMyContracts with mccs
let acc : [Party] -> MyContract -> [Party]
acc ps mc = if mc.sig `elem` ps
then delete mc.sig ps
else error $ show mc <> " not found in " <> show ps <> " from " <> show sigs
rem <- foldl acc sigs . map (._2) <$> query operator
assertMsg ("Insufficient contracts created. Not found for " <> show rem) $ null rem
It is definitely worth copying these four examples (including Alex’s) into DamlStudio and examining the resulting transaction views (I’ve included them at the end of this answer for your convenience).
Ultimately any of these approaches can be made to work, the question will be which has the combination of security, distribution, and scalability required for your specific application.
Transaction Trees for each example
Alex’s example (Single Choice)
TX 1 1970-01-01T00:00:00Z (SingleChoice:32:3)
#1:0
│ disclosed to (since): 'controller' (1), 'p1' (1), 'p2' (1), 'p3' (1)
└─> 'controller' exercises CreateMyContracts on #0:0 (SingleChoice:ContractCreator)
children:
#1:1
│ disclosed to (since): 'controller' (1), 'p1' (1), 'p2' (1), 'p3' (1)
└─> 'p1' creates SingleChoice:MyContract
with
sig = 'p1'
#1:2
│ disclosed to (since): 'controller' (1), 'p1' (1), 'p2' (1), 'p3' (1)
└─> 'p2' creates SingleChoice:MyContract
with
sig = 'p2'
#1:3
│ disclosed to (since): 'controller' (1), 'p1' (1), 'p2' (1), 'p3' (1)
└─> 'p3' creates SingleChoice:MyContract
with
sig = 'p3'
MultiCtrlr
TX 1 1970-01-01T00:00:00Z (MultiCtrlr:35:3)
#1:0
│ disclosed to (since): 'operator' (1), 'p1' (1), 'p2' (1), 'p3' (1)
└─> 'operator', 'p1',
'p2' and 'p3' exercise CreateMyContracts on #0:0 (MultiCtrlr:ContractCreator)
with
sigs = ['p1', 'p2', 'p3']
children:
#1:1
│ disclosed to (since): 'operator' (1), 'p1' (1), 'p2' (1), 'p3' (1)
└─> 'p1' creates MultiCtrlr:MyContract
with
operator = 'operator'; sig = 'p1'
#1:2
│ disclosed to (since): 'operator' (1), 'p1' (1), 'p2' (1), 'p3' (1)
└─> 'p2' creates MultiCtrlr:MyContract
with
operator = 'operator'; sig = 'p2'
#1:3
│ disclosed to (since): 'operator' (1), 'p1' (1), 'p2' (1), 'p3' (1)
└─> 'p3' creates MultiCtrlr:MyContract
with
operator = 'operator'; sig = 'p3'
MultiCmd
TX 0 1970-01-01T00:00:00Z (MultiCmd:20:3)
#0:0
│ disclosed to (since): 'operator' (0), 'p1' (0)
└─> 'p1' creates MultiCmd:MyContract
with
operator = 'operator'; sig = 'p1'
#0:1
│ disclosed to (since): 'operator' (0), 'p2' (0)
└─> 'p2' creates MultiCmd:MyContract
with
operator = 'operator'; sig = 'p2'
#0:2
│ disclosed to (since): 'operator' (0), 'p3' (0)
└─> 'p3' creates MultiCmd:MyContract
with
operator = 'operator'; sig = 'p3'
Intermediated
TX 4 1970-01-01T00:00:00Z (Intermediated:51:3)
#4:0
│ disclosed to (since): 'operator' (4)
└─> 'operator' exercises CreateMyContracts on #3:0 (Intermediated:ContractCreator)
with
mccs = [#0:0, #1:0, #2:0]
children:
#4:1
│ disclosed to (since): 'operator' (4), 'p1' (4)
└─> 'operator' exercises CreateMyContract on #0:0 (Intermediated:MyContractCreator)
children:
#4:2
│ disclosed to (since): 'operator' (4), 'p1' (4)
└─> 'p1' creates Intermediated:MyContract
with
operator = 'operator'; sig = 'p1'
#4:3
│ disclosed to (since): 'operator' (4), 'p2' (4)
└─> 'operator' exercises CreateMyContract on #1:0 (Intermediated:MyContractCreator)
children:
#4:4
│ disclosed to (since): 'operator' (4), 'p2' (4)
└─> 'p2' creates Intermediated:MyContract
with
operator = 'operator'; sig = 'p2'
#4:5
│ disclosed to (since): 'operator' (4), 'p3' (4)
└─> 'operator' exercises CreateMyContract on #2:0 (Intermediated:MyContractCreator)
children:
#4:6
│ disclosed to (since): 'operator' (4), 'p3' (4)
└─> 'p3' creates Intermediated:MyContract
with
operator = 'operator'; sig = 'p3'
I see, thank you both!