Logo
New RPC users get 35% off their first monthView the offer
OnFinality Learn
Network & Protocol Guides12 min read

Polkadot Finality: GRANDPA Justifications, Finalized Head, and waitForFinalized over JSON-RPC

Learn how Polkadot's GRANDPA finality works and how to query, subscribe, and wait for the finalized head over JSON-RPC to avoid treating un-finalized blocks as permanent.

TL;DR

Polkadot separates block production (BABE) from finality (GRANDPA). The finalized head is the irreversible block certified by a 2/3+1 validator authority set, while the best head is optimistic and can be reverted. Over JSON-RPC, use chain_getFinalizedHead and chain_subscribeFinalizedHeads to track the safe head, and polkadot.js's waitForFinalized to poll until a specific block is finalized. This article explains the mechanism and provides a runnable script to measure best-vs-finalized spread.

Direct Answer: Why You Must Track the Finalized Head, Not the Best Head

When you build on Polkadot, you must not treat the latest block returned by chain_getBlock or chain_getHead as permanent. That block is the best head produced by BABE (the block production engine) and can be reverted if a fork becomes canonical. The only block that is irreversible is the finalized head, certified by GRANDPA (GHOST-based Recursive Ancestor Deriving Prefix Agreement). Over JSON-RPC, you can query it with chain_getFinalizedHead, subscribe to it with chain_subscribeFinalizedHeads, and in polkadot.js use api.rpc.chain.getFinalizedHead() or api.derive.chain.waitForFinalized() to wait until a specific block is finalized. This article explains the GRANDPA mechanism, maps it to JSON-RPC methods, and provides a reproducible script to observe the difference in practice.

For a broader context on Polkadot's network and endpoints, see the Polkadot network page and the Polkadot RPC guide.

GRANDPA Finality Mechanism: How Polkadot Achieves Irreversibility

Polkadot's consensus separates block production from finality. BABE (Blind Assignment for Blockchain Extension) produces blocks in slots, with one block randomly elected per slot. This is optimistic and fast, but a block can be superseded if a different fork gains more work. GRANDPA is a finality gadget that runs in parallel: a set of permissioned validator authorities vote on a chain prefix (a GHOST ancestor) rather than on a single block. Once more than two-thirds of the weighted authority set votes for a prefix, they produce a signed justification that finalizes that prefix irreversibly. This justification is a cryptographic proof that can be verified by any light client.

The key implication is that the chain has two 'head' notions: the best head (most-work block from BABE) and the finalized head (certified by GRANDPA). For any irreversible effect—a payment, an indexer watermark, or an account state you will act on—you should rely on the finalized head. For speculative UIs (e.g., showing recent transactions that might be reverted), the best head is acceptable. This distinction is documented in the Polkadot wiki on GRANDPA and the GRANDPA paper.

Authority-set changes occur across eras. When the validator set changes, GRANDPA uses a new authority set, and justifications from previous eras remain valid for historical blocks. This is why you can verify finality of old blocks even after the set changes.

Mapping Finality to JSON-RPC: Methods and Namespaces

Polkadot's JSON-RPC exposes several methods to query finality. The most important are:

  • chain_getFinalizedHead returns the hash of the latest finalized block. This is the canonical 'safe to trust' marker.

  • chain_getHead (or chain_getBlock with the 'latest' parameter) returns the best block, which is not final.

  • chain_subscribeFinalizedHeads pushes a stream of finalized block headers as they are finalized.

  • chain_subscribeAllHeads pushes both best and finalized heads (and possibly others), allowing you to compare them.

  • The grandpa_* namespace (e.g., grandpa_proveFinality, grandpa_roundState, grandpa_submitGrandpaExtrinsic) provides justification and round evidence. These methods may not be exposed on all public endpoints; check your provider's documentation.

In polkadot.js, api.rpc.chain.getFinalizedHead() and api.rpc.chain.subscribeFinalizedHeads() wrap these methods. Additionally, api.derive.chain.waitForFinalized(blockHash) polls the finalized head until the given block is finalized, throwing an RpcError or TimeoutError if finality stalls. This is the recommended way to wait for finality in applications.

For a deeper dive into RPC methods and endpoint selection, see the Polkadot RPC guide and the Polkadot WebSocket RPC guide.

Practical Example: Measuring Best vs Finalized Head Spread

