Fystack Joins the Stellar Open-Source Ecosystem: Delivering an MPC Wallet on Stellar


Most Stellar wallets today make the same tradeoff every wallet has made since 2014: one private key, held in one place, protected by however careful the person or process holding it happens to be. That's fine until it isn't - a leaked seed phrase, a compromised server, a single point of failure that an attacker only has to hit once.
This post is Deliverable of Fystack's Stellar Community Fund grant: a public writeup of the threshold-signature architecture behind stellar-wallet, the reference wallet we built on top of our open-source MPC signing cluster, mpcium. We cover what MPC threshold signatures actually are, how keygen and signing work end to end in this codebase, what the non-custodial guarantee really means, and how to reproduce the whole stack yourself. The reproduction steps also live in the repository README, so this isn't just a description - it's reviewable and runnable.
The goal isn't to sell you a wallet. It's to show the Stellar ecosystem a pattern for non-custodial key management that any team can adopt without spending months building threshold cryptography from scratch.
Why this exists
Stellar has good primitives for shared control - native multisig and SetOptions signer weights are built into the protocol. But multisig has a cost: every signer is a public key on the account, every policy change is an on-chain operation, and the account's signing structure is visible and fixed at the ledger level. That's the right tool for DAOs and treasuries that want transparent, on-chain governance over who can spend.
It's the wrong tool for a wallet provider that wants to custody keys for thousands of end users without operating thousands of on-chain multisig accounts, and without any single server ever holding a complete private key. That's a different problem - an infrastructure problem, not a governance problem - and it's what threshold signatures solve.
Fystack's grant deliverable applies that infrastructure to Stellar specifically: a real EdDSA keypair, a real G... account, a real 2-of-3 signing cluster, and a wallet UI in front of it so the mechanism isn't theoretical.
MPC threshold signatures, precisely
Multi-party computation (MPC) threshold signing splits a single private key into shares distributed across independent parties, such that no party ever sees the full key, and a threshold number of them - 2 of 3, in this build - must cooperate to produce a valid signature. This is different from multisig in a way worth being precise about:
| Single key | On-chain multisig | MPC threshold signature | |
|---|---|---|---|
| Where the private key lives | One place | N separate keypairs, N public keys on the account | Never assembled - exists only as distributed shares |
| What the chain sees | One normal signature | N signatures from N distinct public keys | One normal Ed25519 signature |
| Policy changes | N/A | On-chain SetOptions transaction |
Off-chain, cluster-side |
| Failure mode for 1 compromised party | Total loss | Attacker has 1 of N keys - needs more | Attacker has 1 share - cryptographically useless alone |
| Account type required | Any | Multisig-configured account | Any normal account |
The important row is the last one. A 2-of-3 MPC wallet doesn't need a special Stellar account type, a smart contract, or extra signers registered on-chain. From Horizon's point of view, it's an ordinary account with an ordinary Ed25519 public key. The threshold logic is entirely in how that key was generated and how it signs - not in anything Stellar has to know about.
Concretely, this build uses distributed key generation (DKG): 3 independent nodes run a keygen protocol together and each ends up holding a share of an EdDSA private key. No node - not even transiently - ever holds the full key. To sign, any 2 of the 3 nodes run a threshold-signing protocol that combines their shares into a single valid Ed25519 signature over the transaction hash, without ever reconstructing the private key at any point in the process. The third node can be offline, compromised, or destroyed, and the wallet keeps working; an attacker needs to compromise 2 of the 3 independent nodes simultaneously to forge a signature, and each individual share is mathematically useless on its own.
Architecture
The stack has four moving parts: a UI, a backend that never touches key material, a message bus, and the signing cluster itself.

The design principle that matters most here: the backend is a relay, not a custodian. It builds unsigned Stellar transactions, computes their hash, and hands that hash to the cluster over NATS. It never generates a key, never holds a share, and never sees a private key in memory at any point. If the backend server is fully compromised, an attacker gets application data and the ability to request signatures - they still need 2 of 3 independent, out-of-process nodes to agree before anything moves.
Consul's job is narrower than it looks: it's peer discovery and liveness, not consensus over money. The nodes register themselves so the backend's cluster package (internal/cluster/client.go) can answer "how many nodes are online right now" before deciding whether to even attempt a keygen or a signing request.
Keygen, step by step
Creating a wallet in the UI triggers this sequence in the backend:

