Queries from a Smart Contract (ZK-SQL onchain)
Queries from a Smart Contract (ZK-SQL onchain)
Overview: https://docs.spaceandtime.io/update/docs/zk-sql-via-smart-contracts
Space and Time (SXT Chain) is the first onchain database for the EVM, enabling smart contracts to execute ZK-verified SQL queries against your custom tables on SXT Chain as well as against tables of historical indexed data secured on SXT Chain from other popular chains such as Ethereum, Base, etc.
In short, your smart contract doesn't execute SQL itself—it submits a precompiled query plan to Space and Time's QueryRouter (relayer) contract along with a small payment + a callback target. The QueryRouter contract runs the query against your SXT Chain tables, generates a Proof of SQL that attests the result is correct (verified onchain), and then the query result is passed back via your contract's callback with the verified result bytes. Inside the callback, your contract verifies the caller is the router executor, deserializes the proven table result, and safely uses the returned rows with both cryptographic guarantees that the data matches the SQL query you requested, and cryptographic guarantees that the underlying table(s) are tamperproof.
Lifecycle of an Onchain Query with Proof of SQL (SXT Chain)
- Request starts onchain: A client smart contract calls our QueryRouter contract via requestQuery(...), passing:
- the proof plan (hex-encoded query plan generated from SQL via the SXT RPC),
- any serialized parameters,
- a version field + optional metadata,
- a callback target (contract + selector) and gas constraints,
- and payment details (amount, refund address, timeout). Payment is typically done by transferring SXT into the client contract and approving the QueryRouter to spend it for that request.
- Query is executed offchain, then proven: Space and Time's network executes the query against the referenced tables and produces a Proof of SQL that attests the result matches the requested query plan (and inputs).
- Router fulfills via a trusted executor callback: When execution completes, a designated QueryRouter executor calls the client contract's callback function with the query result payload.
- Client contract validates the callback origin: The callback must defensively check that:
- msg.sender is the authorized QueryRouter executor, and
- the queryId is one the contract actually requested (e.g., pendingQueries[queryId]).
- Result is consumed onchain: The callback decodes the returned result bytes into a table-like structure (e.g., deserializing a ProofOfSqlTable result) and then the contract can safely use the returned rows (emit events, update state, trigger additional logic), relying on the cryptographic guarantees provided by Proof of SQL as enforced by the QueryRouter fulfillment path.
In this tutorial, we will be running an onchain query against the table created in the "Create Table Hello World Tutorial". This query uses Proof of SQL, a cryptographic protocol that gives smart contracts a guarantee of the correctness of the query result.
The major steps we will walk through are
- Writing a smart contract
- Deploying the smart contract
- Running the query
Step 1: Creating the contract
In order to run a SQL query, we first convert the SQL text to a hex query plan. We will run the simple query:
SELECT NAME FROM TUTORIAL_ABC8D709C80262965344F5240AD123F5CBE51123.HELLO_WORLD WHERE LONGITUDE = 60In order to obtain the query plan, we send a request to an RPC node of the Space and Time network.
curl -X POST https://rpc.mainnet.sxt.network \
-H "Content-Type: application/json" -d '{
"jsonrpc": "2.0", "id": 1, "method": "commitments_v1_evmProofPlan",
"params": {
"query": "SELECT NAME FROM TUTORIAL_ABC8D709C80262965344F5240AD123F5CBE51123.HELLO_WORLD WHERE LONGITUDE = 60"
}
}'The output is
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"proofPlan": "0x0000000000000001000000000000003d5455544f5249414c5f414243384437303943383032363239363533343446353234304144313233463543424535313132332e48454c4c4f5f574f524c440000000000000002000000000000000000000000000000044e414d4500000007000000000000000000000000000000094c4f4e47495455444500000005000000000000000100000000000000044e414d45000000000000000000000000000000020000000000000000000000010000000100000005000000000000003c0000000000000001000000000000000000000000",
"at": "0x87a94aa75b8915f8e973f211ff1831bb608f3c1c99ff36282c47679c81d3dab5"
}
}We then create a constant in our smart contract with this query plan. (That is, using the proofPlan and dropping the 0x.)
// SELECT NAME FROM TUTORIAL_ABC8D709C80262965344F5240AD123F5CBE51123.HELLO_WORLD WHERE LONGITUDE = 60
bytes public constant QUERY_PLAN =
hex"0000000000000001000000000000003d5455544f5249414c5f414243384437303943383032363239363533343446353234304144313233463543424535313132332e48454c4c4f5f574f524c440000000000000002000000000000000000000000000000044e414d4500000007000000000000000000000000000000094c4f4e47495455444500000005000000000000000100000000000000044e414d45000000000000000000000000000000020000000000000000000000010000000100000005000000000000003c0000000000000001000000000000000000000000";We also import the ProofOfSQL library, which has all the necessary components to run this query. In particular, we will be using the QueryRouter contract to run the query.
import "../libraries/ProofOfSQL.sol";When the query method is called on the smart contract, we build the query:
IQueryRouter.Query memory query = IQueryRouter.Query({
innerQuery: QUERY_PLAN,
parameters: ParamsBuilder.serializeParamArray(new bytes[](0)),
version: VERSION,
metadata: ""
});We then need to specify a callback method to be called when the query has executed. We specify the contract, method, along with limits on gas usage:
IQueryRouter.Callback memory callback = IQueryRouter.Callback({
callbackContract: address(this),
selector: HelloWorld.queryCallback.selector,
gasLimit: 100_000,
maxGasPrice: 10 gwei,
callbackData: ""
});Finally, we set query payment details:
uint256 paymentAmount = 100 ether;
IQueryRouter.Payment memory payment = IQueryRouter.Payment({
paymentAmount: paymentAmount, refundTo: msg.sender, timeout: uint64(block.timestamp + 1 hours)
});
IERC20(SXT).safeTransferFrom(msg.sender, address(this), paymentAmount);
IERC20(SXT).safeIncreaseAllowance(QUERY_ROUTER, paymentAmount);Now, we start the query by calling requestQuery:
bytes32 queryId = IQueryRouter(QUERY_ROUTER).requestQuery(query, callback, payment);Once the query is fulfilled, the queryCallback function will be called by the query router.
Inside this function we need to ensure that no one malicious can trigger it:
if (msg.sender != QUERY_ROUTER_EXECUTOR || !pendingQueries[queryId]) revert UnauthorizedCaller();Then, we can use the result of the query inside our contract knowing that the result has been cryptographically verified using the Proof Of SQL protocol.
(, ProofOfSqlTable.Table memory tableResult) = ProofOfSqlTable.__deserializeFromBytes(queryResult);
string[] memory names = ProofOfSqlTable.readVarCharColumn(tableResult, 0);
for (uint256 i = 0; i < names.length; ++i) {
emit QueryFulfilled(names[i]);
}Step 2: Deploying the contract
To deploy the smart contract, we will be using standard Foundry tooling. The contract described above can be found in contracts/src/HelloWorld/HelloWorld.sol. First we clone this repo and switch into the contracts directory.
git clone [email protected]:spaceandtimefdn/sxt-chain-examples.git
cd sxt-chain-examples/contractsNow, we deploy the smart contract with forge. (This can be first run without --broadcast to do a dry run.)
forge create HelloWorld --private-key $PRIVATE_KEY --rpc-url $RPC_URL --broadcast --verify
Our deployed contract has address 0x051d9FDA993323c1e74f8Ff9Bd22b911774e1D93 and can be found here.
Step 3: Running the query
Now, to run the query, we must approve spend of 100 SXT for the contract. To do this, we call approve on the SXT token contract. Because SXT has 18 decimals, we enter 100000000000000000000, which is 100e18.
Now, we can run the query by calling the query method on the new HelloWorld contract
This triggers the query. When the callback is called, the event is emmited, and can be seen here.

Updated 3 days ago