Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tighten type support and fix typing issues #1948

Merged

Conversation

cburgdorf
Copy link
Contributor

@cburgdorf cburgdorf commented Jul 2, 2020

What was wrong?

Currently, any method that expects any of BaseHeaderAPI, BaseBlockAPI, UnsignedTransactionAPI, ReceiptAPI does not properly enforce type safe. In practice that means that a method such as:

def do_something_with_block(block: BlockAPI):`
   ...

Can be called as do_something_with_block(accidential_is_transaction) or do_something_with_block(accidential_is_header) and mypy does not complain about it.

This has allowed serious bugs to fly under the radar: ethereum/trinity#1830

The root cause for this is that anything derived from rlp.Serializable will be seen as Any by mypy. We currently do not enforce this to be invalid but if so, mypy would rightfully reject a series of types:

$ mypy -p eth --config-file mypy.ini 
eth/vm/interrupt.py:28: error: Class cannot subclass 'MissingTrieNode' (has type 'Any')
eth/vm/interrupt.py:54: error: Class cannot subclass 'MissingTrieNode' (has type 'Any')
eth/rlp/transactions.py:55: error: Class cannot subclass 'Serializable' (has type 'Any')
eth/rlp/transactions.py:102: error: Class cannot subclass 'Serializable' (has type 'Any')
eth/rlp/logs.py:20: error: Class cannot subclass 'Serializable' (has type 'Any')
eth/rlp/headers.py:48: error: Class cannot subclass 'Serializable' (has type 'Any')
eth/rlp/headers.py:66: error: Class cannot subclass 'Serializable' (has type 'Any')
eth/rlp/accounts.py:20: error: Class cannot subclass 'Serializable' (has type 'Any')
eth/rlp/receipts.py:23: error: Class cannot subclass 'Serializable' (has type 'Any')
eth/rlp/blocks.py:19: error: Class cannot subclass 'Serializable' (has type 'Any')
eth/db/chain.py:77: error: Class cannot subclass 'Serializable' (has type 'Any')
Found 11 errors in 8 files (checked 228 source files)

How was it fixed?

I believe the best fix would be to do a serious overhaul of py-rlp to enforce strong type support. As I tried to add type support to py-rlp in a minimalistic fashion I noticed another way of approaching this which gets us halfway there without even touching py-rlp (and hence being faster to deliver).

Basically, what this does is rewrite all eth.abc types to not derive from rlp.Serializable and move the rlp.Serializable a level deeper. That means that things such as BaseBlock will still be seen as Any (just like before) but at least we have full type safety at the top level e.g. BlockAPI.

This does already a great job at exposing tons of bugs without opening a huge time sink such as refactoring py-rlp.

To-Do

  • Clean up commit history

Cute Animal Picture

put a cute animal picture link inside the parentheses

@cburgdorf cburgdorf force-pushed the christoph/mypy/fix-typing-errors branch 2 times, most recently from d348683 to 8625942 Compare July 2, 2020 14:09
@cburgdorf cburgdorf force-pushed the christoph/mypy/fix-typing-errors branch 3 times, most recently from 267dcb8 to bed2942 Compare July 6, 2020 12:29
@cburgdorf cburgdorf force-pushed the christoph/mypy/fix-typing-errors branch 2 times, most recently from e7759d7 to e711160 Compare July 6, 2020 13:26
@cburgdorf cburgdorf changed the title Fix a bunch of typing issues in light of an upcoming pyrlp release that exposes type hints Tighten type support and fix typing issues Jul 6, 2020
@cburgdorf cburgdorf force-pushed the christoph/mypy/fix-typing-errors branch from e711160 to be5583b Compare July 6, 2020 15:50
@cburgdorf cburgdorf force-pushed the christoph/mypy/fix-typing-errors branch from be5583b to 3c3102d Compare July 6, 2020 16:19
@cburgdorf cburgdorf requested a review from carver July 6, 2020 16:25
eth/abc.py Outdated
...

@abstractmethod
def copy(self, *args: Any, **kwargs: Any) -> THeader:
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not just 'MiningHeaderAPI', so we can drop THeader?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because BlockHeaderAPI inherits this API and so we don't want to hardcode the return type to MiningHeaderAPI.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But actually, we don't really need it to be on MiningHeaderAPI and can move it down to BlockHeaderAPI and then hardcode it. This makes the usage slightly nicer because otherwise mypy is asking us for a type annotation at every place where we use copy like so:

unused_header: BlockHeaderAPI = header.copy(gas_used=0)

If I move it down to BlockHeaderAPI we can simply do:

unused_header = header.copy(gas_used=0)

...

@abstractmethod
def as_dict(self) -> Dict[Hashable, Any]:
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe add a quick comment on each of the methods that are copied over from rlp.Serializeable, saying that they can be deleted from this API after pyrlp properly exposes the types? (I think just these last three methods)

Edit: and the methods in the other classes, of course.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will do

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added comments on all methods. Btw, I've also tried grouping all of them in a RLPSerializableAPI(Generic[T]) so that we don't have to manually add all these methods to all these different types and while this worked great for >= py37 it yielded an obscure meta class MRO error that I couldn't resolve.

@@ -136,6 +136,6 @@ def from_header(cls, header: BlockHeaderAPI, chaindb: ChainDatabaseAPI) -> "Fron
# Execution API
#
def add_uncle(self, uncle: BlockHeaderAPI) -> "FrontierBlock":
self.uncles.append(uncle)
self.uncles += (uncle,)
Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, hooray for bugfixes. :) I guess worthy of a news fragment, and surprising and problematic that there's no test coverage on this. Maybe add a test that covers it (or at least an issue to do it as part of beta)? Looks like adding uncles was totally broken. (Is this overwritten in different forks, and that's why we didn't notice?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good question. I have to investigate that once more tomorrow.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, the reason seems to be that this is essentially dead code. At least there's no code in Py-EVM or Trinity calling add_uncle. Since this is a public API there's a non-zero chance it is used at another place that I might not be aware of but it seems unlikely. Anyway, I'm not dropping it pre-merge in case I'm overseeing something but I'm happy to send another PR to remove it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Since this is a public API there's a non-zero chance it is used at another place that I might not be aware of but it seems unlikely.

Yeah, very unlikely, since every call to it would immediately fail 😆

I think we can safely remove an API that has been broken for 3 years:

3b0bcc219 eth/vm/forks/frontier/blocks.py   (2019-08-01 11:43:24 -0600 138)     def add_uncle(self, uncle: BlockHeaderAPI) -> "FrontierBlock":
26173f6ed evm/vm/flavors/frontier/blocks.py (2017-06-28 14:50:03 -0600 139)         self.uncles.append(uncle)
26173f6ed evm/vm/flavors/frontier/blocks.py (2017-06-28 14:50:03 -0600 140)         self.header.uncles_hash = keccak(rlp.encode(self.uncles))
26173f6ed evm/vm/flavors/frontier/blocks.py (2017-06-28 14:50:03 -0600 141)         return self

I'm happy to send another PR to remove it.

👍

Copy link
Contributor

Choose a reason for hiding this comment

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

Wait, now I'm confused again. The init method adds uncles as []. So I guess maybe it was working before, and we have to think a little more carefully. It's a mess that it's created as a tuple in one place and a list in another.

@cburgdorf cburgdorf force-pushed the christoph/mypy/fix-typing-errors branch 2 times, most recently from e50d9ae to c2ed99c Compare July 7, 2020 07:45
@cburgdorf cburgdorf force-pushed the christoph/mypy/fix-typing-errors branch from c2ed99c to 4fd6088 Compare July 7, 2020 07:59
@cburgdorf cburgdorf merged commit bd90df8 into ethereum:master Jul 7, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants