Cosigner Architecture
What runs where, how the parts talk, and how a session is coordinated.

Components
Cosigner runtime
One Go binary, shipped as ghcr.io/fystack/cosigner for linux/amd64 and linux/arm64. It joins sessions; it does not coordinate them.
- MPC engine: Fystack's fork of tss-lib, same as MPCium. ECDSA uses GG18 on secp256k1. EdDSA uses tss-lib threshold Ed25519.
- Identity key: Ed25519, created at pairing. Authenticates the cosigner and signs its messages.
- Key store: AES-256-GCM encrypted rows in SQLite (default), Badger or DynamoDB.
- Gateway relay: One outbound gRPC stream. Sends presence every 5 seconds.
- Approval client: Calls your approval callback before every session.
- Audit log: Signed, hash-chained JSONL.
Your services
- cosigner-cli: Installs, verifies, pairs, starts and backs up the runtime.
- Approval callback: Your HTTPS server. Returns
approveorreject. - Storage key source: A password (
manualprofile) or AWS KMS (unattendedprofile).
Fystack services
- Apex: Issues pairing tokens, registers cosigner keys, handles revocation, submits MPC requests.
- Gateway: Accepts cosigner streams, checks identity and revocation, bridges traffic to NATS. One live stream per cosigner across all replicas.
- Orchestrator: Runs the session state machine and signs every control message. Never touches key shares.
- MPCium: Fystack's MPC nodes. See MPCium.
Channels
| From → to | Transport | Auth | Carries |
|---|---|---|---|
| Cosigner → Apex | HTTPS | One-time pairing token | Identity registration |
| Cosigner ↔ Gateway | gRPC stream, TLS | EdDSA JWT, 60s lifetime | All session traffic |
| Gateway ↔ Orchestrator ↔ MPCium | NATS | Ed25519 per message | Coordination inside Fystack |
| Participant ↔ participant | Relayed | X25519 + ChaCha20-Poly1305 | MPC rounds, end-to-end encrypted |
| Cosigner → Callback | HTTPS | RS256 JWT both ways | Approve or reject |
Stream envelopes
Every frame carries the authenticated workspace_id, cosigner_id and participant_id. The Gateway drops frames that don't match the stream identity.
| Kind | Direction | Subject |
|---|---|---|
CONTROL |
Orchestrator → cosigner | mpc.v1.peer.<participant_id>.control |
SESSION_EVENT |
Cosigner → orchestrator | mpc.v1.session.<session_id>.event |
P2P |
Participant ↔ participant | mpc.v1.peer.<to>.session.<session_id>.p2p |
PRESENCE |
Cosigner → orchestrator | mpc.v1.peer.<participant_id>.presence |
Trust boundaries
- Gateway: Relays ciphertext. Can drop or delay traffic, cannot read or forge MPC messages.
- Orchestrator: Signs every control message with a key the cosigner pinned at pairing. Unsigned or mis-signed messages are dropped.
- Apex: Cannot skip your callback. Every session goes to your callback first.
- Your cosigner: Cannot sign alone. The committee is fixed at keygen.
Details in Security Model.
Sessions
Each keygen, sign or reshare is one orchestrator session with a 120 second TTL.

- Admission: Orchestrator verifies the Apex signature, checks the committee and confirms every participant is online. One session per wallet and protocol at a time.
- waiting: Participants receive a signed
session_start, run local checks (the cosigner calls your callback) and replypeer_joined,peer_ready. - key_exchange: Participants exchange signed X25519 keys. Wallets with an approver also need its signed approval here.
- active_mpc: Orchestrator sends
mpc_begin. Participants run tss-lib rounds directly. - completed: All participants report the same result hash. Orchestrator signs and publishes the result.
A session fails on a participant error, a disconnect or mismatched results, and expires after 120 seconds. Either way the orchestrator broadcasts a signed session_abort.
Committees
Threshold t means t + 1 signers (tss-lib semantics). The committee of MPCium nodes and cosigners is fixed at keygen. Every later session must use the same party keys, identity keys and approver roles.
Connections
- One live stream per cosigner identity. A second instance with the same identity is rejected.
- Each session is bound to the stream that was live when it started. If that stream drops, the session fails with
PARTICIPANT_DISCONNECTED. - A revoked cosigner gets
PermissionDeniedand stops.