Skip to content

Runtime API & Outer Node Service

Tin Chung edited this page May 12, 2024 · 5 revisions

Screenshot 2024-05-12 at 13 17 47

Related code commits

Additional dependencies

Below are mixes of core libraries and primitive libraries. To understand the library structure of Substrate. Read this documentation

Crate name Description Type
sc-consensus-babe Subtrate core utilities for BABE Core
sc-consensus-epochs Generic utilities for epoch-based consensus engines. Core
sc-consensus-babe-rpc RPC api for babe. Core
sc-consensus-grandpa Integration of the GRANDPA finality gadget into substrate. Core
sp-consensus-grandpa Primitives for GRANDPA integration, suitable for WASM compilation. Primitive
sc_rpc_spec_v2 Substrate JSON-RPC interface v2. Core
sp_consensus Common utilities for building and using consensus engines in substrate. Primitive
sc_rpc_api A collection of RPC methods and subscriptions supported by all substrate clients. Core
sc_consensus_slots Some consensus algorithms have a concept of slots, which are intervals in time during which certain events can and/or must occur. This crate provides generic functionality for slots. Core
sp_transaction_storage_proof Storage proof primitives. Contains types and basic code to extract storage proofs for indexed transactions. Primitive

What is Runtime API in Substrate?

Substrate node consist of outer node services and a runtime and this separation of responsibilities is an important concept for designing Susbtrate-based chains and building upgradable logic. However, the outer node services and the runtime must communicate with each other to complete many critical operations, including reading and writing data and performing state transitions. The outer node services communicate with the runtime by calling runtime application programming interfaces to perform specific tasks.

Configure collator key set

Add BABE to the SessionKeys

Substrate provides the Session pallet, which allows validators to manage their session keys.

Session keys are "hot keys" that are used by validators to sign consensus-related messages.

  • They are not meant to be used as account keys that control funds and should only be used for their intended purpose.
  • They can be changed regularly; your Controller only needs to create new session keys by signing a session's public key and broadcasting this certificate via an extrinsic.
  • Session keys are also defined generically and made concrete in the runtime.
impl_opaque_keys! {
    pub struct SessionKeys {
-        pub aura: Aura,
+        pub babe: Babe,
+        pub grandpa: Grandpa,
    }
}

You can declare any number of Session keys. The above code declares the BABE session key used in pallet_session because we need the validator to sign BABE consensus-related messages.

Add Runtime API for BABE

We need to add additional sp_consensus_babe package to our runtime. This package contains the primitives types and utilities for the BABE consensus.

sp-consensus-babe = { workspace = true }

[features]
std=[
	"sp-consensus-babe/std",
]

First, we implement the trait BabeApi for the Runtime struct with unimplemented function body. We will finish the implementation of these functions later with explanation.

 impl sp_consensus_babe::BabeApi<Block> for Runtime {
        fn configuration() -> sp_consensus_babe::BabeConfiguration {
            unimplemented!();
        }

        fn current_epoch_start() -> sp_consensus_babe::Slot {
            Babe::current_epoch_start()
        }

        fn current_epoch() -> sp_consensus_babe::Epoch {
            Babe::current_epoch()
        }

        fn next_epoch() -> sp_consensus_babe::Epoch {
            Babe::next_epoch()
        }

        fn generate_key_ownership_proof(
            _slot: sp_consensus_babe::Slot,
            authority_id: sp_consensus_babe::AuthorityId,
        ) -> Option<sp_consensus_babe::OpaqueKeyOwnershipProof> {
            unimplemented!();
        }

        fn submit_report_equivocation_unsigned_extrinsic(
            equivocation_proof: sp_consensus_babe::EquivocationProof<<Block as BlockT>::Header>,
            key_owner_proof: sp_consensus_babe::OpaqueKeyOwnershipProof,
        ) -> Option<()> {
            None
        }
    }
  • sp_consensus_babe::OpaqueKeyOwnershipProof: An opaque type used to represent the key ownership proof at the runtime API boundary. The inner value is an encoded representation of the actual key ownership proof which will be parameterized when defining the runtime. At the runtime API boundary this type is unknown and as such we keep this opaque representation, implementors of the runtime API will have to make sure that all usages of OpaqueKeyOwnershipProof refer to the same type.

