From bef6c818ad04c82385b8f6a08d04398fedc0f475 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Sun, 18 Oct 2026 14:27:45 -0300 Subject: [PATCH] fix(rpc)!: chain_isValidator answers for session validator accounts, not Aura keys, and refuses a value that does not decode exactly (fc-rpc-v2 2.0.1-dev) --- Cargo.lock | 2 +- client/rpc-v2/CHANGELOG.md | 30 +++++++++++ client/rpc-v2/Cargo.toml | 2 +- client/rpc-v2/src/README.md | 12 ++--- client/rpc-v2/src/chain.rs | 100 +++++++++++++++++++++++++---------- client/rpc-v2/src/privacy.rs | 16 ++++-- 6 files changed, 122 insertions(+), 40 deletions(-) create mode 100644 client/rpc-v2/CHANGELOG.md diff --git a/Cargo.lock b/Cargo.lock index 6d38f78e..d9f053e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5627,7 +5627,7 @@ dependencies = [ [[package]] name = "fc-rpc-v2" -version = "2.0.0-dev" +version = "2.0.1-dev" dependencies = [ "hex", "jsonrpsee", diff --git a/client/rpc-v2/CHANGELOG.md b/client/rpc-v2/CHANGELOG.md new file mode 100644 index 00000000..ed224f9d --- /dev/null +++ b/client/rpc-v2/CHANGELOG.md @@ -0,0 +1,30 @@ +# Changelog for `fc-rpc-v2` + +## [Unreleased] + +## [2.0.1-dev] - 2026-10-18 + +### Fixed + +- `chain_isValidator` reads `pallet_session::Validators` (the validator + accounts) instead of `pallet_aura::Authorities` (their Aura keys). An operator + that registered its session keys from its account, with a key that is not the + account, was reported as not a validator. +- A `Session::Validators` value that does not decode exactly, truncated or with + trailing bytes, is an internal error instead of a partial match. The decoding + is a pure function with unit tests. + +### Documentation + +- The module docs of `privacy` list `privacy_getSubtreeRoots`, state that every + endpoint reads the best block, and describe the proof gate as it is: half the + cores at once, a bounded queue, a bounded wait. + +## [2.0.0-dev] + +Orbinum's node RPC beside the Ethereum layer: + +- `privacy_*`: Merkle root, proofs by leaf index and by commitment, subtree + roots, nullifier status and pool statistics of the shielded pool, read from + the best block; proofs run on blocking threads behind a bounded gate. +- `chain_isValidator`: whether an SS58 account is an Aura authority. diff --git a/client/rpc-v2/Cargo.toml b/client/rpc-v2/Cargo.toml index 00d4e132..e74cdfb2 100644 --- a/client/rpc-v2/Cargo.toml +++ b/client/rpc-v2/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "fc-rpc-v2" -version = "2.0.0-dev" +version = "2.0.1-dev" license = "GPL-3.0-or-later WITH Classpath-exception-2.0" description = "Ethereum RPC (web3) compatibility layer for Substrate." authors = { workspace = true } diff --git a/client/rpc-v2/src/README.md b/client/rpc-v2/src/README.md index eb7b8216..a958e8e1 100644 --- a/client/rpc-v2/src/README.md +++ b/client/rpc-v2/src/README.md @@ -43,9 +43,9 @@ io.merge(ChainRpc::new(client.clone()).into_rpc())?; ### `chain_isValidator` -Returns `true` if the given SS58-encoded account is an active Aura block author. +Returns `true` if the given SS58-encoded account validates the current session. A validator is its account, not its Aura key: an operator may register its session keys from that account. -Reads `pallet_aura::Authorities` directly from storage — no runtime API call is made. Intended for UIs that need to gate actions on validator eligibility without waiting for an extrinsic to fail. +Reads `pallet_session::Validators` at the best block directly from storage — no runtime API call is made. Intended for UIs that need to gate actions on validator eligibility without waiting for an extrinsic to fail. **Parameters** @@ -63,11 +63,11 @@ Reads `pallet_aura::Authorities` directly from storage — no runtime API call i { "jsonrpc": "2.0", "result": true, "id": 1 } ``` -**Storage read:** `Twox128("Aura") ++ Twox128("Authorities")` → SCALE `Vec<[u8; 32]>` +**Storage read:** `Twox128("Session") ++ Twox128("Validators")` → SCALE `Vec`, which must decode exactly **Errors** - `InvalidParams` — invalid SS58 address -- `InternalError` — storage decode failure +- `InternalError` — the stored value does not decode exactly (truncated or trailing bytes) --- @@ -240,11 +240,11 @@ All endpoints query pallet storage directly using `sc_client_api::StorageProvide - No runtime upgrade required to add or change these endpoints. - Keys are built with the standard Substrate hasher layout: -### `chain.rs` — `pallet_aura` +### `chain.rs` — `pallet_session` | Storage item | Key layout | |---|---| -| `Authorities` | `Twox128("Aura") ++ Twox128("Authorities")` | +| `Validators` | `Twox128("Session") ++ Twox128("Validators")` | ### `privacy.rs` — `pallet_shielded_pool` diff --git a/client/rpc-v2/src/chain.rs b/client/rpc-v2/src/chain.rs index 173abbbb..e7ddd9a3 100644 --- a/client/rpc-v2/src/chain.rs +++ b/client/rpc-v2/src/chain.rs @@ -1,7 +1,7 @@ //! Chain RPC — general chain state queries. //! //! Endpoints: -//! - `chain_isValidator` — returns `true` if the given SS58 account is an active Aura validator. +//! - `chain_isValidator` — returns `true` if the given SS58 account is an active session validator. use jsonrpsee::{ core::RpcResult, @@ -9,7 +9,7 @@ use jsonrpsee::{ types::error::{ErrorCode, ErrorObject}, }; use sc_client_api::StorageProvider as ScStorageProvider; -use scale_codec::Decode; +use scale_codec::DecodeAll; use sp_blockchain::HeaderBackend; use sp_core::{crypto::Ss58Codec, storage::StorageKey}; use sp_crypto_hashing::twox_128; @@ -20,9 +20,21 @@ use std::{marker::PhantomData, sync::Arc}; // Storage key helpers // ============================================================================ -/// `twox_128("Aura") ++ twox_128("Authorities")` — `StorageValue` key for `pallet_aura`. -fn aura_authorities_key() -> Vec { - [twox_128(b"Aura"), twox_128(b"Authorities")].concat() +/// `twox_128("Session") ++ twox_128("Validators")` — `StorageValue` key for the +/// current session's validator accounts in `pallet_session`. +fn session_validators_key() -> Vec { + [twox_128(b"Session"), twox_128(b"Validators")].concat() +} + +/// Whether `account` is in the SCALE-encoded `Session::Validators` value `raw` +/// (`Vec`, each account its 32 bytes). An absent value means no +/// validators. A value that does not decode exactly is an error, not a miss. +fn contains_validator(raw: Option<&[u8]>, account: &[u8; 32]) -> Result { + let Some(mut bytes) = raw else { + return Ok(false); + }; + let validators = Vec::<[u8; 32]>::decode_all(&mut bytes)?; + Ok(validators.contains(account)) } fn read_storage, BE: sc_client_api::Backend>( @@ -41,9 +53,11 @@ fn read_storage, BE: sc_client_api::Backe #[rpc(server)] pub trait ChainApi { - /// Returns `true` if the given SS58-encoded account is an active Aura validator. + /// Returns `true` if the given SS58-encoded account validates the current session. /// - /// Reads `pallet_aura::Authorities` directly from storage at the best known block. + /// A validator is its account, not its Aura key: an operator may register its + /// session keys from that account, so the key need not equal the account. + /// Reads `pallet_session::Validators` from storage at the best known block. /// No runtime API call is performed. #[method(name = "chain_isValidator")] fn is_validator(&self, account: String) -> RpcResult; @@ -104,38 +118,70 @@ where .map_err(|e| invalid_params(format!("invalid SS58 address: {e:?}")))?; let target: [u8; 32] = *account_id.as_ref(); - // Read `Aura::Authorities` from storage at the best known block. + // Read `Session::Validators` from storage at the best known block. let best = self.client.info().best_hash; - let raw = read_storage(&*self.client, best, aura_authorities_key())?; - - let Some(bytes) = raw else { - // Storage item absent → chain has no configured validators. - return Ok(false); - }; + let raw = read_storage(&*self.client, best, session_validators_key())?; - // SCALE-decode as `Vec<[u8; 32]>`. - // `AuraId = sr25519::Public`, which serialises as a plain 32-byte array. - let authorities = Vec::<[u8; 32]>::decode(&mut bytes.as_slice()) - .map_err(|e| internal_error(format!("failed to decode Aura::Authorities: {e}")))?; - - Ok(authorities.iter().any(|auth| auth == &target)) + contains_validator(raw.as_deref(), &target) + .map_err(|e| internal_error(format!("failed to decode Session::Validators: {e}"))) } } #[cfg(test)] mod tests { - use super::aura_authorities_key; + use super::{contains_validator, session_validators_key}; + use scale_codec::Encode; use sp_crypto_hashing::twox_128; + const ALICE: [u8; 32] = [1; 32]; + const BOB: [u8; 32] = [2; 32]; + + #[test] + fn session_validators_key_is_32_bytes() { + assert_eq!(session_validators_key().len(), 32); + } + + #[test] + fn session_validators_key_uses_correct_hashes() { + let key = session_validators_key(); + assert_eq!(&key[..16], &twox_128(b"Session")); + assert_eq!(&key[16..], &twox_128(b"Validators")); + } + + #[test] + fn absent_value_has_no_validators() { + assert_eq!(contains_validator(None, &ALICE), Ok(false)); + } + + #[test] + fn empty_set_has_no_validators() { + let raw = Vec::<[u8; 32]>::new().encode(); + assert_eq!(contains_validator(Some(&raw), &ALICE), Ok(false)); + } + + #[test] + fn member_is_found() { + let raw = vec![BOB, ALICE].encode(); + assert_eq!(contains_validator(Some(&raw), &ALICE), Ok(true)); + assert_eq!(contains_validator(Some(&raw), &BOB), Ok(true)); + } + + #[test] + fn non_member_is_not_found() { + let raw = vec![BOB].encode(); + assert_eq!(contains_validator(Some(&raw), &ALICE), Ok(false)); + } + #[test] - fn aura_authorities_key_is_32_bytes() { - assert_eq!(aura_authorities_key().len(), 32); + fn truncated_value_is_an_error() { + let raw = vec![ALICE].encode(); + assert!(contains_validator(Some(&raw[..raw.len() - 1]), &ALICE).is_err()); } #[test] - fn aura_authorities_key_uses_correct_hashes() { - let key = aura_authorities_key(); - assert_eq!(&key[..16], &twox_128(b"Aura")); - assert_eq!(&key[16..], &twox_128(b"Authorities")); + fn trailing_bytes_are_an_error() { + let mut raw = vec![ALICE].encode(); + raw.push(0); + assert!(contains_validator(Some(&raw), &ALICE).is_err()); } } diff --git a/client/rpc-v2/src/privacy.rs b/client/rpc-v2/src/privacy.rs index 94b7de50..f6347ff0 100644 --- a/client/rpc-v2/src/privacy.rs +++ b/client/rpc-v2/src/privacy.rs @@ -4,13 +4,19 @@ //! - `privacy_getMerkleRoot` — current Merkle root as hex //! - `privacy_getMerkleProof` — sibling path for a leaf index //! - `privacy_getMerkleProofByCommitment` — sibling path for a commitment +//! - `privacy_getSubtreeRoots` — a tree's level-6 subtree roots //! - `privacy_getNullifierStatus` — whether a nullifier has been spent //! - `privacy_getPoolStats` — aggregate pool statistics //! -//! The two proof endpoints run on blocking threads and at most -//! `MAX_CONCURRENT_PROOFS` at once: a sealed tree's path is rebuilt from -//! leaves, and inline on the connection task a few of them would stall every -//! other RPC. +//! Every endpoint reads the best block, not the finalized one: a wallet builds +//! a path for a note that landed a block ago, and the root it proves against +//! stays valid for the retention window whichever fork imports the spend. +//! +//! The two proof endpoints and `privacy_getSubtreeRoots` run on blocking +//! threads, at most `proof_slots` (half the cores) at once; the rest queue, up +//! to `MAX_QUEUED_PROOFS` for at most `MAX_PROOF_WAIT`, and are refused as busy +//! past that. A sealed tree's path is rebuilt from leaves, and inline on the +//! connection task a few of them would stall every other RPC. use jsonrpsee::{ core::RpcResult, @@ -232,7 +238,7 @@ impl PrivacyRpc { /// /// A request takes a free slot, or waits for one behind at most `max_queued` /// others and for at most `max_wait`; it is refused only when the queue is full -/// or the wait runs out. Both proof RPCs are `blocking`, so each waits on its own +/// or the wait runs out. Every gated RPC is `blocking`, so each waits on its own /// thread. A slot is freed when its permit drops, even if the proof panics. struct ProofGate { state: Mutex,