Block proposal calculation counts slashed indices rather than slashing entries

This commit is contained in:
Jim McDonald 2020-09-29 11:52:00 +01:00
parent eded06afeb
commit 6d3657da5d
No known key found for this signature in database
GPG Key ID: 89CEB61B2AD2A5E7
6 changed files with 372 additions and 43 deletions

4
CHANGELOG.md Normal file
View File

@ -0,0 +1,4 @@
Development:
- block proposal calculation counts slashed indices rather than slashing entries
0.6.0:
- initial release

View File

@ -93,10 +93,16 @@ func parseAndCheckParameters(params ...Parameter) (*parameters, error) {
}
}
if parameters.timeout == 0 {
return nil, errors.New("no timeout specified")
}
if parameters.clientMonitor == nil {
return nil, errors.New("no client monitor specified")
}
if parameters.processConcurrency == 0 {
return nil, errors.New("no process concurrency specified")
}
if parameters.beaconBlockProposalProviders == nil {
if len(parameters.beaconBlockProposalProviders) == 0 {
return nil, errors.New("no beacon block proposal providers specified")
}

View File

@ -0,0 +1,98 @@
// Copyright © 2020 Attestant Limited.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package best
import (
"context"
"sort"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
)
// scoreBeaconBlockPropsal generates a score for a beacon block.
// The score is relative to the reward expected by proposing the block.
func scoreBeaconBlockProposal(ctx context.Context, name string, blockProposal *spec.BeaconBlock) float64 {
if blockProposal == nil {
return 0
}
if blockProposal.Body == nil {
return 0
}
immediateAttestationScore := float64(0)
attestationScore := float64(0)
// Add attestation scores.
for _, attestation := range blockProposal.Body.Attestations {
inclusionDistance := float64(blockProposal.Slot - attestation.Data.Slot)
attestationScore += float64(attestation.AggregationBits.Count()) * (float64(0.75) + float64(0.25)/float64(inclusionDistance))
if inclusionDistance == 1 {
immediateAttestationScore += float64(attestation.AggregationBits.Count()) * (float64(0.75) + float64(0.25)/float64(inclusionDistance))
}
}
// Add slashing scores.
// Slashing reward will be at most MAX_EFFECTIVE_BALANCE/WHISTLEBLOWER_REWARD_QUOTIENT,
// which is 0.0625 Ether.
// Individual attestation reward at 16K validators will be around 90,000 GWei, or .00009 Ether.
// So we state that a single slashing event has the same weight as about 700 attestations.
slashingWeight := float64(700)
// Add proposer slashing scores.
proposerSlashingScore := float64(len(blockProposal.Body.ProposerSlashings)) * slashingWeight
// Add attester slashing scores.
indicesSlashed := 0
for i := range blockProposal.Body.AttesterSlashings {
slashing := blockProposal.Body.AttesterSlashings[i]
indicesSlashed += len(intersection(slashing.Attestation1.AttestingIndices, slashing.Attestation2.AttestingIndices))
}
attesterSlashingScore := slashingWeight * float64(indicesSlashed)
log.Trace().
Uint64("slot", blockProposal.Slot).
Str("provider", name).
Float64("immediate_attestations", immediateAttestationScore).
Float64("attestations", attestationScore).
Float64("proposer_slashings", proposerSlashingScore).
Float64("attester_slashings", attesterSlashingScore).
Float64("total", attestationScore+proposerSlashingScore+attesterSlashingScore).
Msg("Scored block")
return attestationScore + proposerSlashingScore + attesterSlashingScore
}
// intersection returns a list of items common between the two sets.
func intersection(set1 []uint64, set2 []uint64) []uint64 {
sort.Slice(set1, func(i, j int) bool { return set1[i] < set1[j] })
sort.Slice(set2, func(i, j int) bool { return set2[i] < set2[j] })
res := make([]uint64, 0)
set1Pos := 0
set2Pos := 0
for set1Pos < len(set1) && set2Pos < len(set2) {
switch {
case set1[set1Pos] < set2[set2Pos]:
set1Pos++
case set2[set2Pos] < set1[set1Pos]:
set2Pos++
default:
res = append(res, set1[set1Pos])
set1Pos++
set2Pos++
}
}
return res
}

View File

@ -0,0 +1,141 @@
// Copyright © 2020 Attestant Limited.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package best
import (
"context"
"testing"
spec "github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/prysmaticlabs/go-bitfield"
"github.com/stretchr/testify/assert"
)
func aggregationBits(set uint64, total uint64) bitfield.Bitlist {
bits := bitfield.NewBitlist(total)
for i := uint64(0); i < set; i++ {
bits.SetBitAt(i, true)
}
return bits
}
func TestScore(t *testing.T) {
tests := []struct {
name string
block *spec.BeaconBlock
score float64
err string
}{
{
name: "Nil",
score: 0,
},
{
name: "Empty",
block: &spec.BeaconBlock{},
score: 0,
},
{
name: "SingleAttestation",
block: &spec.BeaconBlock{
Slot: 12345,
Body: &spec.BeaconBlockBody{
Attestations: []*spec.Attestation{
{
AggregationBits: aggregationBits(1, 128),
Data: &spec.AttestationData{
Slot: 12344,
},
},
},
},
},
score: 1,
},
{
name: "SingleAttestationDistance2",
block: &spec.BeaconBlock{
Slot: 12345,
Body: &spec.BeaconBlockBody{
Attestations: []*spec.Attestation{
{
AggregationBits: aggregationBits(1, 128),
Data: &spec.AttestationData{
Slot: 12343,
},
},
},
},
},
score: 0.875,
},
{
name: "TwoAttestations",
block: &spec.BeaconBlock{
Slot: 12345,
Body: &spec.BeaconBlockBody{
Attestations: []*spec.Attestation{
{
AggregationBits: aggregationBits(2, 128),
Data: &spec.AttestationData{
Slot: 12344,
},
},
{
AggregationBits: aggregationBits(1, 128),
Data: &spec.AttestationData{
Slot: 12341,
},
},
},
},
},
score: 2.8125,
},
{
name: "AttesterSlashing",
block: &spec.BeaconBlock{
Slot: 12345,
Body: &spec.BeaconBlockBody{
Attestations: []*spec.Attestation{
{
AggregationBits: aggregationBits(50, 128),
Data: &spec.AttestationData{
Slot: 12344,
},
},
},
AttesterSlashings: []*spec.AttesterSlashing{
{
Attestation1: &spec.IndexedAttestation{
AttestingIndices: []uint64{1, 2, 3},
},
Attestation2: &spec.IndexedAttestation{
AttestingIndices: []uint64{2, 3, 4},
},
},
},
},
},
score: 1450,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
score := scoreBeaconBlockProposal(context.Background(), test.name, test.block)
assert.Equal(t, test.score, score)
})
}
}

