Skip to main content
The kate_queryDataProof method retrieves a Merkle proof demonstrating the inclusion of a specific transaction within a block’s data root. This is essential for light clients to verify transaction inclusion without downloading the entire block.

Method Signature

async fn query_data_proof(
    &self,
    transaction_index: u32,
    at: Option<HashOf<Block>>
) -> RpcResult<ProofResponse>

Parameters

transaction_index
u32
required
The index of the transaction within the block’s extrinsics array.Note: Index 0 is typically the timestamp inherent, and the last index is the finality inherent. User transactions are indexed starting from 1.Example: 1 for the first user transaction in the block
at
Hash
default:"null"
Block hash at which to query the transaction proof. If not provided, uses the best (latest finalized) block.Format: 32-byte hexadecimal hash prefixed with 0x

Returns

result
ProofResponse
A proof response containing the Merkle proof and associated roots.Structure:
interface ProofResponse {
  data_proof: {
    roots: {
      data_root: Hash,      // Combined root of blob_root and bridge_root
      blob_root: Hash,      // Merkle root of transaction data
      bridge_root: Hash     // Merkle root of bridge messages (or zero)
    },
    proof: Hash[],          // Merkle proof path
    number_of_leaves: u32,  // Total number of leaves in the tree
    leaf_index: u32,        // Index of this transaction in the tree
    leaf: Hash              // Hash of the transaction data
  },
  message: string | null    // Optional message
}
The data_root is calculated as: keccak256(blob_root || bridge_root)

Example Request

curl -X POST http://localhost:9944 \
  -H "Content-Type: application/json" \
  -d '{
    "jsonrpc": "2.0",
    "method": "kate_queryDataProof",
    "params": [
      1,
      "0xa1b2c3d4e5f6789012345678901234567890123456789012345678901234567890"
    ],
    "id": 1
  }'

Example Response

{
  "jsonrpc": "2.0",
  "result": {
    "data_proof": {
      "roots": {
        "data_root": "0x8f3e1c9a7b2d4e5f6a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4",
        "blob_root": "0x729afe29f4e9fee2624d7ed311bcf57d24683fb78938bcb4e2a6a22c4968795e",
        "bridge_root": "0x0000000000000000000000000000000000000000000000000000000000000000"
      },
      "proof": [
        "0x1a2b3c4d5e6f7890abcdef1234567890abcdef1234567890abcdef1234567890",
        "0x234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12"
      ],
      "number_of_leaves": 4,
      "leaf_index": 0,
      "leaf": "0x729afe29f4e9fee2624d7ed311bcf57d24683fb78938bcb4e2a6a22c4968795e"
    },
    "message": null
  },
  "id": 1
}

Query Latest Block

Query a transaction proof from the most recent finalized block:
{
  "jsonrpc": "2.0",
  "method": "kate_queryDataProof",
  "params": [1],
  "id": 1
}

Proof Verification

To verify the proof:
  1. Hash the transaction data using Keccak256 to get the leaf
  2. Verify the Merkle proof path against the blob_root
  3. Compute the data_root as keccak256(blob_root || bridge_root)
  4. Compare the computed data_root with the one in the block header

Example Verification (JavaScript)

import { keccak256 } from '@ethersproject/keccak256';
import { concat, hexlify } from '@ethersproject/bytes';

function verifyDataProof(proofResponse, transactionData, blockHeader) {
  // Step 1: Hash transaction data
  const leaf = keccak256(transactionData);
  
  // Step 2: Verify Merkle proof
  let computedHash = leaf;
  let { proof, leaf_index } = proofResponse.data_proof;
  
  for (let i = 0; i < proof.length; i++) {
    const proofElement = proof[i];
    
    if (leaf_index % 2 === 0) {
      // Current node is left
      computedHash = keccak256(concat([computedHash, proofElement]));
    } else {
      // Current node is right
      computedHash = keccak256(concat([proofElement, computedHash]));
    }
    
    leaf_index = Math.floor(leaf_index / 2);
  }
  
  // Step 3: Verify computed hash matches blob_root
  const blobRoot = proofResponse.data_proof.roots.blob_root;
  if (computedHash !== blobRoot) {
    return false;
  }
  
  // Step 4: Verify data_root
  const bridgeRoot = proofResponse.data_proof.roots.bridge_root;
  const expectedDataRoot = keccak256(concat([blobRoot, bridgeRoot]));
  
  return expectedDataRoot === proofResponse.data_proof.roots.data_root;
}

// Usage
const proofResponse = await rpc('kate_queryDataProof', [1, blockHash]);
const isValid = verifyDataProof(proofResponse, txData, header);
console.log('Proof valid:', isValid);

Error Responses

Transaction Index Out of Range

If the transaction index doesn’t exist in the block:
{
  "jsonrpc": "2.0",
  "error": {
    "code": 1,
    "message": "Cannot fetch tx data at tx index 100 at block 0xa1b2...7890"
  },
  "id": 1
}

Block Not Finalized

{
  "jsonrpc": "2.0",
  "error": {
    "code": 1,
    "message": "Requested block 0xa1b2...7890 is not finalized"
  },
  "id": 1
}

Invalid Block Hash

{
  "jsonrpc": "2.0",
  "error": {
    "code": 1,
    "message": "Invalid block number: ..."
  },
  "id": 1
}

Implementation Details

Source Code Reference

From /rpc/kate-rpc/src/lib.rs:314-331:
async fn query_data_proof(
    &self,
    tx_idx: u32,
    at: Option<HashOf<Block>>,
) -> RpcResult<ProofResponse> {
    let _metric_observer = MetricObserver::new(ObserveKind::KateQueryDataProof);

    // Calculate proof for block and tx index
    let (api, at, number, _, extrinsics, _) = self.scope(at)?;
    let proof = api
        .data_proof(at, number, extrinsics, tx_idx)
        .map_err(|e| internal_err!("KateApi::data_proof failed: {e:?}"))?
        .ok_or_else(|| {
            internal_err!("Cannot fetch tx data at tx index {tx_idx:?} at block {at:?}")
        })?;

    Ok(proof)
}

Validation Steps

  1. Block Finalization: Ensures the requested block is finalized
  2. Transaction Existence: Verifies the transaction index is valid
  3. Proof Generation: Computes the Merkle proof path
  4. Root Calculation: Derives data_root from blob_root and bridge_root
  5. Metrics: Records query performance (if enabled)

Use Cases

Light Client Transaction Verification

Light clients can verify transaction inclusion without the full block:
// Verify a transaction was included in a block
const txIndex = 1;
const proof = await rpc('kate_queryDataProof', [txIndex, blockHash]);
const header = await rpc('chain_getHeader', [blockHash]);

// Verify proof against header
if (verifyDataProof(proof, txData, header)) {
  console.log('Transaction included in block!');
}

Application Data Verification

Verify that application-specific data was included:
// Submit data
const tx = await submitData(appId, data);
const blockHash = await tx.waitFinalized();

// Get transaction index from block
const block = await api.rpc.chain.getBlock(blockHash);
const txIndex = block.block.extrinsics.findIndex(ext => 
  ext.hash.toHex() === tx.hash.toHex()
);

// Query and verify proof
const proof = await rpc('kate_queryDataProof', [txIndex, blockHash]);

Bridge Message Verification

Verify bridge messages in blocks with bridge activity:
const proof = await rpc('kate_queryDataProof', [txIndex, blockHash]);

// Check if block contains bridge messages
if (proof.data_proof.roots.bridge_root !== '0x00...00') {
  console.log('Block contains bridge messages');
  // Verify bridge message inclusion
}

Data Root Calculation

The data_root is a combined hash ensuring integrity of both transaction data and bridge messages:
function calculateDataRoot(blobRoot, bridgeRoot) {
  // Concatenate blob_root and bridge_root
  const combined = concat([blobRoot, bridgeRoot]);
  
  // Hash with Keccak256
  return keccak256(combined);
}

// Example
const dataRoot = calculateDataRoot(
  '0x729afe29f4e9fee2624d7ed311bcf57d24683fb78938bcb4e2a6a22c4968795e',
  '0x0000000000000000000000000000000000000000000000000000000000000000'
);
This structure allows separate verification of:
  • Transaction data via the blob_root Merkle tree
  • Bridge messages via the bridge_root Merkle tree
  • Combined integrity via the data_root

Transaction Indexing

Understanding transaction indices in Avail blocks:
IndexTypeDescription
0InherentTimestamp inherent (always present)
1TransactionFirst user transaction
2TransactionSecond user transaction
TransactionAdditional transactions
n-1InherentFinality inherent (always present)
To query the first user transaction, use index 1.

Build docs developers (and LLMs) love