Stellar Private Payments: Confidential and Compliant Transfers on Public Rails

Intro

The first post in this series mapped the privacy design space for public chains: cryptographic primitives (ZK proofs, FHE, MPC, TEEs), deployment models (application-level privacy, privacy groups, privacy-preserving rollups, validiums), and the use cases each fits. It introduced shielded pools as a pragmatic solution at the application layer, and it concluded that privacy alone is not acceptable for institutions. A privacy protocol that obfuscates every transfer cannot tell an honest user from a sanctioned one, and full anonymity is not a viable operating model for regulated entities. Compliance safeguards, such as access control, disclosure, and asset-level governance, must be part of the protocol itself.

This second post shows what that looks like in practice. We present Stellar Private Payments (SPP), a shielded pool that Nethermind is building for the Stellar network. It combines privacy with compliance safeguards embedded at the protocol level. SPP enables private payments and asset transfers on a public ledger while giving pool administrators, auditors, and regulators the controls they require: KYC-gated access, the ability to freeze non-compliant users, selective disclosure of individual transactions, and (under design) global viewing keys for authorized oversight. SPP is live today: it is deployed on the Stellar testnet, fully open source, and featured on PSE's Private Transfers Dashboard.

This post builds directly on the concepts introduced in the first one and assumes the reader is familiar with shielded pools, UTXOs, notes, commitments, nullifiers, and viewing keys. Rather than surveying the field again, we make a single argument:

Confidentiality and compliance are not opposing requirements. A shielded pool can enforce both, on public rails, today.

We proceed in three steps. First, we show how compliance is embedded in SPP's protocol, organized around the two surfaces an administrator can control: who may move value, and who may see it. Second, we present the architecture that enforces these guarantees on Stellar. Third, we discuss the UX friction and how we engineered the protocol to make it practical in a payment application. Readers interested in the underlying implementation details (circuits, the Soroban smart contract execution model on Stellar, the browser stack) will find them preserved in the appendix.

We built this in collaboration with the Stellar Development Foundation, who commissioned the work and entrusted us with developing SPP.


Compliance Safeguards in a Shielded Pool

In a shielded pool, the public ledger records only cryptographic commitments, nullifiers, and validity proofs. That is precisely what makes it private yet unacceptable to regulated entities. The lesson of Tornado Cash, discussed in the previous post, is that privacy infrastructure without credible compliance mechanisms becomes an enforcement target rather than an adoption path, as its sanctioning by the U.S. Treasury in 2022 demonstrated.

Compliance obligations map onto two distinct control surfaces:

  1. Write controls govern who may move value and under what conditions: who can deposit into the pool, transfer inside it, and withdraw.
  2. Read controls govern who may see value: which parties can inspect transaction details, under what authorization, and at what scope.

SPP implements both surfaces natively in the protocol. Write controls are enforced by the adoption of Association Sets; read controls are provided by selective disclosure and global viewing keys. We examine each in turn, and close the section with a compliance matrix summarizing who can enforce what, and when.

Write Controls: User-Level Association Sets

An Association Set (AS) is a mechanism for proving, in zero knowledge, that funds in a shielded pool come from legitimate sources. Concretely, an AS is a curated set of deposits, maintained by an Association Set Provider (ASP) according to a stated policy. When transacting, a user proves that their funds are associated with the set, without revealing which specific deposit is theirs. The policy comes in different flavors. It can screen deposits "after the fact", attesting that they do not originate from sanctioned or flagged wallets, or it can authorize funds "up front", admitting only deposits made from wallets that passed a pre-deposit screening or a custom policy check.

Whatever the policy, AS enforcement takes one of two complementary forms:

  1. Membership proofs (allow-list): the user proves their funds are associated with an approved set, e.g., deposits that passed policy screening.
  2. Exclusion proofs (block-list): the user proves their funds are not associated with a flagged set, keeping out funds tied to known bad actors or sanctioned entities.

In both cases, the proof reveals nothing beyond the fact that the policy holds. The model is flexible, as a pool administrator can attach as many sets as needed, each managed by a different ASP, or even combine them.

