Adding new participants
A participant is an institution that runs its own Rayls Sovereign chain (Privacy Node in the code) in a Rayls Private Network. On the Private Network Hub, each participant is identified by its chain's ID.
Adding a participant takes work on both sides:
- the Private Network operator registers the participant on the Hub, activates it and authorises its relayer;
- the institution deploys the Rayls contracts on its chain and starts its private relayer and key service (CTS), which then register the institution's keys on the Hub.
This page describes the operator's steps and what happens on the participant's side. For the participant's installation, see Installing a Private Network.
What the Hub records about a participant
The Hub's participant registry (ParticipantStorageV1) holds one record per participant:
| Field | Meaning |
|---|---|
chainId | The chain ID of the participant's Rayls Sovereign chain. It must be non-zero and unique in the network. Chain ID 999 is taken by the operator (see below). |
role | PARTICIPANT (0), ISSUER (1) or AUDITOR (2) |
status | NEW (0), ACTIVE (1), INACTIVE (2) or FROZEN (3) |
ownerId | A free-text identifier for the owner. The reference scripts use the owner's address. |
name | A display name |
allowedToBroadcast | Whether the participant's chain may send one message to all active participants at once |
createdAt, updatedAt | Set by the contract |
Roles
| Role | What it allows in the code |
|---|---|
PARTICIPANT | Sending and receiving cross-chain messages, Enygma transfers and delivery-versus-payment (DvP) swaps, while active |
ISSUER | Everything a participant can do, plus registering tokens on the Hub. The Hub accepts a token registration only from an active issuer. |
AUDITOR | Nothing beyond PARTICIPANT. When the Hub is deployed, the operator registers itself with this role under chain ID 999, together with the operator's view public key. Every institution encrypts its view key to that public key. |
The role doesn't give access to decrypted data. Only the operator's governance services decrypt cross-chain traffic. See Who sees what.
Statuses
Only ACTIVE participants can take part in the network. The other three statuses have the same effect in the code: the participant can't send or receive cross-chain messages, register tokens, or register keys on the Hub. The contract doesn't restrict which status can follow which; it only refuses to set the status a participant already has.
Before you start
Agree with the institution on:
- its chain ID and name;
- its role:
ISSUERif it will issue tokens that move across the network, otherwisePARTICIPANT; - the owner identifier to record;
- how you'll get its relayer's Hub addresses: either the URL of its key service, or the list of addresses.
You need an account with the ADMIN role on the Hub, or, for steps 1 and 2 only, the optional PRIVATE_NETWORK_OPERATOR role, which can call addParticipants (a list of participants) but not addParticipant. See Operating a Rayls Private Network and Private Network roles.
Step 1: Register the participant
Call addParticipant on ParticipantStorage with the participant's details. The participant is recorded with status NEW, and the Hub sends the new record to every chain in the network, which keeps a copy of the participant list (ParticipantStorageReplicaV1).
Call the contract directly. For example, from a Hardhat script in rayls-sovereign-contracts, run with npx hardhat run:
import { ethers } from 'hardhat';
const operator = new ethers.Wallet(process.env.PRIVATE_KEY_SYSTEM!, new ethers.JsonRpcProvider(process.env.PNH_RPC_URL));
const registry = await ethers.getContractAt('DeploymentProxyRegistryV1', process.env.PNH_DEPLOYMENT_PROXY_REGISTRY!, operator);
const participantStorage = await ethers.getContractAt('ParticipantStorageV1', await registry.getContract('ParticipantStorage'), operator);
await (await participantStorage.addParticipant({
chainId: 200002, // the participant's chain ID
role: 1, // 0 PARTICIPANT, 1 ISSUER, 2 AUDITOR
ownerId: '0x…', // owner identifier
name: 'Bank B',
allowedToBroadcast: true,
})).wait();If an active participant already has that chain ID, the call reverts with ParticipantCore: Participant already exists.
Step 2: Activate the participant
Set the status to ACTIVE (1). The Hub sends the updated record to every chain.
npx hardhat participants:update-status --chainid 200002 --status 1The task uses PNH_RPC_URL, PNH_DEPLOYMENT_PROXY_REGISTRY and PRIVATE_KEY_SYSTEM, and calls ParticipantStorage.updateStatus(chainId, status). The participant must be active before its key service can register its keys in step 5.
Step 3: Authorise the participant's relayer on the Hub
The participant's private relayer sends transactions to the Hub from addresses its key service controls. Each of those addresses needs the RELAYER role in the Hub's access manager.
The key service publishes the addresses at GET /public/addresses?service=private_relayer. The response lists them in private_hub_addresses and private_hub_dvp_operator_addresses; both sets need the role.
The add-authorized-relayers-pnh task fetches the addresses from each participant's key service and grants the role:
# CTS_SERVICE_B_URL is the URL of participant B's key service
npx hardhat add-authorized-relayers-pnh --privacy-nodes B
Hub chain IDThe task stops unless the Hub's chain ID is 1337, the chain ID of the reference Hub. On a Hub with another chain ID, grant the role directly:
grantRole(roleId, address, 0)onRaylsAccessManagerV1, with the ID returned bygetRoleIdByName("RELAYER").
Until its addresses hold the role, the participant's key service waits and doesn't start registration.
Step 4: The participant deploys its contracts
The institution deploys the Rayls contracts on its Rayls Sovereign chain, pointing them at the Hub. At the end of the deployment, its chain asks the Hub for the current participant list and the current list of frozen tokens, so its copies start up to date. See Installing a Private Network.
Step 5: The participant's key service registers its keys
When the institution starts its key service and private relayer, the key service registers the institution on the Hub. Each step is skipped if the Hub already has the data, so a restart is safe:
- Relayer keys. It creates the relayer's signing keys, if it has none, and waits until their Hub addresses hold
RELAYER(step 3). - View key. It creates the institution's view key pair, with ML-KEM-768, a post-quantum key exchange, and publishes the public key on the Hub (
setChainViewData). - Key agreements. For every participant that has a view key on the Hub, including the operator, it runs an ML-KEM key exchange and records the ciphertext on the Hub (
initiateKeyAgreement). The other participants' relayers see it on the Hub and complete their side. These shared secrets are what Enygma and the encrypted messages between chains use. - View key escrow to the operator. It encrypts the institution's view private key, using the secret it shares with the operator, and stores the result on the Hub (
setAuditInfo). This is how the operator can decrypt the institution's cross-chain traffic. Registration doesn't complete without it. - Spend key. It creates the institution's Enygma spend key, and registers a hash of the key and the relayer's Hub addresses on the Hub (
setPaymentSpendPublicKey). The spend key itself never leaves the key service.
The Hub accepts each of these calls only for an active participant, and only from an address with the RELAYER role. If the key service finds that a view key or spend key on the Hub doesn't match the one it holds, it stops.
When the institution's view key escrow lands on the Hub, the operator's governance listener reloads its keys, and from then on decrypts the new participant's cross-chain traffic.
Check the result
The governance API shows the participant once its listener has indexed the Hub events:
curl http://<governance-api>/audit/participants/200002{
"id": "6d3f…",
"createdAt": "2026-10-06T09:12:44Z",
"updatedAt": "2026-10-06T09:13:02Z",
"chainId": 200002,
"name": "Bank B",
"ownerId": "0x…",
"status": "active",
"role": "issuer",
"allowedToBroadcast": true,
"isFlagged": false
}GET /audit/participants lists all participants, and accepts name, chainId, status, role, createdAfter and createdBefore filters. To read the Hub directly, run npx hardhat participants:get-all.
Changing a participant later
| Change | Call on ParticipantStorage | Hardhat task |
|---|---|---|
| Status | updateStatus(chainId, status) | participants:update-status --chainid <id> --status <n> |
| Role | updateRole(chainId, role) | participants:update-role --chainid <id> --role <n> |
| Broadcast permission | updateBroadcastMessagesPermission(chainId, allowed) | participants:update-broadcast-permission --chainid <id> --allow <true or false> |
| Remove | removeParticipant(chainId), which sets the status to INACTIVE. Records are never deleted. | None |
Each change is sent to every chain. To stop a participant temporarily, see Freezing participants.
If a participant's copy of the participant list falls out of date, it can ask the Hub to send the whole list again: requestAllParticipantsDataFromPrivateHub() on its ParticipantStorageReplicaV1, or npx hardhat participants:update-storage-replica --node <ID>.
Updated 8 days ago
