Skip to content

Commit

Permalink
beacon: handle malicious identity (#4779)
Browse files Browse the repository at this point in the history
## Motivation
Closes #4772

## Changes
for malicious identity's 
- proposal: categorized as potentially valid (voting against)
- first vote: accept with weight 0
- following vote: accept with weight 0

malicious identity's weight is not included in the total epoch weight
  • Loading branch information
countvonzero committed Aug 7, 2023
1 parent 2c512aa commit 8f76eff
Show file tree
Hide file tree
Showing 6 changed files with 309 additions and 80 deletions.
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 @@ func (pd *ProtocolDriver) OnAtx(atx *types.ActivationTxHeader) {
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),
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
}

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)
}
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 {
return 0
}
return atx.NumUnits
Expand Down Expand Up @@ -537,7 +543,7 @@ func (pd *ProtocolDriver) initEpochStateIfNotPresent(logger log.Log, epoch types

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 @@ func (pd *ProtocolDriver) initEpochStateIfNotPresent(logger log.Log, epoch types
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
}
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 @@ func (pd *ProtocolDriver) initEpochStateIfNotPresent(logger log.Log, epoch types
} else {
pd.logger.With().Warning("ignoring malicious atx from miner",
header.ID,
log.Bool("malicious", malicious),
log.Stringer("smesher", header.NodeID))
}
if header.NodeID == pd.nodeID {
Expand Down Expand Up @@ -799,8 +817,8 @@ func (pd *ProtocolDriver) sendProposal(ctx context.Context, epoch types.EpochID,
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

0 comments on commit 8f76eff

Please sign in to comment.