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

[Merged by Bors] - beacon: handle malicious identity #4779

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
48 changes: 33 additions & 15 deletions beacon/beacon.go
Original file line number Diff line number Diff line change
Expand Up @@ -304,40 +304,46 @@
if !ok {
return
}
if id, ok := s.minerAtxs[atx.NodeID]; ok && id != atx.ID {
if mi, ok := s.minerAtxs[atx.NodeID]; ok && mi.atxid != atx.ID {
pd.logger.With().Warning("ignoring malicious atx",
log.Stringer("smesher", atx.NodeID),
log.Stringer("previous_atx", id),
log.Stringer("previous_atx", mi.atxid),

Check warning on line 310 in beacon/beacon.go

View check run for this annotation

Codecov / codecov/patch

beacon/beacon.go#L310

Added line #L310 was not covered by tests
log.Stringer("new_atx", atx.ID),
)
return
}
s.minerAtxs[atx.NodeID] = atx.ID
s.minerAtxs[atx.NodeID] = &minerInfo{
atxid: atx.ID,
}
}

func (pd *ProtocolDriver) minerAtxHdr(epoch types.EpochID, nodeID types.NodeID) (*types.ActivationTxHeader, error) {
func (pd *ProtocolDriver) minerAtxHdr(epoch types.EpochID, nodeID types.NodeID) (*types.ActivationTxHeader, bool, error) {
pd.mu.RLock()
defer pd.mu.RUnlock()

st, ok := pd.states[epoch]
if !ok {
return nil, errEpochNotActive
return nil, false, errEpochNotActive

Check warning on line 326 in beacon/beacon.go

View check run for this annotation

Codecov / codecov/patch

beacon/beacon.go#L326

Added line #L326 was not covered by tests
}

id, ok := st.minerAtxs[nodeID]
mi, ok := st.minerAtxs[nodeID]
if !ok {
pd.logger.With().Debug("miner does not have atx in previous epoch",
epoch-1,
log.Stringer("smesher", nodeID),
)
return nil, errMinerNotActive
return nil, false, errMinerNotActive
}
hdr, err := pd.cdb.GetAtxHeader(mi.atxid)
if err != nil {
return nil, false, fmt.Errorf("get miner atx hdr %v: %w", mi.atxid.ShortString(), err)

Check warning on line 339 in beacon/beacon.go

View check run for this annotation

Codecov / codecov/patch

beacon/beacon.go#L339

Added line #L339 was not covered by tests
}
return pd.cdb.GetAtxHeader(id)
return hdr, mi.malicious, nil
}

func (pd *ProtocolDriver) MinerAllowance(epoch types.EpochID, nodeID types.NodeID) uint32 {
atx, err := pd.minerAtxHdr(epoch, nodeID)
if err != nil {
atx, malicious, err := pd.minerAtxHdr(epoch, nodeID)
if err != nil || malicious {

Check warning on line 346 in beacon/beacon.go

View check run for this annotation

Codecov / codecov/patch

beacon/beacon.go#L345-L346

Added lines #L345 - L346 were not covered by tests
return 0
}
return atx.NumUnits
Expand Down Expand Up @@ -537,7 +543,7 @@

var (
epochWeight uint64
miners = make(map[types.NodeID]types.ATXID)
miners = make(map[types.NodeID]*minerInfo)
active bool
nonce *types.VRFPostIndex
// w1 is the weight units at δ before the end of the previous epoch, used to calculate `thresholdStrict`
Expand All @@ -547,9 +553,20 @@
early = ontime.Add(-1 * pd.config.GracePeriodDuration)
)
if err := pd.cdb.IterateEpochATXHeaders(epoch, func(header *types.ActivationTxHeader) error {
epochWeight += header.GetWeight()
malicious, err := pd.cdb.IsMalicious(header.NodeID)
if err != nil {
return err
}

Check warning on line 559 in beacon/beacon.go

View check run for this annotation

Codecov / codecov/patch

beacon/beacon.go#L558-L559

Added lines #L558 - L559 were not covered by tests
if !malicious {
epochWeight += header.GetWeight()
} else {
pd.logger.With().Debug("malicious miner get 0 weight", log.Stringer("smesher", header.NodeID))
}
if _, ok := miners[header.NodeID]; !ok {
miners[header.NodeID] = header.ID
miners[header.NodeID] = &minerInfo{
atxid: header.ID,
malicious: malicious,
}
if header.Received.Before(early) {
w1++
} else if header.Received.Before(ontime) {
Expand All @@ -558,6 +575,7 @@
} else {
pd.logger.With().Warning("ignoring malicious atx from miner",
header.ID,
log.Bool("malicious", malicious),

Check warning on line 578 in beacon/beacon.go

View check run for this annotation

Codecov / codecov/patch

beacon/beacon.go#L578

Added line #L578 was not covered by tests
log.Stringer("smesher", header.NodeID))
}
if header.NodeID == pd.nodeID {
Expand Down Expand Up @@ -799,8 +817,8 @@
return
}

atx, err := pd.minerAtxHdr(epoch, pd.edSigner.NodeID())
if err != nil {
atx, malicious, err := pd.minerAtxHdr(epoch, pd.edSigner.NodeID())
if err != nil || malicious {
return
}

Expand Down
69 changes: 69 additions & 0 deletions beacon/beacon_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/spacemeshos/go-spacemesh/signing"
"github.com/spacemeshos/go-spacemesh/sql"
"github.com/spacemeshos/go-spacemesh/sql/atxs"
"github.com/spacemeshos/go-spacemesh/sql/identities"
"github.com/spacemeshos/go-spacemesh/system/mocks"
)

Expand Down Expand Up @@ -221,6 +222,74 @@ func TestBeacon_MultipleNodes(t *testing.T) {
require.Len(t, beacons, 1)
}

func TestBeacon_MultipleNodes_OnlyOneHonest(t *testing.T) {
numNodes := 5
testNodes := make([]*testProtocolDriver, 0, numNodes)
publisher := pubsubmocks.NewMockPublisher(gomock.NewController(t))
publisher.EXPECT().Publish(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(
func(ctx context.Context, protocol string, data []byte) error {
for _, node := range testNodes {
switch protocol {
case pubsub.BeaconProposalProtocol:
require.NoError(t, node.HandleProposal(ctx, p2p.Peer(node.nodeID.ShortString()), data))
case pubsub.BeaconFirstVotesProtocol:
require.NoError(t, node.HandleFirstVotes(ctx, p2p.Peer(node.nodeID.ShortString()), data))
case pubsub.BeaconFollowingVotesProtocol:
require.NoError(t, node.HandleFollowingVotes(ctx, p2p.Peer(node.nodeID.ShortString()), data))
case pubsub.BeaconWeakCoinProtocol:
}
}
return nil
}).AnyTimes()

atxPublishLid := types.LayerID(types.GetLayersPerEpoch()*2 - 1)
current := atxPublishLid.Add(1)
dbs := make([]*datastore.CachedDB, 0, numNodes)
cfg := NodeSimUnitTestConfig()
bootstrap := types.Beacon{1, 2, 3, 4}
now := time.Now()
for i := 0; i < numNodes; i++ {
node := newTestDriver(t, cfg, publisher)
require.NoError(t, node.UpdateBeacon(types.EpochID(2), bootstrap))
node.mSync.EXPECT().IsSynced(gomock.Any()).Return(true).AnyTimes()
node.mClock.EXPECT().CurrentLayer().Return(current).AnyTimes()
node.mClock.EXPECT().LayerToTime(current).Return(now).AnyTimes()
testNodes = append(testNodes, node)
dbs = append(dbs, node.cdb)

require.ErrorIs(t, node.onNewEpoch(context.Background(), types.EpochID(0)), errGenesis)
require.ErrorIs(t, node.onNewEpoch(context.Background(), types.EpochID(1)), errGenesis)
got, err := node.GetBeacon(types.EpochID(2))
require.NoError(t, err)
require.Equal(t, bootstrap, got)
}
for i, node := range testNodes {
for _, db := range dbs {
createATX(t, db, atxPublishLid, node.edSigner, 1, time.Now().Add(-1*time.Second))
if i != 0 {
require.NoError(t, identities.SetMalicious(db, node.nodeID, []byte("bad"), time.Now()))
}
}
}
var wg sync.WaitGroup
for _, node := range testNodes {
wg.Add(1)
go func(testNode *testProtocolDriver) {
require.NoError(t, testNode.onNewEpoch(context.Background(), types.EpochID(2)))
wg.Done()
}(node)
}
wg.Wait()
beacons := make(map[types.Beacon]struct{})
for _, node := range testNodes {
got, err := node.GetBeacon(types.EpochID(3))
require.NoError(t, err)
require.NotEqual(t, types.EmptyBeacon, got)
beacons[got] = struct{}{}
}
require.Len(t, beacons, 1)
}

func TestBeacon_NoProposals(t *testing.T) {
numNodes := 5
testNodes := make([]*testProtocolDriver, 0, numNodes)
Expand Down
26 changes: 21 additions & 5 deletions beacon/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"math/big"
"time"

bcnmetrics "github.com/spacemeshos/go-spacemesh/beacon/metrics"
"github.com/spacemeshos/go-spacemesh/codec"
"github.com/spacemeshos/go-spacemesh/common/types"
"github.com/spacemeshos/go-spacemesh/log"
Expand Down Expand Up @@ -78,7 +79,7 @@ func (pd *ProtocolDriver) HandleProposal(ctx context.Context, peer p2p.Peer, msg
return err
}

atx, err := pd.minerAtxHdr(m.EpochID, m.NodeID)
atx, malicious, err := pd.minerAtxHdr(m.EpochID, m.NodeID)
if err != nil {
return err
}
Expand All @@ -88,6 +89,11 @@ func (pd *ProtocolDriver) HandleProposal(ctx context.Context, peer p2p.Peer, msg
}

cat := pd.classifyProposal(logger, m, atx.Received, receivedTime, st.proposalChecker)
if cat == valid && malicious {
bcnmetrics.NumMaliciousProps.Inc()
logger.With().Debug("malicious miner proposal potentially valid", log.Stringer("smesher", m.NodeID))
cat = potentiallyValid
}
return pd.addProposal(m, cat)
}

Expand Down Expand Up @@ -274,11 +280,16 @@ func (pd *ProtocolDriver) storeFirstVotes(m FirstVotingMessage, nodeID types.Nod
return errProtocolNotRunning
}

atx, err := pd.minerAtxHdr(m.EpochID, nodeID)
atx, malicious, err := pd.minerAtxHdr(m.EpochID, nodeID)
if err != nil {
return err
}
voteWeight := new(big.Int).SetUint64(atx.GetWeight())
voteWeight := new(big.Int)
if !malicious {
voteWeight.SetUint64(atx.GetWeight())
} else {
pd.logger.With().Debug("malicious miner get 0 weight", log.Stringer("smesher", nodeID))
}

pd.mu.Lock()
defer pd.mu.Unlock()
Expand Down Expand Up @@ -374,11 +385,16 @@ func (pd *ProtocolDriver) storeFollowingVotes(m FollowingVotingMessage, nodeID t
return errProtocolNotRunning
}

atx, err := pd.minerAtxHdr(m.EpochID, nodeID)
atx, malicious, err := pd.minerAtxHdr(m.EpochID, nodeID)
if err != nil {
return err
}
voteWeight := new(big.Int).SetUint64(atx.GetWeight())
voteWeight := new(big.Int)
if !malicious {
voteWeight.SetUint64(atx.GetWeight())
} else {
pd.logger.With().Debug("malicious miner get 0 weight", log.Stringer("smesher", nodeID))
}

firstRoundVotes, err := pd.getFirstRoundVote(m.EpochID, nodeID)
if err != nil {
Expand Down