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.

38 changes: 38 additions & 0 deletions frame/evm/precompile/shielded-pool/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Changelog

All notable changes to `pallet-evm-precompile-shielded-pool` will be documented in this file.

## [0.2.0] - 2026-05-08

### Changed
- **ABI: unshield function signature** - Added `change_encrypted_memo: bytes` parameter (9th parameter) to support stealth address change notes
- Old signature: `unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32,uint256,bytes32)`
- New signature: `unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32,uint256,bytes32,bytes)`
- Selector updated: `0xd21d9a79` → `0xcc1a3b38`
- Head layout: 256 bytes → 288 bytes (9 slots × 32 bytes)
- Dynamic offsets for proof and memo now calculated with proper tail positioning

- **Memo size validation** - All ABI helpers updated to support 176-byte encrypted memos (previously 168 bytes)
- Shield: `encode_shield()` now uses &[0xAB; 176]
- Private transfer: `encode_private_transfer()` memo vectors updated to 176 bytes
- Unshield: change_encrypted_memo supports dynamic encoding of 176-byte encrypted memos

- **Test suite** - All 42 tests passing with updated ABI signatures
- Fixed 8 `encode_unshield()` call sites to include `&[]` change_encrypted_memo parameter
- Updated 7 `encode_private_transfer()` call sites with 176-byte memo sizes
- Updated `do_shield()` helper with 176-byte memo size
- All test functions now compile and pass without errors

### Technical Details
- Selector calculation: `keccak256("unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32,uint256,bytes32,bytes)")[0:4]`
- Dynamic offset calculations properly handle variable-length `bytes` proof and memo parameters
- Compatible with stealth address implementation in parent pallet
- Backward-incompatible change: Previous versions of the precompile cannot decode new ABI signatures

## [0.1.0] - 2026-04-15

### Initial Release
- Basic EVM precompile for shielded pool operations (shield, private_transfer, unshield)
- Support for Groth16 proof verification via `pallet-zk-verifier`
- 168-byte encrypted memo support
- Full test suite with comprehensive ABI validation
2 changes: 1 addition & 1 deletion frame/evm/precompile/shielded-pool/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "pallet-evm-precompile-shielded-pool"
version = "0.1.0"
version = "0.2.0"
authors = { workspace = true }
edition = "2021"
description = "EVM Precompile for Orbinum Shielded Pool Pallet."
Expand Down
37 changes: 32 additions & 5 deletions frame/evm/precompile/shielded-pool/src/calls/unshield.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
//! ABI decoding and call construction for
//! `unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32,uint256,bytes32)`.
//! `unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32,uint256,bytes32,bytes)`.
//!
//! ## Selector
//! `keccak256("unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32,uint256,bytes32)")[0..4]`
//! = `0xd21d9a79`
//! `keccak256("unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32,uint256,bytes32,bytes)")[0..4]`
//! = `0xcc1a3b38`
//!
//! ## ABI layout (`input[4..]`)
//! | Slot (bytes) | Type | Field |
Expand All @@ -16,6 +16,7 @@
//! | 160..192 | `bytes32` | `recipient` (AccountId32) |
//! | 192..224 | `uint256` | `fee` |
//! | 224..256 | `bytes32` | `change_commitment` |
//! | 256..288 | `uint256` | offset → `change_encrypted_memo` |
//!
//! `recipient` is an `AccountId32` encoded as a 32-byte ABI `bytes32` slot.
//! This can be a Substrate-native account or the `AccountId32` derived from
Expand All @@ -24,16 +25,21 @@
//! `change_commitment` is `[0u8; 32]` for a total unshield (no change note).
//! For a partial unshield it is `NoteCommitment(change_value, asset_id, change_owner_pk, change_blinding)`.
//!
//! `change_encrypted_memo` is a dynamic `bytes` field (176 bytes for partial unshield, 0 bytes for total).
//! For a partial unshield it contains: nonce(12) || ciphertext(132) || ephPk(32).
//!
//! `relayer` is derived from `handle.context().caller` — not part of the ABI.

use alloc::vec::Vec;

use fp_evm::{ExitError, PrecompileFailure, PrecompileHandle};
use frame_support::BoundedVec;
use sp_core::U256;

use crate::abi;

/// `keccak256("unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32,uint256,bytes32)")[0..4]`
pub const SELECTOR: [u8; 4] = [0xd2, 0x1d, 0x9a, 0x79];
/// `keccak256("unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32,uint256,bytes32,bytes)")[0..4]`
pub const SELECTOR: [u8; 4] = [0xcc, 0x1a, 0x3b, 0x38];

/// Maximum byte length of a serialised Groth16 proof accepted by the pallet.
const MAX_PROOF_LEN: u32 = 512;
Expand Down Expand Up @@ -102,6 +108,26 @@ where

