JSPM

  • Created
  • Published
  • Downloads 8
  • Score
    100M100P100Q53156F
  • License MIT

SDK to build on top of Axiom, the ZK Coprocessor for Ethereum.

Package Exports

  • @axiom-crypto/experimental
  • @axiom-crypto/experimental/index.js

This package does not declare an exports field, so the exports above have been automatically detected and optimized by JSPM instead. If any package subpath is missing, it is recommended to post an issue to the original package (@axiom-crypto/experimental) to support the "exports" field. If that is not possible, create a JSPM override to customize the exports field for this package.

Readme

Axiom SDK

Axiom is a ZK coprocessor for Ethereum. Utilizing the properties of Zero Knowledge proofs, Axiom allows anyone to prove historical data on-chain and trustlessly use that data in a smart contract.

Getting started (Goerli Testnet)

So we don't start out using real money, our example will be on the Goerli Testnet. If you need Goerli Ether, you can try this faucet or any number of other Goerli faucets via a quick search.

In order to get started, create a config object and pass it to a new Axiom class instance as follows:

const config: AxiomConfig = {
    providerUri: <your Goerli Testnet provider URI (from Alchemy, Infura, etc)>,
    version: "v1",
    chainId: 5, // Goerli; defaults to 1 (Ethereum Mainnet)
    mock: true, // builds proofs without utilizing actual Prover resources
}
const ax = new Axiom(config);

Ensure that the providerUri you are using is for Goerli Testnet or your function calls will fail.

Building a query

The Axiom SDK includes a tool for building a query. Create a new query by calling the newQueryBuilder function, which creates a new QueryBuilder object:

const qb = ax.newQueryBuilder();

Append queries to the instance of QueryBuilder with a blockNumber (required), address (optional), and slot number (optional). The maximum number queries that can be appended to a QueryBuilder object is currently fixed at 64. If you need more queries, split the request up into multiple QueryBuilder objects.

For more information on slots, please see our documentation on finding storage slots.

const UNI_V2_ADDR = "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f";
await qb.append({blockNumber: 9221736});
await qb.append({blockNumber: 9221524, address: UNI_V2_ADDR});
await qb.append({blockNumber: 9221524, address: UNI_V2_ADDR, slot: 0});
await qb.append({blockNumber: 9221524, address: UNI_V2_ADDR, slot: 1});

The append function will validate the value at the slot. If you attempt to read from a slot at an account that has not yet been created, an error will be thrown. Please note that blockNumbers on Goerli versus Mainnet are different.

Call the QueryBuilder's build function to generate the keccakQueryResponse, queryHash, and query calldata for the Axiom contract function:

const { keccakQueryResponse, queryHash, query } = await qb.build();

Submitting a query

You can submit a query to the on-chain Axiom contract by calling the sendQuery function on the contract after providing a signer:

const providerUri = your provider URI (from Alchemy, Infura, etc)>;
const provider = new ethers.JsonRpcProvider(providerUri);
const wallet = new ethers.Wallet(<private key>, provider);
const axiomQuery = new ethers.Contract(
    ax.getAxiomQueryAddress() as string, 
    ax.getAxiomQueryAbi(), 
    wallet
);

const txResult = await axiomQuery.sendQuery(
    keccakQueryResponse,
    <refund adddress>,
    query,
    {
        value: ethers.parseEther("0.01"), // Goerli payment value
    }
);
const txReceipt = await txResult.wait();

Note that if you submit a Query with the exact same appended queries as a previous transaction, it will fail due to the keccakQueryResponse hashing to the same value as before. Additionally, please ensure that the providerUri that you provide matches with the chainId value passed in at the top in the config object (so if chainId is 5, then provide a providerUri for Goerli).

Using the result of the call

After the contract has processed the sendQuery call, it will emit an event that the Prover will use to generate a ZK proof of the data in the query. Once the Prover is done generating the ZK proof, it will write that the keccakQueryResponse for that Query has been fulfilled, and a the following event will be emitted, letting you know that your data is ready to be used:

event QueryFulfilled(bytes32 keccakQueryResponse, uint256 payment, address prover);

Submitting a Query on Mainnet

Submitting a query on Ethereum Mainnet is similar to doing it on testnet, but we'll be changing the chainId to 1 and removing the mock key in the AxiomConfig object. Please be sure to also use a Mainnet providerUri value.

const config: AxiomConfig = {
    providerUri: <your Ethereum Mainnet provider URI (from Alchemy, Infura, etc)>,
    version: "v1",
    chainId: 1,
}
const ax = new Axiom(config);

Next, we'll create the data that we want to prove to append to the QueryBuilder instance as before, but with the very important difference being that blockNumber, address, and slot that we want to prove will likely be different on Mainnet versus Goerli.

const UNI_V2_ADDR = "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f";
await qb.append({blockNumber: 17090300});
await qb.append({blockNumber: 17090217, address: UNI_V2_ADDR});
await qb.append({blockNumber: 17090217, address: UNI_V2_ADDR, slot: 0});
await qb.append({blockNumber: 17090217, address: UNI_V2_ADDR, slot: 1});

Building the query is the same as before:

const { keccakQueryResponse, queryHash, query } = await qb.build();

Calling sendQuery on the Mainnet contract is also the same as before. The payment value required on Mainnet is currently 0.01 ETH, but may change in the future.

const txResult = await axiomQuery.sendQuery(
    keccakQueryResponse,
    <refund adddress>,
    query,
    {
        value: ethers.parseEther("0.01"), // Mainnet payment value
    }
);
const txReceipt = await txResult.wait();

Reading query results

We provide the areResponsesValid view function in AxiomV1Query to help allow users to read block, account, and storage data from verified query results. This function has the following signature:

function areResponsesValid(
    bytes32 keccakBlockResponse,
    bytes32 keccakAccountResponse,
    bytes32 keccakStorageResponse,
    BlockResponse[] calldata blockResponses,
    AccountResponse[] calldata accountResponses,
    StorageResponse[] calldata storageResponses
) external view returns (bool);

To verify data about historic blocks, accounts, or storage, first pass in the queryHash (not the keccakQueryResponse; queryHash is also found as an output of the QueryBuilder build function) from the Axiom SDK to generate the keccakBlockResponse, keccakAccountResponse, and keccakStorageResponse which encode the verified query result on-chain:

import { Axiom, AxiomConfig } from "@axiom-crypto/core";

const config: AxiomConfig = {
    providerUri: <your provider uri (such as from Alchemy, Infura, etc)>,
    version: "v1",
}
const ax = new Axiom(config);
const responseTree = await ax.query.getResponseTreeForQueryHash(<queryHash>);
const keccakBlockResponse = responseTree.blockTree.getHexRoot();
const keccakAccountResponse = responseTree.accountTree.getHexRoot();
const keccakStorageResponse = responseTree.storageTree.getHexRoot();

Then, generate responses for data in historic block headers, accounts, and storage slots by creating BlockResponse, AccountResponse, and StorageResponse objects using getValidationWitness:

const blockResponse: SolidityBlockResponse = ax.query.getValidationWitness(
    responseTree, <blockNumber>
);
const accountResponse: SolidityAccountResponse = ax.query.getValidationWitness(
    responseTree, <blockNumber>, <address>
);
const storageResponse: SolidityStorageResponse = ax.query.getValidationWitness(
    responseTree, <blockNumber>, <address>, <storage slot>
);

After passing these into getValidationWitness and verifying on-chain, read off the data you'd like to use from each object:

  • BlockResponse -- the blockNumber and blockHash
  • AccountResponse -- the blockNumber, addr, nonce, balance, storageRoot, and codeHash
  • StorageResponse -- the blockNumber, addr, slot, and value

Advanced reads

For more advanced users, we offer access to the raw Merkle-ized query results via isKeccakResultValid and isPoseidonResultValid. These allow validation of Keccak and Poseidon encoded block, account, and storage data in the Axiom Query Format. The Poseidon format may be especially useful for ZK developers.

function isKeccakResultValid(
    bytes32 keccakBlockResponse, 
    bytes32 keccakAccountResponse, 
    bytes32 keccakStorageResponse
) external view returns (bool);

function isPoseidonResultValid(
    bytes32 poseidonBlockResponse, 
    bytes32 poseidonAccountResponse, 
    bytes32 poseidonStorageResponse
) external view returns (bool);

Troubleshooting

Submitting the same keccakQueryResponse

Each keccakQueryResponse submitted to AxiomQuery must be unique, therefore if you have already called sendQuery with one keccakQueryResponse, the transaction will fail if you try to submit the same keccakQueryResponse after the first. This means that your calls to the QueryBuilder append function must differ in at least one field by the time build is called.

Provider URIs

Ensure that the providerUri you are using matches the chainId. For example, on Ethereum Mainnet (chainId 1), Alchemy's providerUri looks like:

https://eth-mainnet.g.alchemy.com/v2/UBGccHgGCaE...

Whereas on Goerli (chainId 5), it looks like this:

https://eth-goerli.g.alchemy.com/v2/UBGccHgGCaE...

Block Numbers

Block numbers on Goerli Testnet are significantly lower (9 million) than those on Ethereum Mainnet (17 million), so please ensure that you are using the correct blockNumber values for the chainId that you set.