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

feature: add malicious vote monitor #1597

Merged
merged 3 commits into from
May 11, 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
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
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