Simulate JWT rights in daml script

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?

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

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 to ActAs 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'
2 Likes

I see, thank you both!