Skip to main content

Onboarding

Quorum onboarding binds exact approval credentials to a distributed key and defines how many of them must approve signing or policy management. You can configure the first quorum during Key Generation (DKG) or add it later through Policy Update.

Choose an onboarding path

Key stateOnboarding path
The key does not existAttach the quorum policy to DKG
The key exists without management approvalThe authorization root installs the first quorum policy
The key already has management approvalThe current management quorum authorizes the replacement policy

Initialize a quorum during DKG

The credential used to issue DKG request becomes immutable authorization root. When DKG also carries a policy, MPC Nodes derive approval grants from every exact credential named by management_approval and rules[].approval.

An authorization epoch is the key's monotonically increasing authorization-state version: DKG with a policy starts at epoch 1, and each policy changes or removal advances it by one.

Loading Diagram...

1. Define the approval groups

Use management_approval and Rule.approval to define policy-management and signing quorums.

import {
ChainType,
Operator,
Policy,
Rule,
TransactionAttribute,
TransactionType,
} from '@silencelaboratories/walletprovider-sdk';

const policy = new Policy({
version: '1.0',
epoch: 1,
description: 'Two-person signing and administration',
management_approval: {
members: [
{ method: 'eoa', id: rootAddress },
{ method: 'passkey', id: adminPasskeyId },
{ method: 'jwt', id: adminJwtCredentialId },
],
threshold: 2,
},
rules: [
new Rule({
description: 'Require two approvals for the protected message',
issuer: [{ type: 'UserId', id: initiatorCredentialId }],
approval: {
members: [
{ method: 'eoa', id: rootAddress },
{ method: 'passkey', id: signerPasskeyId },
{ method: 'jwt', id: signerJwtCredentialId },
],
threshold: 2,
},
action: 'allow',
chain_type: ChainType.Ethereum,
conditions: [
{
transaction_type: TransactionType.Eip191,
transaction_attr: TransactionAttribute.Message,
operator: Operator.Eq,
value: 'Approve payroll',
},
],
}),
],
});

An approval identity is always method-qualified. For example, eoa:0xabc... and passkey:0xabc... are different credentials.

2. Generate the key with the policy

import {
EphKeyClaim,
NetworkSigner,
WalletProviderServiceClient,
type AuthModule,
type MPCSignAlgorithm,
} from '@silencelaboratories/walletprovider-sdk';

declare const rootAuth: AuthModule;

const wpClient = new WalletProviderServiceClient({
apiVersion: 'v2',
walletProviderUrl: 'wss://wallet-backend.example',
});
const networkSigner = new NetworkSigner(wpClient, rootAuth);

// Keyshare distribution: 2 of 3 MPC Nodes cooperate to produce a signature.
const threshold = 2;
const parties = 3;
const keySignAlgs: MPCSignAlgorithm[] = ['secp256k1'];

// Ephemeral credential the root uses to initiate later signing requests.
const { ephClaim } = EphKeyClaim.generateKeys('secp256k1', 86400);

const [key] = await networkSigner.generateKey(
threshold,
parties,
keySignAlgs,
ephClaim,
policy,
);

The returned key.keyId can be used in further requests for signature generations, and to retrieve the key's authorization state.

Initialize or extend a quorum with updatePolicy

Policy updates reconcile Policy membership and credential grants atomically. Current members approve the change. Each newly added member separately proves possession of its credential over the same request.

Loading Diagram...

1. Build the replacement policy

Every policy change is bound to an epoch, so you need the key's current epoch before you can build a replacement. The epoch is set when the key is created and changes only through the requests below:

EventResulting epoch
DKG without a policy0
DKG with a policy1
Each successful updatePolicyPrevious epoch plus one, returned in the response
Each successful deletePolicyPrevious epoch plus one, returned in the response

A replacement policy uses the current authorization epoch plus one. If the value you submit is not the epoch the nodes hold, they reject the request before it applies, so a stale update can never overwrite a newer one.

const nextPolicy = new Policy({
version: '1.0',
epoch: currentAuthorizationEpoch + 1,
description: 'Add admin D',
management_approval: {
members: [
{ method: 'eoa', id: rootAddress },
{ method: 'passkey', id: adminPasskeyId },
{ method: 'eoa', id: adminDAddress },
],
threshold: 2,
},
rules: currentRules,
});

2. Collect signatures on each credential holder's client

Every holder reconstructs the same UpdatePolicyRequest. Private credentials remain on their own clients; only the completed UserAuthentication is returned to the Wallet backend.

import {
UpdatePolicyRequest,
UserSignatures,
type AuthModule,
} from '@silencelaboratories/walletprovider-sdk';

const payload = new UpdatePolicyRequest({ keyId, policy: nextPolicy });

async function authenticateUpdate(authModule: AuthModule) {
const signatures = await new UserSignatures(authModule, 'v2').build(
'updatePolicy',
payload,
);
if (!signatures.default) throw new Error('Authentication produced no signature');
return signatures.default;
}

// These calls happen on the corresponding credential holders' clients.
const rootSignature = await authenticateUpdate(rootAuth);
const currentAdminSignature = await authenticateUpdate(currentAdminAuth);
const newAdminSignature = await authenticateUpdate(newAdminAuth);

3. Submit one V2 policy update

import { HttpClient } from '@silencelaboratories/walletprovider-sdk';

const http = new HttpClient('https://wallet-backend.example');
const result = await http.post('/v2/rest/updatePolicy', {
payload,
userSignatures: {
default: rootSignature,
approvals: [currentAdminSignature],
grantees: [newAdminSignature],
},
});

The entry in approvals counts toward the current management threshold. The entry in grantees proves possession and consent for the new grant.

Next, read Core Concepts for the identity, capability, and lifecycle rules behind these flows.