Init-script on a Canton setup

Hi team,

We have built a Daml model and used initial script in Sandbox and everything works fine. Now we are going to deploy this model to a Canton setup.

Several questions in our minds.

(1) Can the initial script be used when I deploy this model in Canton? Or it’s just for Sandbox? I can’t see it clearly from the doc.

(2) Specifically, we have party allocations inside the initial script. If finally these parties need to be enabled/allocated in different participant nodes, what is the best practice to do so?

(3) Or in general, any better ways to incorporate initial script when bringing up a Canton network? I suppose bootstrap is the right way but it seems we have to rewrite many things.

Thanks in advance.

kc

1 Like

You can run Daml Script in distributed setups (e.g. Canton) by specifying the network config in participants.json.

For party allocation there is allocatePartyOn to control the participant it gets allocated on and there are similar *On variants of other primitives.

Note that there is no automatic synchronisation between participants however. So if you submit a transaction on one participant it will not block until that transaction is visible on the other participant. You can do things like poll for a contract id created in that transaction to appear on the other participant via queryContractId to synchronize manually.

1 Like

Hi @cocreature many thanks for your advice. We have completed the test and find something very interesting.

Canton setup (examples/01): participant1 (where Alice is enabled), participant2 (where Bob is enabled). Both connect mydomain.

Daml code (template skeleton): which comes with a template Main:Asset, where Main:setup is the script that have

  1. Alice creates a TV with owner=Alice
  2. Alice exercises “Give” to pass ownership to Bob
  3. Bob exercises “Give” to give ownership back to Alice

So after script is fully executed, an active contract with TV asset only seen by Alice, while Bob’s asset is archived.

With this, we perform this test. Here is the summary
Step 1: bring up Canton network and enable Alice and Bob
Step 2: modify the script such that we use the parties defined in the canton network, i.e. Alice and Bob with namespace.
Step 3: daml build to create the dar file
Step 4: upload the dar file to both participant nodes
Step 5: create participant.json as suggested, Alice and Bob with namespace is used, and right JWT is generated.
Step 6: run daml script

We see some interesting outcome.

For the first round: the script came back with error messages. We see the TV asset is owned by Bob. Our guess is that the last step of script that Bob gave Alice back the asset was not executed.

However, when we ran the script again (step 6), no more error was seen. And the script result is correct (i.e Asset owned by Alice). We repeated several time the step 6, and no error was seen like the first round.

I have repeated this for several times, and the first time executing the script (step 6) always results the same error, and no more error in all subsequent script running.

Sorry for this lengthy follow-up reply. But we wish to understand why the first time of script running always results an error (and a partial running) but the upcoming script running is working well.

Thanks again.

kc

Can you share the participants.json config as well as your Daml script?

Hi @cocreature ,

I just repeated the test again and here are the files.

FYI: Alice is enabled in participant1, and Bob is enabled in participant2.

daml/Main.daml (where script is inside). Note that I have modified the party part in order to use the existing parties enabled in the Canton setup.

module Main where

import Daml.Script

type AssetId = ContractId Asset

template Asset
  with
    issuer : Party
    owner  : Party
    name   : Text
  where
    ensure name /= ""
    signatory issuer
    controller owner can
      Give : AssetId
        with
          newOwner : Party
        do
          create this with
            owner = newOwner

setup : Script AssetId
setup = script do
  Some alice <- pure $ partyFromText "Alice::122057c90c134e1776b8506498f35f163dfed1de7c9e0436c2f2b6cf3eaf3e6bdab6"
  Some bob <- pure $ partyFromText "Bob::122090a134a43489f72651ea05054ddb9e9bbb0b633c0110c42c90bc49c7f52cd164"
  aliceTV <- submit alice do
    createCmd Asset with
      issuer = alice
      owner = alice
      name = "TV"

  bobTV <- submit alice do
    exerciseCmd aliceTV Give with newOwner = bob

  submit bob do
    exerciseCmd bobTV Give with newOwner = alice

