mirror of https://github.com/certusone/vouch.git
Block proposal calculation counts slashed indices rather than slashing entries
This commit is contained in:
parent
eded06afeb
commit
6d3657da5d
|
@ -0,0 +1,4 @@
|
|||
Development:
|
||||
- block proposal calculation counts slashed indices rather than slashing entries
|
||||
0.6.0:
|
||||
- initial release
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue