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
26 changes: 13 additions & 13 deletions src/lean_spec/subspecs/networking/enr/enr.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
from typing_extensions import Self

from lean_spec.subspecs.networking.types import Multiaddr, NodeId, SeqNumber
from lean_spec.types import RLPDecodingError, StrictBaseModel, Uint64
from lean_spec.types import Bytes33, Bytes64, RLPDecodingError, StrictBaseModel, Uint64
from lean_spec.types.rlp import decode_list as rlp_decode_list

from . import keys
Expand Down Expand Up @@ -94,7 +94,7 @@ class ENR(StrictBaseModel):
SCHEME: ClassVar[str] = "v4"
"""Supported identity scheme."""

signature: bytes
signature: Bytes64
"""64-byte secp256k1 signature (r || s concatenated, no recovery id)."""

seq: SeqNumber
Expand All @@ -121,9 +121,10 @@ def identity_scheme(self) -> str | None:
return id_bytes.decode("utf-8") if id_bytes else None

@property
def public_key(self) -> bytes | None:
def public_key(self) -> Bytes33 | None:
"""Get compressed secp256k1 public key (33 bytes)."""
return self.get(keys.SECP256K1)
raw = self.get(keys.SECP256K1)
return Bytes33(raw) if raw is not None and len(raw) == 33 else None

@property
def ip4(self) -> str | None:
Expand Down Expand Up @@ -194,15 +195,10 @@ def is_valid(self) -> bool:

A valid ENR has:
- Identity scheme "v4"
- 33-byte compressed secp256k1 public key
- 64-byte signature
- 33-byte compressed secp256k1 public key (Bytes33)
- 64-byte signature (Bytes64, enforced by type)
"""
return (
self.identity_scheme == self.SCHEME
and self.public_key is not None
and len(self.public_key) == 33
and len(self.signature) == 64
)
return self.identity_scheme == self.SCHEME and self.public_key is not None

def is_compatible_with(self, other: "ENR") -> bool:
"""Check fork compatibility via eth2 fork digest."""
Expand Down Expand Up @@ -274,7 +270,11 @@ def from_string(cls, enr_text: str) -> Self:
if len(items) % 2 != 0:
raise ValueError("ENR key/value pairs must be even")

signature = items[0]
signature_raw = items[0]
if len(signature_raw) != 64:
raise ValueError(f"ENR signature must be 64 bytes, got {len(signature_raw)}")
signature = Bytes64(signature_raw)

seq_bytes = items[1]
seq = int.from_bytes(seq_bytes, "big") if seq_bytes else 0

Expand Down
4 changes: 3 additions & 1 deletion src/lean_spec/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from .basispt import BasisPoint
from .bitfields import BaseBitlist
from .boolean import Boolean
from .byte_arrays import ZERO_HASH, Bytes12, Bytes16, Bytes20, Bytes32, Bytes52
from .byte_arrays import ZERO_HASH, Bytes12, Bytes16, Bytes20, Bytes32, Bytes33, Bytes52, Bytes64
from .collections import SSZList, SSZVector
from .container import Container
from .exceptions import (
Expand All @@ -28,7 +28,9 @@
"Bytes16",
"Bytes20",
"Bytes32",
"Bytes33",
"Bytes52",
"Bytes64",
"ZERO_HASH",
"CamelModel",
"StrictBaseModel",
Expand Down
12 changes: 12 additions & 0 deletions src/lean_spec/types/byte_arrays.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,12 @@ class Bytes32(BaseBytes):
LENGTH = 32


class Bytes33(BaseBytes):
"""Fixed-size byte array of exactly 33 bytes (compressed secp256k1 public key)."""

LENGTH = 33


ZERO_HASH: Bytes32 = Bytes32.zero()
"""All-zero hash (32 bytes of zeros)."""

Expand All @@ -249,6 +255,12 @@ class Bytes52(BaseBytes):
LENGTH = 52


class Bytes64(BaseBytes):
"""Fixed-size byte array of exactly 64 bytes (secp256k1 signature)."""

LENGTH = 64


class Bytes96(BaseBytes):
"""Fixed-size byte array of exactly 96 bytes."""

Expand Down
Loading
Loading