Skip to content
Daml

Party replication

Party replication

Party replication is the process of duplicating an existing party onto an additional participant within a single synchronizer. In this process, the participant that already hosts the party is called the source participant, while the new participant is called the target participant.

The procedure’s complexity and risk depend on whether the party has already participated in a Daml transaction.

Therefore, you should replicate a newly onboarded party before using it, following the simple party replication steps.

Otherwise, you must use the offline party replication procedure.

Note

Party replication is different from party migration. A party migration includes an additional final step: removing (or offboarding) the party from its original participant.

Party offboarding, and thus party migration, is currently not supported.

Party replication authorization

How authorization works

Both the party and the new hosting participant must grant their consent by each issuing a party-to-participant mapping topology transaction. This ensures mutual agreement for the party replication.

External parties

For external parties, changes to the party’s topology must be explicitly authorized with a signature of the external party’s namespace key. Whenever this guide requires party authorization, it distinguishes between local and external parties.

When this guide uses the source participant for actions other than authorizing topology changes, you must use one of the external party’s existing confirming participants.

Parties with multiple owners

When a party is owned by a group of members in a decentralized namespace, a minimum number (a defined threshold) of those owners must approve the new hosting arrangement. This threshold is met once enough individual owners each issue their own party-to-participant mapping topology transaction.

Activation

Completing the mutual authorization process activates the party on the target participant.

Simple party replication

The simplest and safest way to replicate a party is to do so before it becomes a stakeholder in any contract.

Warning

If a party has already participated in any Daml transaction, you must use offline party replication instead.

Simple party replication consists of the following steps. You must execute them in order:

  1. Create the party, either in the namespace of a participant or in a dedicated namespace.

  2. Vet packages.

  3. Authorize one or more additional participants to host the party.

  4. Use the party.

The following demonstrates these steps using two participants:

@ val source = participant1
    source : com.digitalasset.canton.console.LocalParticipantReference = Participant 'participant1'
@ val target = participant2
    target : com.digitalasset.canton.console.LocalParticipantReference = Participant 'participant2'
@ val synchronizerId = source.synchronizers.id_of("mysynchronizer")
    synchronizerId : SynchronizerId = da::1220a82692ab...

1. Create party

Create a party Alice:

@ val alice = source.parties.enable("Alice", synchronizer = Some("mysynchronizer"))
    alice : PartyId = Alice::12201ff69b1d...

Note

In this example, the local party Alice is owned by the source participant, which is a simplification meaning Alice is registered in the participant’s namespace. This is not a requirement.

Alternatively, you can create the party in its own dedicated namespace, or create an external party.

2. Vet packages

Vet packages on the target participant(s) before proceeding.

Note

If you are unfamiliar with this process, read this general explanation of package vetting.

3. Multi-host party

Party Alice needs to agree to be hosted on the target participant.

Because the source participant owns party Alice, you need to issue the party-to-participant mapping topology transaction on the source participant.

Authorize hosting update on the source participant

Local PartyExternal Party
@ source.topology.party_to_participant_mappings
    .propose_delta(
      party = alice,
      adds = Seq(target.id -> ParticipantPermission.Submission),
      store = synchronizerId,
    )
    res5: SignedTopologyTransaction[TopologyChangeOp, PartyToParticipant] = SignedTopologyTransaction(
      TopologyTransaction(
        PartyToParticipant(
          partyId = Alice::12201ff69b1d...,
          participants = Map(
            PAR::participant1::12201ff69b1d... -> Submission,
            PAR::participant2::1220a4d7463b... -> Submission
          )
        ),
        serial = 2,
        operation = Replace,
        hash = SHA-256:20eef8c6481f...
      ),
      signatures = 12201ff69b1d...,
      proposal
    )

A participant can host a party with different permissions. In this example, the target participant will host party Alice with submission permission. This allows party Alice to submit Daml transactions on it.

Unlike local parties who are always first hosted on a single node, and therefore always need to amend their party-to-participant mapping after the fact to be multi-hosted, external parties can do this in one step during the onboarding process. See the onboarding process for more details.

Authorize hosting update on the target participant

To complete the process, the target participant must also agree to host Alice. Issue the same topology transaction on the target participant:

@ target.topology.party_to_participant_mappings
    .propose_delta(
      party = alice,
      adds = Seq(target.id -> ParticipantPermission.Submission),
      store = synchronizerId,
    )
    res6: SignedTopologyTransaction[TopologyChangeOp, PartyToParticipant] = SignedTopologyTransaction(
      TopologyTransaction(
        PartyToParticipant(
          partyId = Alice::12201ff69b1d...,
          participants = Map(
            PAR::participant1::12201ff69b1d... -> Submission,
            PAR::participant2::1220a4d7463b... -> Submission
          )
        ),
        serial = 2,
        operation = Replace,
        hash = SHA-256:20eef8c6481f...
      ),
      signatures = 1220a4d7463b...,
      proposal
    )

Note

The participant permission here must be the same as in the previous step. For external parties in particular, this must be either Confirmation or Observation.

Once the party-to-participant mapping takes effect, the replication is complete. This results in party Alice being multi-hosted on both the source and target participants.

To replicate Alice to more participants, repeat the procedure by first vetting the packages on a newTarget participant. Then, perform the replication again using the original source and newTarget participants.

3.a Replicate party with simultaneous confirmation threshold change (Variant to 3)

Note

For external parties, the threshold is already defined during the onboarding process, so this section does not apply.

To change a party’s confirmation threshold, you must use a different procedure for proposing the party-to-participant mapping than previously shown.

This alternative method allows you to perform the replication and update the threshold in a single operation.

The following example continues from the previous one, demonstrating how to replicate party Alice from the source participant to the newTarget participant while simultaneously setting the confirmation threshold to three. This operation also sets the participant permission to confirmation for all three participants that will host Alice.

@ val newTarget = participant3
    newTarget : com.digitalasset.canton.console.LocalParticipantReference = Participant 'participant3'
@ val hostingParticipants = Seq(source, target, newTarget)
    hostingParticipants : Seq[com.digitalasset.canton.console.LocalParticipantReference] = List(Participant 'participant1', Participant 'participant2', Participant 'participant3')
@ source.topology.party_to_participant_mappings
    .propose(
      alice,
      newParticipants = hostingParticipants.map(_.id -> ParticipantPermission.Confirmation),
      threshold = PositiveInt.three,
      store = synchronizerId,
    )
    res9: SignedTopologyTransaction[TopologyChangeOp, PartyToParticipant] = SignedTopologyTransaction(
      TopologyTransaction(
        PartyToParticipant(
          partyId = Alice::12201ff69b1d...,
          threshold = 3,
          participants = Map(
            PAR::participant1::12201ff69b1d... -> Confirmation,
            PAR::participant2::1220a4d7463b... -> Confirmation,
            PAR::participant3::1220d6908163... -> Confirmation
          )
        ),
        serial = 3,
        operation = Replace,
        hash = SHA-256:7249f1511e32...
      ),
      signatures = 12201ff69b1d...,
      proposal
    )
@ newTarget.topology.party_to_participant_mappings
    .propose(
      alice,
      newParticipants = hostingParticipants.map(_.id -> ParticipantPermission.Confirmation),
      threshold = PositiveInt.three,
      store = synchronizerId,
    )
    res10: SignedTopologyTransaction[TopologyChangeOp, PartyToParticipant] = SignedTopologyTransaction(
      TopologyTransaction(
        PartyToParticipant(
          partyId = Alice::12201ff69b1d...,
          threshold = 3,
          participants = Map(
            PAR::participant1::12201ff69b1d... -> Confirmation,
            PAR::participant2::1220a4d7463b... -> Confirmation,
            PAR::participant3::1220d6908163... -> Confirmation
          )
        ),
        serial = 3,
        operation = Replace,
        hash = SHA-256:7249f1511e32...
      ),
      signatures = 1220d6908163...,
      proposal
    )

Offline party replication

Offline party replication is a multi-step, manual process.

Before replication can start, both the target participant and the party itself must explicitly consent to the new hosting arrangement.

Afterwards, the replication consists of exporting the party’s Active Contract Set (ACS) from a source participant and importing it to the target participant.

