External Signing: Party Onboarding
Onboard External Party
This tutorial demonstrates how to onboard an external party using the Ledger API.Prerequisites
This tutorial uses openssl to create keys on the file system, which is not secure for production use.
./bin/canton -c examples/08-interactive-submission/interactive-submission.conf --bootstrap examples/08-interactive-submission/bootstrap.canton
A runnable script
external_party_onboarding.sh located in the examples/08-interactive-submission directory of the Canton artifact puts together the steps in this tutorial as an example. Run the script from the same directory where you started Canton such that the script can find the canton_ports.json file which contains the port configuration of the running Canton instance, or invoke the script with the hostname and port of the Ledger API using the command line argument -p1 <host>:<port>. Note that the script supports a few command line arguments, which you can see by inspecting the code.To obtain a Canton artifact refer to the getting started section.Onboarding
The onboarding steps are:- Create a private key using openssl for the external party.
- Determine the available synchronizer-id.
- Create the topology transaction to define a new external party.
- Sign the topology transaction.
- Upload the signed topology transaction to the Ledger API.
v2/connected-synchronizers endpoint, assuming that there is exactly one. The party allocation must be repeated for each synchronizer-id the party should be hosted on.
Run this command from the same directory where you started Canton such that the command can find the canton_ports.json file which contains the port configuration of the running Canton instance.
PARTICIPANT1=$(echo "localhost:"$(jq -r ".participant1.jsonApi" canton_ports.json))
SYNCHRONIZER_ID=$(curl -f -sS -L ${PARTICIPANT1}/v2/state/connected-synchronizers | jq ".connectedSynchronizers[0].synchronizerId")
openssl genpkey -algorithm ed25519 -outform DER -out private_key.der
openssl pkey -in private_key.der -pubout -outform DER -out public_key.der 2> /dev/null
PUBLIC_KEY_BASE64=$(base64 -w 0 -i public_key.der)
/v2/parties/external/generate-topology to generate the topology transactions required to onboard the external party. This is fine if the node is trusted. In other scenarios, the transactions should be built manually or inspected before signing, including recomputing the hash.
GENERATE=$(cat << EOF
{
"synchronizer" : $SYNCHRONIZER_ID,
"partyHint" : "Alice",
"publicKey" : {
"format" : "CRYPTO_KEY_FORMAT_DER_X509_SUBJECT_PUBLIC_KEY_INFO",
"keyData": "$PUBLIC_KEY_BASE64",
"keySpec" : "SIGNING_KEY_SPEC_EC_CURVE25519"
},
"otherConfirmingParticipantUids" : []
}
EOF
)
ONBOARDING_TX=$(curl -f -sS -d "$GENERATE" -H "Content-Type: application/json" \
-X POST ${PARTICIPANT1}/v2/parties/external/generate-topology)
PARTY_ID=$(echo $ONBOARDING_TX | jq -r .partyId)
TRANSACTIONS=$(echo $ONBOARDING_TX | jq '.topologyTransactions | map({ transaction : .})')
PUBLIC_KEY_FINGERPRINT=$(echo $ONBOARDING_TX | jq -r .publicKeyFingerprint)
MULTI_HASH=$(echo $ONBOARDING_TX | jq -r .multiHash)
Do not make assumptions on the number of onboarding transactions returned by the endpoint as it is subject to change. To deserialize and inspect the transaction, check out the external party topology transaction tutorial.
echo $MULTI_HASH | base64 --decode > hash_binary.bin
openssl pkeyutl -sign -inkey private_key.der -rawin -in hash_binary.bin -out signature.bin -keyform DER
SIGNATURE=$(base64 -w 0 < signature.bin)
The transactions can be signed one by one, or together as one hash, as done here.
ALLOCATE=$(cat << EOF
{
"synchronizer" : $SYNCHRONIZER_ID,
"onboardingTransactions": $TRANSACTIONS,
"multiHashSignatures": [
{
"format" : "SIGNATURE_FORMAT_CONCAT",
"signature": "$SIGNATURE",
"signedBy" : "$PUBLIC_KEY_FINGERPRINT",
"signingAlgorithmSpec" : "SIGNING_ALGORITHM_SPEC_ED25519"
}
]
}
EOF
)
curl -f -sS -d "$ALLOCATE" -H "Content-Type: application/json" \
-X POST ${PARTICIPANT1}/v2/parties/external/allocate
allocate endpoint is synchronous, meaning it returns only when the party is allocated. The party should now appear on the Ledger API:
curl -f -sS "${PARTICIPANT1}/v2/parties?filter-party=$PARTY_ID"
Onboard Multi-Hosted External Party
This tutorial demonstrates how to onboard an external party using the Ledger API which is hosted on multiple validators. It is a simple extension to the onboard external party tutorial.Prerequisites
Make sure that you have completed the onboard external party tutorial and still have a running Canton example instance.Run The Script
The example script used in the previous tutorial also supports onboarding a multi-hosted external party. It will onboard by default on two nodes if invoked with the--multi-hosted command line argument.
./examples/08-interactive-submission/external_party_onboarding.sh --multi-hosted
The Details of the Script
The flag--multi-hosted will pass the second participant id into the generate-topology request through the
`"otherConfirmingParticipantUids" : [$OTHER_PARTICIPANT_ID]`
observingParticipantUids, confirmationThreshold and more. If not configured, then the confirmation threshold will be set to the number of confirming nodes.
The generated topology transactions then just need to be uploaded to the Ledger API of the second participant:
ALLOCATE=$(cat << EOF
{
"synchronizer" : $SYNCHRONIZER_ID,
"onboardingTransactions": $TRANSACTIONS,
"multiHashSignatures": [{
"format" : "SIGNATURE_FORMAT_CONCAT",
"signature": "$SIGNATURE",
"signedBy" : "$PUBLIC_KEY_FINGERPRINT",
"signingAlgorithmSpec" : "SIGNING_ALGORITHM_SPEC_ED25519"
}]
}
EOF
)
RESULT=$(curl -f -s -d "$ALLOCATE" -H "Content-Type: application/json" \
-X POST ${PARTICIPANT1}/v2/parties/external/allocate)
@ participant1.topology.party_to_participant_mappings.propose(
com.digitalasset.canton.topology.PartyId.tryCreate("Alice", participant1.id.uid.namespace),
newParticipants = Seq(
(participant1.id, ParticipantPermission.Confirmation),
(participant2.id, ParticipantPermission.Confirmation),
),
)
res1: SignedTopologyTransaction[TopologyChangeOp, PartyToParticipant] = SignedTopologyTransaction(
TopologyTransaction(
PartyToParticipant(
Alice::12201ff69b1d...,
PositiveNumeric(1),
Vector(
HostingParticipant(PAR::participant1::12201ff69b1d..., Confirmation, false),
HostingParticipant(PAR::participant2::1220a4d7463b..., Confirmation, false)
),
None
),
serial = 1,
operation = Replace,
hash = SHA-256:483ecaff7581...
),
signatures = 12201ff69b1d...,
proposal
)
@ participant2.topology.party_to_participant_mappings.list_hosting_proposals(sequencer1.synchronizer_id, participant2.id)
res2: Seq[com.digitalasset.canton.admin.api.client.data.topology.ListMultiHostingProposal] = Vector(
ListMultiHostingProposal(
txHash = SHA-256:483ecaff7581...,
party = Alice::12201ff69b1d...,
permission = Confirmation$,
others = PAR::participant1::12201ff69b1d... -> Confirmation$,
threshold = 1
)
)
txHash, which can be obtained from the output of the previous command:
@ val txHash = participant2.topology.party_to_participant_mappings.list_hosting_proposals(sequencer1.synchronizer_id, participant2.id).head.txHash
txHash : TopologyTransaction.TxHash = TxHash(hash = SHA-256:483ecaff7581...)
@ participant2.topology.transactions.authorize(sequencer1.synchronizer_id, txHash)
res4: SignedTopologyTransaction[TopologyChangeOp, TopologyMapping] = SignedTopologyTransaction(
TopologyTransaction(
PartyToParticipant(
Alice::12201ff69b1d...,
PositiveNumeric(1),
Vector(
HostingParticipant(PAR::participant1::12201ff69b1d..., Confirmation, false),
HostingParticipant(PAR::participant2::1220a4d7463b..., Confirmation, false)
),
None
),
serial = 1,
operation = Replace,
hash = SHA-256:483ecaff7581...
),
signatures = Seq(12201ff69b1d..., 1220a4d7463b...),
proposal
)
@ participant1.parties.hosted("Alice")
res5: Seq[ListPartiesResult] = Vector(
ListPartiesResult(
party = Alice::12201ff69b1d...,
participants = Vector(
ParticipantSynchronizers(
participant = PAR::participant1::12201ff69b1d...,
synchronizers = Vector(
SynchronizerPermission(synchronizerId = local::122032922613..., permission = Confirmation)
)
),
ParticipantSynchronizers(
participant = PAR::participant2::1220a4d7463b...,
synchronizers = Vector(
SynchronizerPermission(synchronizerId = local::122032922613..., permission = Confirmation)
)
)
)
)
)