-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Implement Doppelganger Check #9120
Conversation
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.
Haven't checked the syntax of the PR, but the design of the protection looks good to me, simple to read and effective.
Co-authored-by: Potuz <potuz@potuz.net>
Co-authored-by: Potuz <potuz@potuz.net>
Codecov Report
@@ Coverage Diff @@
## develop #9120 +/- ##
===========================================
- Coverage 60.36% 60.10% -0.26%
===========================================
Files 550 550
Lines 39123 39142 +19
===========================================
- Hits 23616 23526 -90
- Misses 12141 12221 +80
- Partials 3366 3395 +29 |
beacon-chain/rpc/validator/status.go
Outdated
} | ||
olderState, err := vs.StateGen.StateBySlot(ctx, params.BeaconConfig().SlotsPerEpoch.Mul(uint64(olderEpoch))) | ||
if err != nil { | ||
return nil, status.Error(codes.Internal, "Could not get previous state") |
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.
"Could not get older state" would be appropriate.
@@ -62,7 +62,7 @@ type Server struct { | |||
Eth1BlockFetcher powchain.POWBlockFetcher | |||
PendingDepositsFetcher depositcache.PendingDepositsFetcher | |||
OperationNotifier opfeed.Notifier | |||
StateGen *stategen.State | |||
StateGen stategen.StateManager |
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.
For my benefit. why this change?
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.
Simply changes it from a struct to an interface, this allows us to mock it correctly for tests. *stategen.State
fulfills the state manager interface.
|
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.
A few comments. There are a few typos with Doppelganger vs Doppleganger. Correct is Doppelganger
.
I have some concerns about the implied safety of this feature. Please correct me if these assumptions are wrong.
- It seems that if a validator was online and attesting, but did not increase in balance, then this implementation could return a false negative.
- A quick restart, the check is ignored.
Why does a validator need to provide the last known attested epoch?
Is it so that restarts would be quick?
If this is the case and that's the trade off we want to make to prevent a 2 epoch mandatory delay on every restart, then we ought to log to the user "WARNING: Validator was running within the last 2 epochs and doppelganger detection will be skipped."
proto/eth/v1alpha1/validator.proto
Outdated
bytes signed_root = 2 [(ethereum.eth.ext.ssz_size) = "32"]; | ||
} | ||
|
||
repeated ValidatorRequest validator_requests = 1; |
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.
Formatting
beacon-chain/rpc/validator/status.go
Outdated
// If the validator's last recorded epoch was | ||
// less than 2 epoch ago, this method will not | ||
// be able to catch duplicates. | ||
if v.Epoch+2 >= currEpoch { | ||
resp.Responses = append(resp.Responses, | ||
ðpb.DoppelGangerResponse_ValidatorResponse{ | ||
PublicKey: v.PublicKey, | ||
DuplicateExists: false, | ||
}) | ||
continue | ||
} |
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.
Why is this the case? It isn't clear to the reader why a request within the last 2 epochs will be marked as false or safe to proceed without actually checking.
shared/featureconfig/flags.go
Outdated
@@ -122,6 +122,10 @@ var ( | |||
Name: "enable-optimized-balance-update", | |||
Usage: "Enables the optimized method of updating validator balances.", | |||
} | |||
enableDoppleGangerProtection = &cli.BoolFlag{ |
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.
Typo with Dopple
This is correct. The odds of this happening are less than 10^{-5} already conditional to being running a doppelganger as I showed in the issue originally. We can do better than this by checking that the balance increased more than certain negative multiple of the
Correct again
I agree with the warning. But let me stress something that passes without notice in all the approaches waiting, like the one implemented in Lighthouse: the point is that the 2 epochs mandatory delay is completely useless in the case that this method fails to find a doppelganger, namely. In the rare event that the user starts two validators simultaneously, then if the mandatory two epochs delay is implemented, then both validators will wait two epochs, not find anything and then start happily validating at the same time and get slashed. So yes, this method will not find a doppelganger if both instances are launched together, but neither will waiting 2 epochs. |
Co-authored-by: Preston Van Loon <preston@prysmaticlabs.com>
Co-authored-by: Preston Van Loon <preston@prysmaticlabs.com>
…abs/geth-sharding into implementDoppleganger
// If the validator's last recorded epoch was | ||
// less than or equal to 2 epochs ago, this method will not | ||
// be able to catch duplicates. This is due to how attestation | ||
// inclusion works, where an attestation for the current epoch | ||
// is able to included in the current or next epoch. Depending | ||
// on which epoch it is included the balance change will be | ||
// reflected in the following epoch. |
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 much more clear! 👍
Usage: "Enables the validator to perform a doppelganger check. (Warning): This is not " + | ||
"a foolproof method to find duplicate instances in the network. Your validator will still be" + | ||
" vulnerable if it is being run in unsafe configurations.", |
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.
Looks good!
validator/client/validator_test.go
Outdated
} | ||
client.EXPECT().CheckDoppelGanger( | ||
gomock.Any(), // ctx | ||
gomock.Any(), // request |
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.
Could you add the actual requests for all test cases? This will add a bit more rigidity to these tests
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 this is actually not trivial, I tried to do it previously, however the mock key manager actually uses a map underneath to store all the keys. When you request for all validating pubkeys, the ordering is non-deterministic, therefore any request mocked in here will be non deterministic too and basically only succeed 1/10 times. If needed I can create a new deterministic key manager to mock this.
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.
Added a custom matcher that will help with this.
…hen attestation history existed. Still some tests to fix due to another bug in attester protection AttestationHistoryForPubKey.
Blocked until #9135 merges |
@@ -132,7 +133,7 @@ func TestAttestToBlockHead_AttestsCorrectly(t *testing.T) { | |||
m.validatorClient.EXPECT().ProposeAttestation( | |||
gomock.Any(), // ctx | |||
gomock.AssignableToTypeOf(ðpb.Attestation{}), | |||
).Do(func(_ context.Context, att *ethpb.Attestation) { | |||
).Do(func(_ context.Context, att *ethpb.Attestation, opts ...grpc.CallOption) { |
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 needed to be updated after updating gomock
r := retrieveLatestRecord(attRec) | ||
if pkey != r.PubKey { | ||
return errors.New("attestation record mismatched public key") | ||
} |
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.
Added this as a sanity check. It may not be necessary but it was some assurance that we received the correct record.
What type of PR is this?
Feature Implementation
What does this PR do? Why is it needed?
Which issues(s) does this PR fix?
Fixes #7985
Other notes for review