Skip to content
This repository has been archived by the owner on Jul 1, 2021. It is now read-only.

ENR #586

Merged
merged 6 commits into from
Jun 13, 2019
Merged

ENR #586

merged 6 commits into from
Jun 13, 2019

Conversation

jannikluhn
Copy link
Contributor

Closes #583. Still WIP: A couple of additional tests are needed and we have to use compressed public keys (working on ethereum/eth-keys#55 now).

How was it fixed?

  • There's an ENR class that exposes the ENR key value pairs via a dict interface, along the sequence number and signature. If the signature is None it is considered unsigned. A signed copy can be created with to_signed_enr.
  • There are special sedes classes ENRSedes and ENRContentSedes to serialize/deserialize an ENR with or without the signature. Values are (de)serialized as binaries, unless there's something better in ENR_KEY_SEDES_MAPPING.
  • Identity schemes (responsible for signing and validating signatures) are represented by children of ENRIdentityScheme which define a method to check if they can be used for a given ENR or not (usually by checking if the ENR contains a public key in the right format). The ENR will use the first matching identity scheme it can find (there will probably only be a few).

Cute Animal Picture

put a cute animal picture link inside the parentheses

@jannikluhn
Copy link
Contributor Author

jannikluhn commented May 20, 2019

Updated to use the new eth-keys API introduced in ethereum/eth-keys#56 and ethereum/eth-keys#58. I still have to add the new version in setup.py, but I can only do this once the new changes have been released. After that, the tests should pass.

I also restructured the code a little in order to have two separate classes for signed and unsigned ENRs (instead of an is_signed property and a signature that may be None).

@jannikluhn jannikluhn mentioned this pull request May 22, 2019
Copy link
Contributor

@hwwhww hwwhww left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Solid work! 👍
Need to fix linter.

p2p/discv5/enr.py Outdated Show resolved Hide resolved
p2p/discv5/identity_schemes.py Outdated Show resolved Hide resolved
p2p/discv5/enr.py Outdated Show resolved Hide resolved
Copy link
Member

@pipermerriam pipermerriam left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you open (or link me to an existing) larger meta-tracking issue to tie these various discv5 PR's together into a larger epoch so that it's easier to understand the big picture as I review these smaller components?

p2p/discv5/enr.py Outdated Show resolved Hide resolved
p2p/discv5/enr.py Show resolved Hide resolved
p2p/discv5/enr.py Outdated Show resolved Hide resolved
p2p/discv5/enr.py Show resolved Hide resolved
p2p/discv5/enr.py Outdated Show resolved Hide resolved
p2p/discv5/identity_schemes.py Show resolved Hide resolved
tests/p2p/discv5/test_enr.py Outdated Show resolved Hide resolved
@jannikluhn jannikluhn mentioned this pull request May 26, 2019
18 tasks
@jannikluhn
Copy link
Contributor Author

Opened a meta issue here (I guess I should have started with that): #668

@jannikluhn
Copy link
Contributor Author

I've addressed the feedback and now I'm waiting for ethereum/eth-keys#58 and a subsequent eth-keys release. Then I'll have a look at the outstanding CI errors.

@pipermerriam
Copy link
Member

pipermerriam commented May 26, 2019 via email

@fjl
Copy link

fjl commented Jun 6, 2019

Very nice to see Trinity will have ENR support :).

Note that EIP-778 was changed one more time to add "tcp6", "udp6" keys. They allow relaying IPv6-specific port numbers.

We have also decided to use base64url encoding (without padding) instead of standard base64.
Base64 without padding is not supported in the Python standard library, but you can use these
functions to do it: fjl/p2p-drafts@23ed342#diff-09ba11ec95dfcee8269304dd2cc0abf3R156

@jannikluhn
Copy link
Contributor Author

  • Updated to latest version of ENR spec (thanks @fjl)
  • reworked tests: clear separation between ENR and identity scheme
  • cleaned up commit history

Unfortunately, now everything breaks due to a version conflict. Pretty much all eth-* libraries require eth_keys < 3.0.0, but here we need eth_keys >= 3.0.0. I guess there's no other way but going through all of them and make a new release? 3.0.0 should be backwards compatible.