participants.json

{
  "participants": {
    "alice": {"host": "localhost", "port": 5011, "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwczovL2RhbWwuY29tL2xlZGdlci1hcGkiOnsibGVkZ2VySWQiOiJwYXJ0aWNpcGFudDEiLCJhcHBsaWNhdGlvbklkIjoiSFRUUC1KU09OLUFQSS1HYXRld2F5IiwiYWN0QXMiOlsiQWxpY2U6OjEyMjA1N2M5MGMxMzRlMTc3NmI4NTA2NDk4ZjM1ZjE2M2RmZWQxZGU3YzllMDQzNmMyZjJiNmNmM2VhZjNlNmJkYWI2Il19fQ.NnLBmg0lg3aV5Rg-UY4CtY8k2XLYKAMCbPMCY_osyc4", "application_id": "HTTP-JSON-API-Gateway"},
    "bob": {"host": "localhost", "port": 5021, "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwczovL2RhbWwuY29tL2xlZGdlci1hcGkiOnsibGVkZ2VySWQiOiJwYXJ0aWNpcGFudDIiLCJhcHBsaWNhdGlvbklkIjoiSFRUUC1KU09OLUFQSS1HYXRld2F5IiwiYWN0QXMiOlsiQm9iOjoxMjIwOTBhMTM0YTQzNDg5ZjcyNjUxZWEwNTA1NGRkYjllOWJiYjBiNjMzYzAxMTBjNDJjOTBiYzQ5YzdmNTJjZDE2NCJdfX0.4oScBbRNoK6cv2HJmWXkjvqq6Vk1P3S_eVAOIFPq02Q", "application_id": "HTTP-JSON-API-Gateway"}
  },
  "party_participants": {
    "Alice::122057c90c134e1776b8506498f35f163dfed1de7c9e0436c2f2b6cf3eaf3e6bdab6": "alice",
    "Bob::122090a134a43489f72651ea05054ddb9e9bbb0b633c0110c42c90bc49c7f52cd164": "bob"
  }
}

The command I run the script is

daml script --dar .daml/dist/testscript-0.0.2.dar --script-name Main:setup --participant-config participants.json

Finally here is the error message in the first run.

Exception in thread "main" com.daml.lf.engine.script.ScriptF$FailedCmd: Command submit failed: ABORTED: ContractNotFound(CN11007-5): Transaction is referring to an unknown contract; reason=Contract could not be found with id ContractId(003704d570ffc59f24aabc11b1af1ce2f1e3212fa85936df20a6a397f1c8b3ecb1ca001220ad63f8a1d62c5b185ed097b52fe2249adb9cedf1f23edf69b4d513a000e2c4bc), participant=participant2
Daml stacktrace:
submit at 334ce6d2a0f4a7d6bf32f5353c0bbfb208345de6967c69e337b5ae4b82f1c485:Main:35
	at com.daml.lf.engine.script.Runner.$anonfun$runWithClients$10(Runner.scala:432)
	at scala.concurrent.impl.Promise$Transformation.run(Promise.scala:439)
	at akka.dispatch.TaskInvocation.run(AbstractDispatcher.scala:53)
	at akka.dispatch.ForkJoinExecutorConfigurator$AkkaForkJoinTask.exec(ForkJoinExecutorConfigurator.scala:48)
	at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
	at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056)
	at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692)
	at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:175)
