Skip to content
Closed
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
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ jobs:
${{ runner.os }}-cargo-

- name: Run sccache
uses: mozilla-actions/sccache-action@v0.0.9
uses: mozilla-actions/sccache-action@v0.0.11

- name: Install Rust toolchain
run: make setup
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ jobs:
${{ runner.os }}-cargo-

- name: Run sccache
uses: mozilla-actions/sccache-action@v0.0.9
uses: mozilla-actions/sccache-action@v0.0.11

- name: Install Rust toolchain
run: make setup
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ jobs:
${{ runner.os }}-cargo-

- name: Run sccache
uses: mozilla-actions/sccache-action@v0.0.9
uses: mozilla-actions/sccache-action@v0.0.11

- name: Install Rust toolchain
run: make setup
Expand Down Expand Up @@ -119,7 +119,7 @@ jobs:
${{ runner.os }}-cargo-

- name: Run sccache
uses: mozilla-actions/sccache-action@v0.0.9
uses: mozilla-actions/sccache-action@v0.0.11

- name: Install Rust toolchain
run: make setup
Expand Down Expand Up @@ -175,7 +175,7 @@ jobs:
fail-on-cache-miss: true

- name: Run sccache
uses: mozilla-actions/sccache-action@v0.0.9
uses: mozilla-actions/sccache-action@v0.0.11

- name: Install Rust toolchain
run: make setup
Expand Down Expand Up @@ -223,7 +223,7 @@ jobs:
fail-on-cache-miss: true

- name: Run sccache
uses: mozilla-actions/sccache-action@v0.0.9
uses: mozilla-actions/sccache-action@v0.0.11

- name: Install Rust toolchain
run: make setup
Expand Down
5 changes: 3 additions & 2 deletions Cargo.lock

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

4 changes: 4 additions & 0 deletions client/rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ pallet-shielded-pool-runtime-api = { workspace = true, features = ["std"] }

[dev-dependencies]
tempfile = "3.21.0"
# Pins the relay's selector whitelist against the precompile's own constants
# (see relay::validation tests). A dev-dependency only: the relay must not
# depend on runtime crates at build time.
pallet-evm-precompile-shielded-pool = { workspace = true, features = ["std"] }
# Substrate
sc-block-builder = { workspace = true }
sc-client-db = { workspace = true, features = ["rocksdb"] }
Expand Down
15 changes: 12 additions & 3 deletions client/rpc/src/relay/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
//!
//! The relay only accepts calls to the ShieldedPool precompile
//! (`0x0000000000000000000000000000000000000801`) with selector
//! `0x47fc44a2` (unshield) or `0x8c0f5d24` (privateTransfer).
//! `0x4e505348` (unshield) or `0x1ec439cf` (privateTransfer).
//! It checks the fee embedded in ABI slot 6 is ≥ the current `min_relay_fee` from
//! `pallet-relayer` (queried dynamically via Runtime API so forkless upgrades take effect immediately).
//!
Expand Down Expand Up @@ -127,11 +127,15 @@ where

// Compute 2× gas floor: the relay must earn at least twice what it spends on EVM gas.
// 1 wei == 1 plank in Orbinum, so no unit conversion is required.
// Saturate rather than `as_u128()`, which panics on a gas_price ≥ 2^128.
// The value comes from the runtime, not calldata, but a panic here would
// still take down the relay RPC — mirror the fee-word hardening in
// operations.rs::fee_at_slot_6.
let base_fee_wei: u128 = self
.client
.runtime_api()
.gas_price(best_hash)
.map(|p| p.as_u128())
.map(|p| p.try_into().unwrap_or(u128::MAX))
.unwrap_or(0);
let effective_min_fee = compute_effective_min_fee(min_fee_planck, base_fee_wei);

Expand Down Expand Up @@ -275,7 +279,12 @@ where
.map(|cfg| cfg.min_fee_planck)
.unwrap_or(MIN_RELAY_FEE_FALLBACK);

let base_fee_wei: u128 = api.gas_price(best_hash).map(|p| p.as_u128()).unwrap_or(0);
// Saturate rather than `as_u128()` (panics ≥ 2^128) — see the sibling
// call above; runtime-sourced, but a panic still kills the relay RPC.
let base_fee_wei: u128 = api
.gas_price(best_hash)
.map(|p| p.try_into().unwrap_or(u128::MAX))
.unwrap_or(0);
let min_fee = compute_effective_min_fee(min_fee_planck, base_fee_wei);

let balance = {
Expand Down
53 changes: 41 additions & 12 deletions client/rpc/src/relay/operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,16 @@
use ethereum_types::U256;

/// 4-byte ABI selector for `unshield(...)`.
pub(crate) const SELECTOR_UNSHIELD: [u8; 4] = [0x47, 0xfc, 0x44, 0xa2];
/// `keccak256("unshield(bytes,bytes32,bytes32,uint32,uint256,bytes32,uint256,bytes32,bytes,uint32)")[0..4]`.
/// Must match the precompile's `calls::unshield::SELECTOR` — see the
/// consistency test in `validation.rs`.
pub(crate) const SELECTOR_UNSHIELD: [u8; 4] = [0x4e, 0x50, 0x53, 0x48];

/// 4-byte ABI selector for `privateTransfer(...)`.
pub(crate) const SELECTOR_PRIVATE_TRANSFER: [u8; 4] = [0x8c, 0x0f, 0x5d, 0x24];
/// `keccak256("privateTransfer(bytes,bytes32,bytes32[],bytes32[],bytes[],uint32,uint256,uint32,bytes)")[0..4]`.
/// Must match the precompile's `calls::private_transfer::SELECTOR` — see the
/// consistency test in `validation.rs` (guards against ME-8 recurring).
pub(crate) const SELECTOR_PRIVATE_TRANSFER: [u8; 4] = [0x1e, 0xc4, 0x39, 0xcf];

/// Describes how to validate calldata for a specific relayable on-chain operation.
///
Expand All @@ -38,9 +44,32 @@ pub(crate) trait RelayableOperation: Send + Sync {
fn extract_fee(&self, calldata: &[u8]) -> u128;
}

/// `unshield(proof, root, nullifier, asset_id, amount, recipient, fee)` — `0x47fc44a2`
/// Reads the relay fee from ABI slot 6 (`calldata[196..228]`), the position both
/// operations share.
///
/// Fee is in ABI slot 6: `calldata[196..228]`.
/// Saturates instead of panicking on a value above `u128::MAX`. Calldata reaches
/// this from an unauthenticated RPC call, and `U256::as_u128` panics outright on
/// anything wider — one crafted 32-byte word would take down the handler.
/// Saturating is safe because the result is only ever compared against the fee
/// floor: an absurd fee clears it here and is then rejected by the EVM dry-run,
/// which is what would have happened anyway.
///
/// The slice is bounds-checked by the caller's `min_calldata_len()` gate (292 or
/// 324, both well past 228).
fn fee_at_slot_6(calldata: &[u8]) -> u128 {
let Ok(bytes) = <[u8; 32]>::try_from(&calldata[196..228]) else {
return 0; // unreachable behind the length gate; a zero fee fails the floor
};
U256::from_big_endian(&bytes)
.try_into()
.unwrap_or(u128::MAX)
}

/// `unshield(proof, root, nullifier, asset_id, amount, recipient, fee,
/// change_commitment, change_encrypted_memo, circuit_version)` — `0x4e505348`
///
/// Fee is in ABI slot 6: `calldata[196..228]`. The head is 10 slots (320 bytes)
/// plus the 4-byte selector = 324 minimum.
pub(crate) struct UnshieldOp;

impl RelayableOperation for UnshieldOp {
Expand All @@ -53,18 +82,19 @@ impl RelayableOperation for UnshieldOp {
}

fn min_calldata_len(&self) -> usize {
228
324
}

fn extract_fee(&self, calldata: &[u8]) -> u128 {
let bytes: [u8; 32] = calldata[196..228].try_into().unwrap();
U256::from_big_endian(&bytes).as_u128()
fee_at_slot_6(calldata)
}
}

/// `privateTransfer(proof, root, nullifiers, commitments, memos, asset_id, fee)` — `0x8c0f5d24`
/// `privateTransfer(proof, root, nullifiers, commitments, memos, asset_id, fee,
/// circuit_version, ovk_blob)` — `0x1ec439cf`
///
/// Fee is in ABI slot 6: `calldata[196..228]`.
/// Fee is in ABI slot 6: `calldata[196..228]`. The head is 9 slots (288 bytes)
/// plus the 4-byte selector = 292 minimum.
pub(crate) struct PrivateTransferOp;

impl RelayableOperation for PrivateTransferOp {
Expand All @@ -77,12 +107,11 @@ impl RelayableOperation for PrivateTransferOp {
}

fn min_calldata_len(&self) -> usize {
228
292
}

fn extract_fee(&self, calldata: &[u8]) -> u128 {
let bytes: [u8; 32] = calldata[196..228].try_into().unwrap();
U256::from_big_endian(&bytes).as_u128()
fee_at_slot_6(calldata)
}
}

Expand Down
Loading
Loading