To see the difference between best and finalized heads in practice, run the following Node.js script. It subscribes to both allHeads and finalizedHeads, prints the block number spread over a 60-second interval, and then uses waitForFinalized to confirm a specific block is finalized. You need a Polkadot endpoint (e.g., wss://rpc.polkadot.io) and the @polkadot/api package.

Replace YOUR_ENDPOINT with your own endpoint. If you use OnFinality's API service, you get a dedicated endpoint with access to all RPC methods. Note that public endpoints may have rate limits; see RPC pricing for details.

// Save as finality-check.js
// Run: node finality-check.js
const { ApiPromise, WsProvider } = require('@polkadot/api');

const WS_URL = process.env.WS_URL || 'wss://rpc.polkadot.io';
const INTERVAL_MS = 60000; // 60 seconds

async function main() {
  const provider = new WsProvider(WS_URL);
  const api = await ApiPromise.create({ provider });

  console.log('Connected to', WS_URL);

  // Subscribe to all heads (best and finalized)
  const unsubAll = await api.rpc.chain.subscribeAllHeads((header) => {
    console.log(`All head: #${header.number} hash=${header.hash}`);
  });

  // Subscribe to finalized heads
  const unsubFinalized = await api.rpc.chain.subscribeFinalizedHeads((header) => {
    console.log(`Finalized head: #${header.number} hash=${header.hash}`);
  });

  // Wait for interval
  await new Promise(resolve => setTimeout(resolve, INTERVAL_MS));

  // Get current best and finalized
  const best = await api.rpc.chain.getHead();
  const finalized = await api.rpc.chain.getFinalizedHead();
  const bestHeader = await api.rpc.chain.getHeader(best);
  const finalizedHeader = await api.rpc.chain.getHeader(finalized);
  console.log(`\nAfter ${INTERVAL_MS/1000}s:`);
  console.log(`Best block: #${bestHeader.number}`);
  console.log(`Finalized block: #${finalizedHeader.number}`);
  console.log(`Spread: ${bestHeader.number - finalizedHeader.number} blocks`);

  // Wait for a specific block to be finalized (e.g., the best block we just saw)
  try {
    const finalizedHash = await api.derive.chain.waitForFinalized(best);
    console.log(`Block #${bestHeader.number} is finalized with hash ${finalizedHash}`);
  } catch (e) {
    console.error('waitForFinalized failed:', e.message);
  }

  // Cleanup
  await unsubAll();
  await unsubFinalized();
  await api.disconnect();
}

main().catch(console.error);

Expected Output and Results Table

When you run the script, you will see a stream of headers. The finalized head will typically lag behind the best head by a few blocks, but the spread can grow if validators are offline or the network is congested. The exact numbers vary by network conditions and are not fixed. Fill in the table below with your observations to characterize your endpoint's behavior.

Note: The script uses waitForFinalized on the best block at the end of the interval. If finality is slow, this may time out. The timeout is configurable in polkadot.js; by default it may throw after a certain period.

  • Record the best block number and finalized block number at several points during the interval.
  • Calculate the spread (best - finalized) and note any changes.
  • If you use a custom endpoint, compare the spread with a public endpoint to see if there are differences in finality propagation.
  • If you see a large spread (e.g., > 10 blocks), investigate whether the network is under stress or your endpoint is not keeping up.
| Time (s) | Best Block | Finalized Block | Spread |
|----------|------------|-----------------|--------|
| 0        |            |                 |        |
| 15       |            |                 |        |
| 30       |            |                 |        |
| 45       |            |                 |        |
| 60       |            |                 |        |

Failure and Fix Checklist: When Finality Stalls or Lags

Finality lag is normal, but an extended period where the finalized head does not advance indicates a problem. Here is a checklist to diagnose and handle it:

  • Check if the best head is advancing: If the best head is also stalled, the node may be disconnected or the network is down. Verify your WebSocket connection and try another endpoint.

  • Check authority set health: If validators are offline, GRANDPA cannot finalize. You can query grandpa_roundState (if available) to see the current round and votes. Public endpoints may not expose this; use a dedicated endpoint from OnFinality's API service if needed.

  • Decide whether to act on 'best': If finality stalls, you must decide whether to continue acting on the best head. For irreversible operations, it is safer to halt until finality resumes. For speculative UIs, you can continue but clearly indicate that blocks are not final.

  • Use waitForFinalized with a timeout: In your application, always set a reasonable timeout when waiting for finality. If it times out, log the error and alert your team.

  • Monitor justification availability: If you need to prove finality, use grandpa_proveFinality to get a justification. Note that historical justifications may be pruned on full nodes; archive nodes may retain them. See Querying Polkadot historical state over RPC for more.

  • Understand authority-set changes: When the validator set changes, finality may briefly pause. This is normal and should resolve within a few rounds.

Limitations and Tradeoffs of Finality Queries

While the JSON-RPC methods are standard, there are limitations to be aware of:

  • Endpoint availability: Not all public endpoints expose the grandpa_* methods. If you need them, use a dedicated endpoint from a provider like OnFinality's API service.

  • Historical justification pruning: Full nodes may prune old justifications to save space. Archive nodes are more likely to retain them, but they are more expensive. Check your node's configuration.

  • Finality latency is variable: The time to finality depends on network conditions, validator performance, and the number of authorities. Do not assume a fixed latency; always design for eventual finality.

  • waitForFinalized may throw: If the block is never finalized (e.g., due to a fork), the promise may reject. Handle errors gracefully.

  • Privacy and rate limits: Public endpoints may rate-limit subscriptions. For production, use a dedicated endpoint with higher limits. See RPC pricing for options.

For a comparison with other finality models, see the OP-Stack finality stages article.

Next Steps: Build Reliable Polkadot Integrations

Now that you understand GRANDPA finality and how to query it, you can build more reliable integrations. Start by using chain_getFinalizedHead for any state you intend to act on, and subscribe to chain_subscribeFinalizedHeads for real-time updates. Use waitForFinalized when you need to confirm a specific transaction is irreversible.

For further reading, explore the OnFinality Learn hub for more guides on Polkadot and other networks. If you are concerned about latency, see Polkadot RPC latency. For WebSocket best practices, see Polkadot WebSocket RPC. And for historical state queries, see Querying Polkadot historical state over RPC.

If you need a production-grade endpoint with access to all RPC methods, consider OnFinality's API service. We provide dedicated endpoints with high availability and low latency, so you can focus on building rather than managing infrastructure.

Never Worry about Infrastructure Again

OnFinality takes away the heavy lifting of DevOps so you can build smarter and faster.

Get Started