Skip to content

Commit

Permalink
Add --network CLI parameter (#45)
Browse files Browse the repository at this point in the history
  • Loading branch information
eth2353 authored Dec 20, 2024
1 parent d590286 commit 53b68c9
Show file tree
Hide file tree
Showing 28 changed files with 778 additions and 32 deletions.
3 changes: 2 additions & 1 deletion compose-example.yaml
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
services:
validator-client:
container_name: validator-client
image: ghcr.io/serenita-org/vero:v0.8.2
image: ghcr.io/serenita-org/vero:v0.9.0
command:
- "--network=holesky"
- "--remote-signer-url=http://remote-signer:9000"
- "--beacon-node-urls=http://beacon-node-1:1234,http://beacon-node-2:1234,http://beacon-node-3:1234"
- "--fee-recipient=0x0000000000000000000000000000000000000000"
Expand Down
7 changes: 7 additions & 0 deletions docs/running_vero.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ Check out the [example docker compose file](../compose-example.yaml).

# CLI Reference

#### `--network`

**[required]** The network to use, one of `mainnet,gnosis,holesky,fetch`.

`fetch` is a special case where Vero uses the network specs as returned by the beacon nodes.
___

#### `--remote-signer-url`

**[required]** URL of the remote signer, e.g. `http://remote-signer:9000`
Expand Down
12 changes: 12 additions & 0 deletions src/args.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@

import msgspec

from spec.configs import Network


class CLIArgs(msgspec.Struct, kw_only=True):
network: Network
remote_signer_url: str
beacon_node_urls: list[str]
beacon_node_urls_proposal: list[str]
Expand Down Expand Up @@ -91,6 +94,14 @@ def _process_graffiti(graffiti: str) -> bytes:
def parse_cli_args(args: Sequence[str]) -> CLIArgs:
parser = argparse.ArgumentParser(description="Vero validator client.")

_network_choices = [e.value for e in list(Network)]
parser.add_argument(
"--network",
type=str,
required=True,
choices=_network_choices,
help="The network to use. 'fetch' is a special case where Vero uses the network specs returned by the beacon node(s).",
)
parser.add_argument(
"--remote-signer-url", type=str, required=True, help="URL of the remote signer."
)
Expand Down Expand Up @@ -193,6 +204,7 @@ def parse_cli_args(args: Sequence[str]) -> CLIArgs:
)
]
return CLIArgs(
network=Network(parsed_args.network),
remote_signer_url=_validate_url(parsed_args.remote_signer_url),
beacon_node_urls=beacon_node_urls,
beacon_node_urls_proposal=[
Expand Down
38 changes: 24 additions & 14 deletions src/providers/beacon_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@
from remerkleable.complex import Container
from yarl import URL

from args import CLIArgs
from observability import get_service_name, get_service_version
from observability.api_client import RequestLatency, ServiceType
from schemas import SchemaBeaconAPI, SchemaRemoteSigner, SchemaValidator
from spec.attestation import Attestation, AttestationData
from spec.base import Genesis, Spec, parse_spec
from spec.configs import Network, get_network_spec
from spec.sync_committee import SyncCommitteeContributionClass

_TIMEOUT_DEFAULT_CONNECT = 1
Expand Down Expand Up @@ -123,18 +125,25 @@ def score(self, value: int) -> None:
self._score = max(0, min(value, 100))
_BEACON_NODE_SCORE.labels(host=self.host).set(self._score)

async def _initialize_full(self) -> None:
async def _initialize_full(self, cli_args: CLIArgs) -> None:
self.genesis = await self.get_genesis()
self.spec = await self.get_spec()
self.node_version = await self.get_node_version()

# Regularly refresh these values
self.scheduler.add_job(
self.get_spec,
"interval",
minutes=10,
id=f"{self.__class__.__name__}.get_spec-{self.host}",
)
if cli_args.network == Network.FETCH:
# Fetch the spec values from the beacon node
self.spec = await self.get_spec()
else:
# Use included hardcoded spec values for known networks
self.spec = get_network_spec(network=cli_args.network)
bn_spec = await self.get_spec()
if self.spec != bn_spec:
self.logger.warning(
f"Spec values returned by beacon node not equal to hardcoded spec values."
f"\nBeacon node:\n{bn_spec}"
f"\nHardcoded:\n{self.spec}"
)

# Regularly refresh the version of the beacon node
self.node_version = await self.get_node_version()
self.scheduler.add_job(
self.get_node_version,
"interval",
Expand All @@ -145,11 +154,11 @@ async def _initialize_full(self) -> None:
self.score = 100
self.initialized = True

async def initialize_full(self, function: str | None = None) -> None:
async def initialize_full(self, cli_args: CLIArgs) -> None:
try:
await self._initialize_full()
await self._initialize_full(cli_args=cli_args)
self.logger.info(
f"Initialized beacon node at {self.base_url}{f' [{function}]' if function else ''}",
f"Initialized beacon node at {self.base_url}",
)
except Exception as e:
self.logger.error(
Expand All @@ -164,8 +173,9 @@ async def initialize_full(self, function: str | None = None) -> None:
self.initialize_full,
"date",
next_run_time=next_run_time,
kwargs=dict(function=function),
kwargs=dict(cli_args=cli_args),
id=f"{self.__class__.__name__}.initialize_full-{self.host}",
replace_existing=True,
)

@staticmethod
Expand Down
20 changes: 16 additions & 4 deletions src/providers/multi_beacon_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
from schemas import SchemaBeaconAPI, SchemaValidator
from spec.attestation import Attestation, AttestationData
from spec.block import BeaconBlockClass
from spec.configs import Network
from spec.sync_committee import SyncCommitteeContributionClass

(_ERRORS_METRIC,) = get_shared_metrics()
Expand Down Expand Up @@ -84,10 +85,13 @@ def __init__(
]

self._attestation_consensus_threshold = cli_args.attestation_consensus_threshold
self.cli_args = cli_args

async def initialize(self) -> None:
# Attempt to fully initialize the connected beacon nodes
await asyncio.gather(*(bn.initialize_full() for bn in self.beacon_nodes))
await asyncio.gather(
*(bn.initialize_full(cli_args=self.cli_args) for bn in self.beacon_nodes)
)

successfully_initialized = len([b for b in self.beacon_nodes if b.initialized])
if successfully_initialized < self._attestation_consensus_threshold:
Expand Down Expand Up @@ -305,6 +309,11 @@ async def _produce_best_block(
start_time = asyncio.get_running_loop().time()
remaining_timeout = timeout

# Only compare consensus block value on Gnosis Chain
# since the execution payload value is in a different
# currency (xDAI) and not easily comparable
_compare_consensus_block_value_only = self.cli_args.network in [Network.GNOSIS]

while pending and remaining_timeout > 0:
done, pending = await asyncio.wait(
pending,
Expand All @@ -321,9 +330,12 @@ async def _produce_best_block(
)
continue

block_value = int(response.consensus_block_value) + int(
response.execution_payload_value
)
if _compare_consensus_block_value_only:
block_value = int(response.consensus_block_value)
else:
block_value = int(response.consensus_block_value) + int(
response.execution_payload_value
)

if block_value > best_block_value:
best_block_value = block_value
Expand Down
25 changes: 15 additions & 10 deletions src/spec/base.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import copy
import logging
from typing import TypeVar

Expand Down Expand Up @@ -38,35 +39,39 @@ class Spec(Container):
def from_obj(cls: type[SpecV], obj: ObjType) -> SpecV:
if not isinstance(obj, dict):
raise ObjParseException(f"obj '{obj}' is not a dict")

# Create a copy since we manipulate the dict
_obj = copy.deepcopy(obj)

fields = cls.fields()
for k in list(obj.keys()):
for k in list(_obj.keys()):
if k not in fields:
del obj[k] # Remove extra keys/fields
del _obj[k] # Remove extra keys/fields

# Handle missing value for INTERVALS_PER_SLOT from some CL clients
# TODO report and get rid of this workaround?
logger = logging.getLogger("spec-parser")
if "INTERVALS_PER_SLOT" not in obj:
if "INTERVALS_PER_SLOT" not in _obj:
logger.debug(
"Missing spec value for INTERVALS_PER_SLOT, using default of 3",
)
obj["INTERVALS_PER_SLOT"] = 3
_obj["INTERVALS_PER_SLOT"] = 3

# Handle missing value for MAX_BLOB_COMMITMENTS_PER_BLOCK from Prysm
# TODO report and get rid of this workaround?
if "MAX_BLOB_COMMITMENTS_PER_BLOCK" not in obj:
if "MAX_BLOB_COMMITMENTS_PER_BLOCK" not in _obj:
logger.warning(
"Missing spec value for MAX_BLOB_COMMITMENTS_PER_BLOCK, using default of 4096",
)
obj["MAX_BLOB_COMMITMENTS_PER_BLOCK"] = 4096
_obj["MAX_BLOB_COMMITMENTS_PER_BLOCK"] = 4096

if any(field not in obj for field in fields):
missing = set(fields.keys()) - set(obj.keys())
if any(field not in _obj for field in fields):
missing = set(fields.keys()) - set(_obj.keys())
raise ObjParseException(
f"obj '{obj}' is missing required field(s): {missing}",
f"_obj '{_obj}' is missing required field(s): {missing}",
)

return cls(**{k: fields[k].from_obj(v) for k, v in obj.items()})
return cls(**{k: fields[k].from_obj(v) for k, v in _obj.items()})


class SpecPhase0(Spec):
Expand Down
47 changes: 47 additions & 0 deletions src/spec/configs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import os
from enum import Enum
from pathlib import Path

from spec.base import Spec, parse_spec


class Network(Enum):
MAINNET = "mainnet"
GNOSIS = "gnosis"
HOLESKY = "holesky"

# Special case where Vero uses the network specs returned by the beacon node(s)
FETCH = "fetch"


def parse_yaml_file(fp: Path) -> dict[str, str]:
return_dict: dict[str, str] = {}
with Path.open(fp) as f:
for line in f:
line = line.strip().split("#", maxsplit=1)[0]
if line == "":
continue

name, value = line.split(": ", maxsplit=1)
if name in return_dict:
raise ValueError(f"{name} already defined as {return_dict[name]}")
return_dict[name] = value.strip()
return return_dict


def get_network_spec(network: Network) -> Spec:
spec_dict = {}

spec_dict.update(parse_yaml_file(Path(__file__).parent / f"{network.value}.yaml"))

preset_files_dir = (
Path(__file__).parent / "presets" / f"{spec_dict['PRESET_BASE'].strip("'")}"
)
for fname in os.listdir(preset_files_dir):
spec_dict.update(
parse_yaml_file(
Path(preset_files_dir) / fname,
)
)

return parse_spec(data=spec_dict)
46 changes: 46 additions & 0 deletions src/spec/configs/gnosis.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
PRESET_BASE: 'gnosis'

# Free-form short name of the network that this configuration applies to - known
# canonical network names include:
# * 'mainnet' - there can be only one
# * 'prater' - testnet
# Must match the regex: [a-z0-9\-]
CONFIG_NAME: 'gnosis'

# Genesis
# ---------------------------------------------------------------
# Dec 08, 2021, 13:00 UTC
MIN_GENESIS_TIME: 1638968400
GENESIS_FORK_VERSION: 0x00000064


# Forking
# ---------------------------------------------------------------
# Some forks are disabled for now:
# - These may be re-assigned to another fork-version later
# - Temporarily set to max uint64 value: 2**64 - 1

# Altair
ALTAIR_FORK_VERSION: 0x01000064
ALTAIR_FORK_EPOCH: 512
# Bellatrix
BELLATRIX_FORK_VERSION: 0x02000064
BELLATRIX_FORK_EPOCH: 385536 # 2022-11-30T19:23:40.000Z
# Capella
CAPELLA_FORK_VERSION: 0x03000064
CAPELLA_FORK_EPOCH: 648704 # 2023-08-01T11:34:20.000Z
# Deneb
DENEB_FORK_VERSION: 0x04000064
DENEB_FORK_EPOCH: 889856 # 2024-03-11T18:30:20.000Z

# Time parameters
# ---------------------------------------------------------------
# 5 seconds
SECONDS_PER_SLOT: 5

# phase0
INTERVALS_PER_SLOT: 3
TARGET_AGGREGATORS_PER_COMMITTEE: 16
# altair
TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE: 16
SYNC_COMMITTEE_SUBNET_COUNT: 4
41 changes: 41 additions & 0 deletions src/spec/configs/holesky.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Extends the mainnet preset
PRESET_BASE: 'mainnet'
CONFIG_NAME: holesky

# Genesis
# ---------------------------------------------------------------
# Sep-28-2023 11:55:00 +UTC
MIN_GENESIS_TIME: 1695902100
GENESIS_FORK_VERSION: 0x01017000


# Forking
# ---------------------------------------------------------------
# Some forks are disabled for now:
# - These may be re-assigned to another fork-version later
# - Temporarily set to max uint64 value: 2**64 - 1

# Altair
ALTAIR_FORK_VERSION: 0x02017000
ALTAIR_FORK_EPOCH: 0
# Bellatrix
BELLATRIX_FORK_VERSION: 0x03017000
BELLATRIX_FORK_EPOCH: 0
# Capella
CAPELLA_FORK_VERSION: 0x04017000
CAPELLA_FORK_EPOCH: 256
# Deneb
DENEB_FORK_VERSION: 0x05017000
DENEB_FORK_EPOCH: 29696

# Time parameters
# ---------------------------------------------------------------
# 12 seconds
SECONDS_PER_SLOT: 12

# phase0
INTERVALS_PER_SLOT: 3
TARGET_AGGREGATORS_PER_COMMITTEE: 16
# altair
TARGET_AGGREGATORS_PER_SYNC_SUBCOMMITTEE: 16
SYNC_COMMITTEE_SUBNET_COUNT: 4
Loading

0 comments on commit 53b68c9

Please sign in to comment.