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

Implement Doppelganger Check #9120

Merged
merged 28 commits into from
Jul 2, 2021
Merged

Implement Doppelganger Check #9120

merged 28 commits into from
Jul 2, 2021

Conversation

nisdas
Copy link
Member

@nisdas nisdas commented Jun 29, 2021

What type of PR is this?

Feature Implementation

What does this PR do? Why is it needed?

  • Checks for validator balances in the last N epochs, and determines if there is a positive change.
  • Add tests for the feature.
  • Gate it behind a flag.

Which issues(s) does this PR fix?

Fixes #7985

Other notes for review

@terencechain terencechain changed the title Implement DoppleGanger Check Implement Doppelganger Check Jun 29, 2021
Copy link
Contributor

@potuz potuz left a 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.

beacon-chain/rpc/validator/status.go Outdated Show resolved Hide resolved
beacon-chain/rpc/validator/status.go Outdated Show resolved Hide resolved
nisdas and others added 3 commits June 30, 2021 14:14
@codecov
Copy link

codecov bot commented Jun 30, 2021

Codecov Report

Merging #9120 (4b74efa) into develop (2df024a) will decrease coverage by 0.25%.
The diff coverage is 54.54%.

@@             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     

@nisdas nisdas marked this pull request as ready for review June 30, 2021 11:02
@nisdas nisdas requested a review from a team as a code owner June 30, 2021 11:02
@nisdas nisdas requested review from kasey, jmozah and rauljordan and removed request for a team June 30, 2021 11:02
}
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")
Copy link
Contributor

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
Copy link
Contributor

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?

Copy link
Member Author

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.

@yorickdowne
Copy link
Contributor

config.go and flags.go misspell "doppelganger" as "doppleganger". Correct spelling is "doppelganger" (well, anglicized, never mind the lack of umlauts :)).

Copy link
Member

@prestonvanloon prestonvanloon left a 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."

bytes signed_root = 2 [(ethereum.eth.ext.ssz_size) = "32"];
}

repeated ValidatorRequest validator_requests = 1;
Copy link
Member

Choose a reason for hiding this comment

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

Formatting

Comment on lines 128 to 138
// 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,
&ethpb.DoppelGangerResponse_ValidatorResponse{
PublicKey: v.PublicKey,
DuplicateExists: false,
})
continue
}
Copy link
Member

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.

beacon-chain/rpc/validator/status.go Show resolved Hide resolved
beacon-chain/rpc/validator/status.go Show resolved Hide resolved
beacon-chain/rpc/validator/status.go Show resolved Hide resolved
@@ -122,6 +122,10 @@ var (
Name: "enable-optimized-balance-update",
Usage: "Enables the optimized method of updating validator balances.",
}
enableDoppleGangerProtection = &cli.BoolFlag{
Copy link
Member

Choose a reason for hiding this comment

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

Typo with Dopple

validator/client/validator.go Outdated Show resolved Hide resolved
validator/client/validator.go Outdated Show resolved Hide resolved
validator/client/validator.go Outdated Show resolved Hide resolved
validator/client/validator.go Outdated Show resolved Hide resolved
@potuz
Copy link
Contributor

potuz commented Jul 1, 2021

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.

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 base_reward but the gain is minimal (if any). Notice that attesting and getting a negative reward for 2 epochs can only happen if the validator was assigned two epochs in a row to a slot 0 and missed target and head in both votes in a row. This is incredibly rare.

  • A quick restart, the check is ignored.

Correct again

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."

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.

Comment on lines +128 to +134
// 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.
Copy link
Member

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! 👍

Comment on lines +127 to +129
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.",
Copy link
Member

Choose a reason for hiding this comment

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

Looks good!

}
client.EXPECT().CheckDoppelGanger(
gomock.Any(), // ctx
gomock.Any(), // request
Copy link
Member

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

Copy link
Member Author

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.

Copy link
Member

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.
@prestonvanloon
Copy link
Member

Blocked until #9135 merges

@@ -132,7 +133,7 @@ func TestAttestToBlockHead_AttestsCorrectly(t *testing.T) {
m.validatorClient.EXPECT().ProposeAttestation(
gomock.Any(), // ctx
gomock.AssignableToTypeOf(&ethpb.Attestation{}),
).Do(func(_ context.Context, att *ethpb.Attestation) {
).Do(func(_ context.Context, att *ethpb.Attestation, opts ...grpc.CallOption) {
Copy link
Member

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

Comment on lines +405 to +408
r := retrieveLatestRecord(attRec)
if pkey != r.PubKey {
return errors.New("attestation record mismatched public key")
}
Copy link
Member

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.

prestonvanloon
prestonvanloon previously approved these changes Jul 1, 2021
@nisdas nisdas merged commit 5d65ace into develop Jul 2, 2021
@delete-merged-branch delete-merged-branch bot deleted the implementDoppleganger branch July 2, 2021 04:11
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.

Check on-chain for attestations at validator launch
6 participants