# SSZ-QL
> [!Note]
> Implementing this endpoint is a part of [EPF6](https://github.com/eth-protocol-fellows/cohort-six) project by [Jun](https://github.com/syjn99) and [Nando](https://github.com/fernantho). You could check our project [proposal](https://hackmd.io/@junsong/HkQ9XBEHel) to get more context ([motivation](https://hackmd.io/@junsong/HkQ9XBEHel#Motivation), progress, etc.).
## SSZ-QL
> [!Note]
> Cases we have to consider from [Light Client Roadmap for Electra](https://hackmd.io/@etan-status/electra-lc#SSZ-query-language):
> - Requesting any sub-tree of an SSZ object (notably, only fields that the client knows about, to stay forward compatible) ✅
> - Allowing to request summaries (hash_tree_root) instead of fully expanded subtrees, on a per-field basis. ✅ 🤔 summary property in the json schema. I think it is not totally correct.
> I guess an example of this would be to request the whole beacon state without the full validators and balances subtree. Only their summaries.
> - Filtering, e.g., a transaction with certain root ✅
> - Simple back-references, e.g., a receipt at the same index in its tree as the found transaction in the other tree ✅ alias property
> - Specification relative to where the proof should be anchored ✅
> [!CAUTION]
> This document is a draft currently under construction.
## 1. Introduction
This specification introduces the Simple Serialize Query Language (SSZ-QL) and its Generic Merkle Proofs (GMP) component, establishing a unified framework for extracting data from any Consensus-Layer object while providing cryptographic proofs that attest to the integrity and validity of the retrieved information.
This specification heavily relies on [SSZ](https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md) and [Merkle Proof](https://github.com/ethereum/consensus-specs/blob/dev/ssz/merkle-proofs.md).
## 2. Terminology & Notation
- **Anchor/root**: a `Bytes32` Merkle root hash thatcommits to of all leaf nodes in the Merkle Tree. It is computed as `hash_tree_root(merkle_tree)` e.g. `beacon_block_root = hash_tree_root(beacon_block)` and `beacon_state_root = hash_tree_root(beacon_state)`. SSZ-QL also allows anchors that are roots of Merkle subtrees e.g. `validators_root = hash_tree_root(beacon_state.validators)`.
- **Leaf node**: a node in a Merkle tree that has no child nodes. **In SSZ Merkleization, a node that corresponds directly to a serialized SSZ chunk (a 32-byte value).** Leaf nodes are the fundamental units from which higher-level branch nodes are derived through successive pairwise hashing, ultimately producing the aforementioned `hash_tree_root` of the SSZ object.
- [**Generalized index**](https://github.com/ethereum/consensus-specs/blob/dev/ssz/merkle-proofs.md#generalized-merkle-tree-index): a generalized index is an integer that represents a node in a binary Merkle tree where each node has a generalized index `2 ** depth + index in row`.
```
1 --depth = 0 2**0 + 0 = 1
2 3 --depth = 1 2**1 + 0 = 2, 2**1+1 = 3
4 5 6 7 --depth = 2 2**2 + 0 = 4, 2**2 + 1 = 5...
```
This representation yields a node index for each piece of data in the Merkle tree.
- [**Path**](https://github.com/ethereum/consensus-specs/blob/dev/ssz/merkle-proofs.md#ssz-object-to-index): a list of nodes that represents the path from the root to the specified node.
- [**Summaries and expansions**](https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md#summaries-and-expansions):
Let `A` be an object derived from another object `B` by replacing some of the
(possibly nested) values of `B` by their `hash_tree_root`. We say `A` is a
"summary" of `B`, and that `B` is an "expansion" of `A`. Notice
`hash_tree_root(A) == hash_tree_root(B)`.
We similarly define "summary types" and "expansion types". For example,
[`BeaconBlock`](../specs/phase0/beacon-chain.md#beaconblock) is an expansion
type of
[`BeaconBlockHeader`](../specs/phase0/beacon-chain.md#beaconblockheader). Notice
that objects expand to at most one object of a given expansion type. For
example, `BeaconBlockHeader` objects uniquely expand to `BeaconBlock` objects.
## 3. Data & Proof Model (transport-agnostic core)
### [3.1 Recap SSZ object model](https://github.com/ethereum/consensus-specs/tree/dev/ssz)
#### TODO in we consider necessary
### 3.2 Query primitives
SSZ-QL exposes five primitives: anchor, path, filter, summary and alias:
#### 3.2.1 Anchoring model
An anchor is the node (root or subtree) the client already trusts and against which proofs are verified. SSZ-QL allows the usage of any anchor/root either it is a `hash_tree_root(CL object/merkle tree)` or `hash_tree_root(merkle subtree)`.
- A request may provide multiple anchors.
- Every query leaf must be a descendant of the anchor.
##### Examples
```json
"anchor": "beacon_block",
"anchor": "beacon_state.validators"
```
#### 3.2.2 Paths
- A path is a dot-notation plus optional [index]/[start:end[:step]] slices.
- Paths must match existing field names or indices in the SSZ type.
##### Examples
```json
.validators[100].withdrawal_credentials
.fork.current_version
.validators[100:200:5]
```
#### 3.2.3 Filters
- A filter is a boolean expression evaluated per element when a path yields a list/vector.
- Elements that evaluate to false are dropped out before proceeding to the proof construction.
- Typical operators are: `==, !=, >, <, in, &&, ||`
##### Examples
```json
"filter": "effective_balance >= 32e9 && slashed == false"
"filter": "withdrawals_credentials == 0x0123456789012345678901234567890123456789"
```
#### 3.2.4 Summaries
A summary replaces a subtree's expanded value with its `hash_tree_root()`.
Summaries are selected on a per-item list as shown in the examples below.
##### Examples
```json
{
"anchor": "beacon_state",
"path": ".",
"summaries": [".balances"]
}
```
```json
{
"anchor": "beacon_state",
"path": ".validators[100]",
"summaries": [".withdrawal_credentials"]
}
```
#### 3.2.5 Aliases
Alias names the current selection's index (or indices) so later can reference them.
Back references use $<alias>.index inside a path.
##### Examples
```json
{
"path": ".transactions[*]",
"filter": ".hash == '0xabc…'",
"alias": "tx"
},
{
"path": ".receipts[$tx.index]"
}
```
### 3.4 Proof model
As per [Merkle Proofs](https://github.com/ethereum/consensus-specs/blob/dev/ssz/merkle-proofs.md) specification in Consensus Layer, SSZ-QL also uses the proving methodology, having single Merkle Proofs as a special case of Merkle Multiproofs where the array of leaves to prove contains only one item.
This "unified" way of providing Merkle proofs provides users with a strong user experience at building system that consumes this proofs.
### 3.5 Input validation
TBD
We need to assess the validity of the queries.
ANALYZE the cascade:
- check if valid anchor
- check if path is valid considering the anchor
- check if filter is valid considering the path
The provided data input is validated by checking if the path is a descendant of the anchor:
```python
def is_descendant(child, ancestor):
while child > 1:
if child == ancestor:
return True
child //= 2
return False
assert is_descendant(leaf_gi, anchor_gi)
```
*Code 1: check if leaf is descendant of the anchor*
```python
def is_filtering_available(path, filter):
assert is_filtering_available(".validators", "*")
```
*Code 2: check if filters is valid*
## 4. Bindings
### API Rest (Beacon API)
A new `SSZQL` namespace is suggested. This namespace is set to hold at least the following endpoint:
- Endpoint: `/eth/v1/beacon/query`
- Method: `POST`
The query object contains the anchors/roots, paths, filters, summaries and aliases.
#### Parameters
This query contains no parameters, all the properties are specified in the request body.
#### Request Body
| Name | Type | Description |
| --- | --- | --- |
| `query` | `object` | A detailed query object. |
| `include_proof` | `boolean` | Whether including a merkle proof or not. Default to false. |
| `multiproof` | `boolean` | Whether using multiproof method for efficiency. Default to false. |
Every request MUST satisfy the JSON schema below.
```json
{
"$schema":"https://json-schema.org/draft-07/schema",
"title":"Query Object for SSZ QL",
"description":"Schema for SSZ query object",
"type":"object",
"properties":{
"query":{
"type":"array",
"items":{
"type":"object",
"properties":{
"obj_type":{
"type":"string",
"description":"The type of object being queried.",
"enum":[
"state",
"block"
]
},
"id":{
"type":"string",
"description":"Identifier of the object being queried (state ID or block ID, depending on obj_type)."
},
"anchor":{
"type":"string",
"description":"The anchor is the root against which the proofs are verified."
},
"path":{
"type":"string",
"description":"A merkle path to the item"
},
"filter":{
"type":"string",
"description":"Optional predicate applied when the path contains a list/vector"
},
"summaries":{
"type":"array",
"description":"Optional list of sub-paths under this item to return as hash_tree_root instead of expanded values.",
"items":{
"type":"string",
"description":"Path (relative to 'path') whose subtree should be summarized (hash_tree_root)."
},
},
"alias":{
"type":"string",
"description":"Optional variable name that stores the index (or full path) of every element matched by this query item, so later items can reference it with the placeholder `$<alias>.index` (e.g. `.receipts[$tx.index]`)."
}
},
"required":[
"obj_type",
"id",
"anchor",
"path"
]
},
"min_items":1
},
"include_proof":{
"type":"boolean",
"description":"Whether including a merkle proof or not. Default to false."
},
"multiproof": {
"type": "boolean",
"description": "Whether using multiproof method for efficiency. Default to false."
}
},
"required":[
"query"
]
}
```
This JSON schema contains an array of:
- *anchor*: A dot notation that resolves to one root or node in the tree.
```json
"anchor": ".beacon_state"
"anchor": ".beacon_state.validators"
"anchor": ".beacon_block"
```
- *path*: A dot notation that resolves to one or more SSZ leaves.
```json
"path": ".validators[100].withdrawal_credentials"
"path": ".fork.current_version"
"path": ".validators[*]"
```
- *filter*: A boolean expression evaluated per element when path yields a list/vector. Any elements that fail the predicate are dropped before proof
generation. Typical operators are: `==, !=, >, <, in, &&, ||`
```json
"filter": "effective_balance >= 32e9 && slashed == false"
"filter": "withdrawals_credentials == 0x0123456789012345678901234567890123456789"
```
- *summaries*: an array of, the selected subtrees are uniquely identified by its Bytes32 `hash_tree_root()`. When a user requests a summary, SSZ-QL must reply with the whole object with the requested subtree summarized. Useful when the dApp consumer wants a commitment but not the full data:
```json
{
"anchor": "beacon_state",
"path": ".",
"summary": [".balances"]
},
{
"anchor": "beacon_state",
"path": ".",
"summary": [".balances", ".validators"]
}
```
- *alias*: saves the index (or full path) of every element matched by this query item, so later items can reference it with `$<alias>.index`. Enables simple back-references like "receipt at the same position as this transaction".
```json
{
"anchor": "beacon_state",
"path": ".validators[*]",
"filter": ".slashed==true",
"alias": "slashed_validators"
},
{
"path": ".balances[$slashed_validators.index]"
}
```
### Response
| Code | Description | Content |
| --- | --- | --- |
| `200` | Success | Response with the [schema](#Response-Schema) |
| `400` | Invalid ID or malformed request | N/A |
| `404` | Not found | N/A |
| `500` | Internal server error | N/A |
| `503` | Beacon node is currently syncing and not serving request on that endpoint | N/A |
See [Example sections](#Example) for example responses.
#### Query Result
The content for success query follows typical Beacon API format: it includes `version`, `execution_optimistic`, `finalized`, and `data`.
For non-multiproof mode and multiproof mode, `data` contains an array with [`QueryResult`](#QueryResult) and the state root (`root`). In case multiproof is disabled, this `data` array one element per query elemente. In case multiproof is enabled, the `data` array contains only one element.
##### `QueryValueResult`
| Field | Type | Description |
| --- | --- | --- |
| `paths` | Array\<String\> | An array of Merkle paths for the queried items. Each path corresponds to the value at the respective index in the `results` array. |
| `results` | Array of any | An array of the actual values in JSON format. |
##### `QueryProofResult`
| Field | Type | Description |
| --- | --- | --- |
| `leaves` | Array\<String\> | An array of the actual values (leaves) corresponding to the queried paths. Each value is typically represented as a **32-byte hash**. |
| `gindices` | Array\<Number\> | An array of generalized indices for each value in the `leaves` array. |
| `proofs` | Array\<String\> | An array of proof that are sorted **descending order** by the generalized index. |
---
### API Binding - examples
For these examples, all the requests target the same endpoint:
POST `/eth/v1/beacon/query`
### 1. SSZ-QL without proof (`include_proof` is false)
```json
{
"query": [
{
"obj_type": "state",
"id": "finalized",
"path": ".genesis_validators_root"
},
{
"obj_type": "state",
"id": "finalized",
"path": ".validators[100]"
}
],
"include_proof": false,
"multiproof": false
}
```
Response:
```json
{
"version": "electra",
"execution_optimistic": true,
"finalized": true,
"data": {
"root": "0xf1f2f3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1",
"values": {
"paths": [
".genesis_validators_root",
".validators[100]"
],
"results": [
"0x4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95",
{
"pubkey": "0x8f2a41e5b6c71234abcd5678ef90ff11223344556677889900aabbccddeeff0",
"withdrawal_credentials": "0x00aa2753bbcc...99",
"effective_balance": "32000000000",
"slashed": false,
"activation_eligibility_epoch": "64",
"activation_epoch": "64",
"exit_epoch": "18446744073709551615",
"withdrawable_epoch": "18446744073709551615"
}
]
}
}
}
```
### 2. With proof (`non-multiproof` mode)
Request body:
```json
{
"query": [
{
"obj_type": "state",
"id": "finalized",
"path": ".genesis_validators_root"
},
{
"obj_type": "state",
"id": "finalized",
"path": ".validators[100]"
}
],
"include_proof": true,
"multiproof": false
}
```
Response:
```json
{
"version": "electra",
"execution_optimistic": true,
"finalized": true,
"data": {
"root": "0xf1f2f3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1",
"values": {
"paths": [
".genesis_validators_root",
".validators[100]"
],
"results": [
"0x4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95",
{
"pubkey": "0x8f2a41e5b6c71234abcd5678ef90ff11223344556677889900aabbccddeeff0",
"withdrawal_credentials": "0x00aa2753bbcc...99",
"effective_balance": "32000000000",
"slashed": false,
"activation_eligibility_epoch": "64",
"activation_epoch": "64",
"exit_epoch": "18446744073709551615",
"withdrawable_epoch": "18446744073709551615"
}
]
},
"proofs": [
{
"leaves": [
"0x4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95"
],
"gindices": [
"65"
],
"proofs": [
"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
"0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
]
},
{
"leaves": [
"0x12345678deadbeefcafebabef00dabad12345678deadbeefcafebabef00dabad"
],
"gindices": [
"8796093023233"
],
"proofs": [
"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
"0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
]
}
]
}
}
```
### 3. SSZ-QL with multiproofs
Request body:
```json
{
"query": [
{
"obj_type": "state",
"id": "finalized",
"path": ".genesis_validators_root"
},
{
"obj_type": "state",
"id": "finalized",
"path": ".validators[100]"
}
],
"include_proof": true,
"multiproof": true
}
```
Response:
```json
{
"version": "electra",
"execution_optimistic": true,
"finalized": true,
"data": {
"root": "0xf1f2f3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1",
"values": {
"paths": [
".genesis_validators_root",
".validators[100]"
],
"results": [
"0x4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95",
{
"pubkey": "0x8f2a41e5b6c71234abcd5678ef90ff11223344556677889900aabbccddeeff0",
"withdrawal_credentials": "0x00aa2753bbcc...99",
"effective_balance": "32000000000",
"slashed": false,
"activation_eligibility_epoch": "64",
"activation_epoch": "64",
"exit_epoch": "18446744073709551615",
"withdrawable_epoch": "18446744073709551615"
}
]
},
"proofs": [
{
"leaves": [
"0x4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95",
"0x12345678deadbeefcafebabef00dabad12345678deadbeefcafebabef00dabad"
],
"gindices": [
"65",
"8796093023233"
],
"proofs": [
"0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
"0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
"0xdddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
"0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee"
]
}
]
}
}
```
### 4. SSZ-QL Summary request
Request body:
```json
{
"query": [
{
"obj_type": "state",
"id": "finalized",
"anchor": "beacon_state",
"path": ".validators",
"summary": true
},
{
"obj_type": "state",
"id": "finalized",
"anchor": "beacon_state",
"path": ".balances",
"summary": true
},
],
"include_proof": true
}
```
Response:
```json
{
"version": "electra",
"execution_optimistic": true,
"finalized": true,
"data": {
"result": [
{
"paths": [".validators", ".balances"],
"leaves": [
"0x4b363db94e286120d76eb905340fdd4e54bfe9f06bf33ff6cf5ad27f511bfe95",
"0x0500000000000000000000000000000000000000000000000000000000000000"
],
"gindices": [65, 269],
"proofs": [
"0x5730c65f00000000000000000000000000000000000000000000000000000000",
"0xbe3dc5b7843f6b253970803030a18501814c97ac893ec03560ce4962688f857c",
"0x8d637afd2d258e4d079ca7f00dd4c857a61431af7262ede447b712a25d71e4bb",
"0x09ef197f8757969dce6a9379281f5c5b1ab7aeba924631d6ac5e560e817733c0",
"0x323688e7370b5f72bdadc1cdd2f2f4d9cd7065647a6f54a961def1222684e6d6",
"0xb7edabb0a1e1cb42fe1138558ca73510c890ba5dfc013aca0e2bf9a7c26af0a7",
"0x0400000000000000000000000000000000000000000000000000000000000000",
"0x8e38229b2010e3cde27597c6e4852c4e6cdca82e03574383993cd60250e9ed3a",
"0xe05db90000000000000000000000000000000000000000000000000000000000",
"0x96a9cb37455ee3201aed37c6bd0598f07984571e5f0593c99941cb50af942cb1"
]
}
],
"root": "0x2d178ffec45f6576ab4b61446f206c206c837fa3f324ac4d93a3eece8aad6d66"
}
}
```
### Library
SSZ-QL can also be invoked directly without the need of the API.
```python
class QueryRequest:
root_object: object
items: List[QueryItems]
include_proof: bool = False
```
## Resources
- [Simple Serialize](https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md)
- [Merkle Proof](https://github.com/ethereum/consensus-specs/blob/dev/ssz/merkle-proofs.md)