BABE and GRANDPA Node

nodes/babe-grandpa-node

The babe-grandpa-node uses the BABE Proof of Authority consensus engine to determine who may author blocks, and the GRANDPA finality gadget to provide deterministic finality to past blocks. This is the same design used in Polkadot. Understanding this recipe requires familiarity with Substrate's block import pipeline.

In this recipe we will learn about:

The Block Import Pipeline

The babe-grandpa node's block import pipeline will have three layers. The inner-most layer is the Substrate Client, as always. We will wrap the client with a GrandpaBlockImport, and wrap that with a BabeBlockImport.

We begin by creating the block import for GRANDPA. In addition to the block import itself, we get back a grandpa_link. This link is a channel over which the block import can communicate with the background task that actually casts GRANDPA votes. The details of the GRANDPA protocol are beyond the scope of this recipe.

let (grandpa_block_import, grandpa_link) =
	sc_finality_grandpa::block_import(
		client.clone(), &(client.clone() as std::sync::Arc<_>), select_chain
	)?;

In addition to actual blocks, this same block import will be used to import Justifications, so we clone it right after constructing it.

let justification_import = grandpa_block_import.clone();

With the GRANDPA block import created, we can now create the BABE block import. The BABE block import is the outer-most layer of the block import onion and it wraps the GRANDPA block import.

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

Again we are given back a BABE link which will be used to communicate with the import queue and background authoring worker.

The Import Queue

With the block import pipeline setup, we can proceed to creating the import queue which will feed blocks from the network into the import pipeline. We make it using BABE's import_queue helper function. Notice that it requires the BABE link, and the entire block import pipeline which we refer to as babe_block_import because BABE is the outermost layer.

let import_queue = sc_consensus_babe::import_queue(
	babe_link.clone(),
	babe_block_import.clone(),
	Some(Box::new(justification_import)),
	None,
	client,
	inherent_data_providers.clone(),
)?;

The Finality Proof Provider

Occasionally in the operation of a blockchain, other nodes will contact our node asking for proof that a particular block is finalized. To respond to these requests, we include a finality proof provider.

.with_finality_proof_provider(|client, backend| {
	let provider = client as Arc<dyn StorageAndProofProvider<_, _>>;
	Ok(Arc::new(GrandpaFinalityProofProvider::new(backend, provider)) as _)
})?

Spawning the BABE Authorship Task

Any node that is acting as an authority and participating in BABE consensus, must run an async authorship task. We begin by creating an instance of BabeParams.

let babe_config = sc_consensus_babe::BabeParams {
	keystore: service.keystore(),
	client,
	select_chain,
	env: proposer,
	block_import,
	sync_oracle: service.network(),
	inherent_data_providers: inherent_data_providers.clone(),
	force_authoring,
	babe_link,
	can_author_with,
};

With the parameters established, we can now create and spawn the authorship future.

let babe = sc_consensus_babe::start_babe(babe_config)?;
service.spawn_essential_task("babe", babe);

Spawning the GRANDPA Task

Just as we needed an async worker to author blocks with BABE, we need an async worker to listen to and cast GRANDPA votes. Again, we begin by creating an instance of GrandpaParams

let grandpa_config = sc_finality_grandpa::GrandpaParams {
	config: grandpa_config,
	link: grandpa_link,
	network: service.network(),
	inherent_data_providers: inherent_data_providers.clone(),
	telemetry_on_connect: Some(service.telemetry_on_connect_stream()),
	voting_rule: sc_finality_grandpa::VotingRulesBuilder::default().build(),
	prometheus_registry: service.prometheus_registry(),
};

With the parameters established, we can now create and spawn the authorship future.

service.spawn_essential_task(
	"grandpa-voter",
	sc_finality_grandpa::run_grandpa_voter(grandpa_config)?
);

Disabled GRANDPA

Proof of Authority networks generally contain many full nodes that are not authorities. When GRANDPA is present in the network, we still need to tell the node how to interpret GRANDPA-related messages it may receive (just ignore them) and ensure that the correct inherents are still included in blocks in the case that the node is an authority in BABE but not GRANDPA.

sc_finality_grandpa::setup_disabled_grandpa(
	service.client(),
	&inherent_data_providers,
	service.network(),
)?;

Constraints on the Runtime

Runtime APIs

Both BABE and GRANDPA rely on getting their authority sets from the runtime via the BabeAPI and the GrandpaAPI. So trying to build this node with a runtime that does not provide these APIs will fail to compile.

Pre-Runtime Digests

Just as we cannot use this node with a runtime that does not provide the appropriate runtime APIs, we also cannot use a runtime designed for this node with different consensus engines.

Because BABE is a slot-based consensus engine, it must inform the runtime which slot each block was intended for. To do this, it uses a technique known as a pre-runtime digest. It has two kinds, PrimaryPreDigest and SecondaryPlainPreDigest. The BABE authorship task automatically inserts these digest items in each block it authors.

Because the runtime needs to interpret these pre-runtime digests, they are not optional. That means runtimes that expect the pre-digests cannot be used, unmodified, in nodes that don't provide the pre-digests. Unlike other runtimes in the Recipes where runtimes can be freely swapped between nodes, the babe-grandpa-runtime can only be used in a node that is actually running BABE