diff --git a/Cargo.lock b/Cargo.lock index b9d1a775..38c8ff3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9958,6 +9958,7 @@ dependencies = [ "sc-consensus", "sc-consensus-aura", "sc-consensus-grandpa", + "sc-consensus-grandpa-rpc", "sc-consensus-manual-seal", "sc-executor", "sc-network", diff --git a/Cargo.toml b/Cargo.toml index f3dbd196..d8d4c13b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -109,6 +109,7 @@ sc-client-db = { version = "0.54.0", default-features = false } sc-consensus = { version = "0.57.0" } sc-consensus-aura = { version = "0.58.0" } sc-consensus-grandpa = { version = "0.43.0" } +sc-consensus-grandpa-rpc = { version = "0.44.0" } sc-consensus-manual-seal = { version = "0.59.0" } sc-executor = { version = "0.50.0" } sc-keystore = { version = "42.0.0" } diff --git a/template/node/Cargo.toml b/template/node/Cargo.toml index 6a531d81..5cfa93b6 100644 --- a/template/node/Cargo.toml +++ b/template/node/Cargo.toml @@ -33,6 +33,7 @@ sc-client-api = { workspace = true } sc-consensus = { workspace = true } sc-consensus-aura = { workspace = true } sc-consensus-grandpa = { workspace = true } +sc-consensus-grandpa-rpc = { workspace = true } sc-consensus-manual-seal = { workspace = true } sc-executor = { workspace = true } sc-network = { workspace = true } diff --git a/template/node/src/rpc/mod.rs b/template/node/src/rpc/mod.rs index b87c55af..51c27337 100644 --- a/template/node/src/rpc/mod.rs +++ b/template/node/src/rpc/mod.rs @@ -19,7 +19,7 @@ use sp_consensus_aura::sr25519::AuthorityId as AuraId; use sp_core::H256; use sp_inherents::CreateInherentDataProviders; use sp_keystore::Keystore; -use sp_runtime::traits::Block as BlockT; +use sp_runtime::traits::{Block as BlockT, NumberFor}; // Runtime use orbinum_runtime::{AccountId, Balance, Hash, Nonce}; @@ -30,6 +30,29 @@ pub use self::eth::{create_eth, EthDeps, LogsJournalConfig}; use self::relayer_author::{RelayerAuthor, RelayerAuthorApiServer}; use self::storage_override::EeSuffixStorageOverride; +/// GRANDPA-specific dependencies. +/// +/// Wiring these is what lets a chain be verified from the outside: Hyperbridge's relayer +/// builds Orbinum finality proofs through `grandpa_proveFinality`, and without the RPC it +/// reports the method as missing rather than as unauthorised. +/// +/// Everything here serves finality data for blocks that are already final — public by +/// definition, and independently verifiable against the authority set. No keys, no +/// private state. `sc-consensus-grandpa-rpc` marks none of the three methods `unsafe`, +/// so they are served under the `--rpc-methods Safe` the public endpoint runs with. +pub struct GrandpaDeps { + /// Shared with the voter, not a fresh empty one: a detached state makes + /// `grandpa_roundState` answer with zeroed rounds instead of failing, which reads as + /// "working" while telling the caller nothing. + pub shared_voter_state: sc_consensus_grandpa::SharedVoterState, + /// Current authority set, for `grandpa_roundState`. + pub shared_authority_set: sc_consensus_grandpa::SharedAuthoritySet>, + /// Justification notifications, for `grandpa_subscribeJustifications`. + pub justification_stream: sc_consensus_grandpa::GrandpaJustificationStream, + /// Serves `grandpa_proveFinality`. + pub finality_provider: Arc>, +} + /// Full client dependencies. pub struct FullDeps { /// The client instance to use. @@ -45,6 +68,8 @@ pub struct FullDeps { pub keystore: Arc, /// Ethereum-compatibility specific dependencies. pub eth: EthDeps, + /// GRANDPA finality RPC dependencies. + pub grandpa: GrandpaDeps, } pub struct DefaultEthConfig(std::marker::PhantomData<(C, BE)>); @@ -94,11 +119,13 @@ where u64: From<<::Header as sp_runtime::traits::Header>::Number>, CIDP: CreateInherentDataProviders + Send + 'static, CT: fp_rpc::ConvertTransaction<::Extrinsic> + Send + Sync + 'static, + NumberFor: sc_consensus_grandpa::BlockNumberOps, { use pallet_ismp_rpc::{IsmpApiServer, IsmpRpcHandler}; use pallet_relayer_rpc::{Relayer, RelayerApiServer}; use pallet_transaction_payment_rpc::{TransactionPayment, TransactionPaymentApiServer}; use pallet_zk_verifier_rpc::{ZkVerifier, ZkVerifierApiServer}; + use sc_consensus_grandpa_rpc::{Grandpa, GrandpaApiServer}; use sc_consensus_manual_seal::rpc::{ManualSeal, ManualSealApiServer}; use substrate_frame_rpc_system::{System, SystemApiServer}; @@ -113,9 +140,20 @@ where command_sink, keystore, eth, + grandpa, } = deps; io.merge(System::new(client.clone(), pool.clone()).into_rpc())?; + io.merge( + Grandpa::new( + subscription_task_executor.clone(), + grandpa.shared_authority_set, + grandpa.shared_voter_state, + grandpa.justification_stream, + grandpa.finality_provider, + ) + .into_rpc(), + )?; io.merge(TransactionPayment::new(client.clone()).into_rpc())?; io.merge(ZkVerifier::new(client.clone()).into_rpc())?; io.merge(Relayer::new(client.clone()).into_rpc())?; diff --git a/template/node/src/service.rs b/template/node/src/service.rs index 29460cfd..9b77778f 100644 --- a/template/node/src/service.rs +++ b/template/node/src/service.rs @@ -438,8 +438,24 @@ where let evm_key = crate::evm_relay_key::resolve(&config); + // GRANDPA pieces for the RPC, pulled out before `grandpa_link` is moved into the + // voter below. `shared_voter_state` is created here rather than at the voter so both + // sides hold the same one — a detached state leaves `grandpa_roundState` reporting + // zeroed rounds forever. + let shared_voter_state = sc_consensus_grandpa::SharedVoterState::empty(); + let grandpa_shared_authority_set = grandpa_link.shared_authority_set().clone(); + let grandpa_justification_stream = grandpa_link.justification_stream().clone(); + let grandpa_finality_provider = sc_consensus_grandpa::FinalityProofProvider::new_for_service( + backend.clone(), + Some(grandpa_shared_authority_set.clone()), + ); + let rpc_builder = { let client = client.clone(); + let shared_voter_state = shared_voter_state.clone(); + let grandpa_shared_authority_set = grandpa_shared_authority_set.clone(); + let grandpa_justification_stream = grandpa_justification_stream.clone(); + let grandpa_finality_provider = grandpa_finality_provider.clone(); // Cloned before the closure takes ownership: the outer `backend` is still // needed after this point (see the mapping-sync setup below). let rpc_backend = backend.clone(); @@ -522,6 +538,12 @@ where }, keystore: keystore.clone(), eth: eth_deps, + grandpa: crate::rpc::GrandpaDeps { + shared_voter_state: shared_voter_state.clone(), + shared_authority_set: grandpa_shared_authority_set.clone(), + justification_stream: grandpa_justification_stream.clone(), + finality_provider: grandpa_finality_provider.clone(), + }, }; crate::rpc::create_full( deps, @@ -677,7 +699,7 @@ where notification_service: grandpa_notification_service, voting_rule: sc_consensus_grandpa::VotingRulesBuilder::default().build(), prometheus_registry, - shared_voter_state: sc_consensus_grandpa::SharedVoterState::empty(), + shared_voter_state, telemetry: telemetry.as_ref().map(|x| x.handle()), offchain_tx_pool_factory: OffchainTransactionPoolFactory::new(transaction_pool), })?;