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 state | Onboarding path |
|---|---|
| The key does not exist | Attach the quorum policy to DKG |
| The key exists without management approval | The authorization root installs the first quorum policy |
| The key already has management approval | The 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.
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.
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:
| Event | Resulting epoch |
|---|---|
| DKG without a policy | 0 |
| DKG with a policy | 1 |
Each successful updatePolicy | Previous epoch plus one, returned in the response |
Each successful deletePolicy | Previous 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.