Skip to content

Commit 78d1ffe

Browse files
committed
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.
1 parent ee64f90 commit 78d1ffe

File tree

24 files changed

+648
-72
lines changed

24 files changed

+648
-72
lines changed

presets/mainnet/electra.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,8 @@ MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP: 8
4848
# ---------------------------------------------------------------
4949
# 2**4 ( = 4) pending deposits
5050
MAX_PENDING_DEPOSITS_PER_EPOCH: 16
51+
52+
# Misc
53+
# ---------------------------------------------------------------
54+
# `floorlog2(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments')) + 1 + ceillog2(MAX_BLOB_COMMITMENTS_PER_BLOCK)` = 7 + 1 + 12 = 20
55+
KZG_COMMITMENT_INCLUSION_PROOF_DEPTH_ELECTRA: 20

presets/mainnet/fulu.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ FIELD_ELEMENTS_PER_CELL: 64
77
# `uint64(2 * 4096)` (= 8192)
88
FIELD_ELEMENTS_PER_EXT_BLOB: 8192
99
# uint64(floorlog2(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments'))
10-
KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH: 4
10+
KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH: 7

presets/minimal/electra.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,8 @@ MAX_PENDING_PARTIALS_PER_WITHDRAWALS_SWEEP: 2
4848
# ---------------------------------------------------------------
4949
# 2**4 ( = 4) pending deposits
5050
MAX_PENDING_DEPOSITS_PER_EPOCH: 16
51+
52+
# Misc
53+
# ---------------------------------------------------------------
54+
# [customized] `floorlog2(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments')) + 1 + ceillog2(MAX_BLOB_COMMITMENTS_PER_BLOCK)` = 7 + 1 + 5 = 13
55+
KZG_COMMITMENT_INCLUSION_PROOF_DEPTH_ELECTRA: 13

presets/minimal/fulu.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ FIELD_ELEMENTS_PER_CELL: 64
77
# `uint64(2 * 4096)` (= 8192)
88
FIELD_ELEMENTS_PER_EXT_BLOB: 8192
99
# uint64(floorlog2(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments'))
10-
KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH: 4
10+
KZG_COMMITMENTS_INCLUSION_PROOF_DEPTH: 7

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ dependencies = [
2323
"py_arkworks_bls12381==0.3.8",
2424
"py_ecc==6.0.0",
2525
"pycryptodome==3.21.0",
26-
"remerkleable==0.1.28",
26+
"remerkleable @ git+https://github.com/etan-status/remerkleable@dev/etan/sc-default",
2727
"ruamel.yaml==0.17.21",
2828
"setuptools==75.8.0",
2929
"trie==3.0.1",

pysetup/spec_builders/deneb.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -87,10 +87,5 @@ def hardcoded_custom_type_dep_constants(cls, spec_object) -> Dict[str, str]:
8787
'FIELD_ELEMENTS_PER_BLOB': spec_object.preset_vars['FIELD_ELEMENTS_PER_BLOB'].value,
8888
'MAX_BLOBS_PER_BLOCK': spec_object.config_vars['MAX_BLOBS_PER_BLOCK'].value,
8989
'MAX_BLOB_COMMITMENTS_PER_BLOCK': spec_object.preset_vars['MAX_BLOB_COMMITMENTS_PER_BLOCK'].value,
90-
}
91-
92-
@classmethod
93-
def hardcoded_func_dep_presets(cls, spec_object) -> Dict[str, str]:
94-
return {
9590
'KZG_COMMITMENT_INCLUSION_PROOF_DEPTH': spec_object.preset_vars['KZG_COMMITMENT_INCLUSION_PROOF_DEPTH'].value,
9691
}

pysetup/spec_builders/eip7732.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ def hardcoded_custom_type_dep_constants(cls, spec_object) -> Dict[str, str]:
3535
@classmethod
3636
def deprecate_constants(cls) -> Set[str]:
3737
return set([
38-
'EXECUTION_PAYLOAD_GINDEX',
38+
'EXECUTION_PAYLOAD_GINDEX_ELECTRA',
3939
])
4040

4141
@classmethod
4242
def deprecate_presets(cls) -> Set[str]:
4343
return set([
44-
'KZG_COMMITMENT_INCLUSION_PROOF_DEPTH',
44+
'KZG_COMMITMENT_INCLUSION_PROOF_DEPTH_ELECTRA',
4545
])

