Skip to content

Commit

Permalink
feature: add malicious vote monitor (#1597)
Browse files Browse the repository at this point in the history
  • Loading branch information
NathanBSC authored May 11, 2023
1 parent 4cc78fd commit b0ad742
Show file tree
Hide file tree
Showing 9 changed files with 347 additions and 12 deletions.
1 change: 1 addition & 0 deletions cmd/geth/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ var (
utils.CheckSnapshotWithMPT,
utils.EnableDoubleSignMonitorFlag,
utils.VotingEnabledFlag,
utils.EnableMaliciousVoteMonitorFlag,
utils.BLSPasswordFileFlag,
utils.BLSWalletDirFlag,
utils.VoteJournalDirFlag,
Expand Down
15 changes: 11 additions & 4 deletions cmd/utils/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -901,6 +901,11 @@ var (
Usage: "Enable voting",
}

EnableMaliciousVoteMonitorFlag = cli.BoolFlag{
Name: "monitor.maliciousvote",
Usage: "Enable malicious vote monitor to check whether any validator violates the voting rules of fast finality",
}

BLSPasswordFileFlag = cli.StringFlag{
Name: "blspassword",
Usage: "File path for the BLS password, which contains the password to unlock BLS wallet for managing votes in fast_finality feature",
Expand Down Expand Up @@ -1159,12 +1164,14 @@ func setLes(ctx *cli.Context, cfg *ethconfig.Config) {
}
}

// setMonitor creates the monitor from the set
// command line flags, returning empty if the monitor is disabled.
func setMonitor(ctx *cli.Context, cfg *node.Config) {
// setMonitors enable monitors from the command line flags.
func setMonitors(ctx *cli.Context, cfg *node.Config) {
if ctx.GlobalBool(EnableDoubleSignMonitorFlag.Name) {
cfg.EnableDoubleSignMonitor = true
}
if ctx.GlobalBool(EnableMaliciousVoteMonitorFlag.Name) {
cfg.EnableMaliciousVoteMonitor = true
}
}

// MakeDatabaseHandles raises out the number of allowed file handles per process
Expand Down Expand Up @@ -1329,7 +1336,7 @@ func SetNodeConfig(ctx *cli.Context, cfg *node.Config) {
setNodeUserIdent(ctx, cfg)
setDataDir(ctx, cfg)
setSmartCard(ctx, cfg)
setMonitor(ctx, cfg)
setMonitors(ctx, cfg)
setBLSWalletDir(ctx, cfg)
setVoteJournalDir(ctx, cfg)

Expand Down
File renamed without changes.
85 changes: 85 additions & 0 deletions core/monitor/malicious_vote_monitor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package monitor

import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics"
lru "github.com/hashicorp/golang-lru"
)

// follow define in core/vote
const (
maxSizeOfRecentEntry = 512
maliciousVoteSlashScope = 256
upperLimitOfVoteBlockNumber = 11
)

var (
violateRule1Counter = metrics.NewRegisteredCounter("monitor/maliciousVote/violateRule1", nil)
violateRule2Counter = metrics.NewRegisteredCounter("monitor/maliciousVote/violateRule2", nil)
)

// two purposes
// 1. monitor whether there are bugs in the voting mechanism, so add metrics to observe it.
// 2. do malicious vote slashing. TODO
type MaliciousVoteMonitor struct {
curVotes map[types.BLSPublicKey]*lru.Cache
}

func NewMaliciousVoteMonitor() *MaliciousVoteMonitor {
return &MaliciousVoteMonitor{
curVotes: make(map[types.BLSPublicKey]*lru.Cache, 21), // mainnet config
}
}

func (m *MaliciousVoteMonitor) ConflictDetect(newVote *types.VoteEnvelope, pendingBlockNumber uint64) bool {
// get votes for specified VoteAddress
if _, ok := m.curVotes[newVote.VoteAddress]; !ok {
voteDataBuffer, err := lru.New(maxSizeOfRecentEntry)
if err != nil {
log.Error("MaliciousVoteMonitor new lru failed", "err", err)
return false
}
m.curVotes[newVote.VoteAddress] = voteDataBuffer
}
voteDataBuffer := m.curVotes[newVote.VoteAddress]
sourceNumber, targetNumber := newVote.Data.SourceNumber, newVote.Data.TargetNumber

//Basic check
// refer to https://github.com/bnb-chain/bsc-genesis-contract/blob/master/contracts/SlashIndicator.sol#LL207C4-L207C4
if !(targetNumber+maliciousVoteSlashScope > pendingBlockNumber) {
return false
}

// UnderRules check
blockNumber := sourceNumber + 1
if !(blockNumber+maliciousVoteSlashScope > pendingBlockNumber) {
blockNumber = pendingBlockNumber - maliciousVoteSlashScope + 1
}
for ; blockNumber <= pendingBlockNumber+upperLimitOfVoteBlockNumber; blockNumber++ {
if voteDataBuffer.Contains(blockNumber) {
voteData, ok := voteDataBuffer.Get(blockNumber)
if !ok {
log.Error("Failed to get voteData info from LRU cache.")
continue
}
if blockNumber == targetNumber {
log.Warn("violate rule1", "VoteAddress", common.Bytes2Hex(newVote.VoteAddress[:]), "voteExisted", voteData.(*types.VoteData), "newVote", newVote.Data)
violateRule1Counter.Inc(1)
// prepare message for slashing
return true
} else if (blockNumber < targetNumber && voteData.(*types.VoteData).SourceNumber > sourceNumber) ||
(blockNumber > targetNumber && voteData.(*types.VoteData).SourceNumber < sourceNumber) {
log.Warn("violate rule2", "VoteAddress", common.Bytes2Hex(newVote.VoteAddress[:]), "voteExisted", voteData.(*types.VoteData), "newVote", newVote.Data)
violateRule2Counter.Inc(1)
// prepare message for slashing
return true
}
}
}

// for simplicity, Just override even if the targetNumber has existed.
voteDataBuffer.Add(newVote.Data.TargetNumber, newVote.Data)
return false
}
210 changes: 210 additions & 0 deletions core/monitor/malicious_vote_monitor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
package monitor

import (
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/stretchr/testify/assert"
)

func TestMaliciousVoteMonitor(t *testing.T) {
//log.Root().SetHandler(log.StdoutHandler)
// case 1, different voteAddress
{
maliciousVoteMonitor := NewMaliciousVoteMonitor()
pendingBlockNumber := uint64(1000)
voteAddrBytes := common.Hex2BytesFixed("000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", types.BLSPublicKeyLength)
voteAddress := types.BLSPublicKey{}
copy(voteAddress[:], voteAddrBytes[:])
vote1 := &types.VoteEnvelope{
VoteAddress: voteAddress,
Signature: types.BLSSignature{},
Data: &types.VoteData{
SourceNumber: uint64(0),
SourceHash: common.BytesToHash(common.Hex2Bytes(string(rune(0)))),
TargetNumber: pendingBlockNumber - maliciousVoteSlashScope - 1,
TargetHash: common.BytesToHash(common.Hex2Bytes(string(rune(1)))),
},
}
assert.Equal(t, false, maliciousVoteMonitor.ConflictDetect(vote1, pendingBlockNumber))
voteAddress[0] = 4
vote2 := &types.VoteEnvelope{
VoteAddress: voteAddress,
Signature: types.BLSSignature{},
Data: &types.VoteData{
SourceNumber: uint64(0),
SourceHash: common.BytesToHash(common.Hex2Bytes(string(rune(0)))),
TargetNumber: pendingBlockNumber - maliciousVoteSlashScope - 1,
TargetHash: common.BytesToHash(common.Hex2Bytes(string(rune(2)))),
},
}
assert.Equal(t, false, maliciousVoteMonitor.ConflictDetect(vote2, pendingBlockNumber))
}

// case 2, target number not in maliciousVoteSlashScope
{
maliciousVoteMonitor := NewMaliciousVoteMonitor()
pendingBlockNumber := uint64(1000)
voteAddrBytes := common.Hex2BytesFixed("000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", types.BLSPublicKeyLength)
voteAddress := types.BLSPublicKey{}
copy(voteAddress[:], voteAddrBytes[:])
vote1 := &types.VoteEnvelope{
VoteAddress: voteAddress,
Signature: types.BLSSignature{},
Data: &types.VoteData{
SourceNumber: uint64(0),
SourceHash: common.BytesToHash(common.Hex2Bytes(string(rune(0)))),
TargetNumber: pendingBlockNumber - maliciousVoteSlashScope - 1,
TargetHash: common.BytesToHash(common.Hex2Bytes(string(rune(1)))),
},
}
assert.Equal(t, false, maliciousVoteMonitor.ConflictDetect(vote1, pendingBlockNumber))
vote2 := &types.VoteEnvelope{
VoteAddress: voteAddress,
Signature: types.BLSSignature{},
Data: &types.VoteData{
SourceNumber: uint64(0),
SourceHash: common.BytesToHash(common.Hex2Bytes(string(rune(0)))),
TargetNumber: pendingBlockNumber - maliciousVoteSlashScope - 1,
TargetHash: common.BytesToHash(common.Hex2Bytes(string(rune(2)))),
},
}
assert.Equal(t, false, maliciousVoteMonitor.ConflictDetect(vote2, pendingBlockNumber))
}

// case 3, violate rule1
{
maliciousVoteMonitor := NewMaliciousVoteMonitor()
pendingBlockNumber := uint64(1000)
voteAddrBytes := common.Hex2BytesFixed("000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", types.BLSPublicKeyLength)
voteAddress := types.BLSPublicKey{}
copy(voteAddress[:], voteAddrBytes[:])
vote1 := &types.VoteEnvelope{
VoteAddress: voteAddress,
Signature: types.BLSSignature{},
Data: &types.VoteData{
SourceNumber: uint64(0),
SourceHash: common.BytesToHash(common.Hex2Bytes(string(rune(0)))),
TargetNumber: pendingBlockNumber - 1,
TargetHash: common.BytesToHash(common.Hex2Bytes(string(rune(1)))),
},
}
assert.Equal(t, false, maliciousVoteMonitor.ConflictDetect(vote1, pendingBlockNumber))
vote2 := &types.VoteEnvelope{
VoteAddress: voteAddress,
Signature: types.BLSSignature{},
Data: &types.VoteData{
SourceNumber: uint64(0),
SourceHash: common.BytesToHash(common.Hex2Bytes(string(rune(0)))),
TargetNumber: pendingBlockNumber - 1,
TargetHash: common.BytesToHash(common.Hex2Bytes(string(rune(2)))),
},
}
assert.Equal(t, true, maliciousVoteMonitor.ConflictDetect(vote2, pendingBlockNumber))
}

// case 4, violate rule2, vote with smaller range first
{
maliciousVoteMonitor := NewMaliciousVoteMonitor()
pendingBlockNumber := uint64(1000)
voteAddrBytes := common.Hex2BytesFixed("000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", types.BLSPublicKeyLength)
voteAddress := types.BLSPublicKey{}
copy(voteAddress[:], voteAddrBytes[:])
vote1 := &types.VoteEnvelope{
VoteAddress: voteAddress,
Signature: types.BLSSignature{},
Data: &types.VoteData{
SourceNumber: pendingBlockNumber - 4,
SourceHash: common.BytesToHash(common.Hex2Bytes(string(rune(0)))),
TargetNumber: pendingBlockNumber - 1,
TargetHash: common.BytesToHash(common.Hex2Bytes(string(rune(1)))),
},
}
assert.Equal(t, false, maliciousVoteMonitor.ConflictDetect(vote1, pendingBlockNumber))
vote2 := &types.VoteEnvelope{
VoteAddress: voteAddress,
Signature: types.BLSSignature{},
Data: &types.VoteData{
SourceNumber: pendingBlockNumber - 2,
SourceHash: common.BytesToHash(common.Hex2Bytes(string(rune(0)))),
TargetNumber: pendingBlockNumber - 3,
TargetHash: common.BytesToHash(common.Hex2Bytes(string(rune(2)))),
},
}
assert.Equal(t, true, maliciousVoteMonitor.ConflictDetect(vote2, pendingBlockNumber))
}

// case 5, violate rule2, vote with larger range first
{
maliciousVoteMonitor := NewMaliciousVoteMonitor()
pendingBlockNumber := uint64(1000)
voteAddrBytes := common.Hex2BytesFixed("000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", types.BLSPublicKeyLength)
voteAddress := types.BLSPublicKey{}
copy(voteAddress[:], voteAddrBytes[:])
vote1 := &types.VoteEnvelope{
VoteAddress: voteAddress,
Signature: types.BLSSignature{},
Data: &types.VoteData{
SourceNumber: pendingBlockNumber - 2,
SourceHash: common.BytesToHash(common.Hex2Bytes(string(rune(0)))),
TargetNumber: pendingBlockNumber - 3,
TargetHash: common.BytesToHash(common.Hex2Bytes(string(rune(1)))),
},
}
assert.Equal(t, false, maliciousVoteMonitor.ConflictDetect(vote1, pendingBlockNumber))
vote2 := &types.VoteEnvelope{
VoteAddress: voteAddress,
Signature: types.BLSSignature{},
Data: &types.VoteData{
SourceNumber: pendingBlockNumber - 4,
SourceHash: common.BytesToHash(common.Hex2Bytes(string(rune(0)))),
TargetNumber: pendingBlockNumber - 1,
TargetHash: common.BytesToHash(common.Hex2Bytes(string(rune(2)))),
},
}
assert.Equal(t, true, maliciousVoteMonitor.ConflictDetect(vote2, pendingBlockNumber))
}

// case 6, normal case
{
maliciousVoteMonitor := NewMaliciousVoteMonitor()
pendingBlockNumber := uint64(1000)
voteAddrBytes := common.Hex2BytesFixed("000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001", types.BLSPublicKeyLength)
voteAddress := types.BLSPublicKey{}
copy(voteAddress[:], voteAddrBytes[:])
vote1 := &types.VoteEnvelope{
VoteAddress: voteAddress,
Signature: types.BLSSignature{},
Data: &types.VoteData{
SourceNumber: pendingBlockNumber - 4,
SourceHash: common.BytesToHash(common.Hex2Bytes(string(rune(0)))),
TargetNumber: pendingBlockNumber - 3,
TargetHash: common.BytesToHash(common.Hex2Bytes(string(rune(1)))),
},
}
assert.Equal(t, false, maliciousVoteMonitor.ConflictDetect(vote1, pendingBlockNumber))
vote2 := &types.VoteEnvelope{
VoteAddress: voteAddress,
Signature: types.BLSSignature{},
Data: &types.VoteData{
SourceNumber: pendingBlockNumber - 3,
SourceHash: common.BytesToHash(common.Hex2Bytes(string(rune(0)))),
TargetNumber: pendingBlockNumber - 2,
TargetHash: common.BytesToHash(common.Hex2Bytes(string(rune(2)))),
},
}
assert.Equal(t, false, maliciousVoteMonitor.ConflictDetect(vote2, pendingBlockNumber))
vote3 := &types.VoteEnvelope{
VoteAddress: voteAddress,
Signature: types.BLSSignature{},
Data: &types.VoteData{
SourceNumber: pendingBlockNumber - 2,
SourceHash: common.BytesToHash(common.Hex2Bytes(string(rune(0)))),
TargetNumber: pendingBlockNumber - 1,
TargetHash: common.BytesToHash(common.Hex2Bytes(string(rune(2)))),
},
}
assert.Equal(t, false, maliciousVoteMonitor.ConflictDetect(vote3, pendingBlockNumber))
}
}
7 changes: 4 additions & 3 deletions core/vote/vote_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ func (voteManager *VoteManager) UnderRules(header *types.Header) (bool, uint64,
continue
}
if voteData.(*types.VoteData).SourceNumber > sourceNumber {
log.Debug(fmt.Sprintf("error: cur vote %d-->%d is within the span of other votes %d-->%d",
log.Debug(fmt.Sprintf("error: cur vote %d-->%d is across the span of other votes %d-->%d",
sourceNumber, targetNumber, voteData.(*types.VoteData).SourceNumber, voteData.(*types.VoteData).TargetNumber))
return false, 0, common.Hash{}
}
Expand All @@ -208,14 +208,15 @@ func (voteManager *VoteManager) UnderRules(header *types.Header) (bool, uint64,
continue
}
if voteData.(*types.VoteData).SourceNumber < sourceNumber {
log.Debug("error: other votes are within span of cur vote")
log.Debug(fmt.Sprintf("error: cur vote %d-->%d is within the span of other votes %d-->%d",
sourceNumber, targetNumber, voteData.(*types.VoteData).SourceNumber, voteData.(*types.VoteData).TargetNumber))
return false, 0, common.Hash{}
}
}
}

// Rule 3: Validators always vote for their canonical chain’s latest block.
// Since the header subscribed to is the canonical chain, so this rule is satisified by default.
// Since the header subscribed to is the canonical chain, so this rule is satisfied by default.
log.Debug("All three rules check passed")
return true, sourceNumber, sourceHash
}
5 changes: 5 additions & 0 deletions eth/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"github.com/ethereum/go-ethereum/consensus/parlia"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/bloombits"
"github.com/ethereum/go-ethereum/core/monitor"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/state/pruner"
"github.com/ethereum/go-ethereum/core/types"
Expand Down Expand Up @@ -300,6 +301,10 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
}
if eth.votePool != nil {
eth.handler.votepool = eth.votePool
if stack.Config().EnableMaliciousVoteMonitor {
eth.handler.maliciousVoteMonitor = monitor.NewMaliciousVoteMonitor()
log.Info("Create MaliciousVoteMonitor successfully")
}
}

eth.miner = miner.New(eth, &config.Miner, chainConfig, eth.EventMux(), eth.engine, eth.isLocalBlock)
Expand Down
Loading

0 comments on commit b0ad742

Please sign in to comment.