Note

  • Connect a single Canton console to both the source and target participants to export and import the party’s ACS file using a single physical machine or environment. Otherwise, you must securely transfer the ACS export file to the target participant’s environment before importing.

  • Offline party replication requires you to disconnect the target participant from all synchronizers before importing the party’s ACS. Hence the name offline party replication.

  • During onboarding, you may notice ACS commitment mismatches on the target participant. This is expected and resolves over time; ignore these errors during the replication procedure.

Warning

Be advised: You must back up the target participant before you start the ACS import!

This ensures you have a clean recovery point if the ACS import is interrupted (crash, unintended node restart, etc.), or if you are otherwise unable to complete these manual operational steps. A backup allows you to safely reset the target participant and still complete the replication.

Trust Assumptions

Offline party replication involves exporting a party’s Active Contract Set (ACS) from a source participant and importing it into a target participant. This process introduces a critical dependency on the integrity of the source participant.

Core Trust Requirement

It is assumed that both the target participant operator and the replicated party maintain a high level of trust in the source participant. Specifically, they must trust the source to:

  • Provide a complete and accurate snapshot of the ACS representing the ledger state at the time of export.

  • Ensure the exported data has not been tampered with or truncated.

Validation Constraints

While the target participant performs integrity checks during the import process, this validation is conducted on a best-effort basis.

Note

Because the target participant was not a witness to the original transactions comprising the ACS, it cannot independently verify the entire historical provenance of the contracts against the underlying ledger. Therefore, the source participant acts as the “root of trust” for the party’s state on the new node.

Offline party replication steps

You must perform the following steps in the exact order listed:

  1. Target: Package Vetting – Ensure the target participant vets all required packages.

  2. Source: Data Retention - Ensure the source participant retains data long enough for the export.

  3. Target: Authorization - Target participant authorizes new hosting with the onboarding flag set.

  4. Target: Isolation - Disconnect from all synchronizers and disable auto-reconnect upon restart.

  5. Source: Party Authorization - Party authorizes the replication with the onboarding flag set.

  6. Source: ACS Export - The participant currently hosting the party exports the ACS.

  7. Target: Backup - Back up the target participant before starting the ACS import.

  8. Target: ACS Import - The target participant imports the ACS.

  9. Target: Reconnect - The target participant reconnects to the synchronizers.

  10. Target: Onboarding Flag Clearance - The target participant issues the onboarding flag clearance.

Warning

You must perform offline party replication carefully and strictly follow the steps in order. Deviating from this flow will cause errors that may require significant manual correction.

This documentation provides a guide. Your environment may require adjustments. Test thoroughly in a test environment before production use.

External parties

For demonstration purposes, we will authorize topology transactions on behalf of external parties using a private ED25519 key in the DER format available on disk called private_key.der. This is NOT a secure way to store private keys. Real-world deployments must secure private keys (using a KMS for example).

Scenario description

The following steps show how to replicate party alice from the source participant to a new target participant on the synchronizer mysynchronizer. The source can be any participant already hosting the party.

@ val source = participant1
    source : com.digitalasset.canton.console.LocalParticipantReference = Participant 'participant1'
@ val target = participant2
    target : com.digitalasset.canton.console.LocalParticipantReference = Participant 'participant2'
@ val alice = source.parties.enable("Alice", synchronizer = Some("mysynchronizer")) // This command creates a local party. For external parties see the external party onboarding documentation (link found above in this page)
    alice : PartyId = Alice::12201ff69b1d...
@ val synchronizerId = source.synchronizers.id_of("mysynchronizer")
    synchronizerId : SynchronizerId = da::1220a82692ab...

1. Vet packages

Ensure the target participant vets all packages associated with contracts where the party is a stakeholder.

The party alice uses the package CantonExamples, which is vetted on the source participant but not yet on the target participant.

@ val mainPackageId = source.dars.list(filterName = "CantonExamples").head.mainPackageId
    mainPackageId : String = "4dffebf4008e845e37f602fa2d54fd596a8b605af564239fc62948c39fdb79e2"
@ target.topology.vetted_packages.list()
    .filter(_.item.packages.exists(_.packageId == mainPackageId))
    .map(r => (r.context.storeId, r.item.participantId))
    res6: Seq[(TopologyStoreId, ParticipantId)] = Vector(
      (Synchronizer(id = Right(value = da::1220a82692ab...::35-0)), PAR::participant1::12201ff69b1d...)
    )

