Overview
This article describes how to recover Canton Coin (CC) as part of a validator disaster recovery procedure.
The process restores Canton Coin balances by re-hosting external parties on a new Canton validator and importing their Active Contract Set (ACS).
Before you begin, ensure you have:
- Your full recovery kit, including all required keys
- A new Canton validator node
- Console access to the node’s participant pod or container
Important:
This procedure covers Canton Coin recovery only. Recovering tokens (e.g. USDCx) requires additional actions performed by the relevant Token Registrar (such as Circle).
Security notice
This procedure requires reconstruction of private key material. The security implications are identical to performing a standard workspace recovery.
Before proceeding:
- Review your organization’s recovery security procedures.
- Perform signing operations on an offline machine whenever possible.
- Transfer data using secure methods only.
Recovery workflow
The recovery process includes the following stages:
- Create a topology transaction.
- Sign the transaction.
- Publish the signed transaction.
- Verify party-to-participant mapping.
- Disconnect the participant.
- Create a database snapshot.
- Obtain the party Active Contract Set (ACS).
- Import the ACS.
- Reconnect the participant.
- Clear the onboarding flag.
The official Canton reference documentation is available here. Some commands below differ slightly from the official documentation to provide an additional safety margin.
Step 1: Create a topology transaction
After accessing the participant container console on the node, run:
// Replace YOUR_PARTY_ID with the external party ID
val partyId = PartyId.tryFromProtoPrimitive("YOUR_PARTY_ID")
val participantId = participant.id
val synchronizerId = participant.synchronizers.id_of("global")
val partyToParticipant = PartyToParticipant.tryCreate(
partyId = partyId,
threshold = PositiveInt.one,
participants = Seq(
HostingParticipant(
participantId,
ParticipantPermission.Confirming,
onboarding = true
)
),
)
import com.digitalasset.canton.admin.api.client.commands.TopologyAdminCommands.Write.GenerateTransactions
val topologyTransaction =
participant.topology.transactions.generate(
Seq(
GenerateTransactions.Proposal(
partyToParticipant,
TopologyStoreId.Synchronizer(synchronizerId),
)
)
).head
topologyTransaction.hash.hash.toHexStringRecord the generated transaction hash. You will sign the bytes represented by this hash in the next step. If recovering multiple parties, repeat this step for each party.
Step 2: Sign the transaction hash
- Restore private key material using your Fireblocks Recovery Utility.
- Use Raw Signing to sign the generated payload.
Requirements:
- Select EDDSA as the signing algorithm.
- Enter the relevant vault account index.
- Generate the signature and securely store it.
If Raw Signing is unavailable, upgrade the Recovery Utility. Repeat this step for each topology transaction created.
Step 3: Publish the signed transaction
Run the following command inside the participant console:
val signature =
Signature.fromExternalSigning(
SignatureFormat.Raw,
HexString.parseToByteString("HASH_SIGNATURE_HEXSTRING").get,
partyId.namespace.fingerprint,
SigningAlgorithmSpec.Ed25519
)
val topologyTxSignedByParty =
SignedTopologyTransaction.create(
topologyTransaction,
NonEmpty(
Set,
SingleTransactionSignature(topologyTransaction.hash, signature)
),
isProposal = false,
ProtocolVersion.v34,
)
val topologyTxSignedByBoth =
participant.topology.transactions.sign(
topologyTxSignedByParty,
TopologyStoreId.Synchronizer(synchronizerId),
)
participant.topology.transactions.load(
topologyTxSignedByBoth,
TopologyStoreId.Synchronizer(synchronizerId),
)For multiple parties, repeat using the corresponding signatures.
Step 4: Verify the party mapping
Confirm the PartyToParticipant mapping was updated:
participant.topology.party_to_participant_mappings.list(
synchronizerId,
filterParty = partyId.filterString
)Expected result:
- The party appears to be hosted on the participant.
-
onboarding = trueis shown.
To verify multiple parties:
participant.topology.party_to_participant_mappings.list(
synchronizerId,
filterParticipant = participant.filterString
)You should see N + 1 parties:
- N recovered parties
- The participant’s own party
Step 5: Disconnect the participant
Disconnect from all synchronizers:
participant.synchronizers.disconnect_all()
Disable automatic reconnection:
participant.synchronizers.modify(
"global",
_.copy(manualConnect = true)
)Repeat for additional synchronizers if applicable.
Step 6: Create a database snapshot
Create a snapshot of your validator database. Because implementations vary by database type, follow your database vendor’s documentation.
Recommended best practice:
- Stop validator and participant containers.
- Create the snapshot.
- Restart containers.
- Reconnect to the console.
Step 7: Obtain the party ACS
Retrieve the Active Contract Set (ACS) directly from the global synchronizer:
curl -sSL --fail-with-body \
'https://scan.sv-1.global.canton.network.sync.global/api/scan/v0/acs/YOUR_PARTY_ID?record_time=YOUR_VALID_FROM' \
-H 'Content-Type: application/json' \
| jq -r .acs_snapshot | base64 -d > acs_snapshotReplace:
-
YOUR_PARTY_ID:target party ID -
YOUR_VALID_FROM:value from the mapping result in Step 4
For multiple parties, query each party separately.
Step 8: Import the ACS
Import the snapshot:
participant.repair.import_acs("<absolute_path_to_acs>")
For multiple imports:
Seq("<path1>", "<path2>", "...").map(
p => participant.repair.import_acs(p)
)Important:
If this step fails, restore the database snapshot created earlier before retrying.
Step 9: Reconnect the participant
First record the current ledger position:
val ledgerEnd = participant.ledger_api.state.end()
Store this value securely.
Reconnect synchronizers:
participant.synchronizers.reconnect_all()
Re-enable automatic reconnection:
participant.synchronizers.modify(
"global",
_.copy(manualConnect = false)
)Step 10: Clear the onboarding flag
Remove onboarding status so transactions can resume:
val (onboarded, minimalSafeClearingTs) =
participant.parties.clear_party_onboarding_flag(
partyId,
synchronizerId,
ledgerEnd
)For multiple parties:
Seq(partyId1, partyId2).map(
party =>
participant.parties.clear_party_onboarding_flag(
party,
synchronizerId,
ledgerEnd
)
)Post-recovery considerations
After recovery completes, review the following items.
Pre-approvals
Wallet providers must:
- Cancel previous pre-approvals configured for Fireblocks validators
- Create new pre-approvals if required
Contract IDs
Contract IDs change after migration. Wallet providers must retrieve updated contract IDs from their validator.
Token recovery
This procedure does not recover tokens. Token recovery requires coordination with the relevant Registrar (for example, Circle for USDCx).