From ae3c4f2d34ff89ed092a5671b9690107d9562637 Mon Sep 17 00:00:00 2001 From: Potuz Date: Sun, 1 May 2022 21:15:56 -0300 Subject: [PATCH 1/2] remove equivocating votes from forkchoice --- .../doubly-linked-tree/forkchoice.go | 37 ++++++++++++++ .../doubly-linked-tree/forkchoice_test.go | 33 +++++++++++++ .../forkchoice/doubly-linked-tree/types.go | 1 + beacon-chain/forkchoice/interfaces.go | 1 + beacon-chain/forkchoice/protoarray/helpers.go | 6 +++ .../forkchoice/protoarray/helpers_test.go | 25 ++++++---- beacon-chain/forkchoice/protoarray/store.go | 48 ++++++++++++++++++- .../forkchoice/protoarray/store_test.go | 29 +++++++++++ beacon-chain/forkchoice/protoarray/types.go | 1 + 9 files changed, 172 insertions(+), 9 deletions(-) diff --git a/beacon-chain/forkchoice/doubly-linked-tree/forkchoice.go b/beacon-chain/forkchoice/doubly-linked-tree/forkchoice.go index 6c5789b654d2..c1e9046417df 100644 --- a/beacon-chain/forkchoice/doubly-linked-tree/forkchoice.go +++ b/beacon-chain/forkchoice/doubly-linked-tree/forkchoice.go @@ -22,6 +22,7 @@ func New(justifiedEpoch, finalizedEpoch types.Epoch) *ForkChoice { proposerBoostRoot: [32]byte{}, nodeByRoot: make(map[[fieldparams.RootLength]byte]*Node), nodeByPayload: make(map[[fieldparams.RootLength]byte]*Node), + slashedIndices: make(map[types.ValidatorIndex]bool), pruneThreshold: defaultPruneThreshold, } @@ -216,6 +217,10 @@ func (f *ForkChoice) AncestorRoot(ctx context.Context, root [32]byte, slot types // validators' latest votes. func (f *ForkChoice) updateBalances(newBalances []uint64) error { for index, vote := range f.votes { + // Skip if validator has been slashed + if f.store.slashedIndices[types.ValidatorIndex(index)] { + continue + } // Skip if validator has never voted for current root and next root (i.e. if the // votes are zero hash aka genesis block), there's nothing to compute. if vote.currentRoot == params.BeaconConfig().ZeroHash && vote.nextRoot == params.BeaconConfig().ZeroHash { @@ -321,3 +326,35 @@ func (f *ForkChoice) ForkChoiceNodes() []*pbrpc.ForkChoiceNode { func (f *ForkChoice) SetOptimisticToInvalid(ctx context.Context, root, payloadHash [fieldparams.RootLength]byte) ([][32]byte, error) { return f.store.setOptimisticToInvalid(ctx, root, payloadHash) } + +// InsertSlashedIndex adds the given slashed validator index to the +// store-tracked list. Votes from these validators are not accounted for +// in forkchoice. +func (f *ForkChoice) InsertSlashedIndex(ctx context.Context, index types.ValidatorIndex) { + f.store.nodesLock.Lock() + defer f.store.nodesLock.Unlock() + f.store.slashedIndices[index] = true + + // Subtract last vote from this equivocating validator + f.votesLock.RLock() + defer f.votesLock.RUnlock() + + if index > types.ValidatorIndex(len(f.balances)) { + return + } + + if index > types.ValidatorIndex(len(f.votes)) { + return + } + + node, ok := f.store.nodeByRoot[f.votes[index].currentRoot] + if !ok || node == nil { + return + } + + if node.balance < f.balances[index] { + node.balance = 0 + } else { + node.balance -= f.balances[index] + } +} diff --git a/beacon-chain/forkchoice/doubly-linked-tree/forkchoice_test.go b/beacon-chain/forkchoice/doubly-linked-tree/forkchoice_test.go index dbda69bfe08e..086fb981c551 100644 --- a/beacon-chain/forkchoice/doubly-linked-tree/forkchoice_test.go +++ b/beacon-chain/forkchoice/doubly-linked-tree/forkchoice_test.go @@ -185,6 +185,39 @@ func TestForkChoice_AncestorLowerSlot(t *testing.T) { require.Equal(t, root, [32]byte{'1'}) } +func TestForkChoice_RemoveEquivocating(t *testing.T) { + ctx := context.Background() + f := setup(1, 1) + // Insert a block it will be head + require.NoError(t, f.InsertOptimisticBlock(ctx, 1, [32]byte{'a'}, params.BeaconConfig().ZeroHash, [32]byte{'A'}, 1, 1)) + head, err := f.Head(ctx, 1, params.BeaconConfig().ZeroHash, []uint64{}, 1) + require.NoError(t, err) + require.Equal(t, [32]byte{'a'}, head) + + // Insert two extra blocks + require.NoError(t, f.InsertOptimisticBlock(ctx, 2, [32]byte{'b'}, [32]byte{'a'}, [32]byte{'B'}, 1, 1)) + require.NoError(t, f.InsertOptimisticBlock(ctx, 3, [32]byte{'c'}, [32]byte{'a'}, [32]byte{'C'}, 1, 1)) + head, err = f.Head(ctx, 1, params.BeaconConfig().ZeroHash, []uint64{}, 1) + require.NoError(t, err) + require.Equal(t, [32]byte{'c'}, head) + + // Insert two attestations for block b, one for c it becomes head + f.ProcessAttestation(ctx, []uint64{1, 2}, [32]byte{'b'}, 1) + f.ProcessAttestation(ctx, []uint64{3}, [32]byte{'c'}, 1) + head, err = f.Head(ctx, 1, params.BeaconConfig().ZeroHash, []uint64{100, 200, 200, 300}, 1) + require.NoError(t, err) + require.Equal(t, [32]byte{'b'}, head) + + // Process b's slashing, c is now head + f.InsertSlashedIndex(ctx, 1) + require.Equal(t, uint64(200), f.store.nodeByRoot[[32]byte{'b'}].balance) + head, err = f.Head(ctx, 1, params.BeaconConfig().ZeroHash, []uint64{100, 200, 200, 300}, 1) + require.Equal(t, uint64(200), f.store.nodeByRoot[[32]byte{'b'}].weight) + require.Equal(t, uint64(300), f.store.nodeByRoot[[32]byte{'c'}].weight) + require.NoError(t, err) + require.Equal(t, [32]byte{'c'}, head) +} + func indexToHash(i uint64) [32]byte { var b [8]byte binary.LittleEndian.PutUint64(b[:], i) diff --git a/beacon-chain/forkchoice/doubly-linked-tree/types.go b/beacon-chain/forkchoice/doubly-linked-tree/types.go index af7db519d371..a6fa3d374367 100644 --- a/beacon-chain/forkchoice/doubly-linked-tree/types.go +++ b/beacon-chain/forkchoice/doubly-linked-tree/types.go @@ -27,6 +27,7 @@ type Store struct { headNode *Node // last head Node nodeByRoot map[[fieldparams.RootLength]byte]*Node // nodes indexed by roots. nodeByPayload map[[fieldparams.RootLength]byte]*Node // nodes indexed by payload Hash + slashedIndices map[types.ValidatorIndex]bool // the list of equivocating validator indices nodesLock sync.RWMutex proposerBoostLock sync.RWMutex } diff --git a/beacon-chain/forkchoice/interfaces.go b/beacon-chain/forkchoice/interfaces.go index 0a955d4eea42..27eab3fe6b78 100644 --- a/beacon-chain/forkchoice/interfaces.go +++ b/beacon-chain/forkchoice/interfaces.go @@ -42,6 +42,7 @@ type BlockProcessor interface { // AttestationProcessor processes the attestation that's used for accounting fork choice. type AttestationProcessor interface { ProcessAttestation(context.Context, []uint64, [32]byte, types.Epoch) + InsertSlashedIndex(context.Context, types.ValidatorIndex) } // Pruner prunes the fork choice upon new finalization. This is used to keep fork choice sane. diff --git a/beacon-chain/forkchoice/protoarray/helpers.go b/beacon-chain/forkchoice/protoarray/helpers.go index 09e5bbba3124..d55562afd89d 100644 --- a/beacon-chain/forkchoice/protoarray/helpers.go +++ b/beacon-chain/forkchoice/protoarray/helpers.go @@ -4,6 +4,7 @@ import ( "context" "github.com/prysmaticlabs/prysm/config/params" + types "github.com/prysmaticlabs/prysm/consensus-types/primitives" pmath "github.com/prysmaticlabs/prysm/math" "go.opencensus.io/trace" ) @@ -15,6 +16,7 @@ func computeDeltas( blockIndices map[[32]byte]uint64, votes []Vote, oldBalances, newBalances []uint64, + slashedIndices map[types.ValidatorIndex]bool, ) ([]int, []Vote, error) { _, span := trace.StartSpan(ctx, "doublyLinkedForkchoice.computeDeltas") defer span.End() @@ -22,6 +24,10 @@ func computeDeltas( deltas := make([]int, len(blockIndices)) for validatorIndex, vote := range votes { + // Skip if validator has been slashed + if slashedIndices[types.ValidatorIndex(validatorIndex)] { + continue + } oldBalance := uint64(0) newBalance := uint64(0) diff --git a/beacon-chain/forkchoice/protoarray/helpers_test.go b/beacon-chain/forkchoice/protoarray/helpers_test.go index fad41ecf97e7..81282904493c 100644 --- a/beacon-chain/forkchoice/protoarray/helpers_test.go +++ b/beacon-chain/forkchoice/protoarray/helpers_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/prysmaticlabs/prysm/config/params" + types "github.com/prysmaticlabs/prysm/consensus-types/primitives" "github.com/prysmaticlabs/prysm/crypto/hash" "github.com/prysmaticlabs/prysm/testing/assert" "github.com/prysmaticlabs/prysm/testing/require" @@ -25,7 +26,8 @@ func TestComputeDelta_ZeroHash(t *testing.T) { newBalances = append(newBalances, 0) } - delta, _, err := computeDeltas(context.Background(), indices, votes, oldBalances, newBalances) + slashedIndices := make(map[types.ValidatorIndex]bool) + delta, _, err := computeDeltas(context.Background(), indices, votes, oldBalances, newBalances, slashedIndices) require.NoError(t, err) assert.Equal(t, int(validatorCount), len(delta)) @@ -52,7 +54,8 @@ func TestComputeDelta_AllVoteTheSame(t *testing.T) { newBalances = append(newBalances, balance) } - delta, _, err := computeDeltas(context.Background(), indices, votes, oldBalances, newBalances) + slashedIndices := make(map[types.ValidatorIndex]bool) + delta, _, err := computeDeltas(context.Background(), indices, votes, oldBalances, newBalances, slashedIndices) require.NoError(t, err) assert.Equal(t, int(validatorCount), len(delta)) @@ -84,7 +87,8 @@ func TestComputeDelta_DifferentVotes(t *testing.T) { newBalances = append(newBalances, balance) } - delta, _, err := computeDeltas(context.Background(), indices, votes, oldBalances, newBalances) + slashedIndices := make(map[types.ValidatorIndex]bool) + delta, _, err := computeDeltas(context.Background(), indices, votes, oldBalances, newBalances, slashedIndices) require.NoError(t, err) assert.Equal(t, int(validatorCount), len(delta)) @@ -113,7 +117,8 @@ func TestComputeDelta_MovingVotes(t *testing.T) { newBalances = append(newBalances, balance) } - delta, _, err := computeDeltas(context.Background(), indices, votes, oldBalances, newBalances) + slashedIndices := make(map[types.ValidatorIndex]bool) + delta, _, err := computeDeltas(context.Background(), indices, votes, oldBalances, newBalances, slashedIndices) require.NoError(t, err) assert.Equal(t, int(validatorCount), len(delta)) @@ -145,7 +150,8 @@ func TestComputeDelta_MoveOutOfTree(t *testing.T) { Vote{indexToHash(1), params.BeaconConfig().ZeroHash, 0}, Vote{indexToHash(1), [32]byte{'A'}, 0}) - delta, _, err := computeDeltas(context.Background(), indices, votes, oldBalances, newBalances) + slashedIndices := make(map[types.ValidatorIndex]bool) + delta, _, err := computeDeltas(context.Background(), indices, votes, oldBalances, newBalances, slashedIndices) require.NoError(t, err) assert.Equal(t, 1, len(delta)) assert.Equal(t, 0-2*int(balance), delta[0]) @@ -173,7 +179,8 @@ func TestComputeDelta_ChangingBalances(t *testing.T) { newBalances = append(newBalances, newBalance) } - delta, _, err := computeDeltas(context.Background(), indices, votes, oldBalances, newBalances) + slashedIndices := make(map[types.ValidatorIndex]bool) + delta, _, err := computeDeltas(context.Background(), indices, votes, oldBalances, newBalances, slashedIndices) require.NoError(t, err) assert.Equal(t, 16, len(delta)) @@ -206,7 +213,8 @@ func TestComputeDelta_ValidatorAppear(t *testing.T) { Vote{indexToHash(1), indexToHash(2), 0}, Vote{indexToHash(1), indexToHash(2), 0}) - delta, _, err := computeDeltas(context.Background(), indices, votes, oldBalances, newBalances) + slashedIndices := make(map[types.ValidatorIndex]bool) + delta, _, err := computeDeltas(context.Background(), indices, votes, oldBalances, newBalances, slashedIndices) require.NoError(t, err) assert.Equal(t, 2, len(delta)) assert.Equal(t, 0-int(balance), delta[0]) @@ -231,7 +239,8 @@ func TestComputeDelta_ValidatorDisappears(t *testing.T) { Vote{indexToHash(1), indexToHash(2), 0}, Vote{indexToHash(1), indexToHash(2), 0}) - delta, _, err := computeDeltas(context.Background(), indices, votes, oldBalances, newBalances) + slashedIndices := make(map[types.ValidatorIndex]bool) + delta, _, err := computeDeltas(context.Background(), indices, votes, oldBalances, newBalances, slashedIndices) require.NoError(t, err) assert.Equal(t, 2, len(delta)) assert.Equal(t, 0-2*int(balance), delta[0]) diff --git a/beacon-chain/forkchoice/protoarray/store.go b/beacon-chain/forkchoice/protoarray/store.go index 56f308e89056..83922701d1bf 100644 --- a/beacon-chain/forkchoice/protoarray/store.go +++ b/beacon-chain/forkchoice/protoarray/store.go @@ -34,6 +34,7 @@ func New(justifiedEpoch, finalizedEpoch types.Epoch, finalizedRoot [32]byte) *Fo nodesIndices: make(map[[32]byte]uint64), payloadIndices: make(map[[32]byte]uint64), canonicalNodes: make(map[[32]byte]bool), + slashedIndices: make(map[types.ValidatorIndex]bool), pruneThreshold: defaultPruneThreshold, } @@ -63,7 +64,7 @@ func (f *ForkChoice) Head( // Using the write lock here because `updateCanonicalNodes` that gets called subsequently requires a write operation. f.store.nodesLock.Lock() defer f.store.nodesLock.Unlock() - deltas, newVotes, err := computeDeltas(ctx, f.store.nodesIndices, f.votes, f.balances, newBalances) + deltas, newVotes, err := computeDeltas(ctx, f.store.nodesIndices, f.votes, f.balances, newBalances, f.store.slashedIndices) if err != nil { return [32]byte{}, errors.Wrap(err, "Could not compute deltas") } @@ -741,3 +742,48 @@ func (f *ForkChoice) ForkChoiceNodes() []*pbrpc.ForkChoiceNode { } return ret } + +// InsertSlashedIndex adds the given slashed validator index to the +// store-tracked list. Votes from these validators are not accounted for +// in forkchoice. +func (f *ForkChoice) InsertSlashedIndex(ctx context.Context, index types.ValidatorIndex) { + f.store.nodesLock.Lock() + defer f.store.nodesLock.Unlock() + f.store.slashedIndices[index] = true + + // Subtract last vote from this equivocating validator + f.votesLock.RLock() + defer f.votesLock.RUnlock() + + if index > types.ValidatorIndex(len(f.balances)) { + return + } + + if index > types.ValidatorIndex(len(f.votes)) { + return + } + + nodeIndex, ok := f.store.nodesIndices[f.votes[index].currentRoot] + if !ok { + return + } + + var node *Node + for nodeIndex != NonExistentNode { + if ctx.Err() != nil { + return + } + + node = f.store.nodes[nodeIndex] + if node == nil { + return + } + + if node.weight < f.balances[index] { + node.weight = 0 + } else { + node.weight -= f.balances[index] + } + nodeIndex = node.parent + } +} diff --git a/beacon-chain/forkchoice/protoarray/store_test.go b/beacon-chain/forkchoice/protoarray/store_test.go index 69527646d217..5266102e55d4 100644 --- a/beacon-chain/forkchoice/protoarray/store_test.go +++ b/beacon-chain/forkchoice/protoarray/store_test.go @@ -773,3 +773,32 @@ func TestStore_UpdateCanonicalNodes_RemoveOldCanonical(t *testing.T) { _, ok := f.store.canonicalNodes[[32]byte{'c'}] require.Equal(t, false, ok) } + +func TestStore_RemoveEquivocating(t *testing.T) { + ctx := context.Background() + f := setup(1, 1) + // Insert a block it will be head + require.NoError(t, f.InsertOptimisticBlock(ctx, 1, [32]byte{'a'}, params.BeaconConfig().ZeroHash, [32]byte{'A'}, 1, 1)) + head, err := f.Head(ctx, 1, params.BeaconConfig().ZeroHash, []uint64{}, 1) + require.NoError(t, err) + require.Equal(t, [32]byte{'a'}, head) + + // Insert two extra blocks + require.NoError(t, f.InsertOptimisticBlock(ctx, 2, [32]byte{'b'}, [32]byte{'a'}, [32]byte{'B'}, 1, 1)) + require.NoError(t, f.InsertOptimisticBlock(ctx, 3, [32]byte{'c'}, [32]byte{'a'}, [32]byte{'C'}, 1, 1)) + head, err = f.Head(ctx, 1, params.BeaconConfig().ZeroHash, []uint64{}, 1) + require.NoError(t, err) + require.Equal(t, [32]byte{'c'}, head) + + // Insert an attestation for block b, it becomes head + f.ProcessAttestation(ctx, []uint64{1}, [32]byte{'b'}, 1) + head, err = f.Head(ctx, 1, params.BeaconConfig().ZeroHash, []uint64{100, 200}, 1) + require.NoError(t, err) + require.Equal(t, [32]byte{'b'}, head) + + // Process b's slashing, c is now head + f.InsertSlashedIndex(ctx, 1) + head, err = f.Head(ctx, 1, params.BeaconConfig().ZeroHash, []uint64{100, 200}, 1) + require.NoError(t, err) + require.Equal(t, [32]byte{'c'}, head) +} diff --git a/beacon-chain/forkchoice/protoarray/types.go b/beacon-chain/forkchoice/protoarray/types.go index 6b2b667eeb14..4c7ee83a79b9 100644 --- a/beacon-chain/forkchoice/protoarray/types.go +++ b/beacon-chain/forkchoice/protoarray/types.go @@ -28,6 +28,7 @@ type Store struct { nodesIndices map[[fieldparams.RootLength]byte]uint64 // the root of block node and the nodes index in the list. canonicalNodes map[[fieldparams.RootLength]byte]bool // the canonical block nodes. payloadIndices map[[fieldparams.RootLength]byte]uint64 // the payload hash of block node and the index in the list + slashedIndices map[types.ValidatorIndex]bool // The list of equivocating validators nodesLock sync.RWMutex proposerBoostLock sync.RWMutex } From 01355e23d2436d3c1ba31bcd8d377041c8fc01d6 Mon Sep 17 00:00:00 2001 From: Potuz Date: Mon, 2 May 2022 07:27:41 -0300 Subject: [PATCH 2/2] shutup deepsource --- beacon-chain/forkchoice/doubly-linked-tree/forkchoice.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beacon-chain/forkchoice/doubly-linked-tree/forkchoice.go b/beacon-chain/forkchoice/doubly-linked-tree/forkchoice.go index c1e9046417df..4febcde7507d 100644 --- a/beacon-chain/forkchoice/doubly-linked-tree/forkchoice.go +++ b/beacon-chain/forkchoice/doubly-linked-tree/forkchoice.go @@ -330,7 +330,7 @@ func (f *ForkChoice) SetOptimisticToInvalid(ctx context.Context, root, payloadHa // InsertSlashedIndex adds the given slashed validator index to the // store-tracked list. Votes from these validators are not accounted for // in forkchoice. -func (f *ForkChoice) InsertSlashedIndex(ctx context.Context, index types.ValidatorIndex) { +func (f *ForkChoice) InsertSlashedIndex(_ context.Context, index types.ValidatorIndex) { f.store.nodesLock.Lock() defer f.store.nodesLock.Unlock() f.store.slashedIndices[index] = true