Hoy en día, los clientes de consenso no pueden proporcionar fácilmente piezas individuales de datos del BeaconState junto con las pruebas necesarias para verificarlos.El sistema Light Client de Ethereum define algunos caminos de prueba, pero no hay manera universal o estándar para que los clientes generen o sirvan estas pruebas. No es realista: el Estado Está en torno , que es demasiado grande para enviar a través de la red rápidamente y pone una carga innecesaria tanto en el nodo como en el usuario. La especificación incluso advierte de que los puntos finales de depuración utilizados para recoger estados completos están destinados sólo para el diagnóstico, no para el uso en el mundo real. Beaconestado Página 12,145,344 271 MB Una solución mucho mejor es utilizar Esto es especialmente útil porque la mayor parte del tamaño del estado proviene de validadores (~232 MB) y saldos (~15 MB); el resto de los campos son alrededor de ~24 MB. Si un usuario necesita solo un campo pequeño, es desperdiciable descargar el estado completo de 271 MB. En lugar de eso, una prueba de Merkle puede entregar sólo la hoja solicitada más su camino de autenticación, generalmente sólo unos pocos kilobytes. Merkle proofs or multiproofs Debido a esto, necesitamos una forma general y estandarizada para que los clientes pidan Esto reduce el ancho de banda, reduce la carga de la CPU y reemplaza las implementaciones dispersas y personalizadas actuales (por ejemplo, El tratamiento especial de ) de sólo los datos que necesitan, El Nimbus historical_summaries Este trabajo también es importante para el futuro de Ethereum. SSZ se está volviendo más central al protocolo: sustituir a la RLP por la SSZ, y el próximo (También llamado el El Will Por lo tanto, la construcción de un método limpio, eficiente y estándar para el acceso a datos basado en pruebas es un paso clave hacia futuras actualizaciones de protocolos. Púrpura (EIP 7919) beam chain lean chain Levantamiento Proposed Solution: Introducing the SSZ Query Language (SSZ-QL) Solución propuesta: Introducción al lenguaje de consulta SSZ (SSZ-QL) La idea de SSZ-QL fue originalmente propuesta por Su pregunta principal fue sencilla pero poderosa: Entonces Kissling “What if we had a standard way to request SSZ field — together with a Merkle proof — directly from any consensus client?” any cualquier Hoy en día, los clientes de consenso no ofrecen un método general o estandarizado para solicitar datos SSZ específicos con pruebas.Algunas soluciones ad-hoc existen (por ejemplo, las consultas básicas de Nimbus utilizadas por la verificación ), pero no hay un lenguaje de consulta SSZ adecuado y universal disponible, y quizás nada listo en el momento en que se escribió esta idea. Web3Signatura La propuesta de Etan describe lo que un lenguaje de consulta SSZ debe permitir: Solicitar cualquier subárbol dentro de un objeto SSZ Seleccionar si un campo se debe expandir por completo o devolver solo como hash_tree_root Filtración (por ejemplo, encontrar una transacción con una cierta raíz) Using back-references (e.g., retrieving the receipt at the same index as a matching transaction) Especificar dónde debe anclarse la prueba Apoyar la compatibilidad con el futuro para que los clientes puedan ignorar de forma segura los campos futuros desconocidos Este tipo de API podría ser utilizado tanto por los clientes de consenso como por los clientes de ejecución. ), las estructuras de solicitud y respuesta pueden incluso generarse automáticamente. Página 7495 Basándose en esta idea, , que están desarrollando esto como parte de su proyecto EPF en prysm, es agregar un nuevo punto final de la API Beacon que soporta SSZ Query Language (SSZ-QL). Este punto final permite a los usuarios recuperar exactamente los datos SSZ que necesitan -no más, no menos- junto con una prueba de Merkle que verifica su corrección. La versión inicial ofrecerá un conjunto de características mínimo pero práctico, que ya cubre la mayoría de los casos reales de uso. (La especificación del proyecto de API está disponible para revisión.) the proposed solution by and Jun Fernando Jun Fernando Más allá de esta versión mínima, también planea crear una especificación completa de SSZ-QL. Esta versión ampliada soportará funciones avanzadas como filtrar, solicitar intervalos de datos y elegir puntos de anclaje personalizados, todo incluido con pruebas de Merkle. Comprender los índices generalizados (GI) antes de sumergirse en SSZ-QL En SSZ, cada objeto —incluyendo todo el Está representado como a . A es simplemente un número que identifica de manera única dentro de este árbol. BeaconState binary Merkle tree generalized index (GI) any node Las reglas son muy simples: El nodo de raíz tiene un índice generalizado:GI = 1 Para cualquier nodo con índice i:hijo izquierdo = 2*i,hijo derecho = 2*i + 1 El árbol entero está numerado como: GI:1 / \ GI:2 GI:3 / \ / \ GI:4 GI:5 GI:6 GI:7 ... Esta numeración hace que las pruebas de Merkle sean fáciles. , usted sabe exactamente dónde se sienta en el árbol y qué hashes hermanos deben incluirse para verificarlo. generalized index of a leaf Example with Beacon State: 0 GenesisTime string 1 GenesisValidatorsRoot string 2 Slot string 3 Fork *Fork 4 LatestBlockHeader *BeaconBlockHeader 5 BlockRoots []string 6 StateRoots []string 7 HistoricalRoots []string 8 Eth1Data *Eth1Data 9 Eth1DataVotes []*Eth1Data 10 Eth1DepositIndex string 11 Validators []*Validator ← (p = 11) 12 Balances []string 13 RandaoMixes []string 14 Slashings []string 15 PreviousEpochAttestations []*pendingAttestation 16 CurrentEpochAttestations []*pedningAttestation 17 JustificationBits string 18 PreviousJustifiedCheckpoint *Checkpoint 19 CurrentJustifiedCheckpoint *Checkpoint 20 FinalizedCheckpoint *Checkpoint Hay 21 campos de nivel superior (indicado 0..20). Para colocar estos en un árbol Merkle, SSZ los pads hasta el siguiente poder de dos (32). 32 hojas → profundidad = 5. Las hojas de nivel superior ocupan el rango GI: 32 ... 63 Calculamos el GI para un campo de nivel superior usando: La fórmula: GI_top = 2^depth + field_index por índice de campo = .validators 11 Así que: GI_validators = 2^5 + 11 = 32 + 11 = 43. En este caso ( ) es el compromiso de toda la Dentro de la global El árbol. 43 validator’s subtree BeaconState Multi-Level Proof: Example With validators[42].withdrawal_credentials Ahora, supongamos que queremos una prueba de: BeaconState.validators[42].withdrawal_credentials Esto requiere : two levels of proof Prove that the entire validator’s subtree is included in the BeaconState root We already know: Top-level GI for validators = 43 Using GI 43, the consensus client collects the sibling hashes on the path from leaf 43 up to root (e.g., ). GI 43 → 21 → 10 → 5 → 2 → 1 This gives the proof: validators_root ---> BeaconState_root Prove that is inside the validator’s subtree validator[42].withdrawal_credentials Now treat the . validators list as its own Merkle tree Inside this subtree: Validator is the 42-nd element → it maps to some leaf index (e.g. chunk ) inside this subtree. 42 k Withdrawal credentials lives inside one of the 32-byte SSZ chunks of validator #42 (for example chunk — number doesn’t matter, just concept). k = 128 We now generate: leaf (withdrawal_credentials chunk) ---> validators_root by collecting sibling hashes inside the local validator-subtree. Final Combined Proof You end up with: 1. Local Level Proof Proves withdrawal_credentials --> validator_root 2. Top-level branch proof Proves validator_root --> BeaconState_root A verifier can now reconstruct the BeaconState root from only: the requested leaf the two lists of sibling nodes the known BeaconState root No full state download needed. ┌───────────────────────────────┐ │ BeaconState Root │ └───────────────────────────────┘ ▲ │ (Top-level Merkle Proof) │ Sibling hashes for GI = 43 │ ┌─────────────────────────────────────────┐ │ validators_root (GI = 43) │ └─────────────────────────────────────────┘ ▲ │ (Local Subtree Proof) │ Proof inside validators list │ for index = 42 │ ┌─────────────────────────────────────────────────────────┐ │ Validator[42] Subtree (list element #42) │ └─────────────────────────────────────────────────────────┘ ▲ │ (Field-level Merkle Proof) │ Sibling hashes inside the │ validator struct │ ┌──────────────────────────────────────────┐ │ validator[42].withdrawal_credentials │ ← requested field └──────────────────────────────────────────┘ Understanding SSZ Serialization Before Computing Generalized Indices Comprender la serialización de SSZ antes de calcular índices generalizados Para calcular la corrección En primer lugar, debes saber cómo funciona el SSH. y diferentes tipos de datos. Los índices generalizados no existen aislados; se derivan de la , y la forma del árbol depende enteramente de cómo SSZ interpreta los campos de estructura de Go subyacentes. generalized index serializes mercaderías shape of the Merkle tree En SSZ, cada campo sólo puede ser una de dos categorías: Base Types (fixed-size values) , , , etc. These are straightforward — they always serialize into a fixed number of bytes. uint64 Bytes32 Bytes20 uint256 Composite Types (like BeaconState), (fixed length), (variable length), , And each of them is serialized in a slightly different way. Container Vector[T, N] List[T, N] Bitvector[N] Bitlist[N] To compute a for any field inside a state, the SSZ tree must first know . This is why the generated files include tags such as: generalized index (g-index) how that field is serialized *.pb.go ssz-size:"8192,32" → Vector ssz-max:"16" → List ssz-size:"?,32" → List of Vector Para calcular un índice generalizado para cualquier campo, primero debemos comprender el Sobre el objeto: SSZ structure cuáles son los campos, si cada campo es una lista o un vector, cuántas piezas ocupa cada campo, y cómo se deben atravesar los tipos de nidos. Esto es exactamente lo que el La función hace en Prysm, ubicado en AnalyzeObject encoding/ssz/query/analyzer.go // AnalyzeObject analyzes given object and returns its SSZ information. func AnalyzeObject(obj SSZObject) (*SszInfo, error) { value := reflect.ValueOf(obj) info, err := analyzeType(value, nil) if err != nil { return nil, fmt.Errorf("could not analyze type %s: %w", value.Type().Name(), err) } // Populate variable-length information using the actual value. err = PopulateVariableLengthInfo(info, value) if err != nil { return nil, fmt.Errorf("could not populate variable length info for type %s: %w", value.Type().Name(), err) } return info, nil } What analyzeType Does Es la función que Y los números salieron Eso sí, es una - Lo hace dependen de los valores de tiempo de ejecución reales, solo en el tipo de Go y las etiquetas de estructura. analyzeType examines a Go value using reflection what kind of SSZ type Paso de análisis de tipo puro not Cuando le damos un campo o estructura, es: Comprueba el tipo de Go (uint, struct, slice, pointer, etc.) Lee las etiquetas de estructuras relacionadas con SSZ como ssz-size y ssz-max Decides : whether this field is a basic SSZ type ( , , ) uint64 uint32 bool a Vector ( ) ssz-size:"N" a List ( ) ssz-max:"N" a Bitvector / Bitlist a Container (struct) Builds an that describes: SszInfo record the SSZ type (List, Vector, Container...) whether it is fixed-sized or variable-sized offsets of fields (for Containers) nested SSZ information for child fields Piensa en Como la función que producen a Para este tipo. analyzeType scans the type definition static SSZ layout blueprint What PopulateVariableLengthInfo Does Mientras Estudios de la , algunos objetos SSZ no se pueden describir por completo sin la . analyzeType Tipo Valor real Los ejemplos: Las listas ([]T) necesitan conocer su longitud actual Los campos de contenedores de tamaño variable necesitan su compensación real Las listas envueltas necesitan el tamaño real de cada elemento fills in this missing runtime information. PopulateVariableLengthInfo y es: Se ve el plan SszInfo creado por analyzeType Mirar el valor real del objeto pasado Computes values that can only be known at runtime: length of Lists sizes of nested variable elements offsets of variable-sized fields inside Containers bitlist length from bytes Procesan todo — for example, a Container with a List containing structs with Lists will all be filled in. recursively Piensa en Como la función que y rellena las mediciones reales basadas en el valor real que pasas. PopulateVariableLengthInfo takes the blueprint from analyzeType Example: Pruebe esta función con una estructura de BeaconState type BeaconState struct { state protoimpl.MessageState `protogen:"open.v1"` GenesisTime uint64 `protobuf:"varint,1001,opt,name=genesis_time,json=genesisTime,proto3" json:"genesis_time,omitempty"` GenesisValidatorsRoot []byte `protobuf:"bytes,1002,opt,name=genesis_validators_root,json=genesisValidatorsRoot,proto3" json:"genesis_validators_root,omitempty" ssz-size:"32"` Slot github_com_OffchainLabs_prysm_v7_consensus_types_primitives.Slot `protobuf:"varint,1003,opt,name=slot,proto3" json:"slot,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/primitives.Slot"` Fork *Fork `protobuf:"bytes,1004,opt,name=fork,proto3" json:"fork,omitempty"` LatestBlockHeader *BeaconBlockHeader `protobuf:"bytes,2001,opt,name=latest_block_header,json=latestBlockHeader,proto3" json:"latest_block_header,omitempty"` BlockRoots [][]byte `protobuf:"bytes,2002,rep,name=block_roots,json=blockRoots,proto3" json:"block_roots,omitempty" ssz-size:"8192,32"` StateRoots [][]byte `protobuf:"bytes,2003,rep,name=state_roots,json=stateRoots,proto3" json:"state_roots,omitempty" ssz-size:"8192,32"` HistoricalRoots [][]byte `protobuf:"bytes,2004,rep,name=historical_roots,json=historicalRoots,proto3" json:"historical_roots,omitempty" ssz-max:"16777216" ssz-size:"?,32"` Eth1Data *Eth1Data `protobuf:"bytes,3001,opt,name=eth1_data,json=eth1Data,proto3" json:"eth1_data,omitempty"` Eth1DataVotes []*Eth1Data `protobuf:"bytes,3002,rep,name=eth1_data_votes,json=eth1DataVotes,proto3" json:"eth1_data_votes,omitempty" ssz-max:"2048"` Eth1DepositIndex uint64 `protobuf:"varint,3003,opt,name=eth1_deposit_index,json=eth1DepositIndex,proto3" json:"eth1_deposit_index,omitempty"` Validators []*Validator `protobuf:"bytes,4001,rep,name=validators,proto3" json:"validators,omitempty" ssz-max:"1099511627776"` Balances []uint64 `protobuf:"varint,4002,rep,packed,name=balances,proto3" json:"balances,omitempty" ssz-max:"1099511627776"` RandaoMixes [][]byte `protobuf:"bytes,5001,rep,name=randao_mixes,json=randaoMixes,proto3" json:"randao_mixes,omitempty" ssz-size:"65536,32"` Slashings []uint64 `protobuf:"varint,6001,rep,packed,name=slashings,proto3" json:"slashings,omitempty" ssz-size:"8192"` PreviousEpochAttestations []*PendingAttestation `protobuf:"bytes,7001,rep,name=previous_epoch_attestations,json=previousEpochAttestations,proto3" json:"previous_epoch_attestations,omitempty" ssz-max:"4096"` CurrentEpochAttestations []*PendingAttestation `protobuf:"bytes,7002,rep,name=current_epoch_attestations,json=currentEpochAttestations,proto3" json:"current_epoch_attestations,omitempty" ssz-max:"4096"` JustificationBits github_com_OffchainLabs_go_bitfield.Bitvector4 `protobuf:"bytes,8001,opt,name=justification_bits,json=justificationBits,proto3" json:"justification_bits,omitempty" cast-type:"github.com/OffchainLabs/go-bitfield.Bitvector4" ssz-size:"1"` PreviousJustifiedCheckpoint *Checkpoint `protobuf:"bytes,8002,opt,name=previous_justified_checkpoint,json=previousJustifiedCheckpoint,proto3" json:"previous_justified_checkpoint,omitempty"` CurrentJustifiedCheckpoint *Checkpoint `protobuf:"bytes,8003,opt,name=current_justified_checkpoint,json=currentJustifiedCheckpoint,proto3" json:"current_justified_checkpoint,omitempty"` FinalizedCheckpoint *Checkpoint `protobuf:"bytes,8004,opt,name=finalized_checkpoint,json=finalizedCheckpoint,proto3" json:"finalized_checkpoint,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } package main import ( "fmt" "github.com/OffchainLabs/prysm/v7/encoding/ssz/query" eth "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" ) func main() { v := &eth.BeaconState{} // Analyze it with Prysm’s existing SSZ analyzer info, _ := query.AnalyzeObject(v) fmt.Println(info.Print()) } La salida: BeaconState (Variable-size / size: 2687377) ├─ genesis_time (offset: 0) uint64 (Fixed-size / size: 8) ├─ genesis_validators_root (offset: 8) Bytes32 (Fixed-size / size: 32) ├─ slot (offset: 40) Slot (Fixed-size / size: 8) ├─ fork (offset: 48) Fork (Fixed-size / size: 16) │ ├─ previous_version (offset: 0) Bytes4 (Fixed-size / size: 4) │ ├─ current_version (offset: 4) Bytes4 (Fixed-size / size: 4) │ └─ epoch (offset: 8) Epoch (Fixed-size / size: 8) ├─ latest_block_header (offset: 64) BeaconBlockHeader (Fixed-size / size: 112) │ ├─ slot (offset: 0) Slot (Fixed-size / size: 8) │ ├─ proposer_index (offset: 8) ValidatorIndex (Fixed-size / size: 8) │ ├─ parent_root (offset: 16) Bytes32 (Fixed-size / size: 32) │ ├─ state_root (offset: 48) Bytes32 (Fixed-size / size: 32) │ └─ body_root (offset: 80) Bytes32 (Fixed-size / size: 32) ├─ block_roots (offset: 176) Vector[Bytes32, 8192] (Fixed-size / size: 262144) ├─ state_roots (offset: 262320) Vector[Bytes32, 8192] (Fixed-size / size: 262144) ├─ historical_roots (offset: 2687377) List[Bytes32, 16777216] (Variable-size / length: 0, size: 0) ├─ eth1_data (offset: 524468) Eth1Data (Fixed-size / size: 72) │ ├─ deposit_root (offset: 0) Bytes32 (Fixed-size / size: 32) │ ├─ deposit_count (offset: 32) uint64 (Fixed-size / size: 8) │ └─ block_hash (offset: 40) Bytes32 (Fixed-size / size: 32) ├─ eth1_data_votes (offset: 2687377) List[Eth1Data, 2048] (Variable-size / length: 0, size: 0) ├─ eth1_deposit_index (offset: 524544) uint64 (Fixed-size / size: 8) ├─ validators (offset: 2687377) List[Validator, 1099511627776] (Variable-size / length: 0, size: 0) ├─ balances (offset: 2687377) List[uint64, 1099511627776] (Variable-size / length: 0, size: 0) ├─ randao_mixes (offset: 524560) Vector[Bytes32, 65536] (Fixed-size / size: 2097152) ├─ slashings (offset: 2621712) Vector[uint64, 8192] (Fixed-size / size: 65536) ├─ previous_epoch_attestations (offset: 2687377) List[PendingAttestation, 4096] (Variable-size / length: 0, size: 0) ├─ current_epoch_attestations (offset: 2687377) List[PendingAttestation, 4096] (Variable-size / length: 0, size: 0) ├─ justification_bits (offset: 2687256) Bitvector[8] (Fixed-size / size: 1) ├─ previous_justified_checkpoint (offset: 2687257) Checkpoint (Fixed-size / size: 40) │ ├─ epoch (offset: 0) Epoch (Fixed-size / size: 8) │ └─ root (offset: 8) Bytes32 (Fixed-size / size: 32) ├─ current_justified_checkpoint (offset: 2687297) Checkpoint (Fixed-size / size: 40) │ ├─ epoch (offset: 0) Epoch (Fixed-size / size: 8) │ └─ root (offset: 8) Bytes32 (Fixed-size / size: 32) └─ finalized_checkpoint (offset: 2687337) Checkpoint (Fixed-size / size: 40) ├─ epoch (offset: 0) Epoch (Fixed-size / size: 8) └─ root (offset: 8) Bytes32 (Fixed-size / size: 32) En la salida del analizador SSZ, el Mostrado para cada campo representa la posición exacta de byte donde ese campo comienza cuando todo el struct es serializado de acuerdo con las reglas SSZ. , empacados estrechamente uno tras otro, y el offset le dice dónde empieza cada uno de estos campos dentro de ese flujo de byte empacado. El campo es un valor de tamaño fijo de 32 bytes, y sus bytes serializados comienzan en posición en el conjunto de bytes codificado por SSZ. El Indica cuántos bytes el campo contribuye a la salida serializada (32 bytes en este caso). Para los tipos de tamaño fijo, el tamaño es predeterminado, mientras que para los tipos de tamaño variable, el analizador calcula el tamaño basado en el valor real. offset fixed-size fields first root (offset: 8) Bytes32 (Fixed-size / size: 32) root 8 size Example: Finding the Merkle Leaf for a Field Using the Offset Ejemplo: Encontrar la hoja de Merkle para un campo usando el offset Tomemos un campo real de la salida del analizador SSZ: ├─ fork (offset: 48) Fork (Fixed-size / size: 16) │ ├─ previous_version (offset: 0) Bytes4 (Fixed-size / size: 4) │ ├─ current_version (offset: 4) Bytes4 (Fixed-size / size: 4) │ └─ epoch (offset: 8) Epoch (Fixed-size / size: 8) Queremos demostrar el campo: fork.epoch El campo “fork” en Comienza en en el flujo de bytes serializado. BeaconState offset 48 Inside , el El campo comienza en (en relación con el comienzo de Fork). fork epoch offset 8 Así que: absolute_offset = base_offset_of_fork + offset_of_epoch_inside_fork absolute_offset = 48 + 8 = 56 bytes comienza en el byte 56 del BeaconState serializado completo. fork.epoch SSZ divide la serialización en : 32-byte chunks Chunk 0 → bytes 0–31 Chunk 1 → bytes 32–63 Chunk 2 → bytes 64–95 … Descubre cuáles son los bytes que contiene : 56 chunk_index = floor(56 / 32) = 1 Así que: Las hojas que contienen Se trata de Leaf / Chunk 1. fork.epoch Es un Entierro fork.epoch 8-byte Dentro de la sección 1 (bytes 32-63): local_offset = 56 - 32 = 24 Así que dentro de la hoja de 32 bytes, los bytes se ven como: [ 0 … 23 ] → unrelated fields [ 24 … 31 ] → fork.epoch (8 bytes) To prove this value, you: Tome chunk 1 → esta es tu hoja. When hashing up the tree, at each level: If chunk is a left child → record the right sibling hash. If chunk is a right child → record the left sibling hash. Continúe hasta que llegue a la raíz de Merkle superior. Los hashes hermanos recogidos forman tu: SSZ Merkle proof branch for fork.epoch Anyone can verify this by recomputing: hash_tree_root(leaf + all_siblings) == state_root Esto introduce dos nuevos endpoints que exponen la versión inicial de En el PRISM: SSZ Query Language (SSZ-QL) /prysm/v1/beacon/states/{state_id}/query /prysm/v1/beacon/blocks/{block_id}/query Ambos endpoints siguen la especificación del punto final SSZ-QL y permiten a los clientes solicitar campos específicos dentro de un BeaconState o BeaconBlock utilizando una cadena de consulta. El servidor devuelve el campo SSZ solicitado codificado como bytes SSZ crudos. La bandera es ignorada: el PR siempre devuelve respuestas sin pruebas de Merkle. include_proof La estructura de la solicitud es: type SSZQueryRequest struct { Query string `json:"query"` IncludeProof bool `json:"include_proof,omitempty"` } Y ambos puntos terminales devuelven una respuesta codificada por SSZ de esta forma: type SSZQueryResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Root []byte `protobuf:"bytes,1,opt,name=root,proto3" json:"root,omitempty" ssz-size:"32"` Result []byte `protobuf:"bytes,2,opt,name=result,proto3" json:"result,omitempty" ssz-max:"1073741824"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } Para la especificación completa y ejemplos, puede consultar aquí Enlace y información del analizador SSZ, en lugar de utilizar un índice generalizado. For now, the implementation locates the requested field using the computed offset size Para obtener más información, puede consultar el trabajo de Jun Song - implementado junto con Fernando como parte de su proyecto EPF en prysm. For more information, you can check out ’s work — implemented together with as part of their EPF project in prysm. Jun Song Fernando Canción Jun Fernando