Distribution and jurisdiction
Splitting a key 3-of-4 only means something if the four share-holders can genuinely fail, or be compelled, independently. Four shares on four VMs in one account, in one datacenter, under one company's control is a 3-of-4 split with a 1-of-1 failure mode. This page is about that gap: what the topology is designed to be, why it is shaped the way it is, and — plainly — what is actually deployed right now.
What the quorum is supposed to buy
The 3-of-4 threshold is doing two different jobs at once, and they pull in opposite directions.
Availability. Any one node can be lost — hardware failure, a provider outage, a region going dark — and the remaining three still form a quorum. Settlement continues. This is why the threshold is 3 and not 4.
Resistance to unilateral control. No single party can sign. A court order, a subpoena, a rogue administrator or a breach at any one of the four yields exactly one Shamir share, which is information-theoretically nothing. To actually move funds you would need three of the four to be compromised or compelled at the same time. This is why the threshold is 3 and not 1 or 2.
The design goal is that the three you would need are not reachable by one action, one company, or one legal system.
The designed topology
Four chain nodes, deliberately one per cloud provider:
| Node | Provider | Why this one |
|---|---|---|
| 1 | GCP | Also hosts the signing layer — the only provider in the set with SEV-SNP hardware matching what the code attests against |
| 2 | Exoscale | Swiss-operated. Puts one node outside the three large hyperscalers, commercially and legally |
| 3 | Hetzner | German, EU. Cost and simplicity; a different corporate entity and a different legal system again |
| 4 | AWS | Session Manager removes port 22 from the internet entirely, so two of the four nodes need no inbound SSH at all |
The runtime is Podman with rootless Quadlets and systemd supervision — no Docker Compose, no Kubernetes, no orchestration layer with cluster-wide credentials that would quietly re-centralise what the topology is trying to separate.
The jurisdictional argument, stated carefully
The Exoscale node is in the set specifically because Switzerland is not an EU member. An Exoscale node and a Hetzner node sit under two genuinely separate sovereign legal systems with separate courts: a compulsory order against one does not reach the other. That separation is worth more than the geographic spread it is often confused with — two datacenters 1,000km apart under the same legal system are, for this purpose, one share.
And now the part that matters more for an honest assessment: GCP and AWS are both US companies, so the CLOUD Act reaches their data regardless of which datacenter it physically sits in. That is the largest cross-border legal-access exposure in this set, and it means the four providers do not represent four fully independent legal systems. They represent roughly three, with two of the four correlated.
Three of four is the signing threshold. If a single US legal instrument can reach two nodes, the margin is one share, not two. That is a real limitation of the current provider selection rather than a hypothetical one, it is understood as such, and adding jurisdictional diversity is a known direction rather than a solved problem. Anyone evaluating this rail for funds that would attract that kind of attention should weigh it directly rather than reading "four providers" as "four jurisdictions".
The signing layer is the narrowest constraint
The confidential VM cannot be distributed the way the nodes can. It requires AMD SEV-SNP, Milan generation, and confidential compute must be enabled at instance creation. Of the four chain-node providers, only GCP offers hardware matching what the attestation code checks; Exoscale, Hetzner and AWS cannot host it at all. Azure's DCasv5 family is the only identified alternative home.
The two-party MPC-TSS method therefore has a specific, acknowledged weakness: the two parties whose independence the protocol depends on are, at present, both constrained to the same provider. The protocol is genuinely two-party — two separate processes with independent state, which is what makes the cryptography meaningful — but "two separate operators in two separate jurisdictions" is not something the available hardware currently permits. Splitting the two parties across GCP and Azure is the obvious step and is not taken yet.
Pre-generated wallet pools
The reason a wallet is available the instant you ask for one — despite generation requiring a quorum of nodes and, for MPC-TSS, a multi-round interactive protocol — is that keys are generated ahead of demand.
Each gateway maintains pools per network and per key method, topping them up in
the background when a pool falls below its threshold, and tracking them in a
local SQLite table kept in sync from the ledger's own wallet-assigned events.
The standard and mpc-tss pools are separate: MPC-TSS keys cost considerably
more to produce, so that pool is smaller and refills in smaller batches.
Two consequences worth understanding:
- Claiming a wallet id is an unauthenticated read. It hands out which id is
currently free. It does not decide who owns one — ownership is established by
a transaction signed with your own identity key, and
Varnir.AssignOwner's consensus vote is what actually rejects a double-assignment. The pool table is a capacity-planning cache, not the source of truth. - A pre-generated key was already split before anyone claimed it. It has no privileged period during which someone held it whole and waiting; the shares were distributed at generation, long before the wallet had an owner.
You can see the unassigned pool for a gateway at scanner.varnir.site/free-wallets.
What is actually running today
Everything above describes the design in infra/chain-node/ and
infra/providers/. It is not what you are connecting to. The public testnet
runs from infra/single-dev/, and the differences are not cosmetic:
| Designed | Running today | |
|---|---|---|
| Chain nodes | 4 VMs, 4 providers | All 4 on a single cloud VM |
| Jurisdictions | CH / DE / US ×2 | One |
| Node hardening | Firewalled, rootless Quadlets, storage port loopback-only | Deliberately permissive; 80/443 restricted to Cloudflare ranges, storage port loopback-only, little else |
| Confidential VM | AMD SEV-SNP, attested at boot, SSH disabled | MOCKCVM — no SEV-SNP hardware, attestation bypassed, ACCESS=1 |
| MPC-TSS parties | Two separate hosts | Two separate containers, same VM |
| TLS between MPC hosts | Verified | NODE_TLS_REJECT_UNAUTHORIZED=0 |
| Assets | — | Testnet only. Sepolia and Nile. No value |
infra/single-dev/README.md states its own position without hedging: keys
living on that host should be treated as already compromised. It is there so
that something real and reachable exists to point a wallet, a scanner and an
SDK integration at, not because it is a testnet in the production sense. It may
graduate into one once the hardening lands; it has not.
So, precisely:
- The cryptography is real and running. Shamir 3-of-4, the two-party threshold ECDSA, the quorum gating on generation and signing, the payload matching, the share hash verification — all of it executes on every wallet the network creates, and you can watch it happen.
- The distribution is not yet real. The shares are held by four node processes that today share a machine, an operator and a jurisdiction. The code that separates them exists; the deployment that separates them does not.
Nothing on this rail handles real funds, and the reason is this row of the table rather than a product decision.
And now the easy part
That is the whole custody model, at the level of detail someone signing off on a payment rail should demand of it. It is worth ending on what it costs you as an integrator, because the answer is: nothing. None of it is your problem to operate.
You do not run a node, hold a share, join a quorum, deploy a confidential VM, or implement any of the threshold cryptography above. You generate a keypair, post two signed transactions, and you have a wallet that can receive money.
import {generateRecoveryPhrase, derivePrivateKeyFromPhrase} from '@varnir/signing';
const phrase = generateRecoveryPhrase(); // 12 words, yours alone
const keyPair = derivePrivateKeyFromPhrase(phrase);
Onboard that identity, claim a wallet, sign the assignment — three steps, all of them signed locally, none of them sending a private key anywhere. The L1 key behind the address you get was generated inside the confidential VM, split before it finished existing, and distributed across the quorum before you asked for it. You never touch it, and neither does anyone else.
Getting started is the complete runnable flow, and it is about forty lines end to end.