POST /walletsvalidates the request and checksrequireNodes(keygenQuorum, "keygen")- keygen needs all 3 nodes online, because DKG only works if every node ends up holding a share. If a node is down, the request fails fast with a clear error instead of hanging.- A wallet row is inserted with status
generating, ands.mpc.CreateWallet(wallet.ID)dispatches aCreateWalletmessage over NATS (internal/mpc/client.go). - The three mpcium nodes run distributed keygen together. Each ends up holding a share of the new EdDSA private key. None of them, and nothing outside the cluster, ever has the complete key.
- On success, the cluster publishes a
KeygenResultEventback over NATS carrying the resulting EdDSA public key - never any share. The backend'sonKeygencallback (internal/app/mpc_callbacks.go) receives it. - The backend derives the Stellar address directly from that public key -
chain.DeriveAddress("stellar", publicKey)builds theG...strkey by prefixing the Ed25519 public key with Stellar's account-ID version byte, appending a CRC16-XModem checksum, and base32-encoding the result (internal/chain/address.go). This is pure, deterministic address derivation - no interaction with the cluster is needed beyond the one public key it already returned. - The wallet flips to
readyand the new address, balance, and status stream to the UI over SSE - no page reload.
A watchKeygenTimeout watchdog (internal/app/mpc_guard.go) fails any wallet still stuck in generating after 90 seconds, and ReconcilePending re-arms or fails these watchdogs on backend restart, so a crash mid-keygen never leaves a wallet silently hanging forever.
Signing, step by step
Sending funds, swapping on the Stellar DEX, and adding a trustline all funnel through the same signing path - only the transaction being built differs.

Two details worth calling out because they show up directly in the UI:
- Signing only needs 2 of 3 nodes, not all 3 -
signQuorum = 2inmpc_guard.go. Keygen needs every node because DKG must distribute a share to each one; signing only needs a threshold-sized quorum to reconstruct a valid signature, which is the entire point of a 2-of-3 scheme rather than requiring unanimous participation for every transaction. - The watchdog timing is deliberately asymmetric: 60 seconds for signing, 90 for keygen, because DKG genuinely takes longer than a signing round. If a transaction is still
signingafter 60 seconds - a node died mid-round, or never responded - the watchdog fails it outright rather than leaving it stuck.ReconcilePendingruns the same logic at startup for anything left in asigningorgeneratingstate by a previous crash: if the cluster meets quorum, it re-arms a fresh watchdog; if not, it fails the record immediately instead of waiting for a timeout that can never fire.
The transaction detail screen makes this concrete rather than abstract: it shows the real on-chain transaction hash from Horizon, the actual 2-of-3 MPC signature in hex, and a signing-progress timeline - policy check, MPC signing, broadcast - so a user isn't asked to trust the "2 of 3" claim, they can see the artifact it produced.

What non-custodial actually means here
It's worth being blunt about the guarantee, because "non-custodial" gets used loosely:
- No single mpcium node ever holds a complete private key, before, during, or after keygen.
- The backend never receives a key share, a private key, or anything that could be combined into one - only public keys (once, after keygen) and signatures (once, after signing).
- Compromising the backend server gives an attacker the ability to request a signature through the normal API - it does not give them anything they can sign with directly, since the backend has no key material to steal.
- Compromising one of the three nodes gives an attacker one share, which is cryptographically useless in isolation. They need a second, independent node to cooperate before a signature can exist.
- The
Settingsscreen exposes this operationally, not just architecturally: live health fornode0/node1/node2and the "Threshold: 2-of-3 - key never assembled on any single node" statement sit right next to each other, because the guarantee should be checkable, not asserted.

