Skip to main content

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

PropertyDescription
ConfidentialityTransaction amounts hidden via commitments
AnonymitySender-receiver relationship unlinkable
Double-spend preventionNullifiers prevent note reuse
VerifiabilityAll 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 shield
  • commitment: 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 A
  • nullifier_b: Nullifier for input note B
  • commitment_c: New output commitment C
  • commitment_d: New output commitment D
  • merkle_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 ownership
  • nullifier: Spent note nullifier
  • value: Amount to unshield
  • recipient: 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

ItemTypePurpose
MerkleRoot[u8; 32]Current tree root
MerkleLeavesBoundedVecAll commitments (max 2²⁰)
NullifierSetMapSpent nullifiers
PoolBalanceBalanceTotal shielded value
HistoricRootsMapValid 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 MaxHistoricRoots parameter
  • 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

ParameterValueDescription
MaxTreeDepth20Max 2²⁰ ≈ 1M commitments
HistoricRootsToKeep100Roots for concurrent proofs

Security Considerations

WIP - Security Audit Pending

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

LimitationDescriptionMitigation
Metadata leakageTransaction timing visibleDelayed transactions
Pool size visibleTotal shielded amount publicExpected behavior
Proof size~192 bytes per proofAcceptable overhead