Upload the missing DAR package to the target participant.

@ target.dars.upload("dars/CantonExamples.dar")
    res7: String = "4dffebf4008e845e37f602fa2d54fd596a8b605af564239fc62948c39fdb79e2"
@ target.topology.vetted_packages.list()
    .filter(_.item.packages.exists(_.packageId == mainPackageId))
    .map(r => (r.context.storeId, r.item.participantId))
    res8: Seq[(TopologyStoreId, ParticipantId)] = Vector(
      (Synchronizer(id = Right(value = da::1220a82692ab...::35-0)), PAR::participant1::12201ff69b1d...),
      (Synchronizer(id = Right(value = da::1220a82692ab...::35-0)), PAR::participant2::1220a4d7463b...)
    )

2. Data Retention

Ensure that the retention period on the source participant is long enough to cover the entire duration between the following two events:

  1. The party-to-participant mapping topology transaction becoming effective.

  2. The completion of the ACS export from the source participant.

If you are unsure whether the current retention period is sufficient, or as an additional precaution, you should temporarily disable automatic pruning on the source participant.

Retrieve the current automatic pruning schedule. This command returns None if no schedule is set.

@ val pruningSchedule = source.pruning.get_schedule()
    pruningSchedule : Option[PruningSchedule] = Some(value = PruningSchedule(cron = "0 0 20 * * ?", maxDuration = 2h, retention = 720h))

Clear the pruning schedule, disabling the automatic pruning on the source node.

@ source.pruning.clear_schedule()

Warning

Manual pruning cannot be programmatically disabled on the source participant. Coordinate closely with other operators to ensure no external automation triggers pruning until the ACS export is complete.

3. Authorize new hosting on the target participant

First, the target participant must agree to host party Alice with the desired participant permission (observation in this example).

Warning

Please ensure the onboarding flag is set with requiresPartyToBeOnboarded = true.

@ val proposal = target.topology.party_to_participant_mappings
    .propose_delta(
      party = alice,
      adds = Seq((target.id, ParticipantPermission.Observation)),
      store = synchronizerId,
      requiresPartyToBeOnboarded = true
    )
    proposal : SignedTopologyTransaction[TopologyChangeOp, PartyToParticipant] = SignedTopologyTransaction(
      TopologyTransaction(
        PartyToParticipant(
          partyId = Alice::12201ff69b1d...,
          participants = Map(
            PAR::participant1::12201ff69b1d... -> Submission,
            PAR::participant2::1220a4d7463b... -> Observation(onboarding)
          )
        ),
        serial = 2,
        operation = Replace,
        hash = SHA-256:4fc27cf93b27...
      ),
      signatures = 1220a4d7463b...,
      proposal
    )

4. Disconnect target participant from all synchronizers

@ target.synchronizers.disconnect_all()

5. Disable auto-reconnect on target participant

Ensure the target participant does not automatically reconnect to the synchronizer upon restart.

@ target.synchronizers.config("mysynchronizer")
    res13: Option[SynchronizerConnectionConfig] = Some(
      value = SynchronizerConnectionConfig(
        synchronizer = Synchronizer 'mysynchronizer',
        sequencerConnections = SequencerConnections(
          connections = Sequencer 'sequencer1' -> GrpcSequencerConnection(
            sequencerAlias = Sequencer 'sequencer1',
            sequencerId = SEQ::sequencer1::1220cb0a22fb...,
            endpoints = http://127.0.0.1:30352
          ),
          sequencer trust threshold = 1,
          sequencer liveness margin = 0,
          submission request amplification = SubmissionRequestAmplification(factor = 1, patience = 0s),
          sequencer connection pool delays = SequencerConnectionPoolDelays(
            minRestartDelay = 0.01s,
            maxRestartDelay = 10s,
            warnValidationDelay = 20s,
            subscriptionRequestDelay = 1s
          )
        ),
        manualConnect = false
      )
    )