@carver
Copy link
Contributor

carver commented Jun 6, 2019

Unfortunately, now everything breaks due to a version conflict. Pretty much all eth-* libraries require eth_keys < 3.0.0, but here we need eth_keys >= 3.0.0. I guess there's no other way but going through all of them and make a new release? 3.0.0 should be backwards compatible.

Yeah, sorry, this is fixed in #699 and was released so it doesn't affect fresh installs. But it hasn't been merged in yet. I'll ping you when it gets merged in, and the rebase should be painless.

@carver
Copy link
Contributor

carver commented Jun 6, 2019

@jannikluhn Okay, a rebase should fix the eth-keys issue now.

@jannikluhn
Copy link
Contributor Author

@carver that didn't do the trick. The problem is that this PR needs eth-keys >= 3.0.0, everything else eth-keys < 3.0.0. So I think there's no other way than updating all of them to use < 4.0.0.

@carver
Copy link
Contributor

carver commented Jun 7, 2019

Ah, right. @pipermerriam instead of propagating a chain of eth-keys releases up to trinity (I think at least these would need new releases: eth-account, web3, py-evm), maybe we can just do a patch release that includes everything that's in 0.3.0?

@pipermerriam
Copy link
Member

v0.2.3 is now available and equal to v0.3.0

setup.py Outdated Show resolved Hide resolved
@carver
Copy link
Contributor

carver commented Jun 10, 2019

Hm, interesting, I think this is a legit issue. The new eth-keys is treating a valid signature as invalid?

_ test_rpc_against_fixtures[/home/circleci/repo/fixtures/BlockchainTests/bcValidBlockTest/SimpleTx3.json:SimpleTx3_Frontier:Frontier] _
[gw1] linux -- Python 3.6.8 /home/circleci/repo/.tox/py36-rpc-blockchain/bin/python

event_bus = <trinity.endpoint.TrinityEventBusEndpoint object at 0x7f2f8c0a39b0>
chain_fixture = {'_info': {'comment': '', 'filledwith': 'testeth 1.5.0.dev2-68+commit.4a43125d.dirty', 'lllcversion': 'Version: 0.5.0-...80832fefd8808454c98c8142a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421880102030405060708c0c0', ...}
fixture_data = ('/home/circleci/repo/fixtures/BlockchainTests/bcValidBlockTest/SimpleTx3.json', 'SimpleTx3_Frontier', 'Frontier')

    @pytest.mark.asyncio
    async def test_rpc_against_fixtures(event_bus, chain_fixture, fixture_data):
        rpc = RPCServer(
            initialize_eth1_modules(MainnetFullChain(None), event_bus)
        )
    
        setup_result, setup_error = await call_rpc(rpc, 'evm_resetToGenesisFixture', [chain_fixture])
        # We need to advance the event loop for modules to be able to pickup the new chain
        await asyncio.sleep(0)
        assert setup_error is None and setup_result is True, "cannot load chain for {0}".format(fixture_data)  # noqa: E501
    
        await validate_accounts(rpc, chain_fixture['pre'])
    
        for block_fixture in chain_fixture['blocks']:
            should_be_good_block = 'blockHeader' in block_fixture
    
            if 'rlp_error' in block_fixture:
                assert not should_be_good_block
                continue
    
            block_result, block_error = await call_rpc(rpc, 'evm_applyBlockFixture', [block_fixture])
    
            if should_be_good_block:
>               assert block_error is None
E               AssertionError: assert 'Invalid Signature' is None

block_error = 'Invalid Signature'
block_fixture = {'blockHeader': {'bloom': '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000...ce': '0x00', ...}, {'data': '0x', 'gasLimit': '0x5208', 'gasPrice': '0x01', 'nonce': '0x03', ...}], 'uncleHeaders': []}
block_result = None
chain_fixture = {'_info': {'comment': '', 'filledwith': 'testeth 1.5.0.dev2-68+commit.4a43125d.dirty', 'lllcversion': 'Version: 0.5.0-...80832fefd8808454c98c8142a056e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421880102030405060708c0c0', ...}
event_bus  = <trinity.endpoint.TrinityEventBusEndpoint object at 0x7f2f8c0a39b0>
fixture_data = ('/home/circleci/repo/fixtures/BlockchainTests/bcValidBlockTest/SimpleTx3.json', 'SimpleTx3_Frontier', 'Frontier')
rpc        = <trinity.rpc.main.RPCServer object at 0x7f2f68721ba8>
setup_error = None
setup_result = True
should_be_good_block = True

@jannikluhn
Copy link
Contributor Author

yeah, that's my fault, this PR should fix it: ethereum/eth-keys#62

@jannikluhn
Copy link
Contributor Author

Finally everything is green 🎉

I think I've addressed all comments so far, but since the last review I've restructured and extended the tests a bit. Anyone wants to have another look? Otherwise this is ready from my side.

def serialize(cls, enr: "BaseENR") -> Tuple[bytes, ...]:
serialized_sequence_number = big_endian_int.serialize(enr.sequence_number)

sorted_key_value_pairs = sorted(enr.items(), key=lambda key_value: key_value[0])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe historically we've measured operator.itemgetter(0) to be faster and slightly more preferred.

serialized_enr: Sequence[bytes]) -> None:
duplicates = set(
key for index, key in enumerate(keys)
if key in keys[index + 1:]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this has some unfortunate time complexity. An itertools.Counter does a pretty nice job of both detecting duplicates and allowing easy reporting of which ones are duplicate.

duplicates = {key for key, num in itertools.Counter(keys).items() if num > 1}

That one-liner does the trick pretty concisely.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

neat! changed it

)


