Building a Balance History Tracker: A Developer's Guide to Trustless Ethereum Data Access using Axiom
Axiom is designed to increase the scalability of smart contract applications by employing zero-knowledge (ZK) proofs. Learn how to build with axiom.
Ethereum's blockchain forms the backbone of decentralized apps – and with Axiom’s alpha release, developers can now trustlessly access block headers, accounts, and storage values from any block in Ethereum's history. Let’s take a look at what Axiom’s new release enables and what we can build with it!
What is Axiom in brief?
In a nutshell, Axiom is designed to increase the scalability of smart contract applications by employing zero-knowledge (ZK) proofs. This trustless system allows developers to interact with and execute computations on all on-chain data, enabling us to access infinite data and computational resources to smart contracts – ultimately addressing the issue of scalability in dApps.
Current Limitations
For Ethereum's smart contracts in particular, there are three main blocks:
Accessing data: Accessing historical on-chain data in Ethereum can often be a cumbersome and trust-dependent task. Developers need to rely on third-party providers that offer archive nodes or maintain their own, which can be resource-intensive.
Data verification: Ensuring the validity of on-chain data is another significant challenge. Axiom addresses this by accompanying the result of each query with a ZK validity proof. This proof verifies that the input data was correctly fetched from the chain and that the computation was accurately applied. Axiom’s smart contract is verified on-chain and is trustless, making input data ready for use by any downstream smart contract. Axiom’s validity proofs enable a considerable shift in how data verification and trust management can be approached on Ethereum.
Computational limitations: While Ethereum supports complex computations, executing these operations on-chain can be expensive due to the gas cost of each transaction. Axiom solves this problem by performing these computations off-chain and creating a ZK proof for the correctness of these operations. Axiom completes the heavy-duty work outside of Ethereum while ensuring the results fit perfectly inside Ethereum’s environment – keeping computations efficient.
How it works under the hood: Axiom's Architecture
Axiom’s architecture is composed of two main components:
AxiomV1 smart contract: Think of this as a secure warehouse that stores the unique fingerprints (hashes) of Ethereum blocks. The AxiomV1 smart contract has a built-in trust mechanism that only accepts and stores data if a certain cryptographic proof (ZK SNARK) verifies it – ensuring that all data stored has been independently and securely verified, keeping its integrity intact.
AxiomV1Query smart contract: This component allows checking (verification) of past data on Ethereum against the secure warehouse provided by AxiomV1 – guaranteeing the data is accurate and trustworthy.
Together, these two components create a secure platform for accessing and computing data – making Axiom a powerful platform for performing a variety of computations on the fetched data, like basic analytics and cryptographic operations.
Getting Started with Axiom
Now we will build a "Balance History Tracker" app using Axiom’s SDK. Our app will monitor the changes in the ETH balance of a specific account throughout Ethereum's history.
Understanding the SDK:
On-chain query submission: Build a query with the Axiom SDK and send it on-chain to the AxiomV1Query smart contract.
Query fulfillment and result verification: An off-chain prover will index the query, generate the result, and provide its validity proof using zero-knowledge (ZK). This proof is then verified on-chain and stored in a Merkle-ized format in the AxiomV1Query contract storage.
Read query results: After on-chain verification, you can fetch the query results from the contract storage using the SDK and integrate them into your dApp.
Prerequisites
Node.js installed
Follow this simple guide to install the latest version of Node.js
Ether.js installed
Follow this simple guide to install ether.js
Step 1: Installing and Configuring the SDK
Use your preferred package manager to install the SDK package:
# For npm
npm i @axiom-crypto/core
# For yarn
yarn add @axiom-crypto/core
# For pnpm
pnpm add @axiom-crypto/core
Step 2: Setting up Axiom SDK
In your app’s main file, in my case index.js
, import the following modules:
import { ethers } from 'ethers';
import { Axiom, AxiomConfig } from "@axiom-crypto/core"
Then, define your configuration parameters and initialize the Axiom SDK (you’ll need a JSON-RPC provider and a chainId)
const config: AxiomConfig = {
providerUri: "<your-provider-uri>",
version: "v1",
chainId: <chainId>, // 1 for mainnet, 5 for Goerli
mock: false, // builds real proofs
};
const ax = new Axiom(config);
Replace <your-provider-uri>
with your JSON-RPC provider URI.
Step 3: Build a Query
We’ll query the balance of an Ethereum account at a specific block. Initialize a new query and append the query parameters:
const qb = ax.newQueryBuilder();
await qb.append({
blockNumber: 17090300,
address: "<ethereum-account-address>"
});
You need to replace <ethereum-account-address>
with the address of the Ethereum account you want to query.
The current release allows up to 64 pieces of on-chain data per query. Each piece of data is specified by a blockNumber(required), an address (optional), and a slot (optional) for block headers, accounts, and account storage, respectively.
Step 4: Submit the Query On-Chain
The sendQuery
function in the AxiomV1Query
contract facilitates querying of historic on-chain data. To submit a query, you need to set up an ethers.js
environment:
import { ethers } from "ethers";
const providerUri = <your provider URI>
const provider = new ethers.JsonRpcProvider(providerUri);
const wallet = new ethers.Wallet(<private key>, provider);
const axiomV1Query = new ethers.Contract(
ax.getAxiomQueryContractAddress() as string,
ax.getAxiomQueryAbi(),
wallet
);
Next, construct and send the query transaction using the output of the QueryBuilder
from the previous step:
const { keccakQueryResponse, queryHash, query } = await qb.build();
const tx = await axiomV1Query.sendQuery(
keccakQueryResponse,
"<refund-address>",
query,
{
value: ethers.parseEther("0.01"), // Fee for the query
gasPrice: ethers.parseUnits("100", "gwei"),
}
);
Step 5: Fetching and Handling the Query Results
With the query sent, we now wait for the fulfillment and fetch the results:
let responseTree = await ax.query.getResponseTreeForQuery(keccakQueryResponse);
const keccakBlockResponse = responseTree.blockTree.getHexRoot();
const keccakAccountResponse = responseTree.accountTree.getHexRoot();
const keccakStorageResponse = responseTree.storageTree.getHexRoot();
From the response tree, we obtain the block hash and account root hash. We can then verify the account balance at the specified block:
const validationWitness = ax.query.getValidationWitness(responseTree, 17090300, "<ethereum-account-address>");
const balance = ethers.BigNumber.from(validationWitness.accountResponse.accountBalance);
console.log(`Balance: ${ethers.utils.formatEther(balance)} ETH`);
This script will output the Ether balance of the account at block 17090300
.
Step 6: Verifying the Query Results On-Chain
After fetching the query results, you can verify them on-chain using the areResponsesValid
function in the AxiomV1Query
contract. This function allows you to read block, account, and storage data from verified query results.
First, you need to import the AxiomV1Query contract interface into your own contract:
import "@axiom-crypto/core/contracts/AxiomV1Query.sol";
Then, you can create an instance of the AxiomV1Query contract:
AxiomV1Query axiomV1Query = AxiomV1Query(<AxiomV1Query contract address>);
Next, you can call the areResponsesValid
function with the necessary parameters. These parameters include the block, account, and storage responses that you obtained from the response tree:
bool isValid = axiomV1Query.areResponsesValid(
keccakBlockResponse,
keccakAccountResponse,
keccakStorageResponse,
[],
[validationWitness.accountResponse],
[]
);
In this example, keccakBlockResponse
, keccakAccountResponse
, and keccakStorageResponse
are the hashes of the block, account, and storage data that you obtained from the response tree. The validationWitness
is the witness containing the true values of the account data we are interested in.
This function will return a boolean indicating whether the responses are valid or not. If the function returns true, it means that the responses are valid and you can trust the data.
You’ll need to replace <AxiomV1Query contract address>
with the actual address of the AxiomV1Query contract. In addition, you need to replace keccakBlockResponse
, keccakAccountResponse
, keccakStorageResponse
, and validationWitness
with the actual values that you obtained from the response tree.
This step allows you to verify the historic balance of an Ethereum account from an on-chain smart contract, which is now located in validationWitness.accountResponse.accountBalance.
Additional Info
Axiom provides details about Ethereum blocks and Axiom queries via the Block
and Query
classes, wrapped by the Axiom
class. They offer info about block and query data, with return objects useful for query response verification and data reading.
Wrapping it Up
And that’s it! You've now built an Ethereum account balance history tracker using Axiom's SDK. — you can extend this app to monitor balance changes over a range of blocks or across multiple accounts. If you want to learn more about Axiom’s features you can visit the official documentation and join their Discord to stay up-to-date.