@ target.synchronizers.modify("mysynchronizer", _.copy(manualConnect=true))
@ target.synchronizers.config("mysynchronizer")
    res15: Option[SynchronizerConnectionConfig] = Some(
      value = SynchronizerConnectionConfig(
        synchronizer = Synchronizer 'mysynchronizer',
        sequencerConnections = SequencerConnections(
          connections = Sequencer 'sequencer1' -> GrpcSequencerConnection(
            sequencerAlias = Sequencer 'sequencer1',
            sequencerId = SEQ::sequencer1::1220cb0a22fb...,
            endpoints = http://127.0.0.1:30352
          ),
          sequencer trust threshold = 1,
          sequencer liveness margin = 0,
          submission request amplification = SubmissionRequestAmplification(factor = 1, patience = 0s),
          sequencer connection pool delays = SequencerConnectionPoolDelays(
            minRestartDelay = 0.01s,
            maxRestartDelay = 10s,
            warnValidationDelay = 20s,
            subscriptionRequestDelay = 1s
          )
        ),
        manualConnect = true
      )
    )

6. Authorize new hosting for the party

To locate the topology transaction that authorizes the new hosting arrangement later, record the current ledger end offset on the source participant:

@ val beforeActivationOffset = source.ledger_api.state.end()
    beforeActivationOffset : Long = 23L

Only after the target participant has been disconnected from all synchronizers, have party Alice agree to be hosted on it.

Warning

Again, please ensure the onboarding flag is set with requiresPartyToBeOnboarded = true for a local party, and with onboarding = HostingParticipant.Onboarding() for external party.

Local PartyExternal Party
@ source.topology.party_to_participant_mappings
    .propose_delta(
      party = alice,
      adds = Seq((target.id, ParticipantPermission.Observation)),
      store = synchronizerId,
      requiresPartyToBeOnboarded = true
    )
    res17: SignedTopologyTransaction[TopologyChangeOp, PartyToParticipant] = SignedTopologyTransaction(
      TopologyTransaction(
        PartyToParticipant(
          partyId = Alice::12201ff69b1d...,
          participants = Map(
            PAR::participant1::12201ff69b1d... -> Submission,
            PAR::participant2::1220a4d7463b... -> Observation(onboarding)
          )
        ),
        serial = 2,
        operation = Replace,
        hash = SHA-256:4fc27cf93b27...
      ),
      signatures = 12201ff69b1d...,
      proposal
    )

For external parties, we need to authorize the new hosting proposal by signing the transaction hash with Alice’s external key.

First write the hash to a file:

@ val tmpDir = better.files.File(s"/tmp/canton/offline_party_replication").createDirectories()
    tmpDir : better.files.File = /tmp/canton/offline_party_replication
@ (tmpDir / "target_obs_topology_tx.hash").createFileIfNotExists().outputStream.apply(proposal.hash.hash.getCryptographicEvidence.writeTo(_))

Then sign the hash. As mentioned before, we use a local private key and the openssl command-line tool to sign the hash here for demonstration purposes. In real deployments, use a secure storage / signing solution.

TMP_DIR=$(echo "/tmp/canton/offline_party_replication")
openssl pkeyutl -sign -inkey private_key.der -rawin -in $TMP_DIR/target_obs_topology_tx.hash -out $TMP_DIR/target_obs_topology_tx.sig -keyform DER

Finally, load the transaction with Alice’s signature:

@ val aliceSignature = Signature.fromExternalSigning(
    format = SignatureFormat.Concat,
    signature = (tmpDir / "target_obs_topology_tx.sig").inputStream()(com.google.protobuf.ByteString.readFrom),
    signedBy = alice.fingerprint,
    signingAlgorithmSpec = SigningAlgorithmSpec.Ed25519,
  )
    aliceSignature : Signature = Signature(
      signature = a94d1a6dfe5b,
      format = Concat,
      signedBy = 1220c3e4f5cd...,
      signingAlgorithmSpec = Some(Ed25519)
    )
@ val proposalSignedByAlice = proposal.addSingleSignature(aliceSignature)
    proposalSignedByAlice : SignedTopologyTransaction[TopologyChangeOp, PartyToParticipant] = SignedTopologyTransaction(
      TopologyTransaction(
        PartyToParticipant(
          partyId = Alice::1220c3e4f5cd...,
          participants = Map(
            PAR::participant1::12201ff69b1d... -> Confirmation,
            PAR::participant2::1220a4d7463b... -> Observation(onboarding)
          )
        ),
        serial = 2,
        operation = Replace,
        hash = SHA-256:5871abe57095...
      ),
      signatures = Seq(1220a4d7463b..., 1220c3e4f5cd...),
      proposal
    )
