Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions deny.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ ignore = [
# Unmaintained (transitive)
"RUSTSEC-2025-0161", # libsecp256k1 0.7.2 unmaintained (from fc-rpc, fp-account/pallet-evm)
"RUSTSEC-2026-0105", # core2 0.4.0 unmaintained, all versions yanked (from litep2p → cid, no upgrade path)
"RUSTSEC-2026-0119", # hickory-proto 0.24.4 - O(n²) CPU exhaustion in DNS name compression (from libp2p-dns → sc-network)
"RUSTSEC-2026-0118", # hickory-proto 0.25.2 - unbounded loop in NSEC3 validation (from libp2p-dns → sc-network)

# Allowed warnings (8 total)
"RUSTSEC-2024-0388", # derivative 2.2.0 unmaintained (from ark-r1cs-std)
Expand Down
2 changes: 1 addition & 1 deletion frame/shielded-pool/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pallet-shielded-pool"
version = "0.5.2"
version = "0.6.0"
description = "Shielded pool pallet for private transactions using ZK proofs"
authors = ["Orbinum Team"]
license = "GPL-3.0-or-later"
Expand Down
10 changes: 6 additions & 4 deletions frame/shielded-pool/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ FRAME pallet for privacy-preserving transactions in Orbinum using ZK-SNARKs.

## Status

MVP in active development. Core shield / transfer / unshield flows are functional. Audit and disclosure features are present but under active review.
MVP in active development. Core shield / transfer / unshield flows are functional, including partial unshield with automatic change note handling. Audit and disclosure features are present but under active review.

## What this pallet does

Implements a UTXO-style shielded pool where:

- Public tokens enter via `shield` — converted to on-chain commitments.
- Value moves privately via `private_transfer` — only nullifiers and new commitments appear on-chain.
- Tokens exit via `unshield` — revealed when the user chooses.
- Tokens exit via `unshield` — revealed when the user chooses. Supports partial withdrawal: if `change_commitment != [0u8;32]`, the remaining value is wrapped into a new note re-inserted into the Merkle tree.

A Poseidon Merkle tree tracks all commitments. A nullifier set prevents double-spending. All state transitions require a valid Groth16 proof verified by `pallet-zk-verifier`.

Expand All @@ -22,8 +22,8 @@ A Poseidon Merkle tree tracks all commitments. A nullifier set prevents double-s
|-----------|--------|-------------|
| `shield` | Signed | Deposit tokens; insert one commitment into the Merkle tree |
| `shield_batch` | Signed | Deposit and insert multiple commitments in one call |
| `private_transfer` | Signed | ZK-proven private transfer between notes |
| `unshield` | Signed | ZK-proven withdrawal to a public account |
| `private_transfer` | Unsigned | ZK-proven private transfer between notes |
| `unshield` | Unsigned | ZK-proven withdrawal to a public account. Accepts a `change_commitment` for partial unshield |
| `disclose` | Signed | Selective disclosure of a note to an auditor |
| `register_asset` | Signed | Register a new asset for multi-asset support |

Expand Down Expand Up @@ -72,6 +72,8 @@ The previous Clean Architecture layers (`domain/`, `application/`, `infrastructu
- Double-spend prevention: nullifiers are recorded on first use and rejected thereafter.
- Merkle root validation: only the current root and historic roots within `MaxHistoricRoots` are accepted.
- ZK proof verification: all state-changing extrinsics require a Groth16 proof validated by `pallet-zk-verifier`.
- Recipient encoding: the `recipient` field is passed as-is (LE field element) to the verifier — consistent with `Bn254Fr::from_le_bytes_mod_order` and the TypeScript SDK convention (`bytesToBigintLE`).
- Change commitment uniqueness: the pallet rejects a `change_commitment` that already exists in the Merkle tree before inserting it.

These are design properties of the current MVP. No formal security audit has been performed.

Expand Down
5 changes: 3 additions & 2 deletions frame/shielded-pool/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ mod benchmarks {
let (caller, asset_id) = setup_benchmark_env::<T>();
let amount: BalanceOf<T> = T::MinShieldAmount::get() * 10u32.into();
let commitment = Commitment([1u8; 32]);
// Memo must be exactly 136 bytes (MAX_ENCRYPTED_MEMO_SIZE): nonce(12) + data(108) + MAC(16)
// Memo must be exactly 168 bytes (MAX_ENCRYPTED_MEMO_SIZE): nonce(12) + data(108) + MAC(16) + ephPk(32)
let memo_bytes = vec![0u8; MAX_ENCRYPTED_MEMO_SIZE as usize];
let encrypted_memo = FrameEncryptedMemo(memo_bytes.try_into().unwrap());

Expand Down Expand Up @@ -163,7 +163,8 @@ mod benchmarks {
amount,
recipient,
fee,
None,
Hash::default(), // change_commitment: [0u8; 32] for total unshield
None, // relayer
);
}

Expand Down
6 changes: 6 additions & 0 deletions frame/shielded-pool/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,8 @@ pub mod pallet {
amount: BalanceOf<T>,
/// Recipient account
recipient: T::AccountId,
/// Change note commitment inserted into the Merkle tree (None for total unshield)
change_commitment: Option<Hash>,
},

/// Merkle root was updated
Expand Down Expand Up @@ -818,6 +820,9 @@ pub mod pallet {
amount: BalanceOf<T>,
recipient: T::AccountId,
fee: BalanceOf<T>,
// Commitment of the change note. Must be [0u8; 32] for total unshield.
// For partial unshield, must equal NoteCommitment(change_value, asset_id, change_owner_pk, change_blinding).
change_commitment: Hash,
// EVM address of the relay node that signed the tx (from precompile caller); None for direct Substrate.
relayer: Option<sp_core::H160>,
) -> DispatchResult {
Expand All @@ -832,6 +837,7 @@ pub mod pallet {
amount,
recipient,
fee,
change_commitment,
relayer,
)
}
Expand Down
1 change: 1 addition & 0 deletions frame/shielded-pool/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ impl ZkVerifierPort for MockZkVerifier {
_recipient: &[u8; 32],
_asset_id: u32,
_fee: u128,
_change_commitment: &[u8; 32],
_version: Option<u32>,
) -> Result<bool, sp_runtime::DispatchError> {
// Validate basic format
Expand Down
2 changes: 1 addition & 1 deletion frame/shielded-pool/src/operations/private_transfer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ mod tests {
}

fn short_memo() -> EncryptedMemo {
// 32 bytes — too short (not 104)
// 32 bytes — too short (not 168)
EncryptedMemo::new(vec![0x01u8; 32]).unwrap()
}

Expand Down
Loading
Loading