diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 6c08754809..13cc78e4da 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -170,6 +170,7 @@ var ( utils.CheckSnapshotWithMPT, utils.EnableDoubleSignMonitorFlag, utils.VotingEnabledFlag, + utils.EnableMaliciousVoteMonitorFlag, utils.BLSPasswordFileFlag, utils.BLSWalletDirFlag, utils.VoteJournalDirFlag, diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index c1b7bc9e93..0dbac558e2 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -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", @@ -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 @@ -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) diff --git a/core/monitor/double_sign_mointor.go b/core/monitor/double_sign_monitor.go similarity index 100% rename from core/monitor/double_sign_mointor.go rename to core/monitor/double_sign_monitor.go diff --git a/core/monitor/malicious_vote_monitor.go b/core/monitor/malicious_vote_monitor.go new file mode 100644 index 0000000000..a817f2097a --- /dev/null +++ b/core/monitor/malicious_vote_monitor.go @@ -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) + 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) + 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 +} diff --git a/core/monitor/malicious_vote_monitor_test.go b/core/monitor/malicious_vote_monitor_test.go new file mode 100644 index 0000000000..7e3599184b --- /dev/null +++ b/core/monitor/malicious_vote_monitor_test.go @@ -0,0 +1,209 @@ +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) { + // 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: uint64(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: uint64(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: uint64(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: uint64(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: uint64(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: uint64(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: uint64(pendingBlockNumber - 4), + SourceHash: common.BytesToHash(common.Hex2Bytes(string(rune(0)))), + TargetNumber: uint64(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(pendingBlockNumber - 2), + SourceHash: common.BytesToHash(common.Hex2Bytes(string(rune(0)))), + TargetNumber: uint64(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: uint64(pendingBlockNumber - 2), + SourceHash: common.BytesToHash(common.Hex2Bytes(string(rune(0)))), + TargetNumber: uint64(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: uint64(pendingBlockNumber - 4), + SourceHash: common.BytesToHash(common.Hex2Bytes(string(rune(0)))), + TargetNumber: uint64(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: uint64(pendingBlockNumber - 4), + SourceHash: common.BytesToHash(common.Hex2Bytes(string(rune(0)))), + TargetNumber: uint64(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: uint64(pendingBlockNumber - 3), + SourceHash: common.BytesToHash(common.Hex2Bytes(string(rune(0)))), + TargetNumber: uint64(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: uint64(pendingBlockNumber - 2), + SourceHash: common.BytesToHash(common.Hex2Bytes(string(rune(0)))), + TargetNumber: uint64(pendingBlockNumber - 1), + TargetHash: common.BytesToHash(common.Hex2Bytes(string(rune(2)))), + }, + } + assert.Equal(t, false, maliciousVoteMonitor.ConflictDetect(vote3, pendingBlockNumber)) + } +} diff --git a/core/vote/vote_manager.go b/core/vote/vote_manager.go index d7a36cac62..d50b6d66f1 100644 --- a/core/vote/vote_manager.go +++ b/core/vote/vote_manager.go @@ -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{} } @@ -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 } diff --git a/eth/backend.go b/eth/backend.go index a8e9aa2037..b137118e74 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -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" @@ -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) diff --git a/eth/handler.go b/eth/handler.go index 92e2b78f10..1ce579a0d0 100644 --- a/eth/handler.go +++ b/eth/handler.go @@ -29,6 +29,7 @@ import ( "github.com/ethereum/go-ethereum/consensus/beacon" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/forkid" + "github.com/ethereum/go-ethereum/core/monitor" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/eth/downloader" @@ -133,11 +134,12 @@ type handler struct { checkpointNumber uint64 // Block number for the sync progress validator to cross reference checkpointHash common.Hash // Block hash for the sync progress validator to cross reference - database ethdb.Database - txpool txPool - votepool votePool - chain *core.BlockChain - maxPeers int + database ethdb.Database + txpool txPool + votepool votePool + maliciousVoteMonitor *monitor.MaliciousVoteMonitor + chain *core.BlockChain + maxPeers int downloader *downloader.Downloader blockFetcher *fetcher.BlockFetcher @@ -641,6 +643,11 @@ func (h *handler) Start(maxPeers int) { h.voteCh = make(chan core.NewVoteEvent, voteChanSize) h.votesSub = h.votepool.SubscribeNewVoteEvent(h.voteCh) go h.voteBroadcastLoop() + + if h.maliciousVoteMonitor != nil { + h.wg.Add(1) + go h.startMaliciousVoteMonitor() + } } // announce local pending transactions again @@ -659,6 +666,22 @@ func (h *handler) Start(maxPeers int) { go h.chainSync.loop() } +func (h *handler) startMaliciousVoteMonitor() { + defer h.wg.Done() + voteCh := make(chan core.NewVoteEvent, voteChanSize) + votesSub := h.votepool.SubscribeNewVoteEvent(voteCh) + defer votesSub.Unsubscribe() + for { + select { + case event := <-voteCh: + pendingBlockNumber := h.chain.CurrentHeader().Number.Uint64() + 1 + h.maliciousVoteMonitor.ConflictDetect(event.Vote, pendingBlockNumber) + case <-votesSub.Err(): + return + } + } +} + func (h *handler) Stop() { h.txsSub.Unsubscribe() // quits txBroadcastLoop h.reannoTxsSub.Unsubscribe() // quits txReannounceLoop diff --git a/node/config.go b/node/config.go index cd87e26362..76fbc40e45 100644 --- a/node/config.go +++ b/node/config.go @@ -205,6 +205,9 @@ type Config struct { // EnableDoubleSignMonitor is a flag that whether to enable the double signature checker EnableDoubleSignMonitor bool `toml:",omitempty"` + // EnableMaliciousVoteMonitor is a flag that whether to enable the malicious vote checker + EnableMaliciousVoteMonitor bool `toml:",omitempty"` + // BLSPasswordFile is the file that contains BLS wallet password. BLSPasswordFile string `toml:",omitempty"`