Skip to content

Moonbeam 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. In contrast, Moonbeam uses a hybrid consensus protocol based on Delegated Proof-of-Stake (DPoS), which provides deterministic finality. DPoS is an evolution of Polkadot's Nominated Proof of Stake (NPoS) concept, that puts more power into the hands of token holders by allowing delegators to choose which collator candidate they want to support and in what magnitude.

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 in 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 collator 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 words, 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 Delegated 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.

However, the deterministic finality of Moonbeam can be used to provide a better user experience than is currently possible in Ethereum. The strategy to check for transaction finality is fairly simple:

  1. You ask the network for 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 Moonbeam RPC Endpoints

Moonbeam has added support for two custom RPC endpoints, moon_isBlockFinalized and moon_isTxFinalized, that can be used to check whether an on-chain event is finalized.

For more information you can go to the Finality RPC Endpoints section of the Moonbeam Custom API page.

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.

The code snippets rely 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. The same is true for eth_getBlockByNumber and eth_getTransactionReceipt, to check if the given transaction hash is included in the block.

To test out the examples in this guide on Moonbeam or Moonriver, you will need to have your own endpoint and API key which you can get from one of the supported Endpoint Providers.

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.

import Web3 from 'web3';

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

// Define the Web3 provider for Moonbeam
// This can also be adapted for Moonriver or Moonbase Alpha
const web3 = new Web3('RPC-API-ENDPOINT-HERE'); // Insert your RPC URL here

// 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,
  ]);

  // 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);

    // 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', [
      txBlockNumber,
      false,
    ]);

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

main();
import ethers from 'ethers';

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

// Define the RPC of the Provider for Moonbeam
// This can be adapted for Moonriver or Moonbase Alpha
const providerRPC = {
  moonbeam: {
    name: 'moonbeam',
    rpc: 'RPC-API-ENDPOINT-HERE', // Insert your RPC URL here
    chainId: 1284,
  }
};

// Define the Web3 provider
const web3Provider = new ethers.providers.JsonRpcProvider(providerRPC.moonbeam.rpc, {
  chainId: providerRPC.moonbeam.chainId,
  name: providerRPC.moonbeam.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]);

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

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

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

main();
from web3 import Web3

# Define the TxHash to Check Finality
txHash = 'tx_hash'

# Set the RPC_address for Moonbeam
# This can also be adapted for Moonriver or Moonbase Alpha
RPC_address = 'RPC-API-ENDPOINT-HERE' # Insert your RPC URL here

# Define the Web3 provider
web3Provider = Web3(Web3.HTTPProvider(RPC_address))

# asynchronous JSON RPC API request
def customWeb3Request(method, params):
    response = web3Provider.provider.make_request(method, params)
    return response

if __name__ == "__main__":
    # Get the latest finalized block of the Substrate chain
    # Uses Polkadot JSON-RPC
    finalizedHeadHash = customWeb3Request('chain_getFinalizedHead', [])

    # Get finalized block header to retrieve number
    # Uses Polkadot JSON-RPC
    finalizedBlockHeader =  customWeb3Request('chain_getHeader', [finalizedHeadHash["result"]])
    finalizedBlockNumber = int(finalizedBlockHeader["result"]["number"], 16)

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

    # If block number of receipt is not null, compare it against finalized head
    if txReceipt is not None :
        txBlockNumber = int(txReceipt["result"]["blockNumber"], 16)

        # As a safety check, get given block to check if transaction is included
        # Uses Ethereum JSON-RPC
        txBlock = customWeb3Request('eth_getBlockByNumber', [txBlockNumber, bool(0)])

        print('Current finalized block number is ' + str(finalizedBlockNumber))
        print('Your transaction in block ' +str(txBlockNumber) + ' is finalized? ' + str(finalizedBlockNumber >= txBlockNumber))
        print('Your transaction is indeed in block ' +str(txBlockNumber) + '? ' + str(txHash in txBlock["result"]["transactions"]) )
    else:
        print("Your transaction has not been included in the canonical chain")

Checking Tx Finality with Substrate Libraries

The Polkadot.js API package and Python Substrate Interface package provides developers a way to interact with Substrate chains using JavaScript and Python.

Given a transaction hash (tx_hash), the following code snippets fetch the current finalized block and compare it with the block number of the transaction you've provided. The code relies on three RPC requests from the Substrate JSON-RPC:

  • chain_getFinalizedHead - the first request gets the block hash of the last finalized block
  • chain_getHeader - the second request gets the block header for a given block hash
  • eth_getTransactionReceipt - this retrieves the ETH transaction receipt given the transaction hash.

You can find more information about Polkadot.js and the Substrate JSON RPC in their official documentation site, and more about Python Substrate Interface in their official documentation site.

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 for Moonbeam
const wsProvider = new WsProvider('wss://wss.api.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();
from substrateinterface import SubstrateInterface

# Define the Ethereum TxHash to Check Finality
txHash = 'txHash'

# Point API Provider to Moonbeam
# This can also be adapted for Moonriver or Moonbase Alpha
moonbeamAPIProvider = SubstrateInterface(
    url="wss://wss.api.moonbeam.network",
)

if __name__ == "__main__":

    # Get the latest finalized block header of the chain
    finalizedBlockHeader =  moonbeamAPIProvider.get_block_header(finalized_only = True)
    # Get the finalized block number from the block header
    finalizedBlockNumber = finalizedBlockHeader["header"]["number"]
    # Get the transaction receipt of the given tx hash through a custom RPC request
    txReceipt = moonbeamAPIProvider.rpc_request('eth_getTransactionReceipt', [txHash])

    # Check if txReceipt is null 
    if txReceipt is None:
        print('The transaction hash cannot be found in the canonical chain.')
    else:
        #Get the block number of the tx
        txBlockNumber = int(txReceipt["result"]["blockNumber"], 16)
        #Get the transaction block through a custom RPC request
        txBlock = moonbeamAPIProvider.rpc_request('eth_getBlockByNumber', [txBlockNumber, False])

        print("Current finalized block number is", finalizedBlockNumber)
        print("Your transaction in block", txBlockNumber, "is finalized? " + str(txBlockNumber <= finalizedBlockNumber))
        print("Your transaction is indeed in block", txBlockNumber, "? "+ str(txHash in txBlock["result"]["transactions"]))