Ethereum Name Record (ENR) library for Python

Python library for ENR (EIP-778) records

Contents

Quickstart

ENR Creation

You can create an ENR record as follows.

>>> from eth_keys import keys
>>> from eth_enr import UnsignedENR, ENR
>>> private_key = keys.PrivateKey(b'unicornsrainbowsunicornsrainbows')
>>> unsigned_enr = UnsignedENR(
... sequence_number=1,
... kv_pairs={
...     b'id': b'v4',
...     b'secp256k1': private_key.public_key.to_compressed_bytes(),
...     b'unicorns': b'rainbows',
... })
>>> enr = unsigned_enr.to_signed_enr(private_key.to_bytes())
>>> enr
enr:-Ie4QNRDUVEiOYTwwki59qs5SY_ofKSCbFL2BuslZ9fsZXGEMOlfxkFGpojFUj_ArnHMh4bv6E26frE1NII7z4xK9I0BgmlkgnY0iXNlY3AyNTZrMaEDvfDdonz3wUFd66sirz_3a0oRlsc9rlKp0SQeHEkcC6iIdW5pY29ybnOIcmFpbmJvd3M
>>> enr == ENR.from_repr("enr:-Ie4QNRDUVEiOYTwwki59qs5SY_ofKSCbFL2BuslZ9fsZXGEMOlfxkFGpojFUj_ArnHMh4bv6E26frE1NII7z4xK9I0BgmlkgnY0iXNlY3AyNTZrMaEDvfDdonz3wUFd66sirz_3a0oRlsc9rlKp0SQeHEkcC6iIdW5pY29ybnOIcmFpbmJvd3M")  # recover an ENR from it's text representation
True

Storing ENR records

You can use the eth_enr.ENRDB to store ENR records. The underlying storage is flexible and accepts any dictionary-like object.

>>> from eth_keys import keys
>>> from eth_enr import UnsignedENR, ENRDB
>>> private_key = keys.PrivateKey(b'unicornsrainbowsunicornsrainbows')
>>> unsigned_enr = UnsignedENR(
... sequence_number=1,
... kv_pairs={
...     b'id': b'v4',
...     b'secp256k1': private_key.public_key.to_compressed_bytes(),
... })
>>> enr = unsigned_enr.to_signed_enr(private_key.to_bytes())
>>> enr_db = ENRDB({})
>>> enr_db.get_enr(enr.node_id)  # not yet in database
Traceback (most recent call last):
  File "/home/piper/.pyenv/versions/3.6.9/lib/python3.6/doctest.py", line 1330, in __run
    compileflags, 1), test.globs)
  File "<doctest default[6]>", line 1, in <module>
    enr_db.get_enr(enr.node_id)  # not yet in database
  File "/home/piper/projects/eth-enr/eth_enr/enr_db.py", line 57, in get_enr
    return rlp.decode(self.db[self._get_enr_key(node_id)], sedes=ENR)  # type: ignore
KeyError: b'l?\x85b\xc8\x03\xbf\xae5\xa8\xf5K\x85\x82\xa2\x89V\xb9%\x93M\x03\xdd\xb4Xu\xe1\x8e\x85\x93\x12\xc1:enr'
>>> enr_db.set_enr(enr)
>>> enr_db.get_enr(enr.node_id)
enr:-HW4QDBN_uzB2BgXNgpjCN83hSE13oI46ZtFOmWnmYkGTZWrfRF6Yk60HcoiyuLDXqCTcj8fqk2DWetU2ZYJrXUEylIBgmlkgnY0iXNlY3AyNTZrMaEDvfDdonz3wUFd66sirz_3a0oRlsc9rlKp0SQeHEkcC6g
>>> updated_enr = UnsignedENR(
... sequence_number=2,
... kv_pairs={
...     b'id': b'v4',
...     b'secp256k1': private_key.public_key.to_compressed_bytes(),
... }).to_signed_enr(private_key.to_bytes())
>>> enr_db.set_enr(updated_enr)
>>> enr_db.set_enr(enr, raise_on_error=True)  # throws exception due to old sequence number
Traceback (most recent call last):
  File "/home/piper/.pyenv/versions/3.6.9/lib/python3.6/doctest.py", line 1330, in __run
    compileflags, 1), test.globs)
  File "<doctest default[11]>", line 1, in <module>
    enr_db.set_enr(enr)  # throws exception due to old sequence number
  File "/home/piper/projects/eth-enr/eth_enr/enr_db.py", line 51, in set_enr
    f"Cannot overwrite existing ENR ({existing_enr.sequence_number}) with old one "
