-
Notifications
You must be signed in to change notification settings - Fork 772
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
Add Clique Support #1032
Add Clique Support #1032
Conversation
Codecov Report
Flags with carried forward coverage won't be shown. Click here to find out more. |
Ok, made some good progress here today and we at least have got some substantial start here, this will likely take some time to mature though, there are also still larger parts missing, in particular the signer voting (respectively the extraction of an up-to-date signer list from the voting info from the blocks, read the EIP if you are interested) and the signature validation, both likely for implementation in If someone has a good idea where to get or how to easily generate PoA specific blockchain test data - so a file like testdata.json but with Rinkeby or Goerli blocks - that would really helpful, got a bit stuck on this front and at the end deactivated some tests for now. |
@holgerd77 We only care about the blocks in this testdata right? The transactions in them don't really matter? If that's the case we can just pull the first ~10 blocks from Goerli/Rinkeby using web3 I'd say? |
@jochem-brouwer I had some problems with missing transactions as well, e.g. on running (testing) the general |
634758f
to
a634a84
Compare
This is now functionally ready I would say, namely this PR implements the following at this point:
There is some further work to be done and for example some test cases to be finished and/or reactivated, this also needs some polishing in general, so I would expect this to take another 1-2 weeks at least until we get this merged here. Since the changes are so broad I will nevertheless put this open this for review already - together with adding a "do-not-merge" label - since it will make sense to not do an all-at-once review here but rather give some continued thoughts and polish together. You are also very invited to open PRs towards this branch if you want to continue to work here or make something better. I have some dedicated questions and remarks as well, will put these directly along the related code parts. Cheers 🥳 👾 🌻 Holger |
f72c975
to
893a0b4
Compare
I've rebased on master and added some commits, here are the summary of changes:
Pending TODOs I will be working on next
One future TODO for myself: I found having to write a lot of |
Great, I'm ok with all the changes, yes, CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT is not yet used, this should be integrated into the updateSignerStates method (at least that was my intention, came over it though) to not endlessly bloat the list of signer states on ongoing updates. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reviewed up till 893a0b4
Everything ok. 😄
893a0b4
to
436067a
Compare
okay I believe I've completed all remaining TODOs! all tests are passing. The last task is to handle reorgs by removing added signers and votes for deleted blocks, which I will add soon. I also wanted to ask if we should additionally validate the
Another validation we can add is that (edit: realized there are still a few commented out test cases at the bottom of |
@ryanio I honestly didn't understand this whole turn-no-turn part of the spec. 😋 If you get it just implement, at the moment it is not taken in (as I also perceived this as non-mission-critical, but of course would be good to have). The uncle validation is automatically done in Block.validateUnclesHash() which is comparing against |
Have added a new factory helper method |
* remove contradictory votes for [signer, beneficiary] * clear all votes for signer after signer is added or removed * when beneficiary vote is touched/counted, count both auth and drop votes
…cently signed" * add remaining EIP-225 test cases
8bf671a
to
141159c
Compare
ok, I believe I've completed everything and this is now ready for review! I will give it one last look over tomorrow to see if I can add any helpful additional clarifying comments or typedocs, but meanwhile it is open for review. Please let me know if anything can be improved or clarified. Thanks! |
@@ -44,5 +44,5 @@ export default function blockFromRpc(blockParams: any, uncles: any[] = [], optio | |||
|
|||
const uncleHeaders = uncles.map((uh) => blockHeaderFromRpc(uh, options)) | |||
|
|||
return Block.fromBlockData({ header, transactions, uncleHeaders }) | |||
return Block.fromBlockData({ header, transactions, uncleHeaders }, options) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a bug fix 🥴 I found common wasn't being passed properly to the returned block in blockFromRpc
when using it for the goerli block test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
…ature validation)
One thing we can consider adding in the future is a helper to create a block with a certain clique signature seal, otherwise we currently have to manually put together the |
This is also one last implementation detail in EIP-225 for the client when broadcasting clique blocks, noting here for subsequent work:
|
Yes, I thought this as well and already wanted to add, just couldn't come up with an API I was satisfied with so I put this aside. Think we should add this rather sooner than later, I would assume this is a somewhat common use case also useful beside our own signed block creation one. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, have reviewed the rest of the commits, thanks Ryan, really great PR with these substantial functionality additions and completions and extensive test additions.
Have some comments (likely) to address, will nevertheless give this a go to have this huge PR out of the way.
I will also merge with admin merge for procedural reasons, since Ryan has already reviewed my initial code and I have now done the review of the remaining code changes, so technically this should make sense, hope that is ok.
if (!activeSigners.find(aSigner => aSigner.toBuffer().equals(cSigner.toBuffer()))) { | ||
throw new Error('checkpoint signer not found in active signers list: ' + cSigner.toString()) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is actually just checking one way, there can still be the case that we have an active signer still there which should have been removed at some point (so the whole check should be for equality of the lists).
Won't make this a blocker for review but we likely want to add on occasion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh right good point, I will change to a strict equality check of the same order, that would be much better.
const limit = this.CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT | ||
if (length > limit) { | ||
this._cliqueLatestVotes = this._cliqueLatestVotes.slice(length - limit, length) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was my initial thought as well to trim both the signer states AND the votes (sorry, just looked it up, also still had this as a comment in along CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT
), but the votes actually can't/shouldn't be discarded, these can always still be relevant for vote count (not even directly after an epoch transition block since there can incidentally be a reorg directly upon/around an epoch transition block).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I guess for the votes we might want to define a safe-number-of-blocks waiting periods (longest anticipated reorg block counts) and then discard the votes from before the epoch transition.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
right now all votes are being cleared on epoch transitions:
ethereumjs-monorepo/packages/blockchain/src/index.ts
Lines 822 to 826 in 6721321
if (this._common.consensusAlgorithm() === 'clique') { | |
// Reset all votes on epoch transition blocks | |
if (header.cliqueIsEpochTransition()) { | |
this._cliqueLatestVotes = [] | |
await this.cliqueUpdateVotes() |
which is correct as per this part of the spec:
Attack vector: Spamming signer
... The solution is to place a moving window of W blocks after which votes are considered stale. We’ll call this an epoch.
Keeping in mind reorgs would be important though as you mention, but I have to think more about how we would manage that.
Perhaps we could keep some history of stale votes, but the vote counting logic in cliqueUpdateVotes
should only count votes from the last active epoch in case of this edge scenario? starts getting a bit tricky 🤔
(edit: I added this to the new follow up PR)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't actually think it's too tricky: if there is a reorg, then all vote and signer state entries with block numbers higher than the reorg block number will be deleted, the signers from the last singer state stack item will be become the new active signers. Everything else from that moment on then continues (restarts) as before.
If I don't get this wrong, votes need to be kept relatively long in this scenario, so i think "EPOCH_LENGTH + REORG_SAFE_PERIOD" block numbers - so something like 30050, since if a reorg is happening from let's say 30020 -> 29950 and there is voting taking place in these last 50 blocks before the epoch transition, theoretically all existing votes from this 0-29999 epoch might be relevant for the pre-epoch voting. Does this make sense?
For Signer States I would think - all states from the last REORG_SAFE_PERIOD blocks + minimally one state - should be enough if I am not forgetting something.
But I think we can also generally be a bit more generous here - e.g. take a somewhat longer REORG_SAFE_PERIOD of 100 or something or keep more signer states, nothing lost here, this doesn't take that much space and eventually the date might be interesting for analytical purposes.
packages/blockchain/src/index.ts
Outdated
const length = this._cliqueLatestSignerStates.length | ||
const limit = this.CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT | ||
if (length > limit) { | ||
this._cliqueLatestSignerStates = this._cliqueLatestSignerStates.slice(length - limit, length) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My idea for CLIQUE_SIGNER_HISTORY_BLOCK_LIMIT
was to have a defined period of blocks (therefore the name) and delete the old signer states once these number of blocks have passed. Not sure, this version of just keeping some number of signer states should work as well - I guess - but then I would rename the constant.
Also maybe 100 might be a bit too much then? But not sure, maybe that's ok, shouldn't be too much extra data.
@@ -44,5 +44,5 @@ export default function blockFromRpc(blockParams: any, uncles: any[] = [], optio | |||
|
|||
const uncleHeaders = uncles.map((uh) => blockHeaderFromRpc(uh, options)) | |||
|
|||
return Block.fromBlockData({ header, transactions, uncleHeaders }) | |||
return Block.fromBlockData({ header, transactions, uncleHeaders }, options) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
@@ -130,4 +130,5 @@ export interface JsonHeader { | |||
|
|||
export interface Blockchain { | |||
getBlock(hash: Buffer): Promise<Block> | |||
cliqueActiveSigners(): Address[] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, this is - as far as I oversee - the only breaking change up till now in the PR. Maybe we don't want to do this for now and keep the option of doing a non-breaking release? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Update: ah, just seeing that this is in Block
. A bit confused, are people likely using this or is this just for us for testing purposes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah I needed this for the validateCliqueDifficulty
method which needs to calculate if the signer is in or out of turn. I thought it would be good in the block header next to the pow difficulty check method, but you are right that it adds to the blockchain interface here. I can move it over to blockchain and leave the interface here as before if you think that would be better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just rechecked, so if this is used for typing in Block
functions like async validate(blockchain: Blockchain): Promise<void>
then this change is definitely breaking on backwards-compatibility, so we should find another solution here.
Maybe alternatively just leave the interface as-is and manually check in validateCliqueDifficulty()
for blockchain.cliqueActiveSigners()
to be present and if not throw? And additionally add some notes on the blockchain
consuming methods in Block
that these methods needs a clique supporting Blockchain
object if run with a PoA setting? 🤔
Not sure if this is the best/optimal solution though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that sounds like a good solution for now, I like keeping the validation in the block header since it's a logical place to validate the difficulty (although it requires the context of blockchain).
i can be convinced to move it but let's see how the solution looks this way. (added in commit: d58a901)
clique: { | ||
period: number | ||
epoch: number | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was too strict and breaks typing in non-PoA contexts, have fixed this in 6fe4a1d.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok great, I was wondering, it might be helpful if all the options were optional, at least for my use in forCustomChain
, I only wanted to set epoch
to 3
, but I had to provide all the other values:
ethereumjs-monorepo/packages/blockchain/test/clique.spec.ts
Lines 535 to 548 in 6721321
const common = Common.forCustomChain( | |
'rinkeby', | |
{ | |
consensus: { | |
type: 'poa', | |
algorithm: 'clique', | |
clique: { | |
period: 15, | |
epoch: 3, | |
}, | |
}, | |
}, | |
'chainstart' | |
) |
I understand this is a limitation of how the code is set up in forCustomChain
though, since it's using the spread operator, it does not allow for deep nesting replacement of single values, it would replace the whole block provided. Not sure if there is a better technique we could use.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't be a fan if we loosen this here in general since the type safety of these chain and HF definition JSON files is so underdeveloped already and should rather be improved towards the other side at some PiT. I get the point though. Allow any
here for the function signature as the most simple/naive solution? 😜 Not sure, if there is something better, this should of course be preferred.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, agree, in general would love some more type safety in common coming from the json files.
what I meant though is I would love to do this:
const common = Common.forCustomChain(
'rinkeby',
{ consensus: { clique: { epoch: 3 } } },
'chainstart'
)
however the current way the code is set up, then { consensus: type, algorithm, clique: { period } }
would all be undefined. we would need some more sophisticated logic for deeply replacing nested values, rather than the simple solution of using the spread operator (here).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah ok, thanks for the clarification.
just thinking about this, how about a new BlockOption:
then I can create a helper method that would easily concat it with the passed-in (or blank) extraData. (edit: actually looks like we only need the privateKey, not the address) |
I think that's an option, at some point I also thought about adding an option to the constructor, do you think this is ok from a security PoV to request the private key from the signer? This was my main reason to hesitate, but rather based on some blurry feeling than some more solid analysis here. P.S.: not sure if this was clever - at least it gives a neutral perspective - just to let you know that I answered these questions here before looking at your follow-up clique PR #1074. |
This PR aims to add clique support as defined in EIP-225 throughout the library stack, so likely touching
Common
,Block
,Blockchain
, eventually alsoClient
.Some references:
Still WIP.
Note: mention
Block
bug fix from def4177 in release notes