Skip to content

Commit

Permalink
use atx grading for block proposal (#4717)
Browse files Browse the repository at this point in the history
## Motivation
<!-- Please mention the issue fixed by this PR or detailed motivation -->
part of #4089
<!-- `Closes #XXXX, closes #XXXX, ...` links mentioned issues to this PR and automatically closes them when this it's merged -->

## Changes
- grade atxs as described in https://community.spacemesh.io/t/grading-atxs-for-the-active-set/335, let s be the start of the epoch, and δ the network propagation time.

  - grade 0/evil: ATX was received at time t >= s-3δ, or an equivocation proof was received by time s-δ.
  - grade 1/acceptable: ATX was received at time t < s-3δ before the start of the epoch, and no equivocation proof was received by time s-δ.
  - grade 2/good: ATX was received at time t < s-4δ, and no equivocation proof was received for that id until time s.

- miner only include grade 2/good ATXs in its active set for its first ballot of the epoch

- newly sync'ed node uses the first block of epoch for active set in ref ballot, since it doesn't have accurate received timestamp for atxs or malfeasance proof
  • Loading branch information
countvonzero committed Jul 31, 2023
1 parent 21b4768 commit a975106
Show file tree
Hide file tree
Showing 20 changed files with 586 additions and 150 deletions.
4 changes: 2 additions & 2 deletions beacon/beacon.go
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,7 @@ func (pd *ProtocolDriver) initEpochStateIfNotPresent(logger log.Log, epoch types
ontime = pd.clock.LayerToTime(epoch.FirstLayer())
early = ontime.Add(-1 * pd.config.GracePeriodDuration)
)
if err := pd.cdb.IterateEpochATXHeaders(epoch, func(header *types.ActivationTxHeader) bool {
if err := pd.cdb.IterateEpochATXHeaders(epoch, func(header *types.ActivationTxHeader) error {
epochWeight += header.GetWeight()
if _, ok := miners[header.NodeID]; !ok {
miners[header.NodeID] = header.ID
Expand All @@ -563,7 +563,7 @@ func (pd *ProtocolDriver) initEpochStateIfNotPresent(logger log.Log, epoch types
if header.NodeID == pd.nodeID {
active = true
}
return true
return nil
}); err != nil {
return nil, err
}
Expand Down
6 changes: 0 additions & 6 deletions datastore/interface.go

This file was deleted.

12 changes: 6 additions & 6 deletions datastore/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -200,18 +200,18 @@ func (db *CachedDB) GetEpochWeight(epoch types.EpochID) (uint64, []types.ATXID,
weight uint64
ids []types.ATXID
)
if err := db.IterateEpochATXHeaders(epoch, func(header *types.ActivationTxHeader) bool {
if err := db.IterateEpochATXHeaders(epoch, func(header *types.ActivationTxHeader) error {
weight += header.GetWeight()
ids = append(ids, header.ID)
return true
return nil
}); err != nil {
return 0, nil, err
}
return weight, ids, nil
}

// IterateEpochATXHeaders iterates over ActivationTxs that target an epoch.
func (db *CachedDB) IterateEpochATXHeaders(epoch types.EpochID, iter func(*types.ActivationTxHeader) bool) error {
func (db *CachedDB) IterateEpochATXHeaders(epoch types.EpochID, iter func(*types.ActivationTxHeader) error) error {
ids, err := atxs.GetIDsByEpoch(db, epoch-1)
if err != nil {
return err
Expand All @@ -221,8 +221,8 @@ func (db *CachedDB) IterateEpochATXHeaders(epoch types.EpochID, iter func(*types
if err != nil {
return err
}
if !iter(header) {
return nil
if err := iter(header); err != nil {
return err
}
}
return nil
Expand All @@ -239,7 +239,7 @@ func (db *CachedDB) GetLastAtx(nodeID types.NodeID) (*types.ActivationTxHeader,
}
}

// GetEpochAtx gets the atx header of specified node ID in the specified epoch.
// GetEpochAtx gets the atx header of specified node ID published in the specified epoch.
func (db *CachedDB) GetEpochAtx(epoch types.EpochID, nodeID types.NodeID) (*types.ActivationTxHeader, error) {
vatx, err := atxs.GetByEpochAndNodeID(db, epoch, nodeID)
if err != nil {
Expand Down
29 changes: 5 additions & 24 deletions hare/eligibility/oracle.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,10 @@ import (
"github.com/spacemeshos/go-spacemesh/datastore"
"github.com/spacemeshos/go-spacemesh/hare/eligibility/config"
"github.com/spacemeshos/go-spacemesh/log"
"github.com/spacemeshos/go-spacemesh/miner"
"github.com/spacemeshos/go-spacemesh/signing"
"github.com/spacemeshos/go-spacemesh/sql"
"github.com/spacemeshos/go-spacemesh/sql/ballots"
"github.com/spacemeshos/go-spacemesh/sql/blocks"
"github.com/spacemeshos/go-spacemesh/sql/certificates"
"github.com/spacemeshos/go-spacemesh/system"
)

Expand Down Expand Up @@ -393,14 +392,15 @@ func (o *Oracle) computeActiveSet(ctx context.Context, targetEpoch types.EpochID
)
return activeSet, nil
}
bid, err := certificates.FirstInEpoch(o.cdb, targetEpoch)

activeSet, err := miner.ActiveSetFromBlock(o.cdb, targetEpoch)
if err != nil && !errors.Is(err, sql.ErrNotFound) {
return nil, err
}
if bid == types.EmptyBlockID {
if len(activeSet) == 0 {
return o.activeSetFromRefBallots(targetEpoch)
}
return o.activeSetFromBlock(bid)
return activeSet, nil
}

func (o *Oracle) computeActiveWeights(targetEpoch types.EpochID, activeSet []types.ATXID) (map[types.NodeID]uint64, error) {
Expand All @@ -415,25 +415,6 @@ func (o *Oracle) computeActiveWeights(targetEpoch types.EpochID, activeSet []typ
return weightedActiveSet, nil
}

func (o *Oracle) activeSetFromBlock(bid types.BlockID) ([]types.ATXID, error) {
block, err := blocks.Get(o.cdb, bid)
if err != nil {
return nil, fmt.Errorf("actives get block: %w", err)
}
activeMap := make(map[types.ATXID]struct{})
for _, r := range block.Rewards {
// only the reference ballots record the active set
ballot, err := ballots.FirstInEpoch(o.cdb, r.AtxID, block.LayerIndex.GetEpoch())
if err != nil {
return nil, fmt.Errorf("actives get ballot: %w", err)
}
for _, id := range ballot.ActiveSet {
activeMap[id] = struct{}{}
}
}
return maps.Keys(activeMap), nil
}

func (o *Oracle) activeSetFromRefBallots(epoch types.EpochID) ([]types.ATXID, error) {
beacon, err := o.beacons.GetBeacon(epoch)
if err != nil {
Expand Down
38 changes: 38 additions & 0 deletions miner/atx_grader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package miner

import (
"errors"
"time"

"github.com/spacemeshos/go-spacemesh/common/types"
"github.com/spacemeshos/go-spacemesh/sql"
)

// AtxGrade describes the grade of an ATX as described in
// https://community.spacemesh.io/t/grading-atxs-for-the-active-set/335
//
// let s be the start of the epoch, and δ the network propagation time.
// grade 0: ATX was received at time t >= s-3δ, or an equivocation proof was received by time s-δ.
// grade 1: ATX was received at time t < s-3δ before the start of the epoch, and no equivocation proof was received by time s-δ.
// grade 2: ATX was received at time t < s-4δ, and no equivocation proof was received for that id until time s.
type AtxGrade int

const (
Evil AtxGrade = iota
Acceptable
Good
)

func GradeAtx(msh mesh, nodeID types.NodeID, atxReceived, epochStart time.Time, delta time.Duration) (AtxGrade, error) {
proof, err := msh.GetMalfeasanceProof(nodeID)
if err != nil && !errors.Is(err, sql.ErrNotFound) {
return Good, err
}
if atxReceived.Before(epochStart.Add(-4*delta)) && (proof == nil || !proof.Received().Before(epochStart)) {
return Good, nil
}
if atxReceived.Before(epochStart.Add(-3*delta)) && (proof == nil || !proof.Received().Before(epochStart.Add(-delta))) {
return Acceptable, nil
}
return Evil, nil
}
109 changes: 109 additions & 0 deletions miner/atx_grader_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package miner_test

import (
"os"
"testing"
"time"

"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"

"github.com/spacemeshos/go-spacemesh/common/types"
"github.com/spacemeshos/go-spacemesh/hare/mocks"
"github.com/spacemeshos/go-spacemesh/miner"
"github.com/spacemeshos/go-spacemesh/sql"
)

const layersPerEpoch = 3

func TestMain(m *testing.M) {
types.SetLayersPerEpoch(layersPerEpoch)

res := m.Run()
os.Exit(res)
}

func TestGradeAtx(t *testing.T) {
const delta = 10
for _, tc := range []struct {
desc string
malicious bool
// distance in second from the epoch start time
atxReceived, malReceived int
result miner.AtxGrade
}{
{
desc: "very early atx",
atxReceived: -41,
result: miner.Good,
},
{
desc: "very early atx, late malfeasance",
atxReceived: -41,
malicious: true,
malReceived: 0,
result: miner.Good,
},
{
desc: "very early atx, malicious",
atxReceived: -41,
malicious: true,
malReceived: -10,
result: miner.Acceptable,
},
{
desc: "very early atx, early malicious",
atxReceived: -41,
malicious: true,
malReceived: -11,
result: miner.Evil,
},
{
desc: "early atx",
atxReceived: -31,
result: miner.Acceptable,
},
{
desc: "early atx, late malicious",
atxReceived: -31,
malicious: true,
malReceived: -10,
result: miner.Acceptable,
},
{
desc: "early atx, early malicious",
atxReceived: -31,
malicious: true,
malReceived: -11,
result: miner.Evil,
},
{
desc: "late atx",
atxReceived: -30,
result: miner.Evil,
},
{
desc: "very late atx",
atxReceived: 0,
result: miner.Evil,
},
} {
tc := tc
t.Run(tc.desc, func(t *testing.T) {
mockMsh := mocks.NewMockmesh(gomock.NewController(t))
epochStart := time.Now()
nodeID := types.RandomNodeID()
if tc.malicious {
proof := &types.MalfeasanceProof{}
proof.SetReceived(epochStart.Add(time.Duration(tc.malReceived) * time.Second))
mockMsh.EXPECT().GetMalfeasanceProof(nodeID).Return(proof, nil)
} else {
mockMsh.EXPECT().GetMalfeasanceProof(nodeID).Return(nil, sql.ErrNotFound)
}
atxReceived := epochStart.Add(time.Duration(tc.atxReceived) * time.Second)
got, err := miner.GradeAtx(mockMsh, nodeID, atxReceived, epochStart, delta*time.Second)
require.NoError(t, err)
require.Equal(t, tc.result, got)
})
}
}
6 changes: 5 additions & 1 deletion miner/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
//go:generate mockgen -package=miner -destination=./mocks.go -source=./interface.go

type proposalOracle interface {
GetProposalEligibility(types.LayerID, types.Beacon, types.VRFPostIndex) (*EpochEligibility, error)
ProposalEligibility(types.LayerID, types.Beacon, types.VRFPostIndex) (*EpochEligibility, error)
}

type conservativeState interface {
Expand All @@ -28,6 +28,10 @@ type nonceFetcher interface {
VRFNonce(types.NodeID, types.EpochID) (types.VRFPostIndex, error)
}

type mesh interface {
GetMalfeasanceProof(nodeID types.NodeID) (*types.MalfeasanceProof, error)
}

type layerClock interface {
AwaitLayer(layerID types.LayerID) <-chan struct{}
CurrentLayer() types.LayerID
Expand Down
50 changes: 44 additions & 6 deletions miner/mocks.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit a975106

Please sign in to comment.