Skip to content

Consensus & Finality

Moonbeam v Ethereum - Consensus and Finality Banner

Introduction

While Moonbeam strives to be compatible with Ethereum's Web3 API and EVM, there are some important Moonbeam differences that developers should know and understand in terms of consensus and finality.

In short, consensus is a way for different parties to agree on a shared state. As blocks are created, nodes in the network must decide which block will represent the next valid state. Finality defines when that valid state cannot be altered or reversed.

At the time of writing, Ethereum uses a consensus protocol based on Proof-of-Work (PoW), which provides probabilistic finality. On the contrary, Moonbeam uses a hybrid consensus protocol based on Nominated Proof-of-Stake (NPoS), which provides deterministic finality.

This guide will outline some of these main differences around consensus and finality, and what to expect when using Moonbeam for the first time.

Ethereum Consensus and Finality

As stated before, Ethereum is currently using a PoW consensus protocol and the longest chain rule, where finality is probabilistic.

Probabilistic finality means that the probability that a block (and all its transactions) will not be reverted increases as more blocks are built on top of it. Therefore, the higher the number of blocks you wait, the higher the certainty that a transaction will not be re-organized, and consequently reverted. As suggested by this blog on finality by Vitalik, "you can wait 13 confirmations for a one-in-a-million chance of the attacker succeeding."

Moonbeam Consensus and Finality

In Polkadot, there are collators and validators. Collators maintain parachains (in this case, Moonbeam) by collecting transactions from users and producing state transition proofs for the Relay Chain validators. The collators set (nodes that produce blocks) are selected based on the stake they have in the network.

For finality, Polkadot/Kusama rely on GRANDPA. GRANDPA provides deterministic finality for any given transaction (block). In other terms, when a block/transaction is marked as final, it can't be reverted except via on-chain governance or forking. Moonbeam follows this deterministic finality.

Main Differences

In terms of consensus, Moonbeam is based on Nominated Proof-of-Stake, while Ethereum relies on Proof-of-Work, which are very different. Consequently, Proof of Work concepts, such as difficulty, uncles, hashrate, generally don’t have meaning within Moonbeam.

For APIs that return values related to Ethereum’s Proof of Work, default values are returned. Existing Ethereum contracts that rely on Proof of Work internals (e.g., mining pool contracts) will almost certainly not work as expected on Moonbeam.

In terms of finality, on Moonbeam, you can check when a transaction is finalized, meaning that it can't be reverted. The strategy is fairly simple: 1. You ask the network the hash of the latest finalized block 2. You retrieve the block number using the hash 3. You compare it with the block number of your transaction. If your transaction was included in a previous block, it is finalized 4. As as safety check, retrieve the block by number, and verify that the given transaction hash is in the block

The following sections outline how you can check for transaction finality using both the Ethereum JSON-RPC (custom Web3 request) and the Substrate (Polkadot) JSON-RPC.

Checking Tx Finality with Ethereum Libraries

You can make calls to the Substrate JSON-RPC using the send method of both Web3.js and Ethers.js.

Custom RPC requests are also possible using Web3.py with the make_request method. You can use the Web3.js example as a baseline.

Note

The code snippets presented in the following sections are not meant for production environments. Please make sure you adapt it for each use-case.

Custom RPC Requests with Web3.js

With Web3.js, you can make custom RPC requests with the web3.currentProvider.send() method. However, at the time of writing, this was not in the official Web3.js documentation.

Given a transaction hash (tx_hash), the following code snippet uses Web3.js to fetch the current finalized block and compare it with the block number of the transaction you've provided.

The code relies on two custom RPC requests from the Substrate JSON-RPC: chain_getFinalizedHead and chain_getHeader. The first request gets the block hash of the last finalized block. The second request gets the block header for a given block hash. It also uses the same custom RPC function for eth_getTransactionReceipt, but this can be modified to use the regular web3.eth.getTransactionReceipt(hash) method. The same is true for eth_getBlockByNumber, to check if the given transactio hash is included in the block.

import Web3 from 'web3';