View File

@ -93,7 +93,7 @@ func (s *Service) BeaconBlockProposal(ctx context.Context, slot uint64, randaoRe
cancel()
mu.Lock()
score := scoreBeaconBlockProposal(ctx, name, slot, proposal)
score := scoreBeaconBlockProposal(ctx, name, proposal)
if score > bestScore || bestProposal == nil {
bestScore = score
bestProposal = proposal
@ -105,44 +105,3 @@ func (s *Service) BeaconBlockProposal(ctx context.Context, slot uint64, randaoRe
return bestProposal, nil
}
// scoreBeaconBlockPropsal generates a score for a beacon block.
// The score is relative to the reward expected by proposing the block.
func scoreBeaconBlockProposal(ctx context.Context, name string, slot uint64, blockProposal *spec.BeaconBlock) float64 {
immediateAttestationScore := float64(0)
attestationScore := float64(0)
// Add attestation scores.
for _, attestation := range blockProposal.Body.Attestations {
inclusionDistance := float64(slot - attestation.Data.Slot)
attestationScore += float64(attestation.AggregationBits.Count()) / inclusionDistance
if inclusionDistance == 1 {
immediateAttestationScore += float64(attestation.AggregationBits.Count()) / inclusionDistance
}
}
// Add slashing scores.
// Slashing reward will be at most MAX_EFFECTIVE_BALANCE/WHISTLEBLOWER_REWARD_QUOTIENT,
// which is 0.0625 Ether.
// Individual attestation reward at 16K validators will be around 90,000 GWei, or .00009 Ether.
// So we state that a single slashing event has the same weight as about 700 attestations.
slashingWeight := float64(700)
// Add proposer slashing scores.
proposerSlashingScore := float64(len(blockProposal.Body.ProposerSlashings)) * slashingWeight
// Add attester slashing scores.
attesterSlashingScore := float64(len(blockProposal.Body.AttesterSlashings)) * slashingWeight
log.Trace().
Uint64("slot", slot).
Str("provider", name).
Float64("immediate_attestations", immediateAttestationScore).
Float64("attestations", attestationScore).
Float64("proposer_slashings", proposerSlashingScore).
Float64("attester_slashings", attesterSlashingScore).
Float64("total", attestationScore+proposerSlashingScore+attesterSlashingScore).
Msg("Scored block")
return attestationScore + proposerSlashingScore + attestationScore
}

View File

@ -0,0 +1,121 @@
// Copyright © 2020 Attestant Limited.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package best_test
import (
"context"
"testing"
eth2client "github.com/attestantio/go-eth2-client"
"github.com/attestantio/vouch/mock"
"github.com/attestantio/vouch/services/metrics/null"
"github.com/attestantio/vouch/strategies/beaconblockproposal/best"
"github.com/rs/zerolog"
"github.com/stretchr/testify/require"
)
func TestService(t *testing.T) {
tests := []struct {
name string
params []best.Parameter
err string
}{
{
name: "ClientMonitorMissing",
params: []best.Parameter{
best.WithLogLevel(zerolog.Disabled),
best.WithClientMonitor(nil),
best.WithProcessConcurrency(1),
best.WithBeaconBlockProposalProviders(map[string]eth2client.BeaconBlockProposalProvider{
"one": mock.NewBeaconBlockProposalProvider(),
"two": mock.NewBeaconBlockProposalProvider(),
"three": mock.NewBeaconBlockProposalProvider(),
}),
},
err: "problem with parameters: no client monitor specified",
},
{
name: "TimeoutMissing",
params: []best.Parameter{
best.WithLogLevel(zerolog.Disabled),
best.WithClientMonitor(null.New(context.Background())),
best.WithTimeout(0),
best.WithProcessConcurrency(1),
best.WithBeaconBlockProposalProviders(map[string]eth2client.BeaconBlockProposalProvider{
"one": mock.NewBeaconBlockProposalProvider(),
"two": mock.NewBeaconBlockProposalProvider(),
"three": mock.NewBeaconBlockProposalProvider(),
}),
},
err: "problem with parameters: no timeout specified",
},
{
name: "ProcessConcurrencyBad",
params: []best.Parameter{
best.WithLogLevel(zerolog.Disabled),
best.WithClientMonitor(null.New(context.Background())),
best.WithProcessConcurrency(0),
best.WithBeaconBlockProposalProviders(map[string]eth2client.BeaconBlockProposalProvider{
"one": mock.NewBeaconBlockProposalProvider(),
"two": mock.NewBeaconBlockProposalProvider(),
"three": mock.NewBeaconBlockProposalProvider(),
}),
},
err: "problem with parameters: no process concurrency specified",
},
{
name: "ProvidersMissing",
params: []best.Parameter{
best.WithLogLevel(zerolog.Disabled),
best.WithClientMonitor(null.New(context.Background())),
best.WithProcessConcurrency(1),
},
err: "problem with parameters: no beacon block proposal providers specified",
},
{
name: "ProvidersEmpty",
params: []best.Parameter{
best.WithLogLevel(zerolog.Disabled),
best.WithClientMonitor(null.New(context.Background())),
best.WithProcessConcurrency(1),
best.WithBeaconBlockProposalProviders(map[string]eth2client.BeaconBlockProposalProvider{}),
},
err: "problem with parameters: no beacon block proposal providers specified",
},
{
name: "Good",
params: []best.Parameter{
best.WithLogLevel(zerolog.Disabled),
best.WithClientMonitor(null.New(context.Background())),
best.WithProcessConcurrency(1),
best.WithBeaconBlockProposalProviders(map[string]eth2client.BeaconBlockProposalProvider{
"one": mock.NewBeaconBlockProposalProvider(),
"two": mock.NewBeaconBlockProposalProvider(),
"three": mock.NewBeaconBlockProposalProvider(),
}),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := best.New(context.Background(), test.params...)
if test.err != "" {
require.EqualError(t, err, test.err)
} else {
require.NoError(t, err)
}
})
}
}