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

testutil/beaconmock: fix fuzz issues #2105

Merged
merged 1 commit into from
Apr 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -834,6 +834,9 @@ func setFeeRecipient(eth2Cl eth2wrap.Client, pubkeys []eth2p0.BLSPubKey, feeReci

var activeVals []*eth2v1.Validator
for _, validator := range vals {
if validator == nil {
return errors.New("validator data cannot be nil")
}
if validator.Status != eth2v1.ValidatorStateActiveOngoing {
continue
}
Expand All @@ -846,6 +849,10 @@ func setFeeRecipient(eth2Cl eth2wrap.Client, pubkeys []eth2p0.BLSPubKey, feeReci

var preps []*eth2v1.ProposalPreparation
for _, val := range activeVals {
if val == nil || val.Validator == nil {
return errors.New("validator data cannot be nil")
}

feeRecipient := feeRecipientFunc(core.PubKeyFrom48Bytes(val.Validator.PublicKey))

var addr bellatrix.ExecutionAddress
Expand Down
25 changes: 25 additions & 0 deletions core/scheduler/scheduler.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,13 @@ func (s *Scheduler) resolveAttDuties(ctx context.Context, slot core.Slot, vals v
return err
}

// Check if any of the attester duties returned are nil..
for _, duty := range attDuties {
if duty == nil {
return errors.New("attester duty cannot be nil")
}
}

remaining := make(map[eth2p0.ValidatorIndex]bool)
for _, index := range vals.Indexes() {
remaining[index] = true
Expand Down Expand Up @@ -347,6 +354,13 @@ func (s *Scheduler) resolveProDuties(ctx context.Context, slot core.Slot, vals v
return err
}

// Check if any of the proposer duties returned are nil.
for _, duty := range proDuties {
if duty == nil {
return errors.New("proposer duty cannot be nil")
}
}

for _, proDuty := range proDuties {
if proDuty.Slot < eth2p0.Slot(slot.Slot) {
// Skip duties for earlier slots in initial epoch.
Expand Down Expand Up @@ -389,6 +403,13 @@ func (s *Scheduler) resolveSyncCommDuties(ctx context.Context, slot core.Slot, v
return err
}

// Check if any of the sync committee duties returned are nil.
for _, duty := range duties {
if duty == nil {
return errors.New("sync committee duty cannot be nil")
}
}

for _, syncCommDuty := range duties {
vIdx := syncCommDuty.ValidatorIndex
pubkey, ok := vals.PubKeyFromIndex(vIdx)
Expand Down Expand Up @@ -585,6 +606,10 @@ func resolveActiveValidators(ctx context.Context, eth2Cl eth2wrap.Client,

var resp []validator
for index, val := range vals {
if val == nil || val.Validator == nil {
return nil, errors.New("validator data cannot be nil")
}

pubkey, err := core.PubKeyFromBytes(val.Validator.PublicKey[:])
if err != nil {
return nil, err
Expand Down
4 changes: 4 additions & 0 deletions core/tracker/incldelay.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ func newInclDelayFunc(eth2Cl eth2wrap.Client, dutiesFunc dutiesFunc, callback fu

var delays []int64
for _, att := range atts {
if att == nil || att.Data == nil {
return errors.New("attestation fields cannot be nil")
}

attSlot := att.Data.Slot
if int64(attSlot) < startSlot {
continue
Expand Down
16 changes: 16 additions & 0 deletions core/validatorapi/validatorapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -923,6 +923,10 @@ func (c Component) ProposerDuties(ctx context.Context, epoch eth2p0.Epoch, valid

// Replace root public keys with public shares
for i := 0; i < len(duties); i++ {
if duties[i] == nil {
return nil, errors.New("proposer duty cannot be nil")
}

pubshare, ok := c.getPubShareFunc(duties[i].PubKey)
if !ok {
// Ignore unknown validators since ProposerDuties returns ALL proposers for the epoch if validatorIndices is empty.
Expand All @@ -942,6 +946,10 @@ func (c Component) AttesterDuties(ctx context.Context, epoch eth2p0.Epoch, valid

// Replace root public keys with public shares.
for i := 0; i < len(duties); i++ {
if duties[i] == nil {
return nil, errors.New("attester duty cannot be nil")
}

pubshare, ok := c.getPubShareFunc(duties[i].PubKey)
if !ok {
return nil, errors.New("pubshare not found")
Expand All @@ -960,6 +968,10 @@ func (c Component) SyncCommitteeDuties(ctx context.Context, epoch eth2p0.Epoch,

// Replace root public keys with public shares.
for i := 0; i < len(duties); i++ {
if duties[i] == nil {
return nil, errors.New("sync committee duty cannot be nil")
}

pubshare, ok := c.getPubShareFunc(duties[i].PubKey)
if !ok {
return nil, errors.New("pubshare not found")
Expand Down Expand Up @@ -1011,6 +1023,10 @@ func (Component) NodeVersion(context.Context) (string, error) {
func (c Component) convertValidators(vals map[eth2p0.ValidatorIndex]*eth2v1.Validator) (map[eth2p0.ValidatorIndex]*eth2v1.Validator, error) {
resp := make(map[eth2p0.ValidatorIndex]*eth2v1.Validator)
for vIdx, val := range vals {
if val == nil || val.Validator == nil {
return nil, errors.New("validator data cannot be nil")
}

var ok bool
val.Validator.PublicKey, ok = c.getPubShareFunc(val.Validator.PublicKey)
if !ok {
Expand Down
97 changes: 86 additions & 11 deletions testutil/beaconmock/beaconmock_fuzz.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package beaconmock

import (
"context"
"sync"

eth2api "github.com/attestantio/go-eth2-client/api"
eth2v1 "github.com/attestantio/go-eth2-client/api/v1"
Expand All @@ -15,15 +16,78 @@ import (

// WithBeaconMockFuzzer configures the beaconmock to return random responses for the all the functions consumed by charon.
func WithBeaconMockFuzzer() Option {
var (
valsMu sync.Mutex
validators map[eth2p0.ValidatorIndex]*eth2v1.Validator
)

setValidators := func(pubkeys []eth2p0.BLSPubKey) {
valsMu.Lock()
defer valsMu.Unlock()

if len(validators) != 0 {
return
}

validators = make(map[eth2p0.ValidatorIndex]*eth2v1.Validator)
for i, pubkey := range pubkeys {
vIdx := eth2p0.ValidatorIndex(i)

validators[vIdx] = &eth2v1.Validator{
Balance: eth2p0.Gwei(31300000000),
Index: vIdx,
Status: eth2v1.ValidatorStateActiveOngoing,
Validator: &eth2p0.Validator{
WithdrawalCredentials: []byte("12345678901234567890123456789012"),
EffectiveBalance: eth2p0.Gwei(31300000000),
PublicKey: pubkey,
ExitEpoch: 18446744073709551615,
WithdrawableEpoch: 18446744073709551615,
},
}
}
}

getValidators := func() map[eth2p0.ValidatorIndex]*eth2v1.Validator {
valsMu.Lock()
defer valsMu.Unlock()

return validators
}

return func(mock *Mock) {
mock.AttesterDutiesFunc = func(context.Context, eth2p0.Epoch, []eth2p0.ValidatorIndex) ([]*eth2v1.AttesterDuty, error) {
mock.AttesterDutiesFunc = func(_ context.Context, epoch eth2p0.Epoch, indices []eth2p0.ValidatorIndex) ([]*eth2v1.AttesterDuty, error) {
var duties []*eth2v1.AttesterDuty
fuzz.New().Fuzz(&duties)
f := fuzz.New().Funcs(
func(duties *[]*eth2v1.AttesterDuty, c fuzz.Continue) {
if c.RandBool() {
fuzz.New().Fuzz(duties)

return
}

// Return expected attester duties
vals := getValidators()
var resp []*eth2v1.AttesterDuty
for _, vIdx := range indices {
var duty eth2v1.AttesterDuty
c.Fuzz(&duty)

duty.PubKey = vals[vIdx].Validator.PublicKey
duty.ValidatorIndex = vIdx
duty.Slot = eth2p0.Slot(int(epoch*16) + c.Intn(16))
resp = append(resp, &duty)
}

*duties = resp
},
)
f.Fuzz(&duties)

return duties, nil
}

mock.ProposerDutiesFunc = func(context.Context, eth2p0.Epoch, []eth2p0.ValidatorIndex) ([]*eth2v1.ProposerDuty, error) {
mock.ProposerDutiesFunc = func(_ context.Context, epoch eth2p0.Epoch, indices []eth2p0.ValidatorIndex) ([]*eth2v1.ProposerDuty, error) {
var duties []*eth2v1.ProposerDuty
fuzz.New().Fuzz(&duties)

Expand Down Expand Up @@ -73,19 +137,30 @@ func WithBeaconMockFuzzer() Option {
}

mock.ValidatorsByPubKeyFunc = func(_ context.Context, _ string, pubkeys []eth2p0.BLSPubKey) (map[eth2p0.ValidatorIndex]*eth2v1.Validator, error) {
f := fuzz.New().Funcs(
func(vals *map[eth2p0.ValidatorIndex]*eth2v1.Validator, c fuzz.Continue) {
if c.RandBool() {
fuzz.New().Funcs(
func(state *eth2v1.ValidatorState, c fuzz.Continue) {
*state = eth2v1.ValidatorState(c.Intn(10))
},
).Fuzz(vals)

return
}

// Return validators with expected keys 50% of the time.
setValidators(pubkeys)
*vals = getValidators()
},
)

var vals map[eth2p0.ValidatorIndex]*eth2v1.Validator
fuzz.New().Fuzz(&vals)
f.Fuzz(&vals)

return vals, nil
}

mock.SlotsPerEpochFunc = func(context.Context) (uint64, error) {
var slots uint64
fuzz.New().Fuzz(&slots)

return slots, nil
}

mock.SyncCommitteeDutiesFunc = func(context.Context, eth2p0.Epoch, []eth2p0.ValidatorIndex) ([]*eth2v1.SyncCommitteeDuty, error) {
var duties []*eth2v1.SyncCommitteeDuty
fuzz.New().Fuzz(&duties)
Expand Down
60 changes: 60 additions & 0 deletions testutil/compose/fuzz/beacon_fuzz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright © 2022-2023 Obol Labs Inc. Licensed under the terms of a Business Source License 1.1

package fuzz_test

import (
"context"
"flag"
"fmt"
"os"
"path"
"testing"
"time"

"github.com/stretchr/testify/require"

"github.com/obolnetwork/charon/testutil"
"github.com/obolnetwork/charon/testutil/compose"
)

//go:generate go test . -run=TestBeaconFuzz -integration -v

var (
integration = flag.Bool("integration", false, "Enable docker based integration test")
sudoPerms = flag.Bool("sudo-perms", false, "Enables changing all compose artefacts file permissions using sudo.")
logDir = flag.String("log-dir", "", "Specifies the directory to store test docker-compose logs. Empty defaults to stdout.")
fuzzTimeout = flag.Duration("fuzz-timeout", time.Minute*10, "Specifies the duration of the beacon fuzz test.")
)

func TestBeaconFuzz(t *testing.T) {
if !*integration {
t.Skip("Skipping beacon fuzz integration test")
}

dir, err := os.MkdirTemp("", "")
require.NoError(t, err)

conf := compose.NewDefaultConfig()
conf.SyntheticBlockProposals = true
conf.Fuzz = true
conf.DisableMonitoringPorts = true
conf.BuildLocal = true
conf.ImageTag = "local"
conf.InsecureKeys = true
require.NoError(t, compose.WriteConfig(dir, conf))

os.Args = []string{"cobra.test"}

autoConfig := compose.AutoConfig{
Dir: dir,
AlertTimeout: *fuzzTimeout,
SudoPerms: *sudoPerms,
}

if *logDir != "" {
autoConfig.LogFile = path.Join(*logDir, fmt.Sprintf("%s.log", t.Name()))
}

err = compose.Auto(context.Background(), autoConfig)
testutil.RequireNoError(t, err)
}