let change_commitment: pallet_shielded_pool::Hash = abi::read_bytes32(params, 224)?;

// Decode change_encrypted_memo as a dynamic bytes field.
// Offset pointer lives at slot 256 (params[256..288]).
let change_encrypted_memo_bytes = if params.len() >= 288 {
abi::decode_bytes_at_slot(params, 256).unwrap_or_default()
} else {
Vec::new()
};

// Convert to EncryptedMemo (max 176 bytes per pallet definition).
// Empty bytes (total unshield) is allowed and results in an empty EncryptedMemo.
let change_encrypted_memo: pallet_shielded_pool::types::EncryptedMemo =
if change_encrypted_memo_bytes.is_empty() {
// Total unshield: empty memo
pallet_shielded_pool::types::EncryptedMemo::default()
} else {
// Partial unshield: create from bytes
pallet_shielded_pool::types::EncryptedMemo::new(change_encrypted_memo_bytes)
.map_err(|_| err("unshield: invalid change_encrypted_memo"))?
};

let relayer = Some(handle.context().caller);

Ok(pallet_shielded_pool::Call::<T>::unshield {
Expand All @@ -113,6 +139,7 @@ where
recipient,
fee,
change_commitment,
change_encrypted_memo,
relayer,
})
}
Expand Down
43 changes: 28 additions & 15 deletions frame/evm/precompile/shielded-pool/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ fn encode_private_transfer(
input
}

