Validium Reference
Verify data availability on Ethereum
In order to verify data availability on Ethereum it is necessary
to first submit data to Avail as a data submission transaction. Data
submitted this way will be included in Avail blocks, but not
interpreted or executed in any way. The submission can be done using
Polkadot-JS
which is a collection of tools for communication with
chains based on Substrate (which is now part of the Polkadot SDK).
Complete example can be found on github (opens in a new tab).
Example of sending data to Avail:
/**
* Submitting data to Avail as a transaction.
*
* @param availApi api instance
* @param data payload to send
* @param account that is sending transaction
* @returns {Promise<unknown>}
*/
async function submitData(availApi, data, account) {
return await new Promise<ISubmittableResult>((res) => {
console.log("Sending transaction...")
availApi.tx.dataAvailability.submitData(data).signAndSend(account, {nonce: -1}, (result: ISubmittableResult) => {
console.log(`Tx status: ${result.status}`)
if (result.isError) {
console.log(`Tx failed!`);
res(result)
}
if (result.isInBlock) {
console.log("Transaction in block, waiting for block finalization...")
}
if (result.isFinalized) {
console.log(`Tx finalized.`)
res(result)
}
})
});
}
Function submitData
receives availApi
api instance, data
that will be submitted,
and the account
which is sending the transaction. In order to create account
it is necessary to create keyring pair for the account that wants to send the data.
This can be done with keyring.addFromUri(secret)
which creates keyring pair via suri
(the secret can be a hex string, mnemonic phrase or a string).
After creating keyring pair, it is possible to submit data in a transaction to the Avail network with
availApi.tx.dataAvailability.submitData(data);
. Once the transaction is included in an Avail block, and bridged to the
Ethereum network it is possible to query for the proof and check the data inclusion.
When DA transaction is included in the finalized Avail block, it will be bridged via VectorX bridge to the Ethereum network.
VectorX is a Zero-Knowledge bridge that bridges a batches of data roots every 360 Avail blocks to the Ethereum network.
Data root is computed in every block using keccak256
hashing function and it consist of 2 sub-tries,
blob sub-trie which is a merkle trie of all the DA extrinsics in the block and bridge sub-trie which is a merkle trie of all the bridge extrinsics.
Final data root is computed hashing sub-trie roots as keccak256(blobRoot, bridgeRoot)
.
After successfully bridging the data root to the Ethereum network,
it is possible to prove that data is available on the Avail network by submitting a Merkle proof to the verification contract.
Proof is available only when the block in which the transaction is included is committed to the VectorX contract,
one such is deployed to the Sepolia network (0xe542db219a7e2b29c7aeaeace242c9a2cd528f96
(opens in a new tab)).
Once the range is committed to the VectorX contract, which
can be checked by calling https://turing-bridge-api.fra.avail.so/avl/head
that returns start
and end
block range that is available on the VectorX contract,
fetching proof can be done via bridge api http call
https://turing-bridge-api.fra.avail.so/eth/proof/<blockHash>?index=<transactionIndex>
where path param blockHash
is a hash of the finalized block in which
the data is included and a query param index
is index of the transaction in the block.
This http endpoint returns a json object that can be used to prove that data is available on the Avail network.
Example:
const proofResponse: ProofData = await fetch(BRIDGE_API_URL + "/eth/proof/" + result.finalized + "?index=" + result.txIndex);
Returned data:
class ProofData {
dataRootProof: Array<string>
leafProof: string
rangeHash: string
dataRootIndex: number
blobRoot: string
bridgeRoot: string
leaf: string
leafIndex: number
}
-
dataRootProof
Merkle proof of batched data root items (does not contain the leaf hash, nor the root). -
leafProof
Merkle proof of items for the leaf (does not contain the leaf hash, nor the root). -
rangeHash
Header rang hash of the items batch. -
dataRootIndex
Index of the data root in the commitment tree. -
blobRoot
Root hash of generated blob merkle sub-tree. -
bridgeRoot
Root hash of generated bridge merkle sub-tree. -
numberOfLeaves
Number of leaves in the original tree. -
leaf
Leaf for which is the proof. -
leafIndex
Index of the leaf the proof is for (starts from 0).
By submitting proof to the verification contract it is possible to verify
that data is available on Avail. Merkle proof is a list of hashes that can be used to prove
that given leaf is a member of the Merkle tree. Example of submitting a proof to the verification contract
deployed on Sepolia network for Turing (0x967F7DdC4ec508462231849AE81eeaa68Ad01389
(opens in a new tab)) can be done by calling verifyBlobLeaf
function.
This will call deployed contracts function verificationContract.verifyBlobLeaf(merkleProofInput)
and return true
or false
depending on the provided proof.
Input params for the verifyBlobLeaf
function:
struct MerkleProofInput {
// proof of inclusion for the data root
bytes32[] dataRootProof;
// proof of inclusion of leaf within blob/bridge root
bytes32[] leafProof;
// abi.encodePacked(startBlock, endBlock) of header range commitment on VectorX
bytes32 rangeHash;
// index of the data root in the commitment tree
uint256 dataRootIndex;
// blob root to check proof against, or reconstruct the data root
bytes32 blobRoot;
// bridge root to check proof against, or reconstruct the data root
bytes32 bridgeRoot;
// leaf being proven
bytes32 leaf;
// index of the leaf in the blob/bridge root tree
uint256 leafIndex;
}
EXAMPLE OF GETTING THE PROOF AND CHECKING IT WITH VERIFICATION CONTRACT FUNCTION USING POLKADOT-JS
AND ETHERS.JS
.
Submit Proof Example
import {ApiPromise, Keyring, WsProvider} from "https://deno.land/x/polkadot@0.2.45/api/mod.ts";
import {API_EXTENSIONS, API_RPC, API_TYPES} from "./api_options.ts";
import {ISubmittableResult} from "https://deno.land/x/polkadot@0.2.45/types/types/extrinsic.ts";
import {ethers} from "npm:ethers@5.4";
import ABI from './abi/availbridge.json' with {type: "json"};
const AVAIL_RPC = "ws://127.0.0.1:9944";
const SURI = "//Alice";
const BRIDGE_ADDRESS = ""; // deployed bridge address
const DATA = ""; // data to send
const BRIDGE_API_URL = ""; // bridge api url
const ETH_PROVIDER_URL = ""; // eth provider url
const availApi = await ApiPromise.create({
provider: new WsProvider(AVAIL_RPC),
rpc: API_RPC,
types: API_TYPES,
signedExtensions: API_EXTENSIONS,
});
const account = new Keyring({type: "sr25519"}).addFromUri(SURI);
/**
* ProofData represents a response from the api that holds proof for
* the blob verification.
*/
class ProofData {
dataRootProof: Array<string>
leafProof: string
rangeHash: string
dataRootIndex: number
blobRoot: string
bridgeRoot: string
leaf: string
leafIndex: number
}
/**
* Submitting data to Avail as a transaction.
*
* @param availApi api instance
* @param data payload to send
* @param account that is sending transaction
* @returns {Promise<unknown>}
*/
async function submitData(availApi, data, account) {
return await new Promise<ISubmittableResult>((res) => {
console.log("Sending transaction...")
availApi.tx.dataAvailability.submitData(data).signAndSend(account, {nonce: -1}, (result: ISubmittableResult) => {
console.log(`Tx status: ${result.status}`)
if (result.isError) {
console.log(`Tx failed!`);
res(result)
}
if (result.isInBlock) {
console.log("Transaction in block, waiting for block finalization...")
}
if (result.isFinalized) {
console.log(`Tx finalized.`)
res(result)
}
})
});
}
let result = await submitData(availApi, DATA, account);
if (result.isFinalized) {
console.log(`DA transaction in finalized block: ${result.blockNumber}, transaction index: ${result.txIndex}`);
}
// wait until the chain head on the Ethereum network is updated with the block range
// in which the Avail DA transaction is included.
while (true) {
let getHeadRsp = await fetch(BRIDGE_API_URL + "/avl/head");
if (getHeadRsp.status != 200) {
console.log("Something went wrong fetching the head.");
break;
}
let headRsp = await getHeadRsp.json();
let blockNumber: number = result.blockNumber.toNumber();
let lastCommittedBlock: number = headRsp.data.end;
if (lastCommittedBlock >= blockNumber) {
console.log("Fetching the blob proof.")
const proofResponse = await fetch(BRIDGE_API_URL + "/eth/proof/" + result.status.asFinalized + "?index=" + result.txIndex);
if (proofResponse.status != 200) {
console.log("Something went wrong fetching the proof.")
console.log(proofResponse)
break;
}
let proof: ProofData = await proofResponse.json();
console.log("Proof fetched:")
console.log(proof);
// call the deployed contract verification function with the inclusion proof.
const provider = new ethers.providers.JsonRpcProvider(ETH_PROVIDER_URL);
const contractInstance = new ethers.Contract(BRIDGE_ADDRESS, ABI, provider);
const isVerified = await contractInstance.verifyBlobLeaf([
proof.dataRootProof,
proof.leafProof,
proof.rangeHash,
proof.dataRootIndex,
proof.blobRoot,
proof.bridgeRoot,
proof.leaf,
proof.leafIndex]
);
console.log(`Blob validation is: ${isVerified}`)
break;
}
console.log("Waiting to bridge inclusion commitment. This can take a while...")
// wait for 1 minute to check again
await new Promise(f => setTimeout(f, 60*1000));
}
Deno.exit(0);