@
    source.topology.transactions.load(
        transactions = Seq(proposalSignedByAlice),
        store = synchronizerId,
    )

7. Export ACS

Export Alice’s ACS from the source participant.

The following command searches internally for the ledger offset where party Alice is activated on the target participant, starting from beginOffsetExclusive.

It then exports Alice’s ACS from the source participant at that exact offset and saves it to a file named party_replication.alice.acs.gz.

@ source.parties
    .export_party_acs(
      party = alice,
      synchronizerId = synchronizerId,
      targetParticipantId = target.id,
      beginOffsetExclusive = beforeActivationOffset,
      exportFilePath = "party_replication.alice.acs.gz",
    )

8. Optional: Re-enable automatic pruning

If you previously disabled automatic pruning on the source participant by following the data retention step, you may now re-enable it.

Run the following command using the original configuration parameters you recorded before disabling the schedule:

@ source.pruning.set_schedule("0 0 20 * * ?", 2.hours, 30.days)

9. Back up target participant

Warning

Please back up the target participant before importing the ACS!

10. Import ACS

Import Alice’s ACS on the target participant:

@ target.parties.import_party_acs(synchronizerId, party = Some(alice), importFilePath = "party_replication.alice.acs.gz")

Note

Providing the party ID is optional for backward compatibility. However, omitting it prevents automatic onboarding flag clearance, requiring you to clear the flag manually.

11. Reconnect target participant to synchronizer

To later find the topology transaction that authorized the new hosting arrangement on the target participant, record the current ledger end offset:

@ val targetLedgerEnd = target.ledger_api.state.end()
    targetLedgerEnd : Long = 27L

Now, reconnect that target participant to the synchronizer.

@ target.synchronizers.reconnect_local("mysynchronizer")
    res27: Boolean = true

12. Optional: Re-enable auto-reconnect on target participant

If you previously disabled auto-reconnect following the earlier step, you may now re-enable it. This is only necessary if the target participant was originally configured to reconnect automatically upon restart.

@ target.synchronizers.modify("mysynchronizer", _.copy(manualConnect=false))

13. Complete the onboarding of the party

To complete the replication, you must clear the previously set onboarding flag using the target participant. This signals that the participant is fully ready to host the party.

If you run protocol version 35 or later and provided the party ID during the ACS import, this clearance is scheduled automatically in the background upon reconnecting to the synchronizer. It executes as soon as it is safe to do so.

Note

Background flag clearance execution is observable in the participant logs.

Optional: Manual onboarding flag clearance

If automatic clearance does not apply, or if there are issues with the background clearance, you must clear the flag manually.

Use the dedicated command below, which safely issues the required topology transaction. It uses the targetLedgerEnd captured earlier to locate the transaction that activated the party on the target participant:

@ val flagStatus = target.parties
    .clear_party_onboarding_flag(alice, synchronizerId, targetLedgerEnd)
    flagStatus : PartyOnboardingFlagStatus = FlagSet(earliest safe time to clear the flag = 2026-05-21T22:43:36.316829Z)

Note

The targetLedgerEnd is a ledger offset on the target participant from where this command starts searching for the effective topology transaction that states that party alice is onboarding on the target participant.

The command returns the onboarding flag clearance status:

  • FlagNotSet: The onboarding flag is cleared.

  • FlagSet: The onboarding flag is still set. Removal is safe only after the indicated timestamp.

If the onboarding flag is still set, the command has internally created a schedule to trigger the onboarding flag clearance at the appropriate time. This happens in the background.

However, because this command is idempotent, you may call it repeatedly. Thus, you may also poll this command until it confirms that the onboarding flag has been cleared. The following snippet demonstrates how this command can be polled.

@ utils.retry_until_true(timeout = 2.minutes, maxWaitPeriod = 1.minutes) {
      val flagStatus = target.parties
        .clear_party_onboarding_flag(alice, synchronizerId, targetLedgerEnd)
      flagStatus match {
       case FlagSet(_) => false
       case FlagNotSet => true
      }
    }

Note

The timeout is based on the default decision timeout of 1 minute.

Summary

You have successfully multi-hosted Alice on source and target participants.