Reproducing it
The entire stack - NATS, Consul, three mpcium nodes, the backend, and the UI - comes up with one command, on a fresh clone, with no manual key setup:
git clone https://github.com/fystack/stellar-wallet.git
cd stellar-wallet
./start.sh
On first run, scripts/gen-keys.sh generates node identities, peers.json, and the event-initiator key into a git-ignored mpcium/ directory using the official mpcium-cli image, then injects the initiator public key into the node config automatically. docker-compose.yaml brings up NATS and Consul, waits for both to report healthy, then starts node0-node2, the backend, and the UI. Open http://localhost:8080, register an account, and create a wallet - that dispatches a real 3-node DKG round, not a mock.
./start.sh --fresh # wipe volumes + regenerate keys, clean start
./stop.sh # stop (keeps the wallet DB + key-shares)
./stop.sh --all # stop + wipe everything
This is Deliverable 3's reproduction requirement in practice: no request to trust the architecture description in this post - clone it and watch the same 2-of-3 signature appear in your own transaction detail modal.
Where this stands today
This build is unaudited and targets Stellar testnet only. It uses development-mode auth (a hardcoded JWT secret) and is meant for experimentation, grant review, and as a reference implementation - not for mainnet funds or production deployments. That's stated plainly in the repository README, and it's worth repeating here rather than burying it: the value of this deliverable is a reviewable, runnable architecture, not a production custody product.
What this unlocks for Stellar
The interesting part isn't this one wallet - it's that the pattern underneath it is reusable. mpcium is a general-purpose MPC signing cluster; stellar-wallet is one chain integration on top of it, following the same shape any other integration would: build an unsigned transaction, hash it, hand the hash to the cluster, attach the signature that comes back. Any Stellar wallet, exchange integration, or payment app that currently manages a hot key in an environment variable - or is about to - can adopt the same architecture without building threshold cryptography from scratch.
If you're building wallet infrastructure on Stellar and want non-custodial key management without a multi-month TSS implementation effort, this is exactly the gap mpcium is meant to close.
- Repo: github.com/fystack/stellar-wallet - clone it, read the code, run it on testnet.
- Signing cluster: github.com/fystack/mpcium - the open-source MPC engine underneath, chain-agnostic.
- Questions about integrating this into your own Stellar app? Join the conversation on Telegram.
Frequently asked questions
Is this the same as Stellar's native multisig?
No. Stellar multisig registers multiple public keys as signers on-chain and requires a configured multisig account. MPC threshold signing produces one ordinary Ed25519 signature from a key that was never assembled in one place - the account looks exactly like any other Stellar account.
Does a 2-of-3 MPC wallet need a special Stellar account type?
No. It's a normal G... account. The threshold logic lives entirely in how the key is generated and how transactions are signed off-chain; Horizon sees a standard signed transaction envelope.
What happens if one of the three nodes goes down?
Signing keeps working - it only needs 2 of 3 nodes (signQuorum = 2). Keygen for a new wallet needs all 3, since distributed key generation must give every node a share.
Can the backend server steal funds if it's compromised?
No key material exists on the backend to steal. It can request signatures through the normal API, but every request still requires 2 of 3 independent cluster nodes to cooperate - compromising the backend alone produces nothing signable.
Is this ready for mainnet?
No. It's unaudited, testnet-only, and uses development-mode auth. It's a reference architecture for review and experimentation, not a production custody deployment.
How long does it take to run this myself?
One command - ./start.sh - after cloning. It generates node identities and keys automatically on first run and brings up the full stack, including a real 3-node MPC cluster, in a few minutes.
What's actually stored where?
Key shares live in per-node volumes local to each mpcium node. The wallet database (addresses, transaction history, status) lives in SQLite on the backend. Neither store ever contains a complete private key.
Why EdDSA and not ECDSA?
Stellar accounts use Ed25519 keys natively, so the signing cluster performs threshold EdDSA - the resulting signature verifies exactly like a normal Stellar signature, with no protocol-level accommodation needed on Stellar's side.
Does this work for assets other than XLM?
Yes. Trustlines, custom asset payments, and DEX swaps all go through the same build-hash-sign-broadcast path - the signing step doesn't change based on the asset.
Where do I see proof the signature is actually 2-of-3, not just claimed?
The transaction detail view shows the real Horizon transaction hash alongside the actual MPC signature hex and a step-by-step signing timeline. Cloning the repo and running a transaction yourself is the strongest proof available. Built as part of Fystack's Stellar Community Fund grant. Explore the code at github.com/fystack/stellar-wallet and the underlying signing engine at github.com/fystack/mpcium, or join the discussion on Telegram.