The concept of AS was first formalized in the Privacy Pools paper by Buterin et al., and production systems have already implemented it, notably Railgun's Private Proofs of Innocence and 0xbow's ASP for Privacy Pools. In all of these designs, the association set is defined over deposits or individual notes; a user proves that their funds are legitimate. Policy attaches to the money.

In SPP, we take a different approach for compliance. Association sets are defined over public keys of users. A public key in the context of SPP is the cryptographic identity in the shielded pool and is deterministically derived from a Stellar wallet.

The AS enforcement remains the same: ZK membership and exclusion proofs; the attested object is the user's key, so policy attaches to the identity, not to individual deposits.

The distinction is worth formalizing, because it determines which compliance capabilities the pool can offer:

Dimension Note-level association sets (Railgun, 0xbow) User-level association sets (SPP)
Attested object An individual deposit or note A user’s public key
Policy Funds-bound Identity-bound
Typical statement “This deposit is not linked to illicit sources” “This user satisfies the pool’s policy and is not blocked”
Revocation semantics Individual deposits must be flagged; a user’s other notes are unreachable without enumerating them Listing a single key affects every note the user owns, present and future
Compliance capabilities Proof of innocent fund origin KYC gating, user freeze, jurisdiction-specific policies

What Identity-Level Policy Unlocks: KYC and Freeze

Two capabilities follow directly from placing the association set at the key level, and neither is achievable with note-level sets alone.

  1. KYC-gated access. The AS turns the pool into a KYC-verified participation domain. Only users whose public key has been admitted, after an offchain verification process defined by the ASP, are allowed to transact. The check happens inside the ZK proof, so the ledger learns nothing about which verified identity is transacting.
  2. Freeze. Adding a public key to the block-list of an AS means that user can no longer transact with the pool, so every note they own becomes unspendable. The administrator does not need to know which notes belong to the user. Conversely, a note-level ASP that wanted to freeze a user would have to enumerate and flag each of their notes individually. By attesting identities instead of funds, SPP makes freeze possible at the account level.

These capabilities carry the trade-off every compliance mechanism does. First, the ASP learns who is operating inside the pool. Second, a KYC-gated pool bounds the anonymity set to the eligible population, so if that population is small or publicly known, linking activity becomes easier despite the cryptography. For institutional deployments, a bounded, verified population is generally the intended operating point, but the degree of anonymity is a consequence of how the pool is gated, and should be treated as an explicit design parameter of each deployment, not an afterthought.

Read Controls: Selective Disclosure and Global Viewing Keys

In-pool read controls address a requirement regulated entities face: proving their books when they are legitimately asked to. Audits, investigations, and reporting obligations all require visibility after the fact. SPP addresses this with two read-side mechanisms at different scopes: selective disclosure, available today, and global viewing keys, under design.

Selective Disclosure

Selective disclosure is user-initiated and note-scoped. The owner of a note can disclose private information about it to a specific party: the amount, the commitment, or the spent status.

Disclosures use ZK proofs to bind the revealed information to a context, so a disclosure produced for one verifier cannot be replayed elsewhere, and the user decides exactly how much to share. Nothing else about the user's activity is revealed, and the pool's confidentiality toward everyone else is unaffected.

Under the hood, selective disclosure is a ZK statement of ownership: the user proves control of a note against a specific state root while revealing only the selected fields required for the check. Informally, they are proving the following statement:

"I own a note with commitment C and amount a in a shielded pool X. If spent, it would produce this nullifier n".

More precisely, the user:

  1. Selects the note (or notes) to disclose.
  2. Binds the proof to external context. The disclosure's context, consisting of the requesting authority's name and identity payload, the proof's purpose, and a unique nonce. A different receipt requires a different proof, which is what makes disclosures non-replayable.
  3. Proves in ZK that:
    1. they know a note whose commitment equals C and which is part of the pool's note commitment tree;
    2. they own the note's spending key;
    3. n is the valid nullifier for C.
  4. Off-chain sends the proof, along with its public inputs (merkle root, note commitments and hash binding the external context), to the verifier.