// Define the TxHash to Check Finality
const txHash = 'tx_hash';

// Define the Web3 provider
const web3 = new Web3('https://rpc.moonriver.moonbeam.network');

// Define the function for the Custom Web3 Request
const customWeb3Request = async (web3Provider, method, params) => {
  try {
    return await requestPromise(web3Provider, method, params);
  } catch (error) {
    throw new Error(error);
  }
};

// In Web3.js we need to return a promise
const requestPromise = async (web3Provider, method, params) => {
  return new Promise((resolve, reject) => {
    web3Provider.send(
      {
        jsonrpc: '2.0',
        id: 1,
        method,
        params,
      },
      (error, result) => {
        if (error) {
          reject(error.message);
        } else {
          if (result.error) {
            reject(result.error.message);
          }
          resolve(result);
        }
      }
    );
  });
};

const main = async () => {
  // Get the latest finalized block of the Substrate chain
  // Uses Polkadot JSON-RPC
  const finalizedHeadHash = await customWeb3Request(
    web3.currentProvider,
    'chain_getFinalizedHead',
    []
  );

  // Get finalized block header to retrieve number
  // Uses Polkadot JSON-RPC
  const finalizedBlockHeader = await customWeb3Request(web3.currentProvider, 'chain_getHeader', [
    finalizedHeadHash.result,
  ]);
  const finalizedBlockNumber = parseInt(finalizedBlockHeader.result.number, 16);

  // Get the transaction receipt of the given tx hash
  // Uses Ethereum JSON-RPC
  const txReceipt = await customWeb3Request(web3.currentProvider, 'eth_getTransactionReceipt', [
    txHash,
  ]);

  // As a safety check, get given block to check if transaction is included
  // Uses Ethereum JSON-RPC
  const txBlock = await customWeb3Request(web3.currentProvider, 'eth_getBlockByNumber', [
    txReceipt.result.blockNumber,
    false,
  ]);

  // If block number of receipt is not null, compare it against finalized head
  if (txReceipt) {
    // Convert to Number
    const txBlockNumber = parseInt(txReceipt.result.blockNumber, 16);

    console.log(`Current finalized block number is ${finalizedBlockNumber}`);
    console.log(
      `Your transaction in block ${txBlockNumber} is finalized? ${
        finalizedBlockNumber >= txBlockNumber
      }`
    );
    console.log(
      `Your transaction in indeed in block ${txBlockNumber}? ${txBlock.result.transactions.includes(
        txHash
      )}`
    );
  } else {
    console.log('Your transaction has not been included in the canonical chain');
  }
};

main();

Custom RPC Requests with Ethers.js

With Ethers.js, you can make custom RPC requests with the JsonRpcProvider web3 provider. This will enable the web3Provider.send() method, as detailed in their documentation site.

Given a transaction hash (tx_hash), the following code snippet uses Ethers.js to fetch the current finalized block and compare it with the block number of the transaction you've provided.

The code relies on two custom RPC requests from the Substrate JSON-RPC: chain_getFinalizedHead and chain_getHeader. The first request gets the block hash of the last finalized block. The second request gets the block header for a given block hash. It also uses the same custom RPC function for eth_getTransactionReceipt, but this can be modified to use the regular web3Provider.getTransactionReceipt(hash) method. The same is true for eth_getBlockByNumber, to check if the given transactio hash is included in the block.

import ethers from 'ethers';

// Define the TxHash to Check Finality
const txHash = 'tx_hash';

// Define the RPC of the Provider
const providerRPC = {
  moonriver: {
    name: 'moonriver',
    rpc: 'https://rpc.moonriver.moonbeam.network',
    chainId: 1285,
  },
};

// Define the Web3 provider
const web3Provider = new ethers.providers.JsonRpcProvider(providerRPC.moonriver.rpc, {
  chainId: providerRPC.moonriver.chainId,
  name: providerRPC.moonriver.name,
});

// Define the function for the Custom Web3 Request
const customWeb3Request = async (web3Provider, method, params) => {
  try {
    return await web3Provider.send(method, params);
  } catch (error) {
    throw new Error(error.body);
  }
};

