Hoje, os clientes consensuais não podem facilmente fornecer pedaços individuais de dados do BeaconState juntamente com as provas necessárias para verificá-los.O sistema Light Client da Ethereum define alguns caminhos de prova, mas não há maneira universal ou padrão para os clientes gerarem ou servirem essas provas. Não é realista: o Estado Está em torno , que é muito grande para enviar sobre a rede rapidamente e coloca cargas desnecessárias tanto no nó quanto no usuário.A especificação até adverte que os endpoints de depuração usados para pegar estados completos são destinados apenas para diagnósticos, não para uso real. Beaconestão Avaliação 12,145,344 271 MB Uma solução muito melhor é usar Isso é especialmente útil porque a maior parte do tamanho do estado vem de validadores (~232 MB) e saldos (~15 MB); o resto dos campos são cerca de ~24 MB. Se um usuário precisa apenas de um pequeno campo, é desperdiçoso baixar o estado inteiro de 271 MB. Merkle proofs or multiproofs Devido a isso, precisamos de uma maneira geral e padronizada para os clientes solicitarem Isso reduz a largura de banda, reduz a carga da CPU e substitui as implementações dispersas e personalizadas de hoje (por exemplo, Tratamento especial de ). Apenas os dados necessários, O Nimbus historical_summaries This work is also important for the future of Ethereum. SSZ is becoming more central to the protocol: substituir o RLP pelo SSZ, e o próximo (também chamado de A vontade Assim, a construção de um método limpo, eficiente e padrão para acesso de dados baseado em evidências é um passo chave para futuras atualizações de protocolos. Púrpura (EIP-7919) beam chain lean chain Levantamento Proposed Solution: Introducing the SSZ Query Language (SSZ-QL) Solução proposta: Introdução à linguagem de consulta SSZ (SSZ-QL) A ideia de SSZ-QL foi originalmente proposta por Sua pergunta principal era simples, mas poderosa: Dica de Kissling “What if we had a standard way to request SSZ field — together with a Merkle proof — directly from any consensus client?” any qualquer Today, consensus clients do not offer a general or standardized method to request specific SSZ data with proofs. Some ad-hoc solutions exist (for example, Nimbus’ basic queries used by the verifying ), mas não há uma linguagem de consulta SSZ adequada e universal disponível – e talvez nada pronto no momento em que esta ideia foi escrita. WEB3 assinatura A proposta de Etan descreve o que uma linguagem de consulta SSZ deve permitir: Solicitar qualquer subárvore dentro de um objeto SSZ Escolher se um campo deve ser totalmente expandido ou devolvido apenas como hash_tree_root Filtragem (por exemplo, encontrar uma transação com uma determinada raiz) Usando back-references (por exemplo, recuperando o recibo no mesmo índice que uma transação correspondente) Especificar onde a prova deve ser ancorada Suporte à compatibilidade futura para que os clientes possam ignorar com segurança campos futuros desconhecidos Este tipo de API poderia ser usado por clientes de consenso e execução. Com tipos SSZ compatíveis com o futuro (como aqueles de As estruturas de solicitação e resposta podem até ser geradas automaticamente. Página 7495 Com base nessa ideia, , que estão desenvolvendo isso como parte de seu projeto EPF em prysm, é adicionar um novo endpoint da API Beacon que suporta o SSZ Query Language (SSZ-QL). Este endpoint permite que os usuários obtenham exatamente os dados SSZ de que precisam - não mais, não menos - juntamente com uma prova Merkle que verifica sua exatidão. A versão inicial oferecerá um conjunto de recursos mínimo, mas prático, que já cobre a maioria dos casos reais de uso. (A especificação do projeto da API está disponível para revisão.) the proposed solution by and Junho Fernando Junho Fernando Além desta versão mínima, também planeja criar uma especificação SSZ-QL completa. Esta versão expandida suportará recursos avançados como filtrar, solicitar intervalos de dados e escolher pontos de ancoragem personalizados, tudo com provas da Merkle incluídas. Compreender índices generalizados (GI) antes de mergulhar em SSZ-QL Em SSZ, todos os objetos – incluindo todo o É representado como a . A é apenas um número que identifica exclusivamente dentro dessa árvore. BeaconState binary Merkle tree generalized index (GI) any node As regras são muito simples: O nódulo raiz tem um índice generalizado:GI = 1 Para qualquer nó com índice i:criança esquerda = 2*i,criança direita = 2*i + 1 Assim, toda a árvore é numerada como: GI:1 / \ GI:2 GI:3 / \ / \ GI:4 GI:5 GI:6 GI:7 ... Este número torna as provas de Merkle fáceis. , você sabe exatamente onde ele está sentado na árvore e quais hashes irmãos devem ser incluídos para verificá-lo. 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 Existem 21 campos de nível superior (indexados 0..20). Para colocá-los em uma árvore Merkle, o SSZ os coloca até a próxima potência de dois (32). 32 folhas → profundidade = 5. As folhas de nível superior ocupam a faixa GI: 32 ... 63 Calculamos o GI para um campo de nível superior usando: A fórmula: GI_top = 2^depth + field_index para Índice de campo = .validators 11 E assim: GI_validators = 2^5 + 11 = 32 + 11 = 43. Este é o ( ) é o compromisso da folha de todo Dentro do Global uma árvore. 43 validator’s subtree BeaconState Multi-Level Proof: Example With validators[42].withdrawal_credentials Agora, suponhamos que queremos uma prova para: BeaconState.validators[42].withdrawal_credentials Isso exige : 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 Compreender a serialização SSZ antes de computar índices generalizados Para calcular uma correta Antes de mais nada, você precisa entender o que é a SSG. e diferentes tipos de dados. Os índices generalizados não existem isoladamente – eles são derivados da , e a forma da árvore depende inteiramente de como o SSZ interpreta os campos de estruturas Go subjacentes. generalized index Séries mercadorias A forma da árvore Merkle Em SSZ, cada campo só pode ser uma das duas categorias: 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 um índice generalizado para qualquer campo, devemos primeiro entender o Sobre o objeto: SSZ structure Quais áreas existem, Se cada campo é uma lista ou um vetor, Quantas peças cada campo ocupa, e como os tipos de ninhos devem ser atravessados. Isso é exatamente o que o A função funciona em Prysm, localizado em 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 É a função que e os números fora Isso é, é uma - Ele faz dependem dos valores reais do tempo de execução, apenas do tipo Go e das tags de estrutura. analyzeType examines a Go value using reflection what kind of SSZ type Passo de análise de tipo puro not Quando você lhe dá um campo ou estrutura, ele: Verifica o tipo de Go (uint, struct, slice, pointer, etc.) Leia tags de estrutura relacionados com SSZ, como ssz-size e 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 Pense em Como a função que and produces a para esse tipo. analyzeType scans the type definition static SSZ layout blueprint What PopulateVariableLengthInfo Does Embora Estudos da , alguns objetos SSZ não podem ser totalmente descritos sem a . analyzeType Tipo Valor real Alguns exemplos: As listas ([]T) precisam saber o seu comprimento atual Campos de contêiner de tamanho variável precisam de sua compensação real As listas aninhadas precisam do tamanho real de cada elemento Preencha esta informação de tempo de execução faltante. PopulateVariableLengthInfo Isso é: Olha para o blueprint SszInfo criado por analyzeType Olha para o valor real do objeto passado 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 Ele processa tudo Por exemplo, um contêiner com uma lista contendo estruturas com listas será preenchido. recursively Pense em Como a função que e preenche as medições reais com base no valor real que você passa. PopulateVariableLengthInfo takes the blueprint from analyzeType Example: Vamos testar esta função com uma estrutura de BeaconState passando 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()) } A saída: 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) Na saída do analisador SSZ, o Mostrado para cada campo representa a posição exata do byte onde esse campo começa quando todo o struct é serializado de acordo com as regras SSZ. , embalados firmemente um após o outro, e o offset diz-lhe onde cada um desses campos começa dentro desse fluxo de byte embalado. O campo é um valor de tamanho fixo de 32 bytes, e seus bytes serializados começam na posição in the SSZ-encoded byte array. The Indica quantos bytes o campo contribui para a saída serializada (32 bytes neste caso). Para os tipos de tamanho fixo, o tamanho é predeterminado, enquanto para os tipos de tamanho variável, o analisador calcula o tamanho com base no 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 Exemplo: Encontrar a folha de Merkle para um campo usando o offset Vamos tomar um campo real da saída do SSZ Analyzer: ├─ 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 provar o campo: fork.epoch O campo “fork” em Começando em no fluxo de bytes serializados. BeaconState offset 48 Dentro , o O campo começa em (Relativamente ao início de Fork). fork epoch offset 8 E assim: absolute_offset = base_offset_of_fork + offset_of_epoch_inside_fork absolute_offset = 48 + 8 = 56 bytes começa no byte 56 do BeaconState serializado completo. fork.epoch SSZ divide a serialização em : 32-byte chunks Chunk 0 → bytes 0–31 Chunk 1 → bytes 32–63 Chunk 2 → bytes 64–95 … Descubra qual pedaço contém byte : 56 chunk_index = floor(56 / 32) = 1 So: A folha que contém is Leaf / Chunk 1. fork.epoch is an integer fork.epoch 8-byte Dentro do pedaço 1 (bytes 32-63): local_offset = 56 - 32 = 24 Assim, dentro da folha de 32 bytes, os bytes parecem: [ 0 … 23 ] → unrelated fields [ 24 … 31 ] → fork.epoch (8 bytes) To prove this value, you: Página 1 - Esta é a sua folha. 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. Continue até chegar à raiz de Merkle superior. Os hashes irmãos coletados formam o seu: ➡️ SSZ Merkle proof branch for fork.epoch Anyone can verify this by recomputing: hash_tree_root(leaf + all_siblings) == state_root Isso introduz dois novos endpoints que expõem a versão inicial de Sobre o Prism: SSZ Query Language (SSZ-QL) /prysm/v1/beacon/states/{state_id}/query /prysm/v1/beacon/blocks/{block_id}/query Ambos os endpoints seguem a especificação do endpoint SSZ-QL e permitem que os clientes solicitem campos específicos dentro de um BeaconState ou BeaconBlock usando uma cadeia de consulta. O servidor retorna o campo SSZ solicitado codificado como bytes SSZ brutos. A bandeira é ignorada – a PR sempre retorna respostas sem provas da Merkle. include_proof The request structure is: type SSZQueryRequest struct { Query string `json:"query"` IncludeProof bool `json:"include_proof,omitempty"` } E ambos os endpoints retornam uma resposta codificada por SSZ desta 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 mais detalhes e exemplos, consulte aqui O link e informações do analisador SSZ, em vez de usar um índice generalizado. For now, the implementation locates the requested field using the computed offset size Para mais informações, você pode conferir o trabalho de Jun Song – implementado em conjunto com Fernando como parte de seu projeto EPF em prysm. For more information, you can check out ’s work — implemented together with as part of their EPF project in prysm. Jun Song Fernando Jun Song Fernando