Caused by: io.grpc.StatusRuntimeException: ABORTED: ContractNotFound(CN11007-5): Transaction is referring to an unknown contract; reason=Contract could not be found with id ContractId(003704d570ffc59f24aabc11b1af1ce2f1e3212fa85936df20a6a397f1c8b3ecb1ca001220ad63f8a1d62c5b185ed097b52fe2249adb9cedf1f23edf69b4d513a000e2c4bc), participant=participant2
	at io.grpc.Status.asRuntimeException(Status.java:534)
	at io.grpc.stub.ClientCalls$UnaryStreamToFuture.onClose(ClientCalls.java:533)
	at io.grpc.internal.ClientCallImpl.closeObserver(ClientCallImpl.java:553)
	at io.grpc.internal.ClientCallImpl.access$300(ClientCallImpl.java:68)
	at io.grpc.internal.ClientCallImpl$ClientStreamListenerImpl$1StreamClosed.runInternal(ClientCallImpl.java:739)
	at io.grpc.internal.ClientCallImpl$ClientStreamListenerImpl$1StreamClosed.runInContext(ClientCallImpl.java:718)
	at io.grpc.internal.ContextRunnable.run(ContextRunnable.java:37)
	at io.grpc.internal.SerializingExecutor.run(SerializingExecutor.java:123)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
daml-helper: Received ExitFailure 1 when running
Raw command: java -Dlogback.configurationFile=/home/kctam/.daml/sdk/1.15.0/daml-sdk/script-logback.xml -jar /home/kctam/.daml/sdk/1.15.0/daml-sdk/daml-sdk.jar script --dar .daml/dist/testscript-0.0.2.dar --script-name Main:setup --participant-config participants.json

From the result (through Navigator) we see the error happened after Alice sent Bob the TV, and before Bob sent Alice back the TV.

And for upcoming execution of this script, no more error happens, and the contract behaviour is correct according to our script (i.e. Alice gets back the TV, and we see archived contracts in Bob as well.)

Thanks for your attention.

cheers,
kc

You’re running exactly into the lack of synchronization I alluded to in my original answer:

You create bobTV on Alice’s participant but then you try to use it on Bob’s too early and it fails. The reason why it works on the second try is probably due to JIT warmup which makes it sufficiently fast that it works (most of the time at least).

One option for synchronizing is to poll until the contract is visible:

waitForCid : Template t => Party -> ContractId t -> Script ()
waitForCid p cid = do
  r <- queryContractId p cid
  case r of
    None -> do
      sleep (seconds 1)
      waitForCid p cid
    Some _ -> pure ()

setup : Script AssetId
setup = script do
  alice <- allocatePartyOn "Alice" (ParticipantName "p1")
  bob <- allocatePartyOn "Bob" (ParticipantName "p2")
  aliceTV <- submit alice do
    createCmd Asset with
      issuer = alice
      owner = alice
      name = "TV"

  bobTV <- submit alice do
    exerciseCmd aliceTV Give with newOwner = bob

  waitForCid bob bobTV

  submit bob do
    exerciseCmd bobTV Give with newOwner = alice

With that change it works on the first try for me as well.

2 Likes

I opened Easier multi-participant synchronization in Daml Script · Issue #10618 · digital-asset/daml · GitHub to improve the UX for synchronization in Daml Script.

2 Likes

Hi @cocreature ,

Thanks for your advice. With waitForCid, things are working well even in the first round.

One side question (hope the final one). I notice you are using alice <- allocatePartyOn "Alice" (ParticipantName "p1"), while I hardcode the actual PartyID from canton network. I would see yours is more efficient as I guess the script will retrieve the Party from the participant node. However, what is “p1” and “p2”? I have tested the actual participant nodes name (participant1 and participant2 in my cases) but of no luck. What should I put in “p1” or “p2” such that the script can retrieve without big copy and paste from the Canton setup?

Thanks again and have a great day!

kc

allocateParty isn’t about efficiency, it’s just about convenience in this case so I don’t have to worry about referencing an existing party and getting the id of that.

p1 and p2 are the participant names that you use in your participants.json. For me that looked like this to run this example. The exact name doesn’t matter as long as the one you use in your Daml script and the one you use in your participants.json are in sync.

{
  "participants": {
    "p1": {"host": "localhost", "port": 5011},
    "p2": {"host": "localhost", "port": 5021}
  },
  "party_participants": {}
}
1 Like

Thanks for clarification. Yes, “convenience” is the correct term I should use. No need to hardcode the long Party ID then.

cheers,
kc