The verifier learns that the user owns note C with amount a in the given pool. By checking n against the pool contract, they can also learn whether the note has been spent. They can inspect the transaction in which C was created, which may reveal the depositor's address if the note came from a fresh deposit, but reveals nothing if it was created by an in-pool transfer. The verifier cannot spend the note.

The SPP interface includes dedicated flows to generate and verify disclosures over multiple notes, for example, for tax reporting or proof-of-funds. The procedure is handled entirely offchain between the user and the requesting authority.

Global Viewing Keys

Global viewing keys sit on the administrator and regulator side, providing pool-scoped, view-only access to the notes in the pool. Selective disclosure is user-initiated and requires cooperation from the note owner. Some regulated deployments need to go beyond this, with standing visibility for a supervisor or auditor, independent of user action, for duties like AML monitoring or supervisory reporting. This is the gap global viewing keys are designed to fill. They enable an auditor to inspect notes in the pool without granting any spending power.

This capability is currently under design in SPP. We are evaluating two flavors, so the visibility scope can match the legal requirements of each deployment's jurisdiction:

  • Individual notes only: the key holder could view individual notes, such as the values behind the note commitments and their owners, but nothing about how notes relate to one another.
  • Notes with traceability: the key holder could additionally see which notes were consumed to create the note under analysis, tracing funds back to the transaction that introduced them into the pool.

The SPP Compliance Matrix

The table below summarizes SPP's compliance safeguards: which surface each one controls, who can invoke it, when it acts, and what it enforces.

Safeguard Control surface Invoked by When it acts Effect
KYC gating Write ASP (pool administrator or accredited provider) Ex ante, before any interaction with the pool Only verified identities can deposit, transfer, or withdraw
Freeze Write ASP (pool administrator) Immediately upon listing a note public key All notes owned by the listed key become unspendable, including pre-existing ones
Selective disclosure Read User (note owner) On demand, e.g., during an audit or dispute Reveals selected note details to a chosen party, cryptographically bound to that context
Global viewing keys* Read Pool administrator / regulator Continuous, view-only oversight Read access to pool notes, without spending power

*currently under design in SPP

These safeguards are designed to be modular. In future versions of SPP, pool administrators will be able to configure which sets, disclosure mechanisms, and viewing permissions apply, based on their requirements, desired privacy levels, and the legal obligations of their jurisdiction.


SPP Architecture

SPP combines a shielded pool with ASP-based policy controls and in-pool private transfers. The protocol is enforced by a ZK circuit, settled by a set of smart contracts, and driven by a client that does all proving locally. This section presents the components; the appendix covers their internals.

From Accounts to Notes

Stellar, like Ethereum, is an account-based network, where the ledger maps addresses to balances. SPP instead holds value as discrete private notes within the pool, emulating the UTXO model introduced in the first post. Converting balances into notes on deposit is what makes private payments possible.

Smart contract functionality on Stellar is provided by Soroban, a Rust-based platform whose contracts compile to WASM. Two properties of Soroban shaped SPP's design: execution is deterministically metered (every instruction counts against a strict resource limit, so onchain proof verification must be heavily optimized), and persistent storage pays state rent (long-lived data such as Merkle roots and nullifier sets must be managed economically). Both pushed us toward a modular contract architecture with a lean, specialized verifier that we describe in the next section. For a detailed Soroban execution model and state management, we refer the reader to the official Stellar documentation.

On-Chain Components

