Skip to content

Commit

Permalink
Adopt EIP-7688: Forward compatible consensus data structures
Browse files Browse the repository at this point in the history
EIP-4788 exposes the beacon root to smart contracts, but smart contracts
using it need to be redeployed / upgraded whenever the indexing changes
during a fork, even if that fork does not touch any used functionality.

This problem expands further to bridges on other blockchains, or even
into wallet apps on a phone that verify data from the beacon chain
instead of trusting the server. It is quite unrealistic to expect such
projects to all align their release cadence with Ethereum's forks.

EIP-7688 fixes this by defining forward compatibility for beacon chain
data structures. Electra `Profile` retain their Merkleization even when
rebased to `StableContainer` definitions from future forks, enabling
decentralized protocols to drop the requirement for trusted parties to
periodically upgrade beacon state proof verifiers. (+1 squashed commit)
Squashed commits:
[8b92f7b0f] Adopt EIP-7688: Forward compatible consensus data structures

EIP-4788 exposes the beacon root to smart contracts, but smart contracts
using it need to be redeployed / upgraded whenever the indexing changes
during a fork, even if that fork does not touch any used functionality.

This problem expands further to bridges on other blockchains, or even
into wallet apps on a phone that verify data from the beacon chain
instead of trusting the server. It is quite unrealistic to expect such
projects to all align their release cadence with Ethereum's forks.

EIP-7688 fixes this by defining forward compatibility for beacon chain
data structures. Electra `Profile` retain their Merkleization even when
rebased to `StableContainer` definitions from future forks, enabling
decentralized protocols to drop the requirement for trusted parties to
periodically upgrade beacon state proof verifiers.
  • Loading branch information
etan-status committed Jul 22, 2024
1 parent 268166b commit 0abb4b6
Show file tree
Hide file tree
Showing 20 changed files with 330 additions and 43 deletions.
5 changes: 5 additions & 0 deletions presets/mainnet/electra.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,8 @@ MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD: 16
# ---------------------------------------------------------------
# 2**3 ( = 8) pending withdrawals
MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP: 8

# Misc
# ---------------------------------------------------------------
# `floorlog2(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments')) + 1 + ceillog2(MAX_BLOB_COMMITMENTS_PER_BLOCK)` = 7 + 1 + 12 = 20
KZG_COMMITMENT_INCLUSION_PROOF_DEPTH_ELECTRA: 20
5 changes: 5 additions & 0 deletions presets/minimal/electra.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,8 @@ MAX_WITHDRAWAL_REQUESTS_PER_PAYLOAD: 2
# ---------------------------------------------------------------
# 2**0 ( = 1) pending withdrawals
MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP: 1

# Misc
# ---------------------------------------------------------------
# [customized] `floorlog2(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments')) + 1 + ceillog2(MAX_BLOB_COMMITMENTS_PER_BLOCK)` = 7 + 1 + 4 = 12
KZG_COMMITMENT_INCLUSION_PROOF_DEPTH_ELECTRA: 12
4 changes: 4 additions & 0 deletions pysetup/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@ def dependency_order_class_objects(objects: Dict[str, str], custom_types: Dict[s
for key, value in items:
dependencies = []
for line in value.split('\n'):
profile_match = re.match(r'class\s+\w+\s*\(Profile\[\s*(\w+)\s*\]\s*\)\s*:', line)
if profile_match is not None:
dependencies.append(profile_match.group(1)) # SSZ `Profile` base
continue
if not re.match(r'\s+\w+: .+', line):
continue # skip whitespace etc.
line = line[line.index(':') + 1:] # strip of field name
Expand Down
5 changes: 0 additions & 5 deletions pysetup/spec_builders/deneb.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,5 @@ def hardcoded_custom_type_dep_constants(cls, spec_object) -> Dict[str, str]:
'FIELD_ELEMENTS_PER_BLOB': spec_object.preset_vars['FIELD_ELEMENTS_PER_BLOB'].value,
'MAX_BLOBS_PER_BLOCK': spec_object.config_vars['MAX_BLOBS_PER_BLOCK'].value,
'MAX_BLOB_COMMITMENTS_PER_BLOCK': spec_object.preset_vars['MAX_BLOB_COMMITMENTS_PER_BLOCK'].value,
}

@classmethod
def hardcoded_func_dep_presets(cls, spec_object) -> Dict[str, str]:
return {
'KZG_COMMITMENT_INCLUSION_PROOF_DEPTH': spec_object.preset_vars['KZG_COMMITMENT_INCLUSION_PROOF_DEPTH'].value,
}
14 changes: 11 additions & 3 deletions pysetup/spec_builders/electra.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,20 @@ class ElectraSpecBuilder(BaseSpecBuilder):
def imports(cls, preset_name: str):
return f'''
from eth2spec.deneb import {preset_name} as deneb
from eth2spec.utils.ssz.ssz_typing import StableContainer, Profile
'''

@classmethod
def hardcoded_ssz_dep_constants(cls) -> Dict[str, str]:
return {
'FINALIZED_ROOT_GINDEX_ELECTRA': 'GeneralizedIndex(169)',
'CURRENT_SYNC_COMMITTEE_GINDEX_ELECTRA': 'GeneralizedIndex(86)',
'NEXT_SYNC_COMMITTEE_GINDEX_ELECTRA': 'GeneralizedIndex(87)',
'FINALIZED_ROOT_GINDEX_ELECTRA': 'GeneralizedIndex(553)',
'CURRENT_SYNC_COMMITTEE_GINDEX_ELECTRA': 'GeneralizedIndex(278)',
'NEXT_SYNC_COMMITTEE_GINDEX_ELECTRA': 'GeneralizedIndex(279)',
'EXECUTION_PAYLOAD_GINDEX_ELECTRA': 'GeneralizedIndex(137)',
}

@classmethod
def hardcoded_custom_type_dep_constants(cls, spec_object) -> Dict[str, str]:
return {
'KZG_COMMITMENT_INCLUSION_PROOF_DEPTH_ELECTRA': spec_object.preset_vars['KZG_COMMITMENT_INCLUSION_PROOF_DEPTH_ELECTRA'].value,
}
6 changes: 3 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ def _update_constant_vars_with_kzg_setups(constant_vars, preset_name):
constant_vars['KZG_SETUP_G1_MONOMIAL'] = VariableDefinition(constant_vars['KZG_SETUP_G1_MONOMIAL'].value, str(kzg_setups[0]), comment, None)
constant_vars['KZG_SETUP_G1_LAGRANGE'] = VariableDefinition(constant_vars['KZG_SETUP_G1_LAGRANGE'].value, str(kzg_setups[1]), comment, None)
constant_vars['KZG_SETUP_G2_MONOMIAL'] = VariableDefinition(constant_vars['KZG_SETUP_G2_MONOMIAL'].value, str(kzg_setups[2]), comment, None)


def get_spec(file_name: Path, preset: Dict[str, str], config: Dict[str, str], preset_name=str) -> SpecObject:
functions: Dict[str, str] = {}
Expand Down Expand Up @@ -227,7 +227,7 @@ def get_spec(file_name: Path, preset: Dict[str, str], config: Dict[str, str], pr
raise

if parent_class:
assert parent_class == "Container"
assert parent_class in ["Container", "StableContainer", "Profile"]
# NOTE: trim whitespace from spec
ssz_objects[current_name] = "\n".join(line.rstrip() for line in source.splitlines())
else:
Expand Down Expand Up @@ -552,7 +552,7 @@ def run(self):
"pycryptodome==3.15.0",
"py_ecc==6.0.0",
"milagro_bls_binding==1.9.0",
"remerkleable==0.1.28",
"remerkleable @ git+https://github.com/etan-status/remerkleable@dev/etan/sc-default",
"trie==2.0.2",
RUAMEL_YAML_VERSION,
"lru-dict==1.2.0",
Expand Down
2 changes: 1 addition & 1 deletion specs/capella/light-client/full-node.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def block_to_light_client_header(block: SignedBeaconBlock) -> LightClientHeader:
withdrawals_root=hash_tree_root(payload.withdrawals),
)
execution_branch = ExecutionBranch(
compute_merkle_proof(block.message.body, EXECUTION_PAYLOAD_GINDEX))
compute_merkle_proof(block.message.body, execution_payload_gindex_at_slot(block.message.slot)))
else:
# Note that during fork transitions, `finalized_header` may still point to earlier forks.
# While Bellatrix blocks also contain an `ExecutionPayload` (minus `withdrawals_root`),
Expand Down
16 changes: 13 additions & 3 deletions specs/capella/light-client/sync-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- [Containers](#containers)
- [Modified `LightClientHeader`](#modified-lightclientheader)
- [Helper functions](#helper-functions)
- [`execution_payload_gindex_at_slot`](#execution_payload_gindex_at_slot)
- [`get_lc_execution_root`](#get_lc_execution_root)
- [Modified `is_valid_light_client_header`](#modified-is_valid_light_client_header)

Expand Down Expand Up @@ -55,6 +56,16 @@ class LightClientHeader(Container):

## Helper functions

### `execution_payload_gindex_at_slot`

```python
def execution_payload_gindex_at_slot(slot: Slot) -> GeneralizedIndex:
epoch = compute_epoch_at_slot(slot)
assert epoch >= CAPELLA_FORK_EPOCH

return EXECUTION_PAYLOAD_GINDEX
```

### `get_lc_execution_root`

```python
Expand All @@ -79,11 +90,10 @@ def is_valid_light_client_header(header: LightClientHeader) -> bool:
and header.execution_branch == ExecutionBranch()
)

return is_valid_merkle_branch(
return is_valid_normalized_merkle_branch(
leaf=get_lc_execution_root(header),
branch=header.execution_branch,
depth=floorlog2(EXECUTION_PAYLOAD_GINDEX),
index=get_subtree_index(EXECUTION_PAYLOAD_GINDEX),
gindex=execution_payload_gindex_at_slot(header.beacon.slot),
root=header.beacon.body_root,
)
```
2 changes: 1 addition & 1 deletion specs/deneb/light-client/full-node.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def block_to_light_client_header(block: SignedBeaconBlock) -> LightClientHeader:
execution_header.excess_blob_gas = payload.excess_blob_gas

execution_branch = ExecutionBranch(
compute_merkle_proof(block.message.body, EXECUTION_PAYLOAD_GINDEX))
compute_merkle_proof(block.message.body, execution_payload_gindex_at_slot(block.message.slot)))
else:
# Note that during fork transitions, `finalized_header` may still point to earlier forks.
# While Bellatrix blocks also contain an `ExecutionPayload` (minus `withdrawals_root`),
Expand Down
5 changes: 2 additions & 3 deletions specs/deneb/light-client/sync-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,10 @@ def is_valid_light_client_header(header: LightClientHeader) -> bool:
and header.execution_branch == ExecutionBranch()
)

return is_valid_merkle_branch(
return is_valid_normalized_merkle_branch(
leaf=get_lc_execution_root(header),
branch=header.execution_branch,
depth=floorlog2(EXECUTION_PAYLOAD_GINDEX),
index=get_subtree_index(EXECUTION_PAYLOAD_GINDEX),
gindex=execution_payload_gindex_at_slot(header.beacon.slot),
root=header.beacon.body_root,
)
```
15 changes: 11 additions & 4 deletions specs/deneb/p2p-interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ The specification of these changes continues in the same format as the network s
- [Constant](#constant)
- [Preset](#preset)
- [Configuration](#configuration)
- [Custom types](#custom-types)
- [Containers](#containers)
- [`BlobSidecar`](#blobsidecar)
- [`BlobIdentifier`](#blobidentifier)
Expand Down Expand Up @@ -66,6 +67,12 @@ The specification of these changes continues in the same format as the network s
| `MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS` | `2**12` (= 4096 epochs, ~18 days) | The minimum epoch range over which a node must serve blob sidecars |
| `BLOB_SIDECAR_SUBNET_COUNT` | `6` | The number of blob sidecar subnets used in the gossipsub protocol. |

### Custom types

| Name | SSZ equivalent | Description |
| - | - | - |
| `KZGCommitmentInclusionProof` | `Vector[Bytes32, KZG_COMMITMENT_INCLUSION_PROOF_DEPTH]` | Merkle branch of a single `blob_kzg_commitments` list item within `BeaconBlockBody` |

### Containers

#### `BlobSidecar`
Expand All @@ -79,7 +86,7 @@ class BlobSidecar(Container):
kzg_commitment: KZGCommitment
kzg_proof: KZGProof # Allows for quick verification of kzg_commitment
signed_block_header: SignedBeaconBlockHeader
kzg_commitment_inclusion_proof: Vector[Bytes32, KZG_COMMITMENT_INCLUSION_PROOF_DEPTH]
kzg_commitment_inclusion_proof: KZGCommitmentInclusionProof
```

#### `BlobIdentifier`
Expand All @@ -98,12 +105,12 @@ class BlobIdentifier(Container):

```python
def verify_blob_sidecar_inclusion_proof(blob_sidecar: BlobSidecar) -> bool:
gindex = get_subtree_index(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments', blob_sidecar.index))
gindex = get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments', blob_sidecar.index)
return is_valid_merkle_branch(
leaf=blob_sidecar.kzg_commitment.hash_tree_root(),
branch=blob_sidecar.kzg_commitment_inclusion_proof,
depth=KZG_COMMITMENT_INCLUSION_PROOF_DEPTH,
index=gindex,
depth=floorlog2(gindex),
index=get_subtree_index(gindex),
root=blob_sidecar.signed_block_header.message.body_root,
)
```
Expand Down
Loading

0 comments on commit 0abb4b6

Please sign in to comment.