/// `unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32,uint256,bytes32)` selector `0xd21d9a79`
/// `unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32,uint256,bytes32,bytes)` selector `0xcc1a3b38`
#[allow(clippy::too_many_arguments)]
fn encode_unshield(
proof: &[u8],
Expand All @@ -155,27 +155,33 @@ fn encode_unshield(
recipient: [u8; 32],
fee: u128,
change_commitment: [u8; 32],
change_encrypted_memo: &[u8],
) -> Vec<u8> {
// head: 8 slots × 32 = 256 bytes; proof tail appended after
let mut input = vec![0xd2, 0x1d, 0x9a, 0x79];
let mut head = vec![0u8; 256];
head[0..32].copy_from_slice(&u256_word(256));
// head: 9 slots × 32 = 288 bytes; tails (proof, memo) appended after
let mut input = vec![0xcc, 0x1a, 0x3b, 0x38];
let mut head = vec![0u8; 288];
let proof_offset = 288usize;
let memo_offset = proof_offset + encode_bytes(proof).len();

head[0..32].copy_from_slice(&u256_word(proof_offset));
head[32..64].copy_from_slice(&merkle_root);
head[64..96].copy_from_slice(&nullifier);
head[124..128].copy_from_slice(&asset_id.to_be_bytes());
head[128..160].copy_from_slice(&u256_word_u128(amount));
head[160..192].copy_from_slice(&recipient);
head[192..224].copy_from_slice(&u256_word_u128(fee));
head[224..256].copy_from_slice(&change_commitment);
head[256..288].copy_from_slice(&u256_word(memo_offset));
input.extend_from_slice(&head);
input.extend_from_slice(&encode_bytes(proof));
input.extend_from_slice(&encode_bytes(change_encrypted_memo));
input
}

// ─── Convenience: shield then return the current Merkle root ─────────────────

fn do_shield(commitment: [u8; 32], value: u128) {
let input = encode_shield(0, commitment, &[0xAB; 168]);
let input = encode_shield(0, commitment, &[0xAB; 176]);
let mut h = MockHandle::with_value(input, value);
assert_success(ShieldedPoolPrecompile::<Test>::execute(&mut h));
}
Expand Down Expand Up @@ -377,7 +383,7 @@ fn shield_rejects_below_min_amount() {
fn shield_stores_commitment_and_updates_balance() {
new_test_ext().execute_with(|| {
let commitment = [0x11; 32];
let input = encode_shield(0, commitment, &[0xAB; 168]);
let input = encode_shield(0, commitment, &[0xAB; 176]);
let mut h = MockHandle::with_value(input, 1_000);
assert_success(ShieldedPoolPrecompile::<Test>::execute(&mut h));

Expand Down Expand Up @@ -450,7 +456,7 @@ fn private_transfer_rejects_empty_proof() {
root,
&[[0x11; 32], [0x22; 32]],
&[[0x33; 32], [0x44; 32]],
&[vec![0xAA; 168], vec![0xBB; 168]],
&[vec![0xAA; 176], vec![0xBB; 176]],
0,
0,
);
Expand Down Expand Up @@ -491,7 +497,7 @@ fn private_transfer_rejects_mismatched_nullifier_commitment_count() {
root,
&[[0x11; 32], [0x22; 32]], // 2 nullifiers
&[[0x33; 32]], // 1 commitment
&[vec![0xAA; 168]], // 1 memo
&[vec![0xAA; 176]], // 1 memo
0,
0,
);
Expand All @@ -511,7 +517,7 @@ fn private_transfer_rejects_mismatched_commitment_memo_count() {
root,
&[[0x11; 32], [0x22; 32]], // 2 nullifiers
&[[0x33; 32], [0x44; 32]], // 2 commitments
&[vec![0xAA; 168]], // 1 memo — mismatch
&[vec![0xAA; 176]], // 1 memo — mismatch
0,
0,
);
Expand All @@ -535,7 +541,7 @@ fn private_transfer_happy_path() {
root,
&[nullifier_1, nullifier_2],
&[commitment_1, commitment_2],
&[vec![0xAA; 168], vec![0xBB; 168]],
&[vec![0xAA; 176], vec![0xBB; 176]],
0,
0,
);
Expand Down Expand Up @@ -577,7 +583,7 @@ fn private_transfer_rejects_double_spend() {
root,
&[nullifier, [0x02; 32]],
&[[0x03; 32], [0x04; 32]],
&[vec![0xAA; 168], vec![0xBB; 168]],
&[vec![0xAA; 176], vec![0xBB; 176]],
0,
0,
);
Expand All @@ -601,7 +607,7 @@ fn private_transfer_root_updates_after_outputs() {
root_before,
&[[0x11; 32], [0x22; 32]],
&[[0x33; 32], [0x44; 32]],
&[vec![0xAA; 168], vec![0xBB; 168]],
&[vec![0xAA; 176], vec![0xBB; 176]],
0,
0,
);
Expand All @@ -623,7 +629,7 @@ fn private_transfer_root_updates_after_outputs() {
#[test]
fn unshield_rejects_truncated_input() {
new_test_ext().execute_with(|| {
let mut h = MockHandle::new(vec![0xd2, 0x1d, 0x9a, 0x79]);
let mut h = MockHandle::new(vec![0xcc, 0x1a, 0x3b, 0x38]);
expect_error(ShieldedPoolPrecompile::<Test>::execute(&mut h));
});
}
Expand All @@ -642,6 +648,7 @@ fn unshield_rejects_empty_proof() {
recipient_bytes(),
0,
[0u8; 32],
&[],
);
let mut h = MockHandle::new(input);
expect_error(ShieldedPoolPrecompile::<Test>::execute(&mut h));
Expand All @@ -664,6 +671,7 @@ fn unshield_happy_path() {
recipient_bytes(),
0,
[0u8; 32],
&[],
);
let mut h = MockHandle::new(input);
assert_success(ShieldedPoolPrecompile::<Test>::execute(&mut h));
Expand Down Expand Up @@ -697,6 +705,7 @@ fn unshield_rejects_double_spend() {
recipient_bytes(),
0,
[0u8; 32],
&[],
);
let mut h = MockHandle::new(input.clone());
assert_success(ShieldedPoolPrecompile::<Test>::execute(&mut h));
Expand All @@ -720,6 +729,7 @@ fn unshield_full_balance() {
recipient_bytes(),
0,
[0u8; 32],
&[],
);
let mut h = MockHandle::new(input);
assert_success(ShieldedPoolPrecompile::<Test>::execute(&mut h));
Expand All @@ -743,6 +753,7 @@ fn unshield_rejects_zero_recipient() {
[0u8; 32], // zero AccountId32
0,
[0u8; 32],
&[],
);
let mut h = MockHandle::new(input);
expect_error(ShieldedPoolPrecompile::<Test>::execute(&mut h));
Expand All @@ -765,6 +776,7 @@ fn unshield_rejects_zero_amount() {
recipient_bytes(),
0,
[0u8; 32],
&[],
);
let mut h = MockHandle::new(input);
expect_error(ShieldedPoolPrecompile::<Test>::execute(&mut h));
Expand Down Expand Up @@ -794,7 +806,7 @@ fn full_lifecycle_shield_transfer_unshield() {
root_1,
&[nullifier_in, [0x00; 32]],
&[commitment_out_1, commitment_out_2],
&[vec![0xAA; 168], vec![0xBB; 168]],
&[vec![0xAA; 176], vec![0xBB; 176]],
0,
0,
);
Expand All @@ -814,6 +826,7 @@ fn full_lifecycle_shield_transfer_unshield() {
recipient_bytes(),
0,
[0u8; 32],
&[],
);
let mut h_us = MockHandle::new(unshield_input);
assert_success(ShieldedPoolPrecompile::<Test>::execute(&mut h_us));
Expand Down
Loading
Loading