Interacting with the Relay Data Verifier Precompile¶
Introduction¶
Polkadot relies on state proofs to guarantee data integrity at a particular time. A state proof is a concise, cryptographic data structure representing a specific subset of transactions or state data within a trie. It consists of a set of hashes that form a path from the target data to the root hash stored in the block header.
A client can independently reconstruct the root hash and compare it with the original stored in the block header by providing a state proof. If the reconstructed root hash matches the original, it confirms the target data's authenticity, validity, and inclusion within the blockchain.
Polkadot's unique architecture and parachain block validation process means blockchains like Moonbeam have the relay chain storage root hash in their state. Consequently, Moonbeam can provide a mechanism to verify a relay chain state by checking the proof against the stored storage root hash.
Moonbeam's relay data verifier precompiled contract provides an easy way for smart contracts to programmatically build functions that rely on verifying relay chain state in contract calls. Consequently, no oracles are needed to feed relay chain data to Moonbeam. This functionality is readily available at the following contract addresses:
0x0000000000000000000000000000000000000819
0x0000000000000000000000000000000000000819
0x0000000000000000000000000000000000000819
Note
There can be some unintended consequences when using the precompiled contracts on Moonbeam. Please refer to the Security Considerations page for more information.
The Relay Data Verifier Solidity Interface¶
RelayDataVerifier.sol
is a Solidity interface that allows developers to interact with the precompile's methods.
RelayDataVerifier.sol
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.8.3;
/// @dev The RelayDataVerifier contract's address.
address constant RELAY_DATA_VERIFIER_ADDRESS = 0x0000000000000000000000000000000000000819;
/// @dev The RelayDataVerifier contract's instance.
RelayDataVerifier constant RELAY_DATA_VERIFIER_CONTRACT = RelayDataVerifier(
RELAY_DATA_VERIFIER_ADDRESS
);
/// @author The Moonbeam Team
/// @title Relay Proof Verifier Interface
/// @dev The interface that Solidity contracts use to interact with the Relay Proof Verifier
/// precompile.
/// A typical workflow to verify relay chain data is the following:
/// 1. Moonbeam RPC Call: Call `latestRelayBlockNumber` function to get the latest relay
/// block number tracked by the chain in `pallet-storage-root`.
/// 2. Relay RPC Call: Call `chain_getBlockHash(blockNumber)` RPC method to get the relay block hash
/// for the block number obtained in step 1.
/// 3. Relay RPC Call: Call `state_getReadProof(keys, at)` RPC method where `at`
/// is the relay block hash obtained in step 2 to get the 'ReadProof` of the entries.
/// 4. Moonbeam RPC Call: Submit an ethereum transaction (directly or through a SC) to call the
/// `verifyEntry` or `verifyEntries` function to verify the data against the relay block
/// number. The call data contain the relay block number obtained in step 1, and the read
/// proof generated in step 3, along with the key/s to verify.
/// @custom:address 0x0000000000000000000000000000000000000819
interface RelayDataVerifier {
/// @dev ReadProof struct returned by the `state_getReadProof` RPC method.
struct ReadProof {
// The block hash against which the proof is generated
bytes32 at;
/// The storage proof
bytes[] proof;
}
/// @dev Verifies a storage entry in the Relay Chain using a relay block number and a storage
/// proof. This function takes a relay block number, a storage proof, and the key of the storage
/// entry to verify. It returns the value associated with the key if the verification is
/// successful.
/// @custom:selector 27001faa
/// @param relayBlockNumber The relay block number against which the entry is being verified.
/// @param readProof The storage proof used to verify the entry.
/// @param key The key of the storage entry to verify.
/// @return value The value associated with the key, returned as a bytes array.
function verifyEntry(
uint32 relayBlockNumber,
ReadProof calldata readProof,
bytes calldata key
) external returns (bytes memory value);
/// @dev Verifies a set of entries in the Relay Chain and returns the corresponding values.
/// This function takes a relay block number, a storage proof, and an array of keys for the
/// storage entries to verify. It returns an array of values associated with the keys, in the
/// same order as the keys.
/// @custom:selector 2da33a45
/// @param relayBlockNumber The relay block number for which the data is being verified.
/// @param readProof The storage proof used to verify the data.
/// @param keys The keys of the storage entries to verify.
/// @return values The values associated with the keys, returned in the same order as the keys.
function verifyEntries(
uint32 relayBlockNumber,
ReadProof calldata readProof,
bytes[] calldata keys
) external returns (bytes[] memory values);
/// @dev Returns the latest relay block number that has a storage root stored on-chain.
/// @custom:selector aed36869
/// @return relayBlockNumber the lastest relay block number
function latestRelayBlockNumber()
external
view
returns (uint32 relayBlockNumber);
}
The interface includes the following functions:
latestRelayBlockNumber() — retrieves the most recent relay chain block that has its storage root stored on the blockchain itself
None
The latest relay block number that has a storage root stored on-chain.
verifyEntry(uint32 relayBlockNumber, ReadProof calldata readProof, bytes callData key) — verifies a storage entry in the relay chain using a relay block number, a storage proof, and the storage key. It returns the value associated with the key if the verification is successful
relayBlockNumber
- the relay block number for which the data is being verified. The latest relay block number can be obtained from thelatestRelayBlockNumber()
functionreadProof
- a struct defined in the precompile contract, containing the storage proof used to verify the data. TheReadProof
struct is defined as:struct ReadProof { // The block hash against which the proof is generated bytes32 at; /// The storage proof bytes[] proof; }
key
- the storage key for the generated proof
When performing a static call on the verifyEntry
function, you can view the returned value associated with the key in hexadecimal format.
'0x01000000040000000100000000000000f88ce384dca20000000000000000000000370589030a0000000000000000000000203d88792d0000000000000000000000000000000000000000000000000080'
verifyEntries(uint32 relayBlockNumber, ReadProof calldata readProof, bytes[] callData keys) — verifies a set of entries in the relay chain and returns the corresponding values. This function takes a relay block number, a storage proof, and an array of storage keys to verify. It returns an array of values associated with the keys, in the same order as the keys
relayBlockNumber
- the relay block number for which the data is being verified. The latest relay block number can be obtained from thelatestRelayBlockNumber()
functionreadProof
- a struct defined in the precompile contract, containing the storage proof used to verify the data. TheReadProof
struct is defined as:struct ReadProof { // The block hash against which the proof is generated bytes32 at; /// The storage proof bytes[] proof; }
keys
- the storage keys for the generated proof
When performing a static call on the verifyEntries
function, you can view an array containing the corresponding values mapped to their respective keys, represented in hexadecimal format.
['0x01000000040000000100000000000000f88ce384dca20000000000000000000000370589030a0000000000000000000000203d88792d0000000000000000000000000000000000000000000000000080']
Interact with the Solidity Interface¶
A typical workflow to verify relay chain data involves the following steps:
- Moonbeam RPC call - call the
latestRelayBlockNumber
function to get the latest relay block number tracked by the chain in thepallet-storage-root
- Relay RPC call - call the
chain_getBlockHash(blockNumber)
RPC method to get the relay block hash for the block number obtained in step one - Relay RPC call - call the
state_getReadProof(keys, at)
RPC method to retrieve the storage proof, whereat
is the relay block hash obtained in step two, andkeys
is an array of strings which contains the keys for target storage items. For@polkadot/api
, it can be obtained viaapi.query.module.key()
function - Moonbeam RPC call - submit an Ethereum transaction to call the
verifyEntry
orverifyEntries
function to verify the data against the relay block number. The call data should contain the relay block number obtained in step one, the read proof generated in step three, and the key(s) to verify
The following sections will cover how to interact with the Relay Data Verifier Precompile using Ethereum libraries, such as Ethers.js, Web3.js, and Web3.py. The examples in this guide will be on Moonbase Alpha.
Checking Prerequisites¶
To follow along with this tutorial, you will need to have:
- Create or have an account on Moonbase Alpha to test out the different features in the precompile
- The account will need to be funded with
DEV
tokens. You can get DEV tokens for testing on Moonbase Alpha once every 24 hours from the Moonbase Alpha Faucet
Using Ethereum Libraries¶
To interact with the Solidity interface using an Ethereum library, you'll need the precompile's ABI (Application Binary Interface). The ABI for the Relay Chain Data Verifier Precompile is as follows:
Relay Data Verifier Precompile ABI
[
{
inputs: [],
name: 'latestRelayBlockNumber',
outputs: [
{
internalType: 'uint32',
name: 'relayBlockNumber',
type: 'uint32',
},
],
stateMutability: 'view',
type: 'function',
},
{
inputs: [
{
internalType: 'uint32',
name: 'relayBlockNumber',
type: 'uint32',
},
{
components: [
{
internalType: 'bytes32',
name: 'at',
type: 'bytes32',
},
{
internalType: 'bytes[]',
name: 'proof',
type: 'bytes[]',
},
],
internalType: 'struct RelayDataVerifier.ReadProof',
name: 'readProof',
type: 'tuple',
},
{
internalType: 'bytes[]',
name: 'keys',
type: 'bytes[]',
},
],
name: 'verifyEntries',
outputs: [
{
internalType: 'bytes[]',
name: 'values',
type: 'bytes[]',
},
],
stateMutability: 'nonpayable',
type: 'function',
},
{
inputs: [
{
internalType: 'uint32',
name: 'relayBlockNumber',
type: 'uint32',
},
{
components: [
{
internalType: 'bytes32',
name: 'at',
type: 'bytes32',
},
{
internalType: 'bytes[]',
name: 'proof',
type: 'bytes[]',
},
],
internalType: 'struct RelayDataVerifier.ReadProof',
name: 'readProof',
type: 'tuple',
},
{
internalType: 'bytes',
name: 'key',
type: 'bytes',
},
],
name: 'verifyEntry',
outputs: [
{
internalType: 'bytes',
name: 'value',
type: 'bytes',
},
],
stateMutability: 'nonpayable',
type: 'function',
},
];
Once you have the ABI, you can interact with the precompile using the Ethereum library of your choice, such as Ethers.js, Web3.js, or Web3.py. The general steps are as follows:
- Create a provider
- Create a contract instance of the precompile
- Interact with the precompile's functions
The provided code example demonstrates how to use the Ethers.js library to interact with the Moonbase Alpha network and its relay chain, verifying a data entry using the verifyEntry
function.
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.
// For reading local ABI file
import * as fs from 'fs';
// Import Ethers library, to interact with Moonbeam networks
import { ethers } from 'ethers';
// Import Polkadot library, to interact with relay chain
import { ApiPromise, WsProvider } from '@polkadot/api';
const abi = JSON.parse(fs.readFileSync('./RelayChainDataVerifierABI.json'));
const privateKey = 'INSERT_PRIVATE_KEY';
const precompileAddress = '0x0000000000000000000000000000000000000819';
const moonbeamURL = 'https://rpc.api.moonbase.moonbeam.network';
const relayURL = 'wss://relay.api.moonbase.moonbeam.network';
// Create Ethers provider and signer
const provider = new ethers.JsonRpcProvider(moonbeamURL);
const signer = new ethers.Wallet(privateKey, provider);
const precompileContract = new ethers.Contract(precompileAddress, abi, signer);
async function run() {
// Create provider for relay chain
const wsProvider = new WsProvider(relayURL);
const api = await ApiPromise.create({ provider: wsProvider });
// Get the storage key for a random account on relay chain
const key = api.query.system.account.key(
'5CBATpb3yvEM4mhX9Dw3tyuqiWKhq9YBG6ugSbodRUSbodoU'
);
// Find the latest available relay chain block number from Moonbeam
const blockNum = await precompileContract.latestRelayBlockNumber();
// Get the block hash and storage proof from relay chain
const blockHash = await api.rpc.chain.getBlockHash(blockNum);
const proof = await api.rpc.state.getReadProof([key], blockHash);
// This tx will be rejected if the verification failed
const receipt = await precompileContract.verifyEntry(blockNum, proof, key);
await receipt.wait();
console.log(receipt.hash);
}
await run();
// For reading local ABI file
import * as fs from 'fs';
// Import web3js library, to interact with Moonbeam networks
import { Web3 } from 'web3';
// Import Polkadot library, to interact with relay chain
import { ApiPromise, WsProvider } from '@polkadot/api';
const abi = JSON.parse(fs.readFileSync('./RelayChainDataVerifierABI.json'));
const privateKey = 'INSERT_PRIVATE_KEY';
const precompileAddress = '0x0000000000000000000000000000000000000819';
const moonbeamURL = 'https://rpc.api.moonbase.moonbeam.network';
const relayURL = 'wss://relay.api.moonbase.moonbeam.network';
// Create Web3js provider and signer
const web3 = new Web3(moonbeamURL);
const precompileContract = new web3.eth.Contract(abi, precompileAddress);
const account = web3.eth.accounts.privateKeyToAccount(privateKey);
async function run() {
// Create provider for relay chain
const wsProvider = new WsProvider(relayURL);
const api = await ApiPromise.create({ provider: wsProvider });
// Get the storage key for a random account on relay chain
const key = api.query.system.account.key(
'5CBATpb3yvEM4mhX9Dw3tyuqiWKhq9YBG6ugSbodRUSbodoU'
);
// Find the latest available relay chain block number from Moonbeam
const blockNum = await precompileContract.methods
.latestRelayBlockNumber()
.call();
// Get the block hash and storage proof from relay chain
const blockHash = await api.rpc.chain.getBlockHash(blockNum);
const proof = await api.rpc.state.getReadProof([key], blockHash);
const callObject = {
to: precompileAddress,
data: precompileContract.methods
.verifyEntry(blockNum, proof, key)
.encodeABI(),
gas: await precompileContract.methods
.verifyEntry(blockNum, proof, key)
.estimateGas(),
gasPrice: await web3.eth.getGasPrice(),
nonce: await web3.eth.getTransactionCount(account.address),
};
// This tx will be rejected if the verification failed
const tx = await web3.eth.accounts.signTransaction(
callObject,
account.privateKey
);
const receipt = await web3.eth.sendSignedTransaction(tx.rawTransaction);
console.log(receipt.transactionHash);
}
await run();
# Import packages
from eth_account import Account
from substrateinterface import SubstrateInterface
from web3 import Web3
# Initialize variables
abi = INSERT_ABI
privateKey = "INSERT_PRIVATE_KEY"
precompileAddress = "0x0000000000000000000000000000000000000819"
moonbeamURL = "https://rpc.api.moonbase.moonbeam.network"
relayURL = "wss://relay.api.moonbase.moonbeam.network"
# Create provider for Moonbeam network
web3 = Web3(Web3.HTTPProvider(moonbeamURL))
account = Account.from_key(privateKey)
precompileContract = web3.eth.contract(address=precompileAddress, abi=abi)
# Create provider for relay chain
substrate = SubstrateInterface(url=relayURL)
# Get storage key
key = substrate.generate_storage_hash(
storage_module="System",
storage_function="Account",
params=["5CBATpb3yvEM4mhX9Dw3tyuqiWKhq9YBG6ugSbodRUSbodoU"],
)
# Find the latest available relay chain block number from Moonbeam
blockNum = precompileContract.functions.latestRelayBlockNumber().call()
# Get the block hash from relay chain
blockHash = substrate.get_block_hash(blockNum)
# Get the storage proof from relay chain
response = substrate.rpc_request("state_getReadProof", [[key], blockHash])
proof = response["result"]
# Call smart contract
tx = precompileContract.functions.verifyEntry(blockNum, proof, key).build_transaction(
{
"from": Web3.to_checksum_address(account.address),
"nonce": web3.eth.get_transaction_count(
Web3.to_checksum_address(account.address)
),
}
)
tx_create = web3.eth.account.sign_transaction(tx, privateKey)
tx_hash = web3.eth.send_raw_transaction(tx_create.rawTransaction)
tx_receipt = web3.eth.wait_for_transaction_receipt(tx_hash)
| Created: April 2, 2024