class BaseENR(Mapping[bytes, Any], ABC):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are the value types truly Any or can that be constrained more?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now it's just int and bytes, but in theory it can be anything -- we might want to add special types for, e.g., libp2p multiaddrs or public key objects in the future. So to keep it easily extendable I'd prefer to keep it Any.


def get_identity_scheme(self) -> Type[IdentityScheme]:
try:
identity_scheme_id = self[b"id"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe instead of having to do this try/except checking at runtime it should happen during initialization and be memoized somewhere private since it's probably very natural to do that at whatever place the b'id' key is validated to be present.

)


identity_scheme_registry: Dict[bytes, Type["IdentityScheme"]] = {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably still use this pattern in a number of places but I've grown to think that we should be using a registry pattern closer to something like this.

class Registry(collections.UserDict):
    def register_thing(self, thing) -> None:
        ...  # business logic here
        self[thing_key] = thing

default_registry = Registry()
register_thing = default_registry.register_thing

Note that collections.UserDict gives us an easy mechanism for simple registries like this.

https://docs.python.org/3/library/collections.html#collections.UserDict

Note that this requires ALL apis that make use of the registry to have some mechanism for passing in a custom registry. This pattern should result in easier isolation during testing as well as more flexibility when a library needs to override the base registry. I also think this approach has minimal extra overhead with respect to implementation and user facing API since the actualy underlying Registry API can be maintained as private.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds reasonable. I've implemented something like this, but I'm not sure if I fully understood. Please have a look if that's how you imagined it.

b"value1",
b"id",
]),
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be parametrized using pytest.mark.parametrize so it runs as 4 different tests.

Copy link
Member

@pipermerriam pipermerriam left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great. 👍

pass


@default_identity_scheme_registry.register
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might expose the register function as a top level module item that users can import to register a new identity scheme but that's only relevant if you want to expose this as a public API. For now I think it's fine to keep it private.



@pytest.fixture
def test_identity_scheme_registry(mock_identity_scheme):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because pytest is greedy I've tended to avoid naming anything in a module that pytest scrapes with test_ (or classes that start with Test) because it sometimes tries to run them as tests. Naming schemes to get around this that seem appropriate are just to call this identity_scheme_registry or identity_scheme_registry_for_testing

@jannikluhn jannikluhn merged commit fece800 into ethereum:master Jun 13, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement ENRs
5 participants