Add Runtime API for Grandpa

impl sp_consensus_grandpa::GrandpaApi<Block> for Runtime {
        fn grandpa_authorities() -> sp_consensus_grandpa::AuthorityList {
            Grandpa::grandpa_authorities()
        }

        fn current_set_id() -> sp_consensus_grandpa::SetId {
            Grandpa::current_set_id()
        }

        fn submit_report_equivocation_unsigned_extrinsic(
            equivocation_proof: sp_consensus_grandpa::EquivocationProof<
                <Block as BlockT>::Hash,
                NumberFor<Block>,
            >,
            key_owner_proof: sp_consensus_grandpa::OpaqueKeyOwnershipProof,
        ) -> Option<()> {
            let key_owner_proof = key_owner_proof.decode()?;

            Grandpa::submit_unsigned_equivocation_report(
                equivocation_proof,
                key_owner_proof,
            )
        }

        fn generate_key_ownership_proof(
            _set_id: sp_consensus_grandpa::SetId,
            authority_id: GrandpaId,
        ) -> Option<sp_consensus_grandpa::OpaqueKeyOwnershipProof> {
            use codec::Encode;

            Historical::prove((sp_consensus_grandpa::KEY_TYPE, authority_id))
                .map(|p| p.encode())
                .map(sp_consensus_grandpa::OpaqueKeyOwnershipProof::new)
        }
    }

Configure chain specification for BABE / GRANDPA collators

For now, we don't remove code related to AURA yet, we will work on it after BABE is successfully implemented in our Runtime and Node. It would be clearer if you visit the code directly to view the changes but here is some of the changes made to the node/src/chain_spec.rs

pub fn template_session_keys(
-    aura: AuraId,
+    babe: BabeId,
+    grandpa: GrandpaId,
) -> parachain_template_runtime::SessionKeys {
-    parachain_template_runtime::SessionKeys { aura: keys }
+    parachain_template_runtime::SessionKeys { babe, grandpa }
}

fn testnet_genesis(
-    invulnerables: Vec<(AccountId, AuraId)>,
+    invulnerables: Vec<(AccountId, BabeId, GrandpaId)>,
    endowed_accounts: Vec<AccountId>,
    root: AccountId,
    id: ParaId,
) -> serde_json::Value {...}

Learn more about Setting up the Collator.

Configure Outer Node Service

/// Starts a `ServiceBuilder` for a full service.
///
/// Use this macro if you don't actually need the full service, but just the builder in order to
/// be able to perform chain operations.
pub fn new_partial(
    config: &Configuration,
) -> Result<
    Service<
        impl Fn(
            sc_rpc::DenyUnsafe,
            sc_rpc::SubscriptionTaskExecutor,
        ) -> Result<jsonrpsee::RpcModule<()>, sc_service::Error>,
    >,
    sc_service::Error,