The on-chain protocol splits the workload across specialized contracts:

  1. The Shielded Pool: the core state machine for the pool. It handles user deposits and minting of new output notes, private transfers, and withdrawals. It is responsible for storing the Merkle tree of note commitments and the list of consumed nullifiers in persistent storage to prevent double-spending.
  2. The Verifier: verifying a ZK proof on-chain requires specific elliptic curve operations. This contract takes the proof generated by the user's browser, leverages Soroban's host environment for optimized execution, and verifies the proof against the pool's current state. Verification requires a set of public inputs passed to the contract: the Merkle tree root, the public amount being deposited/withdrawn from the pool, the nullifiers being spent, the new output note commitments being created, and the ASP roots being used. Additionally, it includes a hash to ensure the integrity of on-chain data, such as the amount transacted and the recipient in case of a withdrawal.
  3. Two ASP contracts: to decouple policy enforcement from the core privacy logic, ASPs use separate contracts to manage their policies:
    1. The AS based on membership implements a partial Merkle tree on-chain for users to provide membership proofs.
    2. The AS based on exclusion implements a sparse Merkle tree, allowing users to generate proofs of non-membership. Sparse Merkle trees (SMTs) are a Merkle tree variant indexed over a large key space (e.g., all 256-bit values of a hash output) in which only a small number of leaves are non-empty. SMTs support efficient proofs of non-membership, which makes them well-suited to freeze operations.

By separating the cryptographic verification, the compliance lists, and the core UTXO state, the architecture remains modular. An administrator can attach different ASPs, or upgrade policy contracts, without touching the pool's privacy core.

What the Proof Enforces

In a shielded pool, the smart contract cannot see how much is transferred or to whom, so it cannot validate state changes on its own. All validation is offloaded to a ZK circuit. The circuit is the pool's law; a user generates a proof locally showing their transaction satisfies all constraints, and the Soroban contract rejects the transaction if proof verification fails.

Every SPP transaction — deposit, transfer, or withdrawal — carries a single proof enforcing the following guarantees:

Guarantee What the circuit enforces
Ownership and existence Every input note exists in the pool’s commitment tree and is owned by the user controlling the spending key.
No double-spending Each spent note reveals a unique nullifier, derived from the owner’s key and recorded on-chain; re-spending reproduces the same nullifier and is rejected.
Transaction integrity The external details (recipient, public amounts) are cryptographically bound to the proof, so an intercepted proof cannot be redirected to an attacker’s address.
Value conservation Total inputs plus public deposits equal total outputs plus public withdrawals. No value is minted or burned inside the pool.
Policy compliance The prover’s public key is in the membership set (allow-list) and absent from the exclusion set (block-list).

The Lifecycle of a Compliant Transaction

Compliance is verified at every state transition. Because deposits, transfers, and withdrawals are the same statement instantiated with different public amounts, the policy constraint is part of the only proof the pool ever accepts. This is a meaningful difference from note-level designs, where association is typically proven when funds exit the pool; in SPP, a user who falls out of policy cannot deposit, cannot transfer, and cannot withdraw. A freeze binds from the very next transaction the user attempts, whatever its type.

The sequence below shows the lifecycle every SPP transaction follows.

Note what the ledger records at the end of this flow is a valid proof, fresh nullifiers, new commitments, and a public amount. It never records who transacted, with whom, or which identity the policy was checked against. The compliance guarantees hold without the transaction revealing anything more than a vanilla shielded pool would.

The full circuit is open source, and the appendix walks through each constraint, including the note commitment structure, state inclusion, nullifier derivation, malleability protection, value conservation, and policy enforcement.


Making Private Payments Practical

The first post argued that the real barrier to shielded pools is not the cryptography itself, but the UX. Key management, note discovery, Merkle tree maintenance, and proof generation are all operational tasks clients must handle when using privacy solutions. Nethermind is currently building the components for SPP to address this barrier in practice.

Why Shielded Pools Are Hard to Operate

A shielded pool moves most of the protocol's workflow onto the client. It surfaces three operational problems that any practical deployment must solve.

  • Note discovery. Because balances are not public, a user's application must scan the blockchain, attempt to decrypt every posted output with the user's local keys, and keep the ones that belong to them. This is the note discovery problem, and it is the defining workload of a shielded-pool client, as indexing, trial decryption, and state persistence are all running on the user's side.
  • Local proving. ZK proof generation must happen locally because handing keys or notes to a proving service would defeat the purpose of the pool. But proving is generally a computationally intensive task. From an engineering perspective, this requires designing efficient client-side proving with minimal impact on the user workflow.
  • History availability. The pool contract keeps only a compact representation of the state using persistent storage. The bulk data each client needs to reconstruct its view, most notably the encrypted notes, is emitted as Stellar events. Keeping it on-chain forever would be economically inefficient under Soroban's state-rent model. Events, however, are not ledger state: Stellar RPC nodes retain them for a small window (about seven days). A client that joins the pool later, or loses its local database, cannot rebuild its state from public infrastructure alone.