const main = async () => {
  // Get the latest finalized block of the Substrate chain
  // Uses Polkadot JSON-RPC
  const finalizedHeadHash = await customWeb3Request(web3Provider, 'chain_getFinalizedHead', []);

  // Get finalized block header to retrieve number
  // Uses Polkadot JSON-RPC
  const finalizedBlockHeader = await customWeb3Request(web3Provider, 'chain_getHeader', [
    finalizedHeadHash,
  ]);
  const finalizedBlockNumber = parseInt(finalizedBlockHeader.number, 16);

  // Get the transaction receipt of the given tx hash
  // Uses Ethereum JSON-RPC
  const txReceipt = await customWeb3Request(web3Provider, 'eth_getTransactionReceipt', [txHash]);

  // As a safety check, get given block to check if transaction is included
  // Uses Ethereum JSON-RPC
  const txBlock = await customWeb3Request(web3Provider, 'eth_getBlockByNumber', [
    txReceipt.blockNumber,
    false,
  ]);

  // If block number of receipt is not null, compare it against finalized head
  if (txReceipt) {
    // Convert to Number
    const txBlockNumber = parseInt(txReceipt.blockNumber, 16);

    console.log(`Current finalized block number is ${finalizedBlockNumber}`);
    console.log(
      `Your transaction in block ${txBlockNumber} is finalized? ${
        finalizedBlockNumber >= txBlockNumber
      }`
    );
    console.log(
      `Your transaction in indeed in block ${txBlockNumber}? ${txBlock.transactions.includes(
        txHash
      )}`
    );
  } else {
    console.log('Your transaction has not been included in the canonical chain');
  }
};

main();

Checking Tx Finality with Polkadot.js

The Polkadot.js API package provides developers a way to interact with Substrate chains using Javascript.

Given a transaction hash (tx_hash), the following code snippet uses Polkadot.js to fetch the current finalized block and compare it with the block number of the transaction you've provided. You can find all the available information about Polkadot.js and the Substrate JSON RPC in their official documentation site.

The code relies on three RPC requests from the Substrate JSON-RPC: chain_getFinalizedHead, chain_getHeader and eth_getTransactionReceipt. The first request gets the block hash of the last finalized block. The second request gets the block header for a given block hash. The third request is fairly similar to the Ethereum JSON-RPC method, but it is done directly via the Substrate metadata.

import { ApiPromise, WsProvider } from '@polkadot/api';
import { typesBundle } from 'moonbeam-types-bundle';

// Define the TxHash to Check Finality
const txHash = 'tx_hash';

// Define the provider
const wsProvider = new WsProvider('wss://wss.moonriver.moonbeam.network');

const main = async () => {
  // Create the provider using Moonbeam types
  const polkadotApi = await ApiPromise.create({
    provider: wsProvider,
    typesBundle: typesBundle,
  });
  await polkadotApi.isReady;

  // Get the latest finalized block of the Substrate chain
  const finalizedHeadHash = (await polkadotApi.rpc.chain.getFinalizedHead()).toJSON();

  // Get finalized block header to retrieve number
  const finalizedBlockHeader = (await polkadotApi.rpc.chain.getHeader(finalizedHeadHash)).toJSON();

  // Get the transaction receipt of the given tx hash
  const txReceipt = (await polkadotApi.rpc.eth.getTransactionReceipt(txHash)).toJSON();

  // We can not verify if the tx is in block because polkadotApi.rpc.eth.getBlockByNumber
  // does not return the list of tx hash

  // If block number of receipt is not null, compare it against finalized head
  if (txReceipt) {
    console.log(`Current finalized block number is ${finalizedBlockHeader.number}`);
    console.log(
      `Your transaction in block ${txReceipt.blockNumber} is finalized? ${
        finalizedBlockHeader.number >= txReceipt.blockNumber
      }`
    );
  } else {
    console.log('Your transaction has not been included in the canonical chain');
  }
};

main();