Shielded Pool
The Shielded Pool is a UTXO-based privacy mechanism that enables confidential transactions on Orbinum through Zero-Knowledge proofs.
Table of Contents
Overview
The shielded pool maintains a Merkle tree of note commitments. Each commitment represents a private UTXO (Unspent Transaction Output) that can only be spent by its owner using a valid ZK proof.
Key Properties
| Property | Description |
|---|---|
| Confidentiality | Transaction amounts hidden via commitments |
| Anonymity | Sender-receiver relationship unlinkable |
| Double-spend prevention | Nullifiers prevent note reuse |
| Verifiability | All operations verified via ZK proofs |
UTXO Model
Note Structure
A note represents a private balance:
pub struct Note {
/// Value in native tokens
pub value: u128,
/// Owner's public key
pub pk_d: [u8; 32],
/// Random blinding factor
pub blinding: [u8; 32],
}
Commitment
Notes are stored as commitments in the Merkle tree:
commitment = Poseidon(value, pk_d, blinding)
Nullifier
When spending a note, the owner reveals a nullifier:
nullifier = Poseidon(commitment, sk)
- Same commitment always produces same nullifier
- Cannot derive commitment from nullifier
- Prevents double-spending
Operations
Shield
Convert public balance to shielded balance:
Parameters:
value: Amount to shieldcommitment: Poseidon(value, pk_d, blinding)
Events:
Shielded {
sender: AccountId,
commitment: [u8; 32],
value: u128,
}
Private Transfer
Transfer between shielded notes:
Public Inputs:
nullifier_a: Nullifier for input note Anullifier_b: Nullifier for input note Bcommitment_c: New output commitment Ccommitment_d: New output commitment Dmerkle_root: Root when spending
Constraints (verified in ZK):
- Input notes exist in Merkle tree
- Nullifiers correctly computed
- Output commitments correctly formed
value_a + value_b = value_c + value_d(value conservation)
Unshield
Convert shielded balance back to public:
Parameters:
proof: ZK proof of ownershipnullifier: Spent note nullifiervalue: Amount to unshieldrecipient: Public account to receive
Events:
Unshielded {
recipient: AccountId,
nullifier: [u8; 32],
value: u128,
}
Storage Schema
On-Chain Storage
#[pallet::storage]
pub type MerkleRoot<T> = StorageValue<_, [u8; 32], ValueQuery>;
#[pallet::storage]
pub type MerkleLeaves<T> = StorageValue<_, BoundedVec<[u8; 32], ConstU32<1048576>>, ValueQuery>;
#[pallet::storage]
pub type NullifierSet<T> = StorageMap<_, Blake2_128Concat, [u8; 32], bool, ValueQuery>;
#[pallet::storage]
pub type PoolBalance<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>;
#[pallet::storage]
pub type HistoricRoots<T> = StorageMap<_, Blake2_128Concat, [u8; 32], (), OptionQuery>;
Storage Items
| Item | Type | Purpose |
|---|---|---|
MerkleRoot | [u8; 32] | Current tree root |
MerkleLeaves | BoundedVec | All commitments (max 2²⁰) |
NullifierSet | Map | Spent nullifiers |
PoolBalance | Balance | Total shielded value |
HistoricRoots | Map | Valid past roots |
Historic Roots
The pool maintains historic roots to handle concurrent transactions using a FIFO (First-In-First-Out) pruning strategy:
Key properties:
- Proofs built against older roots remain valid as long as those roots exist in
HistoricRoots - FIFO pruning: When the storage limit is reached, the oldest root is removed first
- Configurable retention limit via
MaxHistoricRootsparameter - Ensures recent transactions can reference recent Merkle states
Configuration
Pallet Config
#[pallet::config]
pub trait Config: frame_system::Config {
/// Event type
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
/// Currency for balances
type Currency: Currency<Self::AccountId>;
/// Maximum Merkle tree depth
#[pallet::constant]
type MaxTreeDepth: Get<u32>;
/// Historic roots to keep
#[pallet::constant]
type HistoricRootsToKeep: Get<u32>;
}
Parameters
| Parameter | Value | Description |
|---|---|---|
MaxTreeDepth | 20 | Max 2²⁰ ≈ 1M commitments |
HistoricRootsToKeep | 100 | Roots for concurrent proofs |
Security Considerations
The shielded pool has not undergone a formal security audit. Use at your own risk.
Assumptions
- Poseidon hash function is collision-resistant
- Groth16 proofs are zero-knowledge and sound
- Trusted setup ceremony performed correctly
- Users securely store their spending keys
Known Limitations
| Limitation | Description | Mitigation |
|---|---|---|
| Metadata leakage | Transaction timing visible | Delayed transactions |
| Pool size visible | Total shielded amount public | Expected behavior |
| Proof size | ~192 bytes per proof | Acceptable overhead |
Related Documentation
- ZK Proofs - Proof generation and verification
- System Overview - Full architecture