The first two problems are solved inside the browser; the third requires a piece of infrastructure. We take them in turn.

SPP Client

We made SPP UX accessible via a simple web page. Users just need to open the web application, connect a wallet, and transact privately. There is nothing to install, no node to operate, and no proving service to trust. The proof generation and state management happen locally in the browser, so private keys and notes never leave the device. Behind that simplicity, the client delivers three results:

  • Fast Proving. A private transaction proves in seconds, and the work happens in the background, such that the application is not overloaded and remains fully responsive while the proof is computed. The user experiences a traditional payment flow.
  • Instant balances and history. The client keeps its own private, queryable record of the user's notes with the performance of a native desktop application. Balances, history, and note selection are immediate, even while the client continuously scans the chain for incoming notes in the background.
  • One core, many integration paths. The protocol logic and the operation flows, such as key management, note handling, proving, live in a single exportable core library. For UX purposes, we serve the application as a web app, but the same core can run in a command-line tool or a back-office service. We package the solution so that deployment adapts easily to any infrastructure; a web app for end users, or an embedded component within the systems an institution already operates.

The technical details of how we implemented the core library, the background execution model, and the local database are available in the open-source repository.

The Bootnode: Events History

To solve the history-availability problem, we implemented a bootnode. The bootnode is a lightweight indexing service that archives SPP events from the contract's deployment ledger onward, and re-exposes them through a narrow RPC interface, without the retention limit. A client joining the pool (or recovering from lost local storage) replays history from the bootnode; once it reaches the window the public RPC still covers, the bootnode hands it off to the regular RPC for the most recent ledgers. From the client's perspective, the retention window simply disappears. Please note that the bootnode can also be run self-hosted to minimize trust assumptions.

Two remarkable properties of the bootnode:

  1. Data-availability: The bootnode serves raw, publicly emitted event data, such as the encrypted notes. It cannot forge or inspect the pool state.
  2. Recovery path: Browser-local storage can be lost. The client rebuilds its entire state by replaying history from the bootnode with its own keys, which are deterministically derivable directly from Stellar accounts.


Conclusion

The first post in this series established that privacy is a core requirement for bringing financial services onto public rails, and that privacy without compliance is not an institutional solution. This post presented the system where we put that thesis into practice.

Stellar Private Payments demonstrates that a shielded pool can be governed. By moving association sets from the note level to the identity level, SPP supports compliance at the write controls, KYC-gated access and user freeze. Selective disclosure, and global viewing keys under design, complete the picture on the compliance reads, giving users, auditors, and regulators calibrated visibility instead of the all-or-nothing choice between transparency and anonymity. Finally, the client and infrastructure engineering show that the operational barrier identified in the first post can be engineered away via an improved UX.

SPP is live on the Stellar testnet, fully open source, and featured on PSE's Private Transfers Dashboard. Nethermind is working with Stellar to actively maintain and improve the protocol. Next steps on the protocol roadmap include a larger decentralized trusted-setup ceremony ahead of a mainnet deployment, the public bootnode rollout, and the full UI integration of selective disclosure, and the design of global viewing keys.

The broader point stands beyond this deployment: confidentiality and compliance are jointly practical on public ledgers. Stellar is the first example from our privacy work; more will follow. The next post in the series stays at the application layer and presents a shielded pool design that remains backward compatible with existing regulated-token standards such as CMTA and ERC-3643, bringing the same guarantees to assets institutions already issue.

Author
Antonio Larriba
Cryptography Research Engineer at Nethermind, specializing in zero-knowledge technology, privacy-preserving systems, and advanced cryptography.