Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ indices for you):

```bash
export KEYSYNC_API_KEY=ovi_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
export KEYSYNC_OWNER_NAME="acme-mainnet"
export KEYSYNC_BEACON_URL=http://localhost:5052
export KEYSYNC_PUBKEYS_FILE=/etc/optimum/validators.pubkeys # one pubkey per line

Expand All @@ -41,7 +40,7 @@ Pick whichever list you already have. All three may be combined.
| --- | --- | --- | --- |
| BLS pubkeys | `KEYSYNC_PUBKEYS_FILE` | one pubkey per line | required (resolves the index) |
| Validator indices | `KEYSYNC_INDEX_LIST_FILE` | one index per line | required (resolves the pubkey) |
| Pre-computed records | `KEYSYNC_INDICES_FILE` | JSON of `{validator_index, chain_id, validator_key, owner_name}` | not used |
| Pre-computed records | `KEYSYNC_INDICES_FILE` | JSON of `{validator_index, chain_id, validator_key}` | not used |

`KEYSYNC_INDICES_FILE` is the no-lookup option for air-gapped setups, and it
wins over a beacon-resolved hit for the same `(validator_index, chain_id)`. See
Expand All @@ -59,7 +58,6 @@ All settings come from environment variables, each with a matching flag override
| `KEYSYNC_API_KEY` | yes | `ovi_live_*` from the partners dashboard |
| `KEYSYNC_NETWORK` | no | `mainnet`, `hoodi`, etc. Default: `mainnet` |
| `KEYSYNC_CHAIN_ID` | no | Corresponding chain ID string. Default: `0x1` |
| `KEYSYNC_OWNER_NAME` | yes | Stored in `eth_validator_keys.owner_name` |
| `KEYSYNC_BEACON_URL` | conditional | Required when using a pubkeys or index-list file |
| `KEYSYNC_PUBKEYS_FILE` | conditional | One BLS pubkey per line; beacon resolves the index |
| `KEYSYNC_INDEX_LIST_FILE` | conditional | One validator index per line; beacon resolves the pubkey |
Expand Down
3 changes: 1 addition & 2 deletions examples/indices.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
{
"validator_index": 720396,
"chain_id": "0x1",
"validator_key": "0x800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"owner_name": "acme-mainnet"
"validator_key": "0x800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
},
{
"validator_index": 720397,
Expand Down
2 changes: 1 addition & 1 deletion examples/kubernetes/cronjob.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ spec:
args: ["sync", "--apply", "--log-format", "json"]
envFrom:
- configMapRef:
name: optimum-keysync-config # KEYSYNC_API_URL, KEYSYNC_AUTH_URL, KEYSYNC_NETWORK, KEYSYNC_CHAIN_ID, KEYSYNC_OWNER_NAME, KEYSYNC_BEACON_URL (KEYSYNC_OPERATOR_ID auto-resolved via /me)
name: optimum-keysync-config # KEYSYNC_API_URL, KEYSYNC_AUTH_URL, KEYSYNC_NETWORK, KEYSYNC_CHAIN_ID, KEYSYNC_BEACON_URL (KEYSYNC_OPERATOR_ID auto-resolved via /me)
- secretRef:
name: optimum-keysync-secret # KEYSYNC_API_KEY
volumeMounts:
Expand Down
2 changes: 0 additions & 2 deletions src/optimum_keysync/api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ class ValidatorKeyRegistration:
validator_index: int
chain_id: str
validator_key: str # 0x-prefixed BLS pubkey hex
owner_name: str


class ApiError(RuntimeError):
Expand Down Expand Up @@ -123,7 +122,6 @@ def batch_register_keys(
"validator_index": k.validator_index,
"chain_id": k.chain_id,
"validator_key": k.validator_key,
"owner_name": k.owner_name,
}
for k in chunk
],
Expand Down
7 changes: 0 additions & 7 deletions src/optimum_keysync/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,6 @@ def _common_options(fn):
fn = click.option(
"--chain-id", envvar="KEYSYNC_CHAIN_ID", help="chain id string (default: 0x1)"
)(fn)
fn = click.option(
"--owner-name",
envvar="KEYSYNC_OWNER_NAME",
help="label stored in eth_validator_keys.owner_name",
)(fn)
fn = click.option(
"--beacon-url",
envvar="KEYSYNC_BEACON_URL",
Expand Down Expand Up @@ -127,7 +122,6 @@ def _build_config(kwargs: dict[str, object]) -> KeysyncConfig:
"operator_id": "operator_id",
"network": "network",
"chain_id": "chain_id",
"owner_name": "owner_name",
"beacon_url": "beacon_url",
"pubkeys_file": "pubkeys_file",
"index_list_file": "index_list_file",
Expand Down Expand Up @@ -306,7 +300,6 @@ def _compute_plan(cfg: KeysyncConfig) -> tuple[ReconcilePlan, AuthClient, ApiCli
try:
desired = resolve_desired(
chain_id=cfg.chain_id,
owner_name=cfg.owner_name,
pubkeys_file=cfg.pubkeys_file,
index_list_file=cfg.index_list_file,
indices_file=cfg.indices_file,
Expand Down
2 changes: 0 additions & 2 deletions src/optimum_keysync/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@ class KeysyncConfig(BaseModel):
# a testnet (e.g. network=hoodi with its chain_id).
network: Annotated[str, Field(min_length=1)] = _DEFAULT_NETWORK
chain_id: Annotated[str, Field(min_length=1)] = _DEFAULT_CHAIN_ID
owner_name: Annotated[str, Field(min_length=1, max_length=128)]

beacon_url: str | None = None
pubkeys_file: Path | None = None
Expand Down Expand Up @@ -181,7 +180,6 @@ def pick_path(name: str) -> Path | None:
operator_id=pick("operator_id"),
network=pick("network") or _DEFAULT_NETWORK,
chain_id=pick("chain_id") or _DEFAULT_CHAIN_ID,
owner_name=pick("owner_name") or "",
beacon_url=pick("beacon_url"),
pubkeys_file=pick_path("pubkeys_file"),
index_list_file=pick_path("index_list_file"),
Expand Down
19 changes: 7 additions & 12 deletions src/optimum_keysync/desired_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

The result is a `DesiredSet` carrying every validator the operator wants
assigned, with all the metadata the console API requires to register it
(validator_index, chain_id, validator_key, owner_name).
(validator_index, chain_id, validator_key). The registry's owner_name label
is filled in server-side from the operator the API key resolves to.
"""

from __future__ import annotations
Expand All @@ -23,15 +24,14 @@
class DesiredValidator:
"""One entry in the desired set.

`validator_key` and `owner_name` are required for the
`validator-keys/batch` register call; `validator_index` + `chain_id`
are also enough to drive the assignment call.
`validator_key` is required for the `validator-keys/batch` register
call; `validator_index` + `chain_id` are also enough to drive the
assignment call.
"""

validator_index: int
chain_id: str
validator_key: str
owner_name: str


class DesiredSet:
Expand Down Expand Up @@ -62,7 +62,6 @@ def registrations(self) -> list[ValidatorKeyRegistration]:
validator_index=e.validator_index,
chain_id=e.chain_id,
validator_key=e.validator_key,
owner_name=e.owner_name,
)
for e in self._entries
]
Expand All @@ -71,7 +70,6 @@ def registrations(self) -> list[ValidatorKeyRegistration]:
def resolve(
*,
chain_id: str,
owner_name: str,
pubkeys_file: Path | None,
index_list_file: Path | None,
indices_file: Path | None,
Expand All @@ -95,7 +93,6 @@ def resolve(
validator_index=v.validator_index,
chain_id=chain_id,
validator_key=v.pubkey,
owner_name=owner_name,
)
)

Expand All @@ -114,12 +111,11 @@ def resolve(
validator_index=v.validator_index,
chain_id=chain_id,
validator_key=v.pubkey,
owner_name=owner_name,
)
)

if indices_file is not None:
for e in _read_indices(indices_file, fallback_owner_name=owner_name):
for e in _read_indices(indices_file):
entries.append(e)

return DesiredSet(entries)
Expand Down Expand Up @@ -149,7 +145,7 @@ def _read_index_list(path: Path) -> list[int]:
return out


def _read_indices(path: Path, *, fallback_owner_name: str) -> list[DesiredValidator]:
def _read_indices(path: Path) -> list[DesiredValidator]:
raw = json.loads(path.read_text())
if not isinstance(raw, list):
raise ValueError(f"{path} must be a JSON array of validator records")
Expand All @@ -163,7 +159,6 @@ def _read_indices(path: Path, *, fallback_owner_name: str) -> list[DesiredValida
validator_index=int(row["validator_index"]),
chain_id=str(row["chain_id"]),
validator_key=str(row["validator_key"]),
owner_name=str(row.get("owner_name") or fallback_owner_name),
)
)
except (KeyError, TypeError, ValueError) as e:
Expand Down
1 change: 0 additions & 1 deletion src/optimum_keysync/reconcile.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ def plan(
validator_index=desired_by_key[k].validator_index,
chain_id=desired_by_key[k].chain_id,
validator_key=desired_by_key[k].validator_key,
owner_name=desired_by_key[k].owner_name,
)
for k in sorted(add_keys)
)
Expand Down
6 changes: 2 additions & 4 deletions tests/fixtures/validators.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@
{
"validator_index": 720396,
"chain_id": "0x1",
"validator_key": "0xa1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"owner_name": "acme-mainnet"
"validator_key": "0xa1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
},
{
"validator_index": 720397,
"chain_id": "0x1",
"validator_key": "0xb2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
"owner_name": "acme-mainnet"
"validator_key": "0xb2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2"
},
{
"validator_index": 720398,
Expand Down
1 change: 0 additions & 1 deletion tests/test_api_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ def test_batch_register_keys_aggregates_chunk_counts() -> None:
validator_index=i,
chain_id="0x1",
validator_key=f"0x{i:096x}",
owner_name="acme",
)
for i in range(4)
]
Expand Down
2 changes: 0 additions & 2 deletions tests/test_cli_apply.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,6 @@ def _plan(*, assigns: list[tuple[int, str]], unassigns: list[tuple[int, str]]) -
validator_index=idx,
chain_id=cid,
validator_key=f"0x{idx:096x}",
owner_name="acme",
)
for idx, cid in assigns
)
Expand Down Expand Up @@ -146,7 +145,6 @@ def test_build_config_threads_index_list_file(tmp_path: Any) -> None:
"operator_id": "42",
"network": "mainnet",
"chain_id": "0x1",
"owner_name": "acme",
"beacon_url": "https://beacon.test",
"index_list_file": index_list,
}
Expand Down
1 change: 0 additions & 1 deletion tests/test_cli_lazy_me.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ def _opts(indices_path: Path) -> list[str]:
"--api-key", "ovi_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"--network", "mainnet",
"--chain-id", "0x1",
"--owner-name", "acme",
"--indices-file", str(indices_path),
]

Expand Down
17 changes: 1 addition & 16 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import pytest

from optimum_keysync.config import KeysyncConfig, from_env
from optimum_keysync.config import from_env

_BASE: dict[str, str | Path] = {
"api_url": "https://api.example",
Expand All @@ -15,7 +15,6 @@
"operator_id": "42",
"network": "mainnet",
"chain_id": "0x1",
"owner_name": "acme",
}


Expand Down Expand Up @@ -196,20 +195,6 @@ def test_plaintext_beacon_url_rejected(tmp_path: Path) -> None:
)


def test_owner_name_length_capped() -> None:
with pytest.raises(ValueError):
KeysyncConfig(
api_url="https://b",
auth_url="https://a",
api_key="ovi_live_xxxxxxxxxxxxxxxxxxxx",
operator_id="42",
network="mainnet",
chain_id="0x1",
owner_name="x" * 129,
indices_file=Path("/tmp/x.json"),
)


def test_url_network_chain_defaults_applied_when_unset(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
Expand Down
Loading
Loading