pysetup/spec_builders/electra.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,22 @@ def imports(cls, preset_name: str):
1111
return f'''
1212
from eth2spec.deneb import {preset_name} as deneb
1313
from eth2spec.utils.ssz.ssz_impl import ssz_serialize, ssz_deserialize
14+
from eth2spec.utils.ssz.ssz_typing import StableContainer, Profile
1415
'''
1516

1617
@classmethod
1718
def hardcoded_ssz_dep_constants(cls) -> Dict[str, str]:
1819
return {
19-
'FINALIZED_ROOT_GINDEX_ELECTRA': 'GeneralizedIndex(169)',
20-
'CURRENT_SYNC_COMMITTEE_GINDEX_ELECTRA': 'GeneralizedIndex(86)',
21-
'NEXT_SYNC_COMMITTEE_GINDEX_ELECTRA': 'GeneralizedIndex(87)',
20+
'FINALIZED_ROOT_GINDEX_ELECTRA': 'GeneralizedIndex(553)',
21+
'CURRENT_SYNC_COMMITTEE_GINDEX_ELECTRA': 'GeneralizedIndex(278)',
22+
'NEXT_SYNC_COMMITTEE_GINDEX_ELECTRA': 'GeneralizedIndex(279)',
23+
'EXECUTION_PAYLOAD_GINDEX_ELECTRA': 'GeneralizedIndex(137)',
24+
}
25+
26+
@classmethod
27+
def hardcoded_custom_type_dep_constants(cls, spec_object) -> Dict[str, str]:
28+
return {
29+
'KZG_COMMITMENT_INCLUSION_PROOF_DEPTH_ELECTRA': spec_object.preset_vars['KZG_COMMITMENT_INCLUSION_PROOF_DEPTH_ELECTRA'].value,
2230
}
2331

2432