> {
    let telemetry = config
        .telemetry_endpoints
        .clone()
        .filter(|x| !x.is_empty())
        .map(|endpoints| -> Result<_, sc_telemetry::Error> {
            let worker = TelemetryWorker::new(16)?;
            let telemetry = worker.handle().new_telemetry(endpoints);
            Ok((worker, telemetry))
        })
        .transpose()?;

    let heap_pages = config
        .default_heap_pages
        .map_or(DEFAULT_HEAP_ALLOC_STRATEGY, |h| HeapAllocStrategy::Static {
            extra_pages: h as _,
        });

    let wasm = WasmExecutor::builder()
        .with_execution_method(config.wasm_method)
        .with_onchain_heap_alloc_strategy(heap_pages)
        .with_offchain_heap_alloc_strategy(heap_pages)
        .with_max_runtime_instances(config.max_runtime_instances)
        .with_runtime_cache_size(config.runtime_cache_size)
        .build();

    let executor = ParachainExecutor::new_with_wasm_executor(wasm);

    let (client, backend, keystore_container, task_manager) =
        sc_service::new_full_parts_record_import::<Block, RuntimeApi, _>(
            config,
            telemetry.as_ref().map(|(_, telemetry)| telemetry.handle()),
            executor,
            true,
        )?;
    let client = Arc::new(client);

    let select_chain = sc_consensus::LongestChain::new(backend.clone());

    let telemetry_worker_handle = telemetry.as_ref().map(|(worker, _)| worker.handle());

    let telemetry = telemetry.map(|(worker, telemetry)| {
        task_manager
            .spawn_handle()
            .spawn("telemetry", None, worker.run());
        telemetry
    });

    let transaction_pool = sc_transaction_pool::BasicPool::new_full(
        config.transaction_pool.clone(),
        config.role.is_authority().into(),
        config.prometheus_registry(),
        task_manager.spawn_essential_handle(),
        client.clone(),
    );

    let block_import = ParachainBlockImport::new(client.clone(), backend.clone());

    let (grandpa_block_import, grandpa_link) = sc_consensus_grandpa::block_import(
        client.clone(),
        GRANDPA_JUSTIFICATION_PERIOD,
        &(client.clone() as Arc<_>),
        select_chain.clone(),
        telemetry.as_ref().map(|x| x.handle()),
    )?;
    let justification_import = grandpa_block_import.clone();

    let (babe_block_import, babe_link) = sc_consensus_babe::block_import(
        sc_consensus_babe::configuration(&*client)?,
        grandpa_block_import,
        client.clone(),
    )?;

    let slot_duration = babe_link.config().slot_duration();

    let babe_import_queue_params = sc_consensus_babe::ImportQueueParams {
        link: babe_link.clone(),
        block_import: babe_block_import.clone(),
        justification_import: Some(Box::new(justification_import)),
        client: client.clone(),
        select_chain: select_chain.clone(),
        create_inherent_data_providers: move |_, ()| async move {
            let timestamp = sp_timestamp::InherentDataProvider::from_system_time();

            let slot =
				sp_consensus_babe::inherents::InherentDataProvider::from_timestamp_and_slot_duration(
					*timestamp,
					slot_duration,
				);

            Ok((slot, timestamp))
        },
        spawner: &task_manager.spawn_essential_handle(),
        registry: config.prometheus_registry(),
        telemetry: telemetry.as_ref().map(|x| x.handle()),
        offchain_tx_pool_factory: OffchainTransactionPoolFactory::new(RejectAllTxPool::default()),
    };

    let (import_queue, babe_worker_handle) =
        sc_consensus_babe::import_queue(babe_import_queue_params)?;

    let import_setup = (babe_block_import, grandpa_link, babe_link);
    let (rpc_extensions_builder, rpc_setup) = {
        let (_, grandpa_link, _babe_link) = &import_setup;

        let justification_stream = grandpa_link.justification_stream();
        let shared_authority_set = grandpa_link.shared_authority_set().clone();
        let shared_voter_state = sc_consensus_grandpa::SharedVoterState::empty();
        let shared_voter_state2 = shared_voter_state.clone();

        let finality_proof_provider = sc_consensus_grandpa::FinalityProofProvider::new_for_service(
            backend.clone(),
            Some(shared_authority_set.clone()),
        );

        let client = client.clone();
        let pool = transaction_pool.clone();
        let select_chain = select_chain.clone();
        let keystore = keystore_container.keystore();
        let chain_spec = config.chain_spec.cloned_box();

        let rpc_extensions_builder = move |deny_unsafe, subscription_executor| {
            let deps = FullDeps {
                client: client.clone(),
                pool: pool.clone(),
                select_chain: select_chain.clone(),
                chain_spec: chain_spec.cloned_box(),
                deny_unsafe,
                babe: BabeDeps {
                    babe_worker_handle: babe_worker_handle.clone(),
                    keystore: keystore.clone(),
                },
                grandpa: GrandpaDeps {
                    shared_voter_state: shared_voter_state.clone(),
                    shared_authority_set: shared_authority_set.clone(),
                    justification_stream: justification_stream.clone(),
                    subscription_executor,
                    finality_provider: finality_proof_provider.clone(),
                },
            };

            create_full(deps).map_err(Into::into)
        };

        (rpc_extensions_builder, shared_voter_state2)
    };

    Ok(PartialComponents {
        backend,
        client,
        import_queue,
        keystore_container,
        task_manager,
        transaction_pool,
        select_chain,
        other: (
            block_import,
            rpc_extensions_builder,
            import_setup,
            rpc_setup,
            telemetry,
            telemetry_worker_handle,
        ),
    })
}