eth_enr.exceptions.OldSequenceNumber: Cannot overwrite existing ENR (2) with old one (1)
>>> assert enr_db.get_enr(updated_enr.node_id) == updated_enr

Using the ENRManager

The eth_enr.ENRMAnager automates creation, updating, and storage of ENR records.

>>> from eth_keys import keys
>>> from eth_enr import ENRManager, ENRDB
>>> private_key = keys.PrivateKey(b'unicornsrainbowsunicornsrainbows')
>>> manager = ENRManager(private_key, ENRDB({}))
>>> manager.enr
enr:-HW4QDBN_uzB2BgXNgpjCN83hSE13oI46ZtFOmWnmYkGTZWrfRF6Yk60HcoiyuLDXqCTcj8fqk2DWetU2ZYJrXUEylIBgmlkgnY0iXNlY3AyNTZrMaEDvfDdonz3wUFd66sirz_3a0oRlsc9rlKp0SQeHEkcC6g
>>> manager.enr.sequence_number
1
>>> manager.update((b'foo', b'bar'))
>>> manager.enr
enr:-H24QNUv1DBIpMITIUjJN8s7foWBJ33rR0liWCu4nVDaXk7ACcXpiMiFJHPC8UKTNkXfN3DXGwPX-Q6KL1uMZwNeyGMCg2Zvb4NiYXKCaWSCdjSJc2VjcDI1NmsxoQO98N2ifPfBQV3rqyKvP_drShGWxz2uUqnRJB4cSRwLqA
>>> manager.enr[b'foo']
b'bar'
>>> manager.enr.sequence_number
2
>>> manager.update((b'foo', None))  # `None` triggers removal of a key.
>>> manager.enr
enr:-HW4QFeb9Qg_RNSWamKytj4Eh2eICVKSauQfp4PMY45YQdGzAyFnLjZBU-IuktiGKGiEz2nbEo6w4qNOu_D2Xdmr08gDgmlkgnY0iXNlY3AyNTZrMaEDvfDdonz3wUFd66sirz_3a0oRlsc9rlKp0SQeHEkcC6g
>>> manager.enr[b'foo']
Traceback (most recent call last):
  File "/home/piper/.pyenv/versions/3.6.9/lib/python3.6/doctest.py", line 1330, in __run
    compileflags, 1), test.globs)
  File "<doctest default[10]>", line 1, in <module>
    manager.enr[b'foo']
  File "/home/piper/projects/eth-enr/eth_enr/enr.py", line 93, in __getitem__
    return self._kv_pairs[key]
KeyError: b'foo'

Querying ENR Records

You can use the eth_enr.QueryableENRDB which exposes the same API as eth_enr.ENRDB with one additional eth_enr.QueryableENRDB.query() method.

The eth_enr.QueryableENRDB operates on top of any SQLite3 database using the sqlite3 standard library.