@@ -58,4 +66,4 @@ def verify_and_notify_new_payload(self: ExecutionEngine,
5866
return True
5967
6068
61-
EXECUTION_ENGINE = NoopExecutionEngine()"""
69+
EXECUTION_ENGINE = NoopExecutionEngine()"""

specs/_features/eip7732/beacon-chain.md

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,11 @@
2525
- [`ExecutionPayloadEnvelope`](#executionpayloadenvelope)
2626
- [`SignedExecutionPayloadEnvelope`](#signedexecutionpayloadenvelope)
2727
- [Modified containers](#modified-containers)
28+
- [`StableBeaconBlockBody`](#stablebeaconblockbody)
2829
- [`BeaconBlockBody`](#beaconblockbody)
30+
- [`StableExecutionPayloadHeader`](#stableexecutionpayloadheader)
2931
- [`ExecutionPayloadHeader`](#executionpayloadheader)
32+
- [`StableBeaconState`](#stablebeaconstate)
3033
- [`BeaconState`](#beaconstate)
3134
- [Helper functions](#helper-functions)
3235
- [Math](#math)
@@ -177,12 +180,34 @@ class SignedExecutionPayloadEnvelope(Container):
177180

178181
### Modified containers
179182

180-
#### `BeaconBlockBody`
183+
#### `StableBeaconBlockBody`
181184

182185
**Note:** The Beacon Block body is modified to contain a `Signed ExecutionPayloadHeader`. The containers `BeaconBlock` and `SignedBeaconBlock` are modified indirectly. The field `execution_requests` is removed from the beacon block body and moved into the signed execution payload envelope.
183186

184187
```python
185-
class BeaconBlockBody(Container):
188+
class StableBeaconBlockBody(StableContainer[MAX_BEACON_BLOCK_BODY_FIELDS]):
189+
randao_reveal: Optional[BLSSignature]
190+
eth1_data: Optional[Eth1Data] # Eth1 data vote
191+
graffiti: Optional[Bytes32] # Arbitrary data
192+
proposer_slashings: Optional[List[ProposerSlashing, MAX_PROPOSER_SLASHINGS]]
193+
attester_slashings: Optional[List[StableAttesterSlashing, MAX_ATTESTER_SLASHINGS_ELECTRA]]
194+
attestations: Optional[List[Attestation, MAX_ATTESTATIONS_ELECTRA]]
195+
deposits: Optional[List[Deposit, MAX_DEPOSITS]]
196+
voluntary_exits: Optional[List[SignedVoluntaryExit, MAX_VOLUNTARY_EXITS]]
197+
sync_aggregate: Optional[SyncAggregate]
198+
execution_payload: Optional[StableExecutionPayload] # [Removed in EIP-7732]
199+
bls_to_execution_changes: Optional[List[SignedBLSToExecutionChange, MAX_BLS_TO_EXECUTION_CHANGES]]
200+
blob_kzg_commitments: Optional[List[KZGCommitment, MAX_BLOB_COMMITMENTS_PER_BLOCK]] # [Removed in EIP-7732]
201+
execution_requests: Optional[StableExecutionRequests] # [Removed in EIP-7732]
202+
# PBS
203+
signed_execution_payload_header: Optional[SignedExecutionPayloadHeader] # [New in EIP-7732]
204+
payload_attestations: Optional[List[PayloadAttestation, MAX_PAYLOAD_ATTESTATIONS]] # [New in EIP-7732]
205+
```
206+
207+
#### `BeaconBlockBody`
208+
209+
```python
210+
class BeaconBlockBody(Profile[StableBeaconBlockBody]):
186211
randao_reveal: BLSSignature
187212
eth1_data: Eth1Data # Eth1 data vote
188213
graffiti: Bytes32 # Arbitrary data
@@ -203,12 +228,26 @@ class BeaconBlockBody(Container):
203228
payload_attestations: List[PayloadAttestation, MAX_PAYLOAD_ATTESTATIONS] # [New in EIP-7732]
204229
```
205230

231+
#### `StableExecutionPayloadHeader`
232+
233+
```python
234+
class StableExecutionPayloadHeader(StableContainer[MAX_EXECUTION_PAYLOAD_FIELDS]):
235+
parent_block_hash: Optional[Hash32]
236+
parent_block_root: Optional[Root]
237+
block_hash: Optional[Hash32]
238+
gas_limit: Optional[uint64]
239+
builder_index: Optional[ValidatorIndex]
240+
slot: Optional[Slot]
241+
value: Optional[Gwei]
242+
blob_kzg_commitments_root: Optional[Root]
243+
```
244+
206245
#### `ExecutionPayloadHeader`
207246

208247
**Note:** The `ExecutionPayloadHeader` is modified to only contain the block hash of the committed `ExecutionPayload` in addition to the builder's payment information, gas limit and KZG commitments root to verify the inclusion proofs.
209248

210249
```python
211-
class ExecutionPayloadHeader(Container):
250+
class ExecutionPayloadHeader(Profile[StableExecutionPayloadHeader]):
212251
parent_block_hash: Hash32
213252
parent_block_root: Root
214253
block_hash: Hash32
@@ -219,12 +258,73 @@ class ExecutionPayloadHeader(Container):
219258
blob_kzg_commitments_root: Root
220259
```
221260

261+
#### `StableBeaconState`
262+
263+
```python
264+
class StableBeaconState(StableContainer[MAX_BEACON_STATE_FIELDS]):
265+
# Versioning
266+
genesis_time: Optional[uint64]
267+
genesis_validators_root: Optional[Root]
268+
slot: Optional[Slot]
269+
fork: Optional[Fork]
270+
# History
271+
latest_block_header: Optional[BeaconBlockHeader]
272+
block_roots: Optional[Vector[Root, SLOTS_PER_HISTORICAL_ROOT]]
273+
state_roots: Optional[Vector[Root, SLOTS_PER_HISTORICAL_ROOT]]
274+
# Frozen in Capella, replaced by historical_summaries
275+
historical_roots: Optional[List[Root, HISTORICAL_ROOTS_LIMIT]]
276+
# Eth1
277+
eth1_data: Optional[Eth1Data]
278+
eth1_data_votes: Optional[List[Eth1Data, EPOCHS_PER_ETH1_VOTING_PERIOD * SLOTS_PER_EPOCH]]
279+
eth1_deposit_index: Optional[uint64]
280+
# Registry
281+
validators: Optional[List[Validator, VALIDATOR_REGISTRY_LIMIT]]
282+
balances: Optional[List[Gwei, VALIDATOR_REGISTRY_LIMIT]]
283+
# Randomness
284+
randao_mixes: Optional[Vector[Bytes32, EPOCHS_PER_HISTORICAL_VECTOR]]
285+
# Slashings
286+
slashings: Optional[Vector[Gwei, EPOCHS_PER_SLASHINGS_VECTOR]] # Per-epoch sums of slashed effective balances
287+
# Participation
288+
previous_epoch_participation: Optional[List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT]]
289+
current_epoch_participation: Optional[List[ParticipationFlags, VALIDATOR_REGISTRY_LIMIT]]
290+
# Finality
291+
justification_bits: Optional[Bitvector[JUSTIFICATION_BITS_LENGTH]] # Bit set for every recent justified epoch
292+
previous_justified_checkpoint: Optional[Checkpoint]
293+
current_justified_checkpoint: Optional[Checkpoint]
294+
finalized_checkpoint: Optional[Checkpoint]
295+
# Inactivity
296+
inactivity_scores: Optional[List[uint64, VALIDATOR_REGISTRY_LIMIT]]
297+
# Sync
298+
current_sync_committee: Optional[SyncCommittee]
299+
next_sync_committee: Optional[SyncCommittee]
300+
# Execution
301+
latest_execution_payload_header: Optional[StableExecutionPayloadHeader]
302+
# Withdrawals
303+
next_withdrawal_index: Optional[WithdrawalIndex]
304+
next_withdrawal_validator_index: Optional[ValidatorIndex]
305+
# Deep history valid from Capella onwards
306+
historical_summaries: Optional[List[HistoricalSummary, HISTORICAL_ROOTS_LIMIT]]
307+
deposit_requests_start_index: Optional[uint64]
308+
deposit_balance_to_consume: Optional[Gwei]
309+
exit_balance_to_consume: Optional[Gwei]
310+
earliest_exit_epoch: Optional[Epoch]
311+
consolidation_balance_to_consume: Optional[Gwei]
312+
earliest_consolidation_epoch: Optional[Epoch]
313+
pending_deposits: Optional[List[PendingDeposit, PENDING_DEPOSITS_LIMIT]]
314+
pending_partial_withdrawals: Optional[List[PendingPartialWithdrawal, PENDING_PARTIAL_WITHDRAWALS_LIMIT]]
315+
pending_consolidations: Optional[List[PendingConsolidation, PENDING_CONSOLIDATIONS_LIMIT]]
316+
# PBS
317+
latest_block_hash: Optional[Hash32] # [New in EIP-7732]
318+
latest_full_slot: Optional[Slot] # [New in EIP-7732]
319+
latest_withdrawals_root: Optional[Root] # [New in EIP-7732]
320+
```
321+
222322
#### `BeaconState`
223323

224324
*Note*: The `BeaconState` is modified to track the last withdrawals honored in the CL. The `latest_execution_payload_header` is modified semantically to refer not to a past committed `ExecutionPayload` but instead it corresponds to the state's slot builder's bid. Another addition is to track the last committed block hash and the last slot that was full, that is in which there were both consensus and execution blocks included.
225325

226326
```python
227-
class BeaconState(Container):
327+
class BeaconState(Profile[StableBeaconState]):
228328
# Versioning
229329
genesis_time: uint64
230330
genesis_validators_root: Root

specs/capella/light-client/full-node.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def block_to_light_client_header(block: SignedBeaconBlock) -> LightClientHeader:
4545
withdrawals_root=hash_tree_root(payload.withdrawals),
4646
)
4747
execution_branch = ExecutionBranch(
48-
compute_merkle_proof(block.message.body, EXECUTION_PAYLOAD_GINDEX))
48+
compute_merkle_proof(block.message.body, execution_payload_gindex_at_slot(block.message.slot)))
4949
else:
5050
# Note that during fork transitions, `finalized_header` may still point to earlier forks.
5151
# While Bellatrix blocks also contain an `ExecutionPayload` (minus `withdrawals_root`),

specs/capella/light-client/sync-protocol.md

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
- [Containers](#containers)
1313
- [Modified `LightClientHeader`](#modified-lightclientheader)
1414
- [Helper functions](#helper-functions)
15+
- [`execution_payload_gindex_at_slot`](#execution_payload_gindex_at_slot)
1516
- [`get_lc_execution_root`](#get_lc_execution_root)
1617
- [Modified `is_valid_light_client_header`](#modified-is_valid_light_client_header)
1718

@@ -53,6 +54,16 @@ class LightClientHeader(Container):
5354

5455
## Helper functions
5556

57+
### `execution_payload_gindex_at_slot`
58+
59+
```python
60+
def execution_payload_gindex_at_slot(slot: Slot) -> GeneralizedIndex:
61+
epoch = compute_epoch_at_slot(slot)
62+
assert epoch >= CAPELLA_FORK_EPOCH
63+
64+
return EXECUTION_PAYLOAD_GINDEX
65+
```
66+
5667
### `get_lc_execution_root`
5768

5869
```python
@@ -77,11 +88,10 @@ def is_valid_light_client_header(header: LightClientHeader) -> bool:
7788
and header.execution_branch == ExecutionBranch()
7889
)
7990

80-
return is_valid_merkle_branch(
91+
return is_valid_normalized_merkle_branch(
8192
leaf=get_lc_execution_root(header),
8293
branch=header.execution_branch,
83-
depth=floorlog2(EXECUTION_PAYLOAD_GINDEX),
84-
index=get_subtree_index(EXECUTION_PAYLOAD_GINDEX),
94+
gindex=execution_payload_gindex_at_slot(header.beacon.slot),
8595
root=header.beacon.body_root,
8696
)
8797
```

specs/deneb/light-client/full-node.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ def block_to_light_client_header(block: SignedBeaconBlock) -> LightClientHeader:
5151
execution_header.excess_blob_gas = payload.excess_blob_gas
5252

5353
execution_branch = ExecutionBranch(
54-
compute_merkle_proof(block.message.body, EXECUTION_PAYLOAD_GINDEX))
54+
compute_merkle_proof(block.message.body, execution_payload_gindex_at_slot(block.message.slot)))
5555
else:
5656
# Note that during fork transitions, `finalized_header` may still point to earlier forks.
5757
# While Bellatrix blocks also contain an `ExecutionPayload` (minus `withdrawals_root`),

specs/deneb/light-client/sync-protocol.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,10 @@ def is_valid_light_client_header(header: LightClientHeader) -> bool:
7575
and header.execution_branch == ExecutionBranch()
7676
)
7777

78-
return is_valid_merkle_branch(
78+
return is_valid_normalized_merkle_branch(
7979
leaf=get_lc_execution_root(header),
8080
branch=header.execution_branch,
81-
depth=floorlog2(EXECUTION_PAYLOAD_GINDEX),
82-
index=get_subtree_index(EXECUTION_PAYLOAD_GINDEX),
81+
gindex=execution_payload_gindex_at_slot(header.beacon.slot),
8382
root=header.beacon.body_root,
8483
)
8584
```

specs/deneb/p2p-interface.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- [Constant](#constant)
1212
- [Preset](#preset)
1313
- [Configuration](#configuration)
14+
- [Custom types](#custom-types)
1415
- [Containers](#containers)
1516
- [`BlobSidecar`](#blobsidecar)
1617
- [`BlobIdentifier`](#blobidentifier)
@@ -70,6 +71,12 @@ The specification of these changes continues in the same format as the network s
7071
| `MIN_EPOCHS_FOR_BLOB_SIDECARS_REQUESTS` | `2**12` (= 4096 epochs, ~18 days) | The minimum epoch range over which a node must serve blob sidecars |
7172
| `BLOB_SIDECAR_SUBNET_COUNT` | `6` | The number of blob sidecar subnets used in the gossipsub protocol. |
7273

74+
### Custom types
75+
76+
| Name | SSZ equivalent | Description |
77+
| - | - | - |
78+
| `KZGCommitmentInclusionProof` | `Vector[Bytes32, KZG_COMMITMENT_INCLUSION_PROOF_DEPTH]` | Merkle branch of a single `blob_kzg_commitments` list item within `BeaconBlockBody` |
79+
7380
### Containers
7481

7582
#### `BlobSidecar`
@@ -83,7 +90,7 @@ class BlobSidecar(Container):
8390
kzg_commitment: KZGCommitment
8491
kzg_proof: KZGProof # Allows for quick verification of kzg_commitment
8592
signed_block_header: SignedBeaconBlockHeader
86-
kzg_commitment_inclusion_proof: Vector[Bytes32, KZG_COMMITMENT_INCLUSION_PROOF_DEPTH]
93+
kzg_commitment_inclusion_proof: KZGCommitmentInclusionProof
8794
```
8895

8996
#### `BlobIdentifier`
@@ -102,12 +109,12 @@ class BlobIdentifier(Container):
102109

103110
```python
104111
def verify_blob_sidecar_inclusion_proof(blob_sidecar: BlobSidecar) -> bool:
105-
gindex = get_subtree_index(get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments', blob_sidecar.index))
112+
gindex = get_generalized_index(BeaconBlockBody, 'blob_kzg_commitments', blob_sidecar.index)
106113
return is_valid_merkle_branch(
107114
leaf=blob_sidecar.kzg_commitment.hash_tree_root(),
108115
branch=blob_sidecar.kzg_commitment_inclusion_proof,
109-
depth=KZG_COMMITMENT_INCLUSION_PROOF_DEPTH,
110-
index=gindex,
116+
depth=floorlog2(gindex),
117+
index=get_subtree_index(gindex),
111118
root=blob_sidecar.signed_block_header.message.body_root,
112119
)
113120
```

0 commit comments

Comments
 (0)