diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..d356dc22dc --- /dev/null +++ b/.dockerignore @@ -0,0 +1,23 @@ +# Git +.git +.gitignore + +# Rust build artifacts +target/ +**/*.rs.bk + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log + +# Docker +Dockerfile* +docker-compose*.yml +.dockerignore \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 23cd93cfa0..8999a37e9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2651,7 +2651,7 @@ dependencies = [ [[package]] name = "ethereum" version = "0.15.0" -source = "git+https://github.com/rust-ethereum/ethereum?rev=bbb544622208ef6e9890a2dbc224248f6dd13318#bbb544622208ef6e9890a2dbc224248f6dd13318" +source = "git+https://github.com/rust-ethereum/ethereum.git?rev=bbb544622208ef6e9890a2dbc224248f6dd13318#bbb544622208ef6e9890a2dbc224248f6dd13318" dependencies = [ "bytes", "ethereum-types", @@ -2711,7 +2711,6 @@ dependencies = [ [[package]] name = "evm" version = "0.42.0" -source = "git+https://github.com/rust-ethereum/evm?branch=v0.x#6ca5a72bc8942f4860137155dd9033526fd362a5" dependencies = [ "auto_impl", "environmental", @@ -2731,7 +2730,6 @@ dependencies = [ [[package]] name = "evm-core" version = "0.42.0" -source = "git+https://github.com/rust-ethereum/evm?branch=v0.x#6ca5a72bc8942f4860137155dd9033526fd362a5" dependencies = [ "parity-scale-codec", "primitive-types", @@ -2742,7 +2740,6 @@ dependencies = [ [[package]] name = "evm-gasometer" version = "0.42.0" -source = "git+https://github.com/rust-ethereum/evm?branch=v0.x#6ca5a72bc8942f4860137155dd9033526fd362a5" dependencies = [ "environmental", "evm-core", @@ -2753,7 +2750,6 @@ dependencies = [ [[package]] name = "evm-runtime" version = "0.42.0" -source = "git+https://github.com/rust-ethereum/evm?branch=v0.x#6ca5a72bc8942f4860137155dd9033526fd362a5" dependencies = [ "auto_impl", "environmental", @@ -3713,6 +3709,7 @@ dependencies = [ "pallet-transaction-payment-rpc-runtime-api", "parity-scale-codec", "scale-info", + "shielding", "sp-api", "sp-block-builder", "sp-consensus-aura", @@ -6830,6 +6827,7 @@ dependencies = [ "parity-scale-codec", "rlp", "scale-info", + "shielding", "sp-core", "sp-io", "sp-runtime", @@ -10404,6 +10402,21 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shielding" +version = "0.1.0" +dependencies = [ + "evm", + "frame-support", + "frame-system", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "shlex" version = "1.3.0" diff --git a/Cargo.toml b/Cargo.toml index 16cfa8ece8..294539d0b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ members = [ "template/runtime", "precompiles", "precompiles/macro", - "precompiles/tests-external", + "precompiles/tests-external", "frame/shielding", ] resolver = "2" @@ -55,7 +55,7 @@ derive_more = "1.0" environmental = { version = "1.1.4", default-features = false } ethereum = { git = "https://github.com/rust-ethereum/ethereum", rev = "bbb544622208ef6e9890a2dbc224248f6dd13318", default-features = false } ethereum-types = { version = "0.15", default-features = false } -evm = { git = "https://github.com/rust-ethereum/evm", branch = "v0.x", default-features = false } +evm = { git = "https://github.com/NP-Eng/evm", branch = "v0.x", default-features = false } futures = "0.3.31" hash-db = { version = "0.16.0", default-features = false } hex = { version = "0.4.3", default-features = false, features = ["alloc"] } @@ -204,6 +204,7 @@ pallet-evm-precompile-sha3fips = { path = "frame/evm/precompile/sha3fips", defau pallet-evm-precompile-simple = { path = "frame/evm/precompile/simple", default-features = false } pallet-evm-test-vector-support = { path = "frame/evm/test-vector-support" } pallet-hotfix-sufficients = { path = "frame/hotfix-sufficients", default-features = false } +shielding = { path = "frame/shielding", default-features = false } # Frontier Utility precompile-utils = { path = "precompiles", default-features = false } # Frontier Template diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..15a891e590 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +FROM rust:1.75-slim + +# Install build dependencies +RUN apt-get update && apt-get install -y \ + build-essential \ + clang \ + libclang-dev \ + libssl-dev \ + pkg-config \ + protobuf-compiler \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy source code from frontier directory +COPY . . + +# Build the node +RUN cd template/node && cargo build --release --bin frontier-template-node + +# Expose ports +EXPOSE 30333 9933 9944 9615 + +# Run the node +ENTRYPOINT ["/app/template/node/target/release/frontier-template-node"] +CMD ["--dev", "--rpc-cors=all", "--rpc-external", "--rpc-methods=Unsafe", "--rpc-port=9933"] diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000000..6260d4d755 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,35 @@ +# Development Dockerfile for Frontier Template Node +FROM rust:1.75-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + build-essential \ + clang \ + libclang-dev \ + libssl-dev \ + pkg-config \ + protobuf-compiler \ + curl \ + git \ + vim \ + nano \ + && rm -rf /var/lib/apt/lists/* + +# Set working directory +WORKDIR /app + +# Copy the entire frontier workspace +COPY . . + +# Create a non-root user +RUN useradd -m -u 1000 -s /bin/bash substrate && \ + chown -R substrate:substrate /app + +# Switch to non-root user +USER substrate + +# Expose ports +EXPOSE 30333 9933 9944 9615 + +# Set default command for development +CMD ["bash"] \ No newline at end of file diff --git a/README-Docker.md b/README-Docker.md new file mode 100644 index 0000000000..600746d60e --- /dev/null +++ b/README-Docker.md @@ -0,0 +1,48 @@ +# Docker Setup for Frontier + +This directory contains Docker configuration files for running the Frontier template node. + +## Quick Start + +### Build +```bash +# Build and run the production node +docker compose up --build +``` + +### Deploy development node +```bash +# Run a development node +docker run -d --name frontier-node -p 9933:9933 -p 9944:9944 frontier-node --dev --rpc-cors=all --rpc-external --rpc-methods=Unsafe --rpc-port 9933 +``` + +## Files + +- `Dockerfile` - Production Docker image for the Frontier template node +- `Dockerfile.dev` - Development Docker image with additional tools +- `docker-compose.yml` - Production Docker Compose configuration +- `docker-compose.dev.yml` - Development Docker Compose configuration with hot reloading +- `.dockerignore` - Files to exclude from Docker build context + +## Ports + +- `30333` - P2P networking +- `9933` - RPC endpoint +- `9944` - WebSocket endpoint +- `9615` - Prometheus metrics +- `9090` - Prometheus (development only) + +## Volumes + +- `frontier_data` - Persistent chain data (production) +- `frontier_dev_data` - Development build cache +- `cargo_cache` - Rust cargo cache for faster builds + +## Development + +The development setup includes: +- Hot reloading of source code +- Debug logging enabled +- Interactive bash shell +- Cargo cache for faster builds +- Optional Prometheus monitoring \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000000..3546d923a1 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,52 @@ +version: '3.8' + +services: + frontier-dev: + build: + context: . + dockerfile: Dockerfile.dev + container_name: frontier-dev + ports: + - "30333:30333" # P2P port + - "9933:9933" # RPC port + - "9944:9944" # WebSocket port + - "9615:9615" # Prometheus metrics + volumes: + - .:/app + - frontier_dev_data:/app/template/node/target + - cargo_cache:/usr/local/cargo + environment: + - RUST_LOG=debug + - RUST_BACKTRACE=1 + working_dir: /app/template/node + stdin_open: true + tty: true + command: bash + networks: + - frontier-dev-network + + # Optional: Add a simple monitoring service for development + prometheus-dev: + image: prom/prometheus:latest + container_name: frontier-prometheus-dev + ports: + - "9090:9090" + volumes: + - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro + command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.path=/prometheus' + - '--web.enable-lifecycle' + restart: unless-stopped + networks: + - frontier-dev-network + +volumes: + frontier_dev_data: + driver: local + cargo_cache: + driver: local + +networks: + frontier-dev-network: + driver: bridge \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000000..f8333aad9d --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +services: + frontier-node: + build: . + ports: + - "30333:30333" + - "9933:9933" + - "9944:9944" + - "9615:9615" + volumes: + - frontier_data:/app/template/node/target/release/chains + command: ["--dev", "--tmp", "--rpc-external", "--rpc-methods", "unsafe"] + +volumes: + frontier_data: \ No newline at end of file diff --git a/docs/SHIELDING_INTEGRATION.md b/docs/SHIELDING_INTEGRATION.md new file mode 100644 index 0000000000..b4c6953a34 --- /dev/null +++ b/docs/SHIELDING_INTEGRATION.md @@ -0,0 +1,231 @@ +# Shielding Pallet Integration Guide + +This guide explains how to use the shielding pallet to store notes from EVM shield operations. + +## Overview + +The shielding pallet has been integrated with the EVM to automatically store notes from shield operations in a Merkle tree. When an EVM contract calls the `shield` function, the note is automatically added to the shielding pallet's Merkle tree. + +## Architecture + +### Components + +1. **EVM Shield Function**: Transfers funds and generates note hashes +2. **OnShield Hook**: Integrates EVM with the shielding pallet +3. **Shielding Pallet**: Stores notes in a Merkle tree +4. **Merkle Tree**: Provides efficient inclusion proofs + +### Flow + +``` +EVM Contract → shield() → OnShield Hook → Shielding Pallet → Merkle Tree +``` + +## Configuration + +### Runtime Configuration + +The integration is configured in `frontier/template/runtime/src/lib.rs`: + +```rust +// EVM configuration +impl pallet_evm::Config for Runtime { + // ... other config ... + type OnShield = ShieldingHook; +} + +// Shielding hook implementation +pub struct ShieldingHook; + +impl pallet_evm::OnShield for ShieldingHook { + fn on_shield(_source: H160, _value: U256, note: H256) -> Result<(), DispatchError> { + // Add the note to the shielding pallet's Merkle tree + shielding::Pallet::::add_note( + frame_system::RawOrigin::None.into(), + note, + ) + } +} + +// Shielding pallet configuration +impl shielding::Config for Runtime { + type MaxTreeDepth = ConstU32<20>; // 2^20 = 1,048,576 notes + type RuntimeEvent = RuntimeEvent; +} +``` + +## Usage + +### 1. From EVM Contracts + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +contract ShieldingExample { + address public constant SHIELDING_POOL = address(0x0000000000000000000000000000000000000000); + + event FundsShielded(address indexed sender, uint256 amount, bytes32 noteHash); + + function shieldFunds(uint256 amount, bytes32 noteHash) external payable { + require(msg.value == amount, "Incorrect amount sent"); + + // Call the shield function - this automatically integrates with the shielding pallet + (bool success, ) = SHIELDING_POOL.call{value: amount}( + abi.encodeWithSignature("shield(address,uint256,bytes32)", msg.sender, amount, noteHash) + ); + + require(success, "Shield operation failed"); + + emit FundsShielded(msg.sender, amount, noteHash); + } +} +``` + +### 2. From Substrate/Polkadot.js Apps + +```javascript +// Add a note directly to the shielding pallet +const noteHash = "0x1234567890abcdef..."; // 32-byte hash +await api.tx.shielding.addNote(noteHash).signAndSend(alice); + +// Query the Merkle root +const merkleRoot = await api.query.shielding.merkleRoot(); + +// Query note count +const noteCount = await api.query.shielding.noteCount(); + +// Query a specific note +const note = await api.query.shielding.notes(0); +``` + +### 3. From Rust Code + +```rust +use frame_system::RawOrigin; +use sp_core::H256; + +// Add a note to the Merkle tree +let note_hash = H256::from_slice(&[1u8; 32]); +let _ = Shielding::add_note(RawOrigin::Signed(alice).into(), note_hash); + +// Get the current Merkle root +let root = Shielding::merkle_root(); + +// Get the note count +let count = Shielding::note_count(); +``` + +## API Reference + +### Shielding Pallet Extrinsics + +- **`add_note(note: H256)`**: Add a note to the Merkle tree +- **`shield_funds(amount, recipient)`**: Shield funds (if implemented) +- **`unshield_funds(amount, proof, nullifier)`**: Unshield funds (if implemented) + +### Shielding Pallet Queries + +- **`merkleRoot()`**: Get the current Merkle root +- **`noteCount()`**: Get the total number of notes +- **`notes(index: u64)`**: Get a specific note by index + +### Events + +- **`NoteAdded { note: H256, index: u64, root: H256 }`**: Emitted when a note is added +- **`MerkleRootUpdated { new_root: H256 }`**: Emitted when the Merkle root is updated + +## Testing + +### Run the Example + +```bash +# Start the node +cargo run --bin frontier-template-node -- --dev + +# In another terminal, run the integration example +node frontier/examples/shielding-integration-example.js +``` + +### Expected Output + +``` +🚀 Demonstrating Shielding Integration... + +📊 Initial State: +Initial Merkle root: 0x0000000000000000000000000000000000000000000000000000000000000000 +Initial note count: 0 + +📝 Test note hash: 0x1234567890abcdef... + +🛡️ Simulating EVM shield operation... +✅ Shield transaction included in block +📋 Shielding event: NoteAdded + - Note: 0x1234567890abcdef... + - Index: 0 + - New root: 0xabcdef1234567890... + +📊 Updated State: +New Merkle root: 0xabcdef1234567890... +New note count: 1 +Root changed: true +Count increased: true + +🔍 Verifying note storage: +✅ Note found at index 0: 0x1234567890abcdef... + Matches our note: true +``` + +## Security Considerations + +### Privacy Properties + +- **Note Privacy**: Individual notes are stored as hashes +- **Merkle Tree**: Efficient inclusion proofs without revealing all data +- **Zero-Knowledge**: Future implementations can add ZK proof verification + +### Limitations + +- **Current Implementation**: Basic Merkle tree without ZK proofs +- **Note Size**: Limited by the Merkle tree depth (2^20 notes) +- **Gas Costs**: Shield operations require gas for both EVM and Substrate operations + +## Future Enhancements + +### Planned Features + +1. **Zero-Knowledge Proofs**: Add ZK proof verification for unshielding +2. **Nullifier System**: Prevent double-spending of shielded notes +3. **Note Encryption**: Encrypt note data for additional privacy +4. **Batch Operations**: Support for batch shielding/unshielding +5. **Cross-Chain**: Support for cross-chain shielded transfers + +### Integration Points + +- **Precompiles**: Add shielding precompiles for easier EVM integration +- **RPC**: Add RPC methods for querying shielding state +- **Frontend**: Add UI components for shielding operations + +## Troubleshooting + +### Common Issues + +1. **Note Not Added**: Check if the OnShield hook is properly configured +2. **Merkle Root Not Updated**: Verify the shielding pallet is working correctly +3. **Gas Limit Exceeded**: Increase gas limit for shield operations + +### Debugging + +```bash +# Check shielding pallet logs +cargo run --bin frontier-template-node -- --dev -l shielding=debug + +# Query shielding state +curl -H "Content-Type: application/json" -d '{"id":1, "jsonrpc":"2.0", "method": "state_call", "params": ["Shielding_merkle_root", "0x"]}' http://localhost:9933 +``` + +## Conclusion + +The shielding pallet integration provides a seamless way to store notes from EVM shield operations in a Substrate-based Merkle tree. This enables privacy-preserving transactions while maintaining compatibility with existing EVM infrastructure. + +For more information, see the [shielding pool documentation](./SHIELDING_POOL.md) and the [example implementation](../examples/shielding-integration-example.js). \ No newline at end of file diff --git a/docs/SHIELDING_POOL.md b/docs/SHIELDING_POOL.md new file mode 100644 index 0000000000..3387f3c83e --- /dev/null +++ b/docs/SHIELDING_POOL.md @@ -0,0 +1,421 @@ +# Merkle Tree Shielding Pool + +This document describes the Merkle tree shielding pool implementation for Frontier, which provides Zcash-like privacy features for EVM-compatible blockchains. + +## Overview + +The shielding pool allows users to perform privacy-preserving transactions by: + +1. **Shielding**: Converting transparent funds into shielded commitments +2. **Transferring**: Moving shielded funds between accounts without revealing amounts or recipients +3. **Unshielding**: Converting shielded funds back to transparent balances + +The implementation uses: +- **Merkle Trees**: For efficient commitment storage and verification +- **Zero-Knowledge Proofs**: For transaction validity without revealing details +- **Nullifiers**: To prevent double-spending of shielded notes +- **Note Encryption**: To protect transaction privacy + +## Architecture + +### Components + +1. **Substrate Pallet** (`frame-shielding`): Core shielding pool logic +2. **EVM Precompile** (`frontier-precompiles`): Smart contract interface +3. **Solidity Interface**: Type-safe smart contract interactions +4. **Runtime Integration**: Configuration and setup + +### Key Features + +- **Privacy**: Transaction amounts and recipients are hidden +- **Efficiency**: Merkle tree provides O(log n) proof generation +- **Security**: Cryptographic proofs ensure transaction validity +- **Compatibility**: Works with existing EVM smart contracts + +## Usage + +### From Smart Contracts + +#### Basic Shielding + +```solidity +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "./ShieldingPool.sol"; + +contract MyShieldingContract { + using ShieldingPoolLibrary for address; + + address constant SHIELDING_POOL = address(0x0000000000000000000000000000000000000010); + + function shieldFunds(address recipient, uint256 amount) external { + (bool success, bytes32 commitment) = SHIELDING_POOL.shield(recipient, amount); + require(success, "Shield operation failed"); + + emit FundsShielded(msg.sender, amount, commitment); + } + + function unshieldFunds(uint256 amount, bytes memory proof, bytes32 nullifier) external { + bool success = SHIELDING_POOL.unshield(amount, proof, nullifier); + require(success, "Unshield operation failed"); + + emit FundsUnshielded(msg.sender, amount, nullifier); + } + + function getMerkleRoot() external view returns (bytes32) { + return SHIELDING_POOL.getMerkleRoot(); + } + + function getShieldedBalance(address account) external view returns (uint256) { + return SHIELDING_POOL.getShieldedBalance(account); + } +} +``` + +#### Advanced Usage with Custom Logic + +```solidity +contract AdvancedShielding { + using ShieldingPoolLibrary for address; + + mapping(address => bytes32[]) public userCommitments; + mapping(address => uint256) public userShieldedBalances; + + function shieldWithTracking(address recipient, uint256 amount) external { + (bool success, bytes32 commitment) = address(0x0000000000000000000000000000000000000010).shield(recipient, amount); + require(success, "Shield failed"); + + userCommitments[recipient].push(commitment); + userShieldedBalances[recipient] += amount; + } + + function batchShield(address[] calldata recipients, uint256[] calldata amounts) external { + require(recipients.length == amounts.length, "Arrays must match"); + + for (uint i = 0; i < recipients.length; i++) { + (bool success, ) = address(0x0000000000000000000000000000000000000010).shield(recipients[i], amounts[i]); + require(success, "Batch shield failed"); + } + } + + function getCommitmentCount() external view returns (uint256) { + return address(0x0000000000000000000000000000000000000010).getCommitmentCount(); + } +} +``` + +### From Substrate Runtime + +#### Direct Pallet Calls + +```rust +use frame_shielding as shielding; +use sp_core::H256; + +// Shield funds +let amount = 1000u128; +let recipient = account_id; +let call = shielding::Call::::shield_funds { amount, recipient }; +let origin = RuntimeOrigin::from(Some(caller)); +call.dispatch(origin)?; + +// Unshield funds +let proof = shielding::ShieldProof::new( + amount.into(), + recipient.encode(), + nullifier, + commitment, +); +let call = shielding::Call::::unshield_funds { amount, proof, nullifier }; +call.dispatch(origin)?; +``` + +#### Using Helper Functions + +```rust +use crate::shielding::helpers; + +// Create a commitment +let commitment = helpers::create_commitment(amount, recipient); + +// Create a nullifier +let nullifier = helpers::create_nullifier(commitment_hash, spending_key); + +// Create a shield proof +let proof = helpers::create_shield_proof(amount, recipient, nullifier, commitment); + +// Get current state +let merkle_root = helpers::get_merkle_root(); +let commitment_count = helpers::get_commitment_count(); +let shielded_balance = helpers::get_shielded_balance(account); +``` + +## Configuration + +### Runtime Parameters + +```rust +parameter_types! { + pub const MaxCommitments: u32 = 1_000_000; // 1 million commitments + pub const MaxNullifiers: u32 = 1_000_000; // 1 million nullifiers + pub const ShieldDeposit: Balance = 1_000_000_000_000_000; // 0.001 tokens + pub const MinShieldAmount: Balance = 1; // 1 wei minimum + pub const MaxShieldAmount: Balance = 1_000_000_000_000_000_000_000_000; // 1 billion tokens max +} +``` + +### Pallet Configuration + +```rust +impl shielding::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Currency = pallet_balances::Pallet; + type MaxCommitments = MaxCommitments; + type MaxNullifiers = MaxNullifiers; + type ShieldDeposit = ShieldDeposit; + type MinShieldAmount = MinShieldAmount; + type MaxShieldAmount = MaxShieldAmount; + type WeightInfo = shielding::weights::SubstrateWeight; +} +``` + +### Precompile Configuration + +```rust +impl pallet_evm::Config for Runtime { + // ... existing config ... + type PrecompilesType = ShieldingPrecompiles; + type PrecompilesValue = ShieldingPrecompiles; +} +``` + +## Security Considerations + +### Cryptographic Assumptions + +- **Blake2b**: Used for hashing commitments and nullifiers +- **Merkle Tree**: Provides efficient inclusion proofs +- **Zero-Knowledge Proofs**: Ensure transaction validity without revealing details + +### Privacy Properties + +- **Amount Privacy**: Transaction amounts are hidden +- **Recipient Privacy**: Recipients are not publicly visible +- **Sender Privacy**: Senders are not linked to transactions +- **Balance Privacy**: Account balances are not revealed + +### Attack Vectors + +1. **Double-Spending**: Prevented by nullifiers +2. **Replay Attacks**: Prevented by unique nullifiers +3. **Front-Running**: Mitigated by commitment schemes +4. **Sybil Attacks**: Limited by economic constraints + +## Gas Costs + +| Operation | Base Cost | Per Byte Cost | Description | +|-----------|-----------|---------------|-------------| +| Shield | 50,000 | 10 | Create commitment and add to Merkle tree | +| Unshield | 60,000 | 10 | Verify proof and transfer funds | +| Transfer | 70,000 | 10 | Transfer between shielded accounts | +| GetMerkleRoot | 2,000 | 0 | Read current Merkle root | +| GetCommitmentCount | 2,000 | 0 | Read commitment count | +| GetCommitment | 2,000 | 10 | Read commitment by index | +| IsNullifierUsed | 2,000 | 10 | Check nullifier status | +| GetShieldedBalance | 2,000 | 10 | Read shielded balance | + +## Testing + +### Unit Tests + +```bash +# Run pallet tests +cargo test -p frame-shielding + +# Run precompile tests +cargo test -p frontier-precompiles + +# Run integration tests +cargo test -p frontier-template-runtime +``` + +### Integration Tests + +```rust +#[test] +fn test_shielding_workflow() { + // 1. Shield funds + let amount = 1000u128; + let recipient = account_id; + assert_ok!(ShieldingPallet::shield_funds(Origin::signed(caller), amount, recipient)); + + // 2. Verify commitment was added + let merkle_root = ShieldingPallet::merkle_root(); + assert_ne!(merkle_root, H256::zero()); + + // 3. Check shielded balance + let balance = ShieldingPallet::shielded_balances(recipient); + assert_eq!(balance, amount); + + // 4. Unshield funds + let proof = create_test_proof(amount, recipient); + let nullifier = create_test_nullifier(); + assert_ok!(ShieldingPallet::unshield_funds(Origin::signed(recipient), amount, proof, nullifier)); +} +``` + +### Smart Contract Tests + +```solidity +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import "./ShieldingPool.sol"; +import "@openzeppelin/contracts/test/Test.sol"; + +contract ShieldingPoolTest is Test { + ShieldingPoolContract public shieldingContract; + + function setUp() public { + shieldingContract = new ShieldingPoolContract(); + } + + function testShieldFunds() public { + address recipient = address(0x123); + uint256 amount = 1000; + + shieldingContract.shieldFunds(recipient, amount); + + bytes32 merkleRoot = shieldingContract.getCurrentMerkleRoot(); + assertTrue(merkleRoot != bytes32(0)); + } + + function testUnshieldFunds() public { + // Test unshielding with valid proof + uint256 amount = 1000; + bytes memory proof = new bytes(32); + bytes32 nullifier = bytes32(uint256(1)); + + shieldingContract.unshieldFunds(amount, proof, nullifier); + + assertTrue(shieldingContract.checkNullifier(nullifier)); + } +} +``` + +## Deployment + +### 1. Add Dependencies + +```toml +# Cargo.toml +[dependencies] +frame-shielding = { path = "../../frame/shielding" } +frontier-precompiles = { path = "../precompiles" } +``` + +### 2. Configure Runtime + +```rust +// runtime/src/lib.rs +pub mod shielding; + +// Add pallet to construct_runtime! +construct_runtime!( + pub enum Runtime where + Block = Block, + NodeBlock = opaque::Block, + UncheckedExtrinsic = UncheckedExtrinsic + { + // ... other pallets ... + Shielding: frame_shielding = 42, + } +); +``` + +### 3. Initialize Genesis + +```rust +// runtime/src/lib.rs +impl frame_shielding::GenesisConfig { + pub fn build(&self) { + // Initialize with empty Merkle tree + } +} +``` + +### 4. Deploy Smart Contracts + +```bash +# Compile Solidity contracts +npx hardhat compile + +# Deploy to network +npx hardhat run scripts/deploy.js --network localhost +``` + +## Monitoring + +### Events + +The shielding pool emits the following events: + +- `FundsShielded`: When funds are shielded +- `FundsUnshielded`: When funds are unshielded +- `CommitmentAdded`: When a commitment is added to the Merkle tree +- `NullifierUsed`: When a nullifier is used +- `MerkleRootUpdated`: When the Merkle root is updated + +### Metrics + +Key metrics to monitor: + +- **Commitment Count**: Total number of commitments in the Merkle tree +- **Nullifier Count**: Total number of used nullifiers +- **Shielded Balances**: Total value in shielded form +- **Gas Usage**: Gas consumption for shielding operations +- **Proof Verification Time**: Time to verify zero-knowledge proofs + +## Troubleshooting + +### Common Issues + +1. **Insufficient Balance**: Ensure account has enough funds to shield +2. **Invalid Proof**: Check that zero-knowledge proof is correctly generated +3. **Nullifier Already Used**: Each nullifier can only be used once +4. **Merkle Tree Full**: Increase MaxCommitments parameter if needed + +### Debug Commands + +```bash +# Check pallet state +substrate-node query Shielding merkleRoot +substrate-node query Shielding commitmentCount +substrate-node query Shielding shieldedBalances + +# Check precompile +curl -X POST -H "Content-Type: application/json" \ + --data '{"jsonrpc":"2.0","method":"eth_call","params":[{"to":"0x0000000000000000000000000000000000000010","data":"0x23456789"}],"id":1}' \ + http://localhost:9933 +``` + +## Future Enhancements + +1. **Optimistic Updates**: Reduce proof verification time +2. **Batch Operations**: Support for batch shielding/unshielding +3. **Cross-Chain**: Enable shielded transfers between chains +4. **Advanced Privacy**: Support for confidential amounts and recipients +5. **Gas Optimization**: Reduce gas costs for common operations + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Add tests for new functionality +4. Ensure all tests pass +5. Submit a pull request + +## License + +This project is licensed under the Apache 2.0 License - see the LICENSE file for details. \ No newline at end of file diff --git a/frame/ethereum/Cargo.toml b/frame/ethereum/Cargo.toml index 07bdb23aa0..951cc01555 100644 --- a/frame/ethereum/Cargo.toml +++ b/frame/ethereum/Cargo.toml @@ -40,6 +40,7 @@ pallet-timestamp = { workspace = true, features = ["default"] } sp-core = { workspace = true, features = ["default"] } # Frontier fp-self-contained = { workspace = true, features = ["default"] } +shielding = { path = "../shielding" } [features] default = ["std"] diff --git a/frame/ethereum/src/mock.rs b/frame/ethereum/src/mock.rs index a8be8babc0..b1adc7bbd5 100644 --- a/frame/ethereum/src/mock.rs +++ b/frame/ethereum/src/mock.rs @@ -41,6 +41,7 @@ frame_support::construct_runtime! { Timestamp: pallet_timestamp::{Pallet, Call, Storage}, EVM: pallet_evm::{Pallet, Call, Storage, Config, Event}, Ethereum: crate::{Pallet, Call, Storage, Event, Origin}, + Shielding: shielding::{Pallet, Call, Storage, Event}, } } @@ -74,6 +75,15 @@ impl pallet_balances::Config for Test { #[derive_impl(pallet_timestamp::config_preludes::TestDefaultConfig)] impl pallet_timestamp::Config for Test {} +parameter_types! { + pub const MaxTreeDepth: u32 = 4; // Smaller for testing (2^4 -1 = 15 notes) +} + +impl shielding::Config for Test { + type RuntimeEvent = RuntimeEvent; + type MaxTreeDepth = MaxTreeDepth; +} + pub struct FindAuthorTruncated; impl FindAuthor for FindAuthorTruncated { fn find_author<'a, I>(_digests: I) -> Option @@ -98,13 +108,14 @@ impl pallet_evm::Config for Test { type BlockHashMapping = crate::EthereumBlockHashMapping; type CreateOriginFilter = EnsureAllowedCreateAddress; type CreateInnerOriginFilter = EnsureAllowedCreateAddress; - type Currency = Balances; + type Currency = pallet_balances::Pallet; type PrecompilesType = (); type PrecompilesValue = (); type Runner = pallet_evm::runner::stack::Runner; type FindAuthor = FindAuthorTruncated; type GasLimitStorageGrowthRatio = GasLimitStorageGrowthRatio; type Timestamp = Timestamp; + type OnShield = ShieldingOnShield; } #[derive_impl(crate::config_preludes::TestDefaultConfig)] @@ -414,3 +425,11 @@ impl EIP1559UnsignedTransaction { }) } } + +pub struct ShieldingOnShield; +impl pallet_evm::OnShield for ShieldingOnShield { + fn on_shield(_source: sp_core::H160, _value: sp_core::U256, note: sp_core::H256) -> Result<(), sp_runtime::DispatchError> { + let result = ::shielding::Pallet::::add_note_internal(note); + result + } +} diff --git a/frame/ethereum/src/tests/mod.rs b/frame/ethereum/src/tests/mod.rs index 6dafebf053..08b3f3589c 100644 --- a/frame/ethereum/src/tests/mod.rs +++ b/frame/ethereum/src/tests/mod.rs @@ -32,6 +32,7 @@ use fp_self_contained::CheckedExtrinsic; mod eip1559; mod eip2930; mod legacy; +mod shielding; // This ERC-20 contract mints the maximum amount of tokens to the contract creator. // pragma solidity ^0.5.0;` diff --git a/frame/ethereum/src/tests/shielding.rs b/frame/ethereum/src/tests/shielding.rs new file mode 100644 index 0000000000..823fa75f9c --- /dev/null +++ b/frame/ethereum/src/tests/shielding.rs @@ -0,0 +1,228 @@ +// This file is part of Frontier. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Shielding integration tests + +use super::*; +use fp_ethereum::ValidatedTransaction; +use pallet_evm::AddressMapping; +use crate::mock; +use evm::{ExitReason, ExitError}; + +#[test] +fn shielding_with_designated_address_works() { + let initial_balance = 20_000_000; + let (pairs, mut ext) = new_test_ext_with_initial_balance(2, initial_balance); + let alice = &pairs[0]; + let _bob = &pairs[1]; + let substrate_alice = + ::AddressMapping::into_account_id(alice.address); + + ext.execute_with(|| { + // Use the same config as the EVM pallet (CANCUN_CONFIG) + let config = evm::Config::cancun(); + let note = H256::from_slice(&[1u8; 32]); + + // Then simulate the EVM transaction that would transfer funds + let transaction = mock::LegacyUnsignedTransaction { + nonce: U256::zero(), + gas_price: U256::zero(), + gas_limit: U256::from(900_000), + action: ethereum::TransactionAction::Call(config.shielding_pool_address), + value: config.shielding_unit_amount, + input: note.as_bytes().to_vec(), + } + .sign(&alice.private_key); + + assert_ok!(crate::ValidatedTransaction::::apply( + alice.address, + transaction + )); + + assert_eq!(pallet_balances::Pallet::::free_balance(&substrate_alice), initial_balance - config.shielding_unit_amount.as_u64()); + + assert_eq!(::shielding::Pallet::::notes(0), Some(note)); + }); +} + +#[test] +fn shielding_with_multiple_accounts_works() { + let initial_balance = 20_000_000; + let (pairs, mut ext) = new_test_ext_with_initial_balance(3, initial_balance); + let alice = &pairs[0]; + let bob = &pairs[1]; + let charlie = &pairs[2]; + + let substrate_alice = ::AddressMapping::into_account_id(alice.address); + let substrate_bob = ::AddressMapping::into_account_id(bob.address); + let substrate_charlie = ::AddressMapping::into_account_id(charlie.address); + + ext.execute_with(|| { + let config = evm::Config::cancun(); + let note1 = H256::from_slice(&[1u8; 32]); + let note2 = H256::from_slice(&[2u8; 32]); + let note3 = H256::from_slice(&[3u8; 32]); + + // Shield from Alice + let transaction1 = mock::LegacyUnsignedTransaction { + nonce: U256::zero(), + gas_price: U256::zero(), + gas_limit: U256::from(900_000), + action: ethereum::TransactionAction::Call(config.shielding_pool_address), + value: config.shielding_unit_amount, + input: note1.as_bytes().to_vec(), + } + .sign(&alice.private_key); + + // Shield from Bob + let transaction2 = mock::LegacyUnsignedTransaction { + nonce: U256::zero(), + gas_price: U256::zero(), + gas_limit: U256::from(900_000), + action: ethereum::TransactionAction::Call(config.shielding_pool_address), + value: config.shielding_unit_amount, + input: note2.as_bytes().to_vec(), + } + .sign(&bob.private_key); + + // Shield from Charlie + let transaction3 = mock::LegacyUnsignedTransaction { + nonce: U256::zero(), + gas_price: U256::zero(), + gas_limit: U256::from(900_000), + action: ethereum::TransactionAction::Call(config.shielding_pool_address), + value: config.shielding_unit_amount, + input: note3.as_bytes().to_vec(), + } + .sign(&charlie.private_key); + + // Apply all transactions + assert_ok!(crate::ValidatedTransaction::::apply( + alice.address, + transaction1 + )); + assert_ok!(crate::ValidatedTransaction::::apply( + bob.address, + transaction2 + )); + assert_ok!(crate::ValidatedTransaction::::apply( + charlie.address, + transaction3 + )); + + // Verify balances were deducted + assert_eq!(pallet_balances::Pallet::::free_balance(&substrate_alice), initial_balance - config.shielding_unit_amount.as_u64()); + assert_eq!(pallet_balances::Pallet::::free_balance(&substrate_bob), initial_balance - config.shielding_unit_amount.as_u64()); + assert_eq!(pallet_balances::Pallet::::free_balance(&substrate_charlie), initial_balance - config.shielding_unit_amount.as_u64()); + + // Verify notes were stored + assert_eq!(::shielding::Pallet::::notes(0), Some(note1)); + assert_eq!(::shielding::Pallet::::notes(1), Some(note2)); + assert_eq!(::shielding::Pallet::::notes(2), Some(note3)); + }); +} + +#[test] +fn shielding_fails_when_pool_is_full() { + let initial_balance = 100_000_000; // Large balance to fill the pool + let (pairs, mut ext) = new_test_ext_with_initial_balance(1, initial_balance); + let alice = &pairs[0]; + let substrate_alice = + ::AddressMapping::into_account_id(alice.address); + + ext.execute_with(|| { + let config = evm::Config::cancun(); + + // Calculate how many notes we can add (2^4 = 16) + let max_notes = 1 << 4; // MaxTreeDepth = 4 + + // Fill the shielding pool to capacity + for i in 0..max_notes+1 { + let note = H256::from_slice(&[(i % 256) as u8; 32]); + + let transaction = mock::LegacyUnsignedTransaction { + nonce: U256::from(i), + gas_price: U256::zero(), + gas_limit: U256::from(900_000), + action: ethereum::TransactionAction::Call(config.shielding_pool_address), + value: config.shielding_unit_amount, + input: note.as_bytes().to_vec(), + } + .sign(&alice.private_key); + + // All transactions should succeed until the pool is full + assert_ok!(crate::ValidatedTransaction::::apply( + alice.address, + transaction + )); + } + + // Verify the pool is full + assert_eq!(::shielding::Pallet::::note_count(), max_notes as u64); + + // Check balance before the failing transaction + let balance_before_fail = pallet_balances::Pallet::::free_balance(&substrate_alice); + + // Try to add one more note - this should fail + let overflow_note = H256::from_slice(&[255u8; 32]); + let overflow_transaction = mock::LegacyUnsignedTransaction { + nonce: U256::from(max_notes), + gas_price: U256::zero(), + gas_limit: U256::from(900_000), + action: ethereum::TransactionAction::Call(config.shielding_pool_address), + value: config.shielding_unit_amount, + input: overflow_note.as_bytes().to_vec(), + } + .sign(&alice.private_key); + + // This transaction should fail because the pool is full + let result = crate::ValidatedTransaction::::apply( + alice.address, + overflow_transaction + ); + + // The transaction should succeed at the pallet level but fail at the EVM level + assert!(result.is_ok()); + + // Extract the exit reason from the result + let (_, call_info) = result.unwrap(); + let exit_reason = match call_info { + CallOrCreateInfo::Call(info) => info.exit_reason, + CallOrCreateInfo::Create(info) => info.exit_reason, + }; + + // The EVM execution should fail with the shielding error + assert!(matches!(exit_reason, ExitReason::Error(ExitError::Other(_)))); + + // Verify the note count didn't increase + assert_eq!(::shielding::Pallet::::note_count(), max_notes as u64); + + // Verify the overflow note was not added + assert_eq!(::shielding::Pallet::::notes(max_notes as u64), None); + + // Verify the balance was NOT transferred (should remain the same) + let balance_after_fail = pallet_balances::Pallet::::free_balance(&substrate_alice); + assert_eq!(balance_after_fail, balance_before_fail, "Balance should not be deducted when shielding fails"); + + // Verify the shielding pool balance didn't increase + let shielding_pool_account_id = ::AddressMapping::into_account_id(config.shielding_pool_address); + let shielding_pool_balance = pallet_balances::Pallet::::free_balance(&shielding_pool_account_id); + let expected_shielding_pool_balance = max_notes as u64 * config.shielding_unit_amount.as_u64(); + assert_eq!(shielding_pool_balance, expected_shielding_pool_balance, "Shielding pool balance should not increase when transaction fails"); + }); +} + diff --git a/frame/evm/precompile/dispatch/src/mock.rs b/frame/evm/precompile/dispatch/src/mock.rs index 33ff975797..51c635de8a 100644 --- a/frame/evm/precompile/dispatch/src/mock.rs +++ b/frame/evm/precompile/dispatch/src/mock.rs @@ -156,6 +156,7 @@ impl pallet_evm::Config for Test { type Runner = pallet_evm::runner::stack::Runner; type OnChargeTransaction = (); type OnCreate = (); + type OnShield = (); type FindAuthor = FindAuthorTruncated; type GasLimitPovSizeRatio = (); type GasLimitStorageGrowthRatio = (); diff --git a/frame/evm/src/lib.rs b/frame/evm/src/lib.rs index 6925f93494..352b03888d 100644 --- a/frame/evm/src/lib.rs +++ b/frame/evm/src/lib.rs @@ -96,7 +96,7 @@ use frame_system::RawOrigin; use sp_core::{H160, H256, U256}; use sp_runtime::{ traits::{BadOrigin, NumberFor, Saturating, UniqueSaturatedInto, Zero}, - AccountId32, DispatchErrorWithPostInfo, + AccountId32, DispatchError, DispatchErrorWithPostInfo, }; // Frontier use fp_account::AccountId20; @@ -210,6 +210,11 @@ pub mod pallet { /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; + /// Hook to handle shielding operations. + /// This allows the runtime to integrate with the shielding pallet. + #[pallet::no_default_bounds] + type OnShield: OnShield; + /// EVM config used in the module. fn config() -> &'static EvmConfig { &CANCUN_CONFIG @@ -259,6 +264,7 @@ pub mod pallet { type BlockGasLimit = BlockGasLimit; type OnChargeTransaction = (); type OnCreate = (); + type OnShield = (); type FindAuthor = FindAuthorTruncated; type GasLimitPovSizeRatio = GasLimitPovSizeRatio; type GasLimitStorageGrowthRatio = GasLimitStorageGrowthRatio; @@ -1297,7 +1303,6 @@ impl OnCreate for Tuple { /// /// Uses standard Substrate accounts system to hold EVM accounts. pub struct FrameSystemAccountProvider(core::marker::PhantomData); - impl AccountProvider for FrameSystemAccountProvider { type AccountId = T::AccountId; type Nonce = T::Nonce; @@ -1318,3 +1323,15 @@ impl AccountProvider for FrameSystemAccountProvider let _ = frame_system::Pallet::::dec_sufficients(who); } } + +pub trait OnShield { + /// Called when a note is shielded. + /// This allows the runtime to add the note to the shielding pallet's Merkle tree. + fn on_shield(source: H160, value: U256, note: H256) -> Result<(), DispatchError>; +} + +impl OnShield for () { + fn on_shield(_source: H160, _value: U256, _note: H256) -> Result<(), DispatchError> { + Ok(()) + } +} \ No newline at end of file diff --git a/frame/evm/src/mock.rs b/frame/evm/src/mock.rs index 9f6788185d..1948a093fc 100644 --- a/frame/evm/src/mock.rs +++ b/frame/evm/src/mock.rs @@ -81,6 +81,7 @@ impl crate::Config for Test { type PrecompilesValue = MockPrecompiles; type Runner = crate::runner::stack::Runner; type Timestamp = Timestamp; + type OnShield = (); } pub struct FixedGasPrice; diff --git a/frame/evm/src/runner/stack.rs b/frame/evm/src/runner/stack.rs index 9d29d9bd30..ea712373a8 100644 --- a/frame/evm/src/runner/stack.rs +++ b/frame/evm/src/runner/stack.rs @@ -24,7 +24,7 @@ use alloc::{ }; use core::{marker::PhantomData, mem}; use evm::{ - backend::Backend as BackendT, + backend::{Backend as BackendT}, executor::stack::{Accessed, StackExecutor, StackState as StackStateT, StackSubstateMetadata}, gasometer::{GasCost, StorageTarget}, ExitError, ExitReason, ExternalOperation, Opcode, Transfer, @@ -53,7 +53,7 @@ use super::meter::StorageMeter; use crate::{ runner::Runner as RunnerT, AccountCodes, AccountCodesMetadata, AccountProvider, AccountStorages, AddressMapping, BalanceOf, BlockHashMapping, Config, EnsureCreateOrigin, - Error, Event, FeeCalculator, OnChargeEVMTransaction, OnCreate, Pallet, RunnerError, + Error, Event, FeeCalculator, OnChargeEVMTransaction, OnCreate, OnShield, Pallet, RunnerError, }; #[cfg(feature = "forbid-evm-reentrancy")] @@ -552,7 +552,10 @@ where weight_limit, proof_size_base_cost, measured_proof_size_before, - |executor| executor.transact_call(source, target, value, input, gas_limit, access_list), + |executor| { + // Continue with normal EVM execution + executor.transact_call(source, target, value, input, gas_limit, access_list) + }, ) } @@ -1097,6 +1100,28 @@ where // subtle issues in EIP-161. } + fn shield(&mut self, _source: H160, _value: U256, note: H256) -> Result<(), ExitError> { + // Call the OnShield hook to integrate with the shielding pallet + let hook_result = T::OnShield::on_shield(_source, _value, note); + + if let Err(_) = hook_result { + return Err(ExitError::Other("Shielding pallet integration failed".into())); + } + + // Transfer value to shielded pool + let source = T::AddressMapping::into_account_id(_source); + let transfer_result = T::Currency::transfer( + &source, + &T::AddressMapping::into_account_id(self.metadata().gasometer().config().shielding_pool_address), + _value.try_into().map_err(|_| ExitError::OutOfFund)?, + ExistenceRequirement::AllowDeath, + ); + + transfer_result.map_err(|_| ExitError::OutOfFund)?; + + Ok(()) + } + fn is_cold(&self, address: H160) -> bool { self.substate .recursive_is_cold(&|a| a.accessed_addresses.contains(&address)) diff --git a/frame/evm/src/tests.rs b/frame/evm/src/tests.rs index 434dcd90ca..269c37b1a6 100644 --- a/frame/evm/src/tests.rs +++ b/frame/evm/src/tests.rs @@ -1667,3 +1667,4 @@ fn metadata_empty_dont_code_gets_cached() { assert!(>::get(address).is_none()); }); } + diff --git a/frame/shielding/Cargo.toml b/frame/shielding/Cargo.toml new file mode 100644 index 0000000000..e4f680cc72 --- /dev/null +++ b/frame/shielding/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "shielding" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +repository.workspace = true + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.7.5", default-features = false, features = ["derive"] } +scale-info = { version = "2.11.6", default-features = false, features = ["derive"] } + +# Substrate dependencies +frame-support = { workspace = true, default-features = false } +frame-system = { workspace = true, default-features = false } +sp-core = { workspace = true, default-features = false } +sp-runtime = { workspace = true, default-features = false } +sp-std = { workspace = true, default-features = false } +sp-io = { workspace = true, default-features = false } + +# EVM dependency +evm = { workspace = true, default-features = false } + +[features] +default = ["std"] +std = [ + "codec/std", + "scale-info/std", + "frame-support/std", + "frame-system/std", + "sp-core/std", + "sp-runtime/std", + "sp-std/std", + "evm/std", +] +runtime-benchmarks = [ + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", +] diff --git a/frame/shielding/src/lib.rs b/frame/shielding/src/lib.rs new file mode 100644 index 0000000000..16bd2d270f --- /dev/null +++ b/frame/shielding/src/lib.rs @@ -0,0 +1,169 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +pub use pallet::*; + +#[frame_support::pallet] +pub mod pallet { + use frame_support::{ + pallet_prelude::*, + }; + use frame_system::pallet_prelude::*; + use sp_core::H256; + use sp_io::hashing::blake2_256; + use sp_std::vec::Vec; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The maximum depth of the Merkle tree + #[pallet::constant] + type MaxTreeDepth: Get; + + /// The overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + } + + // Merkle root storage + #[pallet::storage] + #[pallet::getter(fn merkle_root)] + pub type MerkleRoot = StorageValue<_, H256, ValueQuery>; + + // Note count + #[pallet::storage] + #[pallet::getter(fn note_count)] + pub type NoteCount = StorageValue<_, u64, ValueQuery>; + + // Notes (leaves) + #[pallet::storage] + #[pallet::getter(fn notes)] + pub type Notes = StorageMap<_, Blake2_128Concat, u64, H256, OptionQuery>; + + // Internal nodes of the Merkle tree for efficient updates + #[pallet::storage] + pub type MerkleNodes = StorageMap<_, Blake2_128Concat, u64, H256, OptionQuery>; + + + #[pallet::error] + pub enum Error { + /// Merkle tree is full + MerkleTreeFull, + /// Invalid tree state + InvalidTreeState, + /// Nullifier already used + NullifierAlreadyUsed, + /// Invalid note + InvalidNote, + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A new note was added to the Merkle tree + NoteAdded { note: H256, index: u64, root: H256 }, + /// The Merkle root was updated + MerkleRootUpdated { new_root: H256 }, + } + + #[pallet::call] + impl Pallet { + /// Add a note to the Merkle tree (requires signed origin) + #[pallet::weight(Weight::from_parts(10_000, 0))] + #[pallet::call_index(0)] + pub fn add_note(origin: OriginFor, note: H256) -> DispatchResult { + let _ = ensure_signed(origin)?; + + Self::add_note_internal(note) + } + } + + impl Pallet { + /// Add a note to the Merkle tree (internal function, no origin required) + /// This is used by the OnShield hook which doesn't have a signed origin + pub fn add_note_internal(note: H256) -> DispatchResult { + // Check if note is valid (not zero) + ensure!(!note.is_zero(), Error::::InvalidNote); + + // Get current count + let count = NoteCount::::get(); + let max_leaves = 1 << T::MaxTreeDepth::get(); + + // Check if tree is full + ensure!(count < max_leaves as u64, Error::::MerkleTreeFull); + + // Add note + Notes::::insert(count, note); + + // Update Merkle tree + let new_root = Self::update_merkle_tree(count, note)?; + + // Update storage + NoteCount::::put(count + 1); + MerkleRoot::::put(new_root); + + // Emit event + Self::deposit_event(Event::NoteAdded { + note, + index: count, + root: new_root, + }); + + Ok(()) + } + + /// Update the Merkle tree by adding a new leaf + fn update_merkle_tree(leaf_index: u64, leaf_hash: H256) -> Result { + let depth = T::MaxTreeDepth::get(); + let mut current_hash = leaf_hash; + let mut current_index = leaf_index; + + // Store the leaf + MerkleNodes::::insert(Self::leaf_to_node_index(current_index, depth), current_hash); + + // Update the tree bottom-up + for _level in 0..depth { + let parent_index = current_index / 2; + let sibling_index = if current_index % 2 == 0 { + current_index + 1 + } else { + current_index - 1 + }; + + // Get sibling hash (or zero if it doesn't exist) + let sibling_hash = MerkleNodes::::get(Self::leaf_to_node_index(sibling_index, depth)) + .unwrap_or(H256::zero()); + + // Compute parent hash + let parent_hash = if current_index % 2 == 0 { + Self::hash_pair(current_hash, sibling_hash) + } else { + Self::hash_pair(sibling_hash, current_hash) + }; + + // Store parent + MerkleNodes::::insert(Self::leaf_to_node_index(parent_index, depth), parent_hash); + + current_hash = parent_hash; + current_index = parent_index; + } + + Ok(current_hash) + } + + /// Convert leaf index to node index in the tree + fn leaf_to_node_index(leaf_index: u64, depth: u32) -> u64 { + let leaf_start = 1 << depth; + leaf_start + leaf_index + } + + /// Hash a pair of hashes + fn hash_pair(left: H256, right: H256) -> H256 { + let mut data = Vec::new(); + data.extend_from_slice(left.as_bytes()); + data.extend_from_slice(right.as_bytes()); + blake2_256(&data).into() + } + } +} + diff --git a/precompiles/tests-external/lib.rs b/precompiles/tests-external/lib.rs index 92ce8bf60b..acd7122e4c 100644 --- a/precompiles/tests-external/lib.rs +++ b/precompiles/tests-external/lib.rs @@ -264,6 +264,7 @@ impl pallet_evm::Config for Runtime { type Runner = pallet_evm::runner::stack::Runner; type OnChargeTransaction = (); type OnCreate = (); + type OnShield = (); type FindAuthor = (); type GasLimitPovSizeRatio = GasLimitPovSizeRatio; type GasLimitStorageGrowthRatio = (); diff --git a/template/runtime/Cargo.toml b/template/runtime/Cargo.toml index ee0429cf20..e488de3d77 100644 --- a/template/runtime/Cargo.toml +++ b/template/runtime/Cargo.toml @@ -57,6 +57,7 @@ pallet-evm-chain-id = { workspace = true } pallet-evm-precompile-modexp = { workspace = true } pallet-evm-precompile-sha3fips = { workspace = true } pallet-evm-precompile-simple = { workspace = true } +shielding = { workspace = true } # Cumulus primitives cumulus-pallet-weight-reclaim = { workspace = true } @@ -113,6 +114,7 @@ std = [ "pallet-evm-precompile-modexp/std", "pallet-evm-precompile-sha3fips/std", "pallet-evm-precompile-simple/std", + "shielding/std", # Cumulus primitives "cumulus-pallet-weight-reclaim/std", ] diff --git a/template/runtime/src/lib.rs b/template/runtime/src/lib.rs index 238c0c8905..de78b91419 100644 --- a/template/runtime/src/lib.rs +++ b/template/runtime/src/lib.rs @@ -374,6 +374,7 @@ impl pallet_evm::Config for Runtime { type Runner = pallet_evm::runner::stack::Runner; type OnChargeTransaction = (); type OnCreate = (); + type OnShield = ShieldingHook; type FindAuthor = FindAuthorTruncated; type GasLimitPovSizeRatio = GasLimitPovSizeRatio; type GasLimitStorageGrowthRatio = GasLimitStorageGrowthRatio; @@ -454,6 +455,24 @@ pub mod pallet_manual_seal { impl pallet_manual_seal::Config for Runtime {} +/// Hook to integrate EVM shielding with the shielding pallet +pub struct ShieldingHook; + +impl pallet_evm::OnShield for ShieldingHook { + fn on_shield(_source: H160, _value: U256, note: H256) -> Result<(), sp_runtime::DispatchError> { + // Add the note to the shielding pallet's Merkle tree + shielding::Pallet::::add_note( + frame_system::RawOrigin::None.into(), + note, + ) + } +} + +impl shielding::Config for Runtime { + type MaxTreeDepth = ConstU32<20>; // 2^20 = 1,048,576 notes + type RuntimeEvent = RuntimeEvent; +} + // Create the runtime by composing the FRAME pallets that were previously configured. #[frame_support::runtime] mod runtime { @@ -506,6 +525,9 @@ mod runtime { #[runtime::pallet_index(11)] pub type ManualSeal = pallet_manual_seal; + + #[runtime::pallet_index(12)] + pub type Shielding = shielding; } #[derive(Clone)] diff --git a/test.log b/test.log new file mode 100644 index 0000000000..966e6a42a9 --- /dev/null +++ b/test.log @@ -0,0 +1,61 @@ + Blocking waiting for file lock on build directory + Compiling evm v0.42.0 (/home/parsa/dev-polk/evm) + Compiling substrate-test-runtime v2.0.0 (https://github.com/paritytech/polkadot-sdk?branch=stable2503#10af808e) + Compiling frontier-template-runtime v0.0.0 (/home/parsa/dev-polk/frontier/template/runtime) + Compiling fp-evm v3.0.0-dev (/home/parsa/dev-polk/frontier/primitives/evm) + Compiling fp-rpc v3.0.0-dev (/home/parsa/dev-polk/frontier/primitives/rpc) + Compiling pallet-evm v6.0.0-dev (/home/parsa/dev-polk/frontier/frame/evm) + Compiling fp-ethereum v1.0.0-dev (/home/parsa/dev-polk/frontier/primitives/ethereum) + Compiling pallet-evm-test-vector-support v1.0.0-dev (/home/parsa/dev-polk/frontier/frame/evm/test-vector-support) + Compiling pallet-evm-precompile-simple v2.0.0-dev (/home/parsa/dev-polk/frontier/frame/evm/precompile/simple) + Compiling pallet-dynamic-fee v4.0.0-dev (/home/parsa/dev-polk/frontier/frame/dynamic-fee) + Compiling pallet-evm-precompile-sha3fips v2.0.0-dev (/home/parsa/dev-polk/frontier/frame/evm/precompile/sha3fips) + Compiling pallet-evm-precompile-modexp v2.0.0-dev (/home/parsa/dev-polk/frontier/frame/evm/precompile/modexp) + Compiling pallet-base-fee v1.0.0 (/home/parsa/dev-polk/frontier/frame/base-fee) + Compiling pallet-evm-precompile-ed25519 v2.0.0-dev (/home/parsa/dev-polk/frontier/frame/evm/precompile/ed25519) + Compiling pallet-evm-precompile-curve25519 v1.0.0-dev (/home/parsa/dev-polk/frontier/frame/evm/precompile/curve25519) + Compiling pallet-evm-precompile-bn128 v2.0.0-dev (/home/parsa/dev-polk/frontier/frame/evm/precompile/bn128) + Compiling pallet-evm-precompile-bls12377 v1.0.0-dev (/home/parsa/dev-polk/frontier/frame/evm/precompile/bls12377) + Compiling pallet-evm-precompile-bw6761 v1.0.0-dev (/home/parsa/dev-polk/frontier/frame/evm/precompile/bw6761) + Compiling pallet-evm-precompile-blake2 v2.0.0-dev (/home/parsa/dev-polk/frontier/frame/evm/precompile/blake2) + Compiling pallet-evm-precompile-bls12381 v1.0.0-dev (/home/parsa/dev-polk/frontier/frame/evm/precompile/bls12381) + Compiling fc-storage v1.0.0-dev (/home/parsa/dev-polk/frontier/client/storage) + Compiling fc-consensus v2.0.0-dev (/home/parsa/dev-polk/frontier/client/consensus) + Compiling fc-db v2.0.0-dev (/home/parsa/dev-polk/frontier/client/db) + Compiling pallet-ethereum v4.0.0-dev (/home/parsa/dev-polk/frontier/frame/ethereum) + Compiling precompile-utils v0.1.0 (/home/parsa/dev-polk/frontier/precompiles) + Compiling pallet-hotfix-sufficients v1.0.0 (/home/parsa/dev-polk/frontier/frame/hotfix-sufficients) + Compiling pallet-evm-precompile-dispatch v2.0.0-dev (/home/parsa/dev-polk/frontier/frame/evm/precompile/dispatch) + Compiling fc-mapping-sync v2.0.0-dev (/home/parsa/dev-polk/frontier/client/mapping-sync) + Compiling fc-cli v1.0.0-dev (/home/parsa/dev-polk/frontier/client/cli) + Compiling fc-rpc v2.0.0-dev (/home/parsa/dev-polk/frontier/client/rpc) + Compiling precompile-utils-macro v0.1.0 (/home/parsa/dev-polk/frontier/precompiles/macro) + Compiling precompile-utils-tests-external v0.1.0 (/home/parsa/dev-polk/frontier/precompiles/tests-external) + Compiling frontier-template-node v0.0.0 (/home/parsa/dev-polk/frontier/template/node) + Compiling substrate-test-runtime-client v2.0.0 (https://github.com/paritytech/polkadot-sdk?branch=stable2503#10af808e) + Finished `test` profile [unoptimized + debuginfo] target(s) in 5m 50s + Running unittests src/lib.rs (target/debug/deps/fc_api-a524cd566e8c934d) + Running unittests src/lib.rs (target/debug/deps/fc_cli-b3ea23d7d027ce82) + Running unittests src/lib.rs (target/debug/deps/fc_consensus-60e7d5df6eaacd77) + Running unittests src/lib.rs (target/debug/deps/fc_db-76afbaf2081e7e6f) + Running unittests src/lib.rs (target/debug/deps/fc_mapping_sync-b61d0cfe4c20ffd7) + Running unittests src/lib.rs (target/debug/deps/fc_rpc-854579151ecd8813) + Running unittests src/lib.rs (target/debug/deps/fc_rpc_core-e4a1b692f4761064) + Running unittests src/lib.rs (target/debug/deps/fc_rpc_v2-97b7158248038193) + Running unittests src/lib.rs (target/debug/deps/fc_rpc_v2_api-c25796847124ebf9) + Running unittests src/lib.rs (target/debug/deps/fc_rpc_v2_types-1b7eccd502a17e99) + Running unittests src/lib.rs (target/debug/deps/fc_storage-421af00f3e53ef63) + Running unittests src/lib.rs (target/debug/deps/fp_account-b3e13c42cf6120ab) + Running unittests src/lib.rs (target/debug/deps/fp_consensus-8996b3fa9babad4a) + Running unittests src/lib.rs (target/debug/deps/fp_dynamic_fee-ded0c690cb5f004e) + Running unittests src/lib.rs (target/debug/deps/fp_ethereum-6f35b4ee913c5db5) + Running unittests src/lib.rs (target/debug/deps/fp_evm-df04548781f21463) + Running unittests src/lib.rs (target/debug/deps/fp_rpc-abd91203d3f99c12) + Running unittests src/lib.rs (target/debug/deps/fp_self_contained-c682c2bbc6526207) + Running unittests src/lib.rs (target/debug/deps/fp_storage-eb78869d8c080fb5) + Running unittests src/main.rs (target/debug/deps/frontier_template_node-0a771d707c2dc142) + Running unittests src/lib.rs (target/debug/deps/frontier_template_runtime-2946a7625ed07773) + Running unittests src/lib.rs (target/debug/deps/pallet_base_fee-5db111a619c57a9b) + Running unittests src/lib.rs (target/debug/deps/pallet_dynamic_fee-b16aa3ecc6c8ab0c) + Running unittests src/lib.rs (target/debug/deps/pallet_ethereum-d2eafbf55f11a5ea) +error: test failed, to rerun pass `-p pallet-ethereum --lib`