>>> import sqlite3
>>> from eth_keys import keys
>>> from eth_enr import UnsignedENR, QueryableENRDB
>>> from eth_enr.constraints import KeyExists
>>> private_key_a = keys.PrivateKey(b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA')
>>> private_key_b = keys.PrivateKey(b'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB')
>>> private_key_c = keys.PrivateKey(b'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC')
>>> enr_a = UnsignedENR(
... sequence_number=1,
... kv_pairs={
...     b'id': b'v4',
...     b'secp256k1': private_key_a.public_key.to_compressed_bytes(),
...     b'unicorns': b'rainbows',
... }).to_signed_enr(private_key_a.to_bytes())
>>> enr_b = UnsignedENR(
... sequence_number=7,
... kv_pairs={
...     b'id': b'v4',
...     b'secp256k1': private_key_b.public_key.to_compressed_bytes(),
...     b'unicorns': b'rainbows',
...     b'cupcakes': b'sparkles',
... }).to_signed_enr(private_key_b.to_bytes())
>>> enr_c = UnsignedENR(
... sequence_number=2,
... kv_pairs={
...     b'id': b'v4',
...     b'secp256k1': private_key_c.public_key.to_compressed_bytes(),
... }).to_signed_enr(private_key_c.to_bytes())
>>> connection = sqlite3.connect(":memory:")
>>> enr_db = QueryableENRDB(connection)
>>> enr_db.set_enr(enr_a)
>>> enr_db.set_enr(enr_b)
>>> enrs_with_unicorns = tuple(enr_db.query(KeyExists(b'unicorns')))
>>> assert enr_a in enrs_with_unicorns
>>> assert enr_b in enrs_with_unicorns
>>> assert enr_c not in enrs_with_unicorns
>>> enrs_with_cupcakes = tuple(enr_db.query(KeyExists(b'cupcakes')))
>>> assert enr_a not in enrs_with_cupcakes
>>> assert enr_b in enrs_with_cupcakes
>>> assert enr_c not in enrs_with_cupcakes

API

Abstract Base Classes

class eth_enr.abc.CommonENRAPI

Bases: collections.abc.Mapping, typing.Generic, abc.ABC

get_signing_message() → bytes
identity_scheme
node_id
public_key
sequence_number
class eth_enr.abc.UnsignedENRAPI

Bases: eth_enr.abc.CommonENRAPI

to_signed_enr(private_key: bytes) → eth_enr.abc.ENRAPI
class eth_enr.abc.ENRAPI

Bases: eth_enr.abc.CommonENRAPI

classmethod from_repr(representation: str, identity_scheme_registry: collections.UserDict) → eth_enr.abc.ENRAPI
signature
validate_signature() → None
class eth_enr.abc.ENRManagerAPI

Bases: abc.ABC

enr
update(*kv_pairs) → None

Update the ENR record with the provided key/value pairs. Providing None for a value will result in the associated key being removed from the ENR.

class eth_enr.abc.IdentitySchemeAPI

Bases: abc.ABC

classmethod create_enr_signature(enr: eth_enr.abc.CommonENRAPI, private_key: bytes) → bytes

Create and return the signature for an ENR.

classmethod extract_node_id(enr: eth_enr.abc.CommonENRAPI) → NewType.<locals>.new_type

Retrieve the node id from an ENR.

classmethod extract_public_key(enr: eth_enr.abc.CommonENRAPI) → bytes

Retrieve the public key from an ENR.

classmethod validate_enr_signature(enr: eth_enr.abc.ENRAPI) → None

Validate the signature of an ENR.

classmethod validate_enr_structure(enr: eth_enr.abc.CommonENRAPI) → None

Validate that the data required by the identity scheme is present and valid in an ENR.

eth_enr.abc.IdentitySchemeRegistryAPI

alias of collections.UserDict

class eth_enr.abc.ENRDatabaseAPI

Bases: abc.ABC

delete_enr(node_id: NewType.<locals>.new_type) → None
get_enr(node_id: NewType.<locals>.new_type) → eth_enr.abc.ENRAPI
set_enr(enr: eth_enr.abc.ENRAPI, raise_on_error: bool = False) → None
class eth_enr.abc.QueryableENRDatabaseAPI

Bases: eth_enr.abc.ENRDatabaseAPI

query(*constraints) → Iterable[eth_enr.abc.ENRAPI]

Classes

class eth_enr.enr.ENR(sequence_number: int, kv_pairs: Mapping[bytes, Any], signature: bytes, identity_scheme_registry: collections.UserDict = {b'v4': <class 'eth_enr.identity_schemes.V4IdentityScheme'>, b'v4-compat': <class 'eth_enr.identity_schemes.V4CompatIdentityScheme'>})

Bases: eth_enr.enr.ENRCommon, eth_enr.sedes.ENRSedes, eth_enr.abc.ENRAPI

classmethod from_repr(representation: str, identity_scheme_registry: collections.UserDict = {b'v4': <class 'eth_enr.identity_schemes.V4IdentityScheme'>, b'v4-compat': <class 'eth_enr.identity_schemes.V4CompatIdentityScheme'>}) → eth_enr.enr.ENR
signature
validate_signature() → None
class eth_enr.enr.UnsignedENR(sequence_number: int, kv_pairs: Mapping[bytes, Any], identity_scheme_registry: collections.UserDict = {b'v4': <class 'eth_enr.identity_schemes.V4IdentityScheme'>, b'v4-compat': <class 'eth_enr.identity_schemes.V4CompatIdentityScheme'>})

Bases: eth_enr.enr.ENRCommon, eth_enr.abc.UnsignedENRAPI

to_signed_enr(private_key: bytes) → eth_enr.enr.ENR
class eth_enr.enr_manager.ENRManager(private_key: eth_keys.datatypes.PrivateKey, enr_db: eth_enr.abc.ENRDatabaseAPI, kv_pairs: Optional[Mapping[bytes, bytes]] = None, identity_scheme_registry: collections.UserDict = {b'v4': <class 'eth_enr.identity_schemes.V4IdentityScheme'>, b'v4-compat': <class 'eth_enr.identity_schemes.V4CompatIdentityScheme'>})

Bases: eth_enr.abc.ENRManagerAPI

enr
logger = <Logger eth_enr.ENRManager (WARNING)>
update(*kv_pairs) → None

Update the ENR record with the provided key/value pairs. Providing None for a value will result in the associated key being removed from the ENR.

class eth_enr.identity_schemes.IdentitySchemeRegistry(**kwargs)

Bases: collections.UserDict

register(identity_scheme_class: Type[IdentitySchemeAPI]) → Type[eth_enr.abc.IdentitySchemeAPI]

Class decorator to register identity schemes.

class eth_enr.identity_schemes.V4IdentityScheme

Bases: eth_enr.abc.IdentitySchemeAPI

classmethod create_enr_signature(enr: eth_enr.abc.CommonENRAPI, private_key: bytes) → bytes

Create and return the signature for an ENR.

classmethod extract_node_id(enr: eth_enr.abc.CommonENRAPI) → NewType.<locals>.new_type

Retrieve the node id from an ENR.

classmethod extract_public_key(enr: eth_enr.abc.CommonENRAPI) → bytes

Retrieve the public key from an ENR.

id = b'v4'
private_key_size = 32
public_key_enr_key = b'secp256k1'
classmethod validate_compressed_public_key(public_key: bytes) → None
classmethod validate_enr_signature(enr: eth_enr.abc.ENRAPI) → None

Validate the signature of an ENR.

classmethod validate_enr_structure(enr: eth_enr.abc.CommonENRAPI) → None

Validate that the data required by the identity scheme is present and valid in an ENR.

classmethod validate_signature(*, message_hash: bytes, signature: bytes, public_key: bytes) → None
classmethod validate_uncompressed_public_key(public_key: bytes) → None
class eth_enr.identity_schemes.V4CompatIdentityScheme

Bases: eth_enr.identity_schemes.V4IdentityScheme

An identity scheme to be used for locally crafted ENRs representing remote nodes that don’t support the ENR extension.

ENRs using this identity scheme have a zero-length signature.

classmethod create_enr_signature(enr: eth_enr.abc.CommonENRAPI, private_key: bytes) → bytes

Create and return the signature for an ENR.

id = b'v4-compat'
classmethod validate_enr_signature(enr: eth_enr.abc.ENRAPI) → None

Validate the signature of an ENR.

class eth_enr.enr_db.ENRDB(db: MutableMapping[bytes, bytes], identity_scheme_registry: collections.UserDict = {b'v4': <class 'eth_enr.identity_schemes.V4IdentityScheme'>, b'v4-compat': <class 'eth_enr.identity_schemes.V4CompatIdentityScheme'>})

Bases: eth_enr.abc.ENRDatabaseAPI

delete_enr(node_id: NewType.<locals>.new_type) → None
get_enr(node_id: NewType.<locals>.new_type) → eth_enr.abc.ENRAPI
identity_scheme_registry
logger = <Logger eth_enr.ENRDB (WARNING)>
set_enr(enr: eth_enr.abc.ENRAPI, raise_on_error: bool = False) → None
class eth_enr.query_db.QueryableENRDB(connection: sqlite3.Connection, identity_scheme_registry: collections.UserDict = {b'v4': <class 'eth_enr.identity_schemes.V4IdentityScheme'>, b'v4-compat': <class 'eth_enr.identity_schemes.V4CompatIdentityScheme'>})

Bases: eth_enr.abc.QueryableENRDatabaseAPI

An implementation of eth_enr.abc.QueryableENRDatabaseAPI on top of the sqlite3 module from the standard library.

For use with an in-memory database:

>>> connection = sqlite3.connect(":memory:")
>>> enr_db = QueryableENRDB(connection)
...

Or use with an on-disk database:

>>> connection = sqlite3.connect("/path/to/db.sqlite3")
>>> enr_db = QueryableENRDB(connection)
...

The database tables will lazily be created upon class instantiation if they are missing.

delete_enr(node_id: NewType.<locals>.new_type) → None

Delete ENR records with the given node_id

Raisees KeyError if there are no records with the given node_id

get_enr(node_id: NewType.<locals>.new_type) → eth_enr.abc.ENRAPI

Retrieve the ENR record with the highest sequence number for the given node_id

Raises KeyError if there are no records with the geven node_id

identity_scheme_registry
logger = <Logger eth_enr.ENRDB (WARNING)>
query(*constraints) → Iterable[eth_enr.abc.ENRAPI]

Query the database for records that match the given constraints.

Support constraints:

Return an iterator of matching ENR records. Only returns the record with the highest sequence number for each node_id.

set_enr(enr: eth_enr.abc.ENRAPI, raise_on_error: bool = False) → None

Write a record to the database.

Raise eth_enr.exceptions.DuplicateRecord if there is an different existing record with the same sequence number.

Constraints

class eth_enr.constraints.KeyExists(key: bytes)

Bases: eth_enr.abc.ConstraintAPI

Constrains ENR database queries to records which have a specified key.

>>> enr_db = ...
>>> from eth_enr.constraints import KeyExists
>>> for enr in enr_db.query(KeyExists(b"some-key")):
...     print("ENR: ", enr)
class eth_enr.constraints.HasUDPIPv4Endpoint

Bases: eth_enr.abc.ConstraintAPI

Constrains ENR database queries to records which have both the "ip" and "udp" keys.

>>> enr_db = ...
>>> from eth_enr.constraints import has_udp_ipv4_endpoint
>>> for enr in enr_db.query(has_udp_ipv4_endpoint):
...     print("ENR: ", enr)
class eth_enr.constraints.HasUDPIPv6Endpoint

Bases: eth_enr.abc.ConstraintAPI

Constrains ENR database queries to records which have both the "ip6" and "udp6" keys.

>>> enr_db = ...
>>> from eth_enr.constraints import has_udp_ipv6_endpoint
>>> for enr in enr_db.query(has_udp_ipv6_endpoint):
...     print("ENR: ", enr)
class eth_enr.constraints.HasTCPIPv4Endpoint

Bases: eth_enr.abc.ConstraintAPI

Constrains ENR database queries to records which have both the "ip" and "tcp" keys.

>>> enr_db = ...
>>> from eth_enr.constraints import has_tcp_ipv4_endpoint
>>> for enr in enr_db.query(has_tcp_ipv4_endpoint):
...     print("ENR: ", enr)
class eth_enr.constraints.HasTCPIPv6Endpoint

Bases: eth_enr.abc.ConstraintAPI

Constrains ENR database queries to records which have both the "ip6" and "tcp6" keys.

>>> enr_db = ...
>>> from eth_enr.constraints import has_tcp_ipv6_endpoint
>>> for enr in enr_db.query(has_tcp_ipv6_endpoint):
...     print("ENR: ", enr)
class eth_enr.constraints.ClosestTo(node_id: NewType.<locals>.new_type)

Bases: eth_enr.abc.ConstraintAPI

Constrains ENR database queries to return records proximate to a specific node_id

>>> enr_db = ...
>>> node_id = ...
>>> from eth_enr.constraints import ClosestTo
>>> for enr in enr_db.query(ClosestTo(node_id)):
...     print("ENR: ", enr)

Exceptions

class eth_enr.exceptions.OldSequenceNumber

Bases: eth_enr.exceptions.BaseENRException

Raised when trying to update an ENR record with a sequence number that is older than the latest sequence number we have seen

class eth_enr.exceptions.DuplicateRecord

Bases: eth_enr.exceptions.BaseENRException

Raised when trying to set an ENR record to a database that already has a different record with the same sequence number.

class eth_enr.exceptions.UnknownIdentityScheme

Bases: eth_enr.exceptions.BaseENRException

Raised when trying to instantiate an ENR with an unknown identity scheme

Release Notes

v0.1.0-alpha.1

  • Launched repository, claimed names for pip, RTD, github, etc

Indices and tables