This example is using Daml SDK 2.6.x.
This is an example canton setup implementing the public reference data pattern in a multi-node Canton setup. The public reference data is maintained by a central admin party, but it is accessible to all participants in the domain.
See also: https://docs.daml.com/canton/usermanual/identity_management.html#party-on-two-nodes.
There are two participant nodes and three parties configured in this example:
- Central: The main participant node
- centralAdmin party: The main party, which is the admin for the reference data
- referenceDataReader party: party, which has read access to the reference data
- centralAdminUser: actAs centralAdmin, actAs referenceDataReader
- TraderOne: A participant which wants to access reference data
- traderOneAdmin party: a party on TraderOne
- traderOneAdminUser: actAs tradeOneAdmin, readAs referenceDataReader
The example uses a canton configuration where identity management is used, users are authenticated with JWT tokens, etc.
This document is a walk through for all the steps needed, but there
are scripts under the bin
folder implementing the necessary
steps.
- config This folder contains the canton configuration and bootstrap scripts for each node.
- docker-compose.yml: Contains the whole setup with postgres database, the domain node, and the two participant nodes.
- daml Folder containing the daml code
- bin Shell scripts implementing the below runbook
- bin/tmux-session.sh: A tmux script to create a new tmux session, where the central participant is started with docker compose
daml build
./bin/generate_participant_keys.sh
(Note: ignore the <<script_header>>
parts in the below code examples)
First we need to generate the keys for the participant nodes, which will be used for the JWT tokens.
<<script_header>>
openssl req -nodes -new -x509 -keyout "config/central/api.key" -out "config/central/api.crt"
openssl req -nodes -new -x509 -keyout "config/traderOne/api.key" -out "config/traderOne/api.crt"
Now we can start the central participant using docker.
Note: You can also use the bin/tmux-session.sh
script to create
a tmux session for the various docker-compose operations.
docker compose up -d central
docker logs -f
When a participant starts, it executes the canton bootstrap script, in this case config/central/canton.bootstrap. This bootstrap script does the following:
- Creates both parties on central
- Creates the admin user
- Uploads the project dar file
- Generates the file
config/central/participant-config.json
The latter file will be used in our scripts the extract party identifiers
./bin/create_user_token.sh central centralAdminUser
In this step we create the JWT token, which will be used to authenticate the centralAdminUser to the ledger API.
export PARTICIPANT_NAME=central
export PARTICIPANT_USER=centralAdminUser
<<jwt_script_header>>
PARTICIPANT_DIR=config/$PARTICIPANT_NAME
PARTICIPANT_ID=$(jq -r ".party_participants | to_entries | .[] | select(.key | startswith(\"${PARTICIPANT_NAME}:\")) | .key" ${PARTICIPANT_DIR}/participant-config.json)
CLAIM=$(echo -n "{\"aud\":\"https://daml.com/jwt/aud/participant/${PARTICIPANT_ID}\",\"sub\":\"${PARTICIPANT_USER}\"}" | base64 -w0 | sed s/\+/-/g |sed 's/\//_/g' | sed -E s/=+$//)
HEADER=$(echo -n '{"alg":"RS256","typ":"JWT"}' | base64 -w0 | sed s/\+/-/g | sed 's/\//_/g' | sed -E s/=+$//)
SIGNATURE=$(echo -n "${HEADER}.${CLAIM}" | openssl dgst -sha256 -sign "config/${PARTICIPANT_NAME}/api.key" -binary | base64 -w0 | sed s/\+/-/g | sed 's/\//_/g' | sed -E s/=+$//)
echo "${HEADER}.${CLAIM}.${SIGNATURE}" > ${PARTICIPANT_USER}.jwt
./bin/init_central_admin.sh
The below snippet calls the CentralAdmin:initCentralAdmin daml script to create the CentralAdmin Role contract, which will be used to create reference data contracts.
Additionally we also create some reference data.
The script’s input are the parties required for this contract. We
extract the parties from the participant-config.json
file.
<<script_header>>
REFERENCE_READER=$(jq -r ".party_participants | to_entries | .[] | select(.key | startswith(\"referenceDataReader:\")) | .key" config/central/participant-config.json)
CENTRAL_ADMIN=$(jq -r ".party_participants | to_entries | .[] | select(.key | startswith(\"centralAdmin:\")) | .key" config/central/participant-config.json)
PARTIES_FILE=$(mktemp)
echo "{\"centralAdmin\":\"${CENTRAL_ADMIN}\",\"referenceDataReader\":\"${REFERENCE_READER}\"}" > $PARTIES_FILE
daml script \
--ledger-host localhost --ledger-port 5011 \
--access-token-file centralAdminUser.jwt \
--dar .daml/dist/public-party-0.0.1.dar \
--input-file $PARTIES_FILE \
--script-name CentralAdmin:initCentralAdmin
daml script \
--ledger-host localhost --ledger-port 5011 \
--access-token-file centralAdminUser.jwt \
--dar .daml/dist/public-party-0.0.1.dar \
--input-file $PARTIES_FILE \
--script-name CentralAdmin:createReferenceData
rm $PARTIES_FILE
docker compose up -d traderOne
For this step we’ll need to execute a few canton administrative commands on both participants. These steps implemented as functions in config/central/canton.bootstrap and config/traderOne/canton.bootstrap.
To execute these commands attach to the canton consoles running in docker in two separate terminals or tmux windows:
docker attach central
docker attach traderOne
Then execute the following canton commands:
- On traderOne:
onboardParticipantStep1()
- On central:
downloadReferenceDataReaderACS()
- On traderOne:
onboardParticipantStep2()
These steps will authorize the referenceDataReader
party on the
traderOne
participant and transfers its ACS. From this point,
the referenceDataReader
party can be used on traderOne
. That
is, we can grant the readAs referenceDataReader rights to the
participant users.
The following three steps in one command
./bin/create_user_token.sh traderOne traderOneAdminUser && \
./bin/propose_participant.sh && \
./bin/onboard_participant.sh
./bin/create_user_token.sh traderOne traderOneAdminUser
This step is the same as for the centralAdminUser
, but for
traderOne
and traderOneAdminUser
.
./bin/propose_participant.sh
Now we need to create the ParticipantAdminRoleProposal
contract
by the tradeOneAdmin
party. This proposal will create the
ParticipantAdminRole contract when accepted by the centralAdmin
party.
<<script_header>>
REFERENCE_READER=$(jq -r ".party_participants | to_entries | .[] | select(.key | startswith(\"referenceDataReader:\")) | .key" config/traderOne/participant-config.json)
CENTRAL_ADMIN=$(jq -r ".party_participants | to_entries | .[] | select(.key | startswith(\"centralAdmin:\")) | .key" config/traderOne/participant-config.json)
PARTICIPANT_ADMIN=$(jq -r ".party_participants | to_entries | .[] | select(.key | startswith(\"traderOneAdmin:\")) | .key" config/traderOne/participant-config.json)
PARTIES_FILE=$(mktemp)
echo "{\"participantAdmin\":\"${PARTICIPANT_ADMIN}\",\"centralAdmin\":\"${CENTRAL_ADMIN}\",\"referenceDataReader\":\"${REFERENCE_READER}\"}" > $PARTIES_FILE
daml script \
--ledger-host localhost --ledger-port 5021 \
--access-token-file traderOneAdminUser.jwt \
--dar .daml/dist/public-party-0.0.1.dar \
--input-file $PARTIES_FILE \
--script-name ParticipantAdmin:proposeParticipant
rm $PARTIES_FILE
./bin/onboard_participant.sh
To accept the above proposal by the centralAdmin
party:
<<script_header>>
REFERENCE_READER=$(jq -r ".party_participants | to_entries | .[] | select(.key | startswith(\"referenceDataReader:\")) | .key" config/traderOne/participant-config.json)
CENTRAL_ADMIN=$(jq -r ".party_participants | to_entries | .[] | select(.key | startswith(\"centralAdmin:\")) | .key" config/traderOne/participant-config.json)
PARTICIPANT_ADMIN=$(jq -r ".party_participants | to_entries | .[] | select(.key | startswith(\"traderOneAdmin:\")) | .key" config/traderOne/participant-config.json)
PARTIES_FILE=$(mktemp)
echo "{\"participantAdmin\":\"${PARTICIPANT_ADMIN}\",\"centralAdmin\":\"${CENTRAL_ADMIN}\",\"referenceDataReader\":\"${REFERENCE_READER}\"}" > $PARTIES_FILE
daml script \
--ledger-host localhost --ledger-port 5011 \
--dar .daml/dist/public-party-0.0.1.dar \
--access-token-file centralAdminUser.jwt \
--input-file $PARTIES_FILE \
--script-name CentralAdmin:onboardParticipant
rm $PARTIES_FILE
<<script_header>>
REFERENCE_READER=$(jq -r ".party_participants | to_entries | .[] | select(.key | startswith(\"referenceDataReader:\")) | .key" config/traderOne/participant-config.json)
CENTRAL_ADMIN=$(jq -r ".party_participants | to_entries | .[] | select(.key | startswith(\"centralAdmin:\")) | .key" config/traderOne/participant-config.json)
PARTICIPANT_ADMIN=$(jq -r ".party_participants | to_entries | .[] | select(.key | startswith(\"traderOneAdmin:\")) | .key" config/traderOne/participant-config.json)
PARTIES_FILE=$(mktemp)
echo "{\"participantAdmin\":\"${PARTICIPANT_ADMIN}\",\"centralAdmin\":\"${CENTRAL_ADMIN}\",\"referenceDataReader\":\"${REFERENCE_READER}\"}" > $PARTIES_FILE
echo "Creating a trade...."
daml script \
--ledger-host localhost --ledger-port 5021 \
--access-token-file traderOneAdminUser.jwt \
--dar .daml/dist/public-party-0.0.1.dar \
--input-file $PARTIES_FILE \
--script-name ParticipantAdmin:createTrade
echo "Creating a trade2...."
daml script \
--ledger-host localhost --ledger-port 5021 \
--access-token-file traderOneAdminUser.jwt \
--dar .daml/dist/public-party-0.0.1.dar \
--input-file $PARTIES_FILE \
--script-name ParticipantAdmin:createTrade2
echo "Creating a trade, but it will fail....."
daml script \
--ledger-host localhost --ledger-port 5021 \
--access-token-file traderOneAdminUser.jwt \
--dar .daml/dist/public-party-0.0.1.dar \
--input-file $PARTIES_FILE \
--script-name ParticipantAdmin:createTradeFail
rm $PARTIES_FILE
Can we simply assign actAs referenceDataReader in a participant, without permission from central?