Embedded Agent

Overview

The high-level facade in @zafeguard/mpc-sdk for the warm-cold wallet shape — one device share, N cloud-agent shares, threshold signing across the cohort, with an optional presignature pool that batches the heavier round.

Overview

EmbeddedAgent is the high-level facade in @zafeguard/mpc-sdk for the warm-cold wallet shape: the user's device holds one MPC share, one or more Zafeguard cloud agents hold the rest, and signing requires the device plus a configurable threshold of agents to cooperate. No single party — device alone, agent alone, or the network alone — can produce a signature.

Cryptographic construction: Pedersen-style DKG (1991) with Feldman commitments, Gennaro-Goldfeder GG18/GG20 threshold ECDSA for the EC-curve signing flows, RFC 8032 EdDSA for Ed25519 / Ed448, BIP-340 for Bitcoin Taproot Schnorr, and BIP-32 for hierarchical derivation. See Cryptographic foundations for the full reference map.

Call await Mpc.init(licenseCert) once at process start, before constructing the first EmbeddedAgent. The class exposes finalizeSignature / verifySignature — local-crypto methods that re-validate the license per call. Without Mpc.init(...) they throw at first use. See License requirement.

The five-method lifecycle covers every state a signer goes through:

import { EmbeddedAgent, Curve, RecoveryKind } from '@zafeguard/mpc-sdk';

const signer = new EmbeddedAgent({
  agents: [{ baseUrl: 'https://node-1.example', apiKey: '...' }],
  threshold: 2,
  curve: Curve.Secp256k1,
});
const loaded     = await signer.create({ keyId, recovery: { kind: RecoveryKind.Noop }, passphrase });
const blob       = loaded.exportedBlob!;        // sealed during create — persist via your HSM-backed storage
const partials   = await loaded.sign({ messageHash });            // cohort partials
const sig        = await loaded.finalizeSignature({               // combine locally
  messageHash: partials.messageHash, partials: partials.partials, derivationPath: partials.derivationPath,
});
const restored   = await signer.load({ keyId, blob, passphrase });
const reshared   = await loaded.reshare({ passphrase });  // reshared.exportedBlob sealed inline

The cohort size is always 1 + agent.length. A single-agent cohort is the simplest 2-of-2 wallet; adding more agents to the array adds redundancy and quorum flexibility. Every agent participates as a distinct DKG party — they're equal participants, not a primary-plus-fallback split.

Vocabulary in one table

These four terms appear throughout the SDK and the docs. They mean different things — keep them straight:

TermWhat it meansHow it's set
CohortThe fixed set of parties that hold key shares for a single wallet. Device counts as one party; every entry in agents[] counts as one.Decided at create() (= DKG). Never changes for the lifetime of the wallet — even reshare keeps the same membership.
ParticipantsTotal count of cohort parties. Equals 1 + agents.length.Implied by the cohort. Surfaced on loaded.participants.
ThresholdHow many cohort parties must cooperate to produce a signature. The t in t-of-n.Constructor argument threshold. Range 2..=participants. Surfaced on loaded.threshold.
QuorumA concrete set of threshold-many parties that actually participate in one signing call. Synonymous with "the signers on this particular sign() round".Picked at sign time — for a 2-of-3 wallet, the device + either agent forms a quorum.

So a 2-of-3 wallet has cohort = 3, participants = 3, threshold = 2, and each sign() call uses one quorum of 2.


Mental model

       Device                       Cloud agents (1..N)
   ┌──────────┐                ┌──────────┐  ┌──────────┐
   │  share₀  │                │  share₁  │  │  share₂  │ ...
   └─────┬────┘                └──────┬───┘  └────┬─────┘
         │                            │           │
         └────── threshold-many partial sigs ─────┘


                      Combined signature
                   (verifies against the wallet's
                    DKG-produced public key)
  • DKG runs once at create(). Every cohort party generates its own share locally; no party ever sees the full private key.

  • Sign is one logical operation that runs in two phases — a cohort round-trip to gather threshold-many partial signatures, then a local combine that assembles the final (r, s, v). The SDK gives you three entry points for the same operation depending on where the two phases need to run:

    • loaded.signMessage({ messageHash }) — the everyday path. Runs both phases and returns the assembled signature directly. Use this when the wallet signs and finalises on the same host.
    • loaded.sign({ messageHash }) then loaded.finalizeSignature({ partials }) — runs the same phases as separate calls. Use only when you need to inspect partials between calls.
    • loaded.sign(...) then Utils.finalizeSignature({ partialSignatures, publicKey }) — used when the host that runs sign() is not the host that finalises. Ship partials to the other host and call Utils.finalizeSignature there. (Scheme.finalizeSignature is the low-level variant when you only have raw bytes, not a handle.)

    Each sign() (or signMessage()) call burns a cohort interaction — it is not idempotent.

  • Reshare rotates the cryptographic material across the cohort while preserving the wallet's public key (and therefore every on-chain address derived from it).

  • Restart vs. recoversigner.load({ blob, passphrase }) is the routine path for "same device, new process" (an app restart or device reboot). signer.load({ recoveryBundle, recovery }) is the disaster path for "new device, original device gone" (a phone you no longer have access to) — it consumes the off-device recovery bundle and the matching recovery credential, not the device-local blob.


What's in this section

  • Quick start → — a working signer in under 30 lines: create → sign → verify → export → restore.
  • Topology → — choose your cohort: single-agent for the simplest 2-of-2 wallet, multi-agent for redundancy, threshold for the signing quorum.
  • Presignature pool → — pre-mint a batch to cut per-sign cohort round-trips from two to one, opt-in auto-refill, and pool export/import for cold-storage handoff.
  • Recovery → — share rotation (reshare()), sealed-bundle backup (the RecoveryKind plugins), and the emergency recovery path that reconstructs the raw root private key off-cluster from a quorum of cohort shards — your escape hatch when the MPC infrastructure becomes unreachable, you need to migrate off MPC entirely, or a regulator demands a single-key extraction.

When NOT to use EmbeddedAgent

EmbeddedAgent assumes your app holds one of the MPC shares and talks to Zafeguard agents over HTTPS. It's the right facade for:

  • Consumer wallets (mobile / browser apps) that want self-custody with one agent as a 2FA-like co-signer.
  • Institutional integrations that run their own MPC nodes (the agent topology lets you point the array at your own cluster).

If your product wants Zafeguard to hold and manage the keys server-side (and your code just triggers signing as a workflow component), use the @zafeguard/caller-sdk instead — that's the higher-level workflow API; the MPC ceremony machinery is hidden inside Zafeguard's components.

If you need raw access to a single MPC cluster node (no embedded device party — pure server-side custody) reach for ClusterAgent, the lower-level client in the same package.

On this page