Ledger API error message - Conflicting discriminators between a global and local contract ID

I’ve submitted a list of commands to the ledger API in a single transaction using the command service (sandbox ledger). I am getting the following error message:

INVALID_ARGUMENT: Disputed: Interpretation error: Error: CRASH: Conflicting discriminators between a global and local contract ID

Can someone explain what this message indicates?

3 Likes

Hi @huw,

This definitely looks like a bug on our side. To help us debug and fix this, it would be great if youcould you share the following information with us:

  1. SDK version
  2. Whether you are using daml sandbox or daml sandbox-classic.
  3. Any --contract-id-seeding flags you specify.

If you can share anything more about the project, maybe even the code and steps to reproduce, that would be fantastic. Feel free to send any confidential information in a PM.

1 Like

Thanks @cocreature , to answer your question:

  1. I’m using SDK 1.11.1
  2. daml sandbox
  3. Not setting any --contract-id-seeding flags

I can’t share the original code I was using but after some debugging I’ve got a minimal dummy example that demonstrates the problem:

Main.daml:

module Main where

import Daml.Script
import DA.Set (Set)
import qualified DA.Set as Set

template A
  with
    id : Int
    mntr : Party
    observers : Set Party
  where
    key (id, mntr) : (Int, Party)
    maintainer key._2

    signatory mntr
    observer observers

    controller mntr can
      A_AddObservers : ContractId A
        with
          moreObservers : Set Party
        do
         create this with observers = Set.union observers moreObservers

template Manager
  with
    manager : Party
  where
    key manager : Party
    maintainer key

    signatory manager

    controller manager can
      nonconsuming Manager_CreateA : ContractId A
        with
          id : Int
          observers : Set Party
        do
          create A with mntr = manager, ..

-- test passes using `daml test` but when using the ledger API java bindings the same thing fails
test : Script ()
test = do
  a <- allocateParty "a"
  b <- allocateParty "b"
  submit a do createCmd Manager with manager = a
  submit a $
    (exerciseByKeyCmd @Manager a Manager_CreateA with id = 1, observers = Set.empty) *>
    (exerciseByKeyCmd @A (1, a) A_AddObservers with moreObservers = Set.singleton b)
  pure ()

Test.java which calls the package defined above using the ledger API:

package test;

import com.daml.ledger.api.v1.admin.PackageManagementServiceGrpc;
import com.daml.ledger.api.v1.admin.PackageManagementServiceOuterClass;
import com.daml.ledger.javaapi.data.Unit;
import com.daml.ledger.rxjava.DamlLedgerClient;
import com.google.protobuf.ByteString;
import da.types.Tuple2;
import io.grpc.ManagedChannel;
import io.grpc.netty.NettyChannelBuilder;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.UUID;
import main.A;
import main.Manager;
import org.junit.jupiter.api.Test;

class DamlKeyUpdateTest {
  private ByteString loadDarFile() throws IOException {
    return ByteString.readFrom(getClass().getResourceAsStream("/daml-key-update-test-1.0.0.dar"));
  }

  @Test
  void test() throws IOException {
    String host = "localhost";
    int port = 6865;
    DamlLedgerClient client = DamlLedgerClient.newBuilder(host, port).build();
    client.connect();
    ManagedChannel channel = NettyChannelBuilder.forAddress(host, port).usePlaintext().build();
    PackageManagementServiceGrpc.newBlockingStub(channel)
        .uploadDarFile(
            PackageManagementServiceOuterClass.UploadDarFileRequest.newBuilder()
                .setDarFile(loadDarFile())
                .build());
    channel.shutdownNow();

    client
        .getCommandClient()
        .submitAndWait(
            UUID.randomUUID().toString(),
            UUID.randomUUID().toString(),
            UUID.randomUUID().toString(),
            "a",
            Collections.singletonList(Manager.create("a")))
        .blockingGet();

    client
        .getCommandClient()
        .submitAndWait(
            UUID.randomUUID().toString(),
            UUID.randomUUID().toString(),
            UUID.randomUUID().toString(),
            "a",
            Arrays.asList(
                Manager.exerciseByKeyManager_CreateA(
                    "a", 1L, new da.set.types.Set<>(Collections.emptyMap())),
                A.exerciseByKeyA_AddObservers(
                    new Tuple2<>(1L, "a"),
                    new da.set.types.Set<>(Collections.singletonMap("b", Unit.getInstance())))))
        .blockingGet(); // this will produce INVALID_ARGUMENT: Disputed: Interpretation error: Error: CRASH: Conflicting discriminators between a global and local contract ID.
  }
}
3 Likes

Thanks for the quick response @huw, we can reproduce the issue and are working on a fix.

1 Like

Hi @huw, we found a bug and are working on a fix. In the meantime, here are two workarounds:

  1. If transactionality is not important here, you can split the two exerciseByKey calls into two separate submits.
test : Script ()
test = do
  a <- allocateParty "a"
  b <- allocateParty "b"
  submit a do createCmd Manager with manager = a
  submit a $
    exerciseByKeyCmd @Manager a Manager_CreateA with id = 1, observers = Set.empty
  submit a $
    exerciseByKeyCmd @A (1, a) A_AddObservers with moreObservers = Set.singleton b
  pure ()
  1. If transactionality is important, you can move the two exerciseByKey calls into a choice and call that via createAndExerciseCmd
template Helper
  with
    p : Party
  where
    signatory p
    choice Help : ContractId A
      with
        a : Party
        b : Party
      controller p
      do exerciseByKey @Manager a Manager_CreateA with id = 1, observers = Set.empty
         exerciseByKey @A (1, a) A_AddObservers with moreObservers = Set.singleton b

test2 : Script ()
test2 = do
  a <- allocateParty "a"
  b <- allocateParty "b"
  submit a do createCmd Manager with manager = a
  submit a $ createAndExerciseCmd (Helper a) (Help a b)
  pure ()
2 Likes

Hi @cocreature, thanks for looking into this. For now I’ll use a workaround, or even easier just set the observers field of A to include all required observers when doing the create, rather than exercising the A_AddObservers choice after creating.

2 Likes