diff --git a/consensus/parlia/parlia.go b/consensus/parlia/parlia.go index 0800bbad22..3ddef0b713 100644 --- a/consensus/parlia/parlia.go +++ b/consensus/parlia/parlia.go @@ -1559,6 +1559,23 @@ func (p *Parlia) Delay(chain consensus.ChainReader, header *types.Header, leftOv return &delay } +// AssembleSignature assemble the signature for block header +func (p *Parlia) AssembleSignature(block *types.Block) (*types.Block, error) { + header := block.Header() + // Don't hold the val fields for the entire sealing procedure + p.lock.RLock() + val, signFn := p.val, p.signFn + p.lock.RUnlock() + sig, err := signFn(accounts.Account{Address: val}, accounts.MimetypeParlia, ParliaRLP(header, p.chainConfig.ChainID)) + if err != nil { + log.Error("Sign for the block header failed when sealing", "err", err) + return nil, err + } + copy(header.Extra[len(header.Extra)-extraSeal:], sig) + block = block.WithSeal(header) + return block, nil +} + // Seal implements consensus.Engine, attempting to create a sealed block using // the local signing credentials. func (p *Parlia) Seal(chain consensus.ChainHeaderReader, block *types.Block, results chan<- *types.Block, stop <-chan struct{}) error { @@ -2118,6 +2135,10 @@ func (p *Parlia) backOffTime(snap *Snapshot, header *types.Header, val common.Ad backOffSteps[i], backOffSteps[j] = backOffSteps[j], backOffSteps[i] }) + for i := uint64(0); i < uint64(n); i++ { + log.Debug("backOffTime", "Number", header.Number, "val", validators[i], "delay", delay+backOffSteps[i]*wiggleTime) + } + delay += backOffSteps[idx] * wiggleTime return delay } diff --git a/core/types/block.go b/core/types/block.go index a23601eecd..4fb68de08c 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -20,6 +20,7 @@ package types import ( "crypto/sha256" "encoding/binary" + "encoding/json" "fmt" "io" "math/big" @@ -604,6 +605,14 @@ func (b *Block) WithWitness(witness *ExecutionWitness) *Block { } } +func (b *Block) DeepCopySidecars(sidecars BlobSidecars) { + b.sidecars = make(BlobSidecars, len(sidecars)) + if len(sidecars) != 0 { + buffer, _ := json.Marshal(sidecars) + json.Unmarshal(buffer, &b.sidecars) + } +} + // Hash returns the keccak256 hash of b's header. // The hash is computed on the first call and cached thereafter. func (b *Block) Hash() common.Hash { diff --git a/core/vote/vote_manager.go b/core/vote/vote_manager.go index 891785482b..35b3412f47 100644 --- a/core/vote/vote_manager.go +++ b/core/vote/vote_manager.go @@ -31,6 +31,7 @@ var notContinuousJustified = metrics.NewRegisteredCounter("votesManager/notConti // Backend wraps all methods required for voting. type Backend interface { IsMining() bool + VoteEnabled() bool EventMux() *event.TypeMux } @@ -136,6 +137,11 @@ func (voteManager *VoteManager) loop() { log.Debug("skip voting because mining is disabled, continue") continue } + if !voteManager.eth.VoteEnabled() { + log.Debug("skip voting because voting is disabled, continue") + continue + } + blockCountSinceMining++ if blockCountSinceMining <= blocksNumberSinceMining { log.Debug("skip voting", "blockCountSinceMining", blockCountSinceMining, "blocksNumberSinceMining", blocksNumberSinceMining) diff --git a/core/vote/vote_pool_test.go b/core/vote/vote_pool_test.go index 8fb3f3daec..5d3b39e66a 100644 --- a/core/vote/vote_pool_test.go +++ b/core/vote/vote_pool_test.go @@ -77,6 +77,7 @@ func newTestBackend() *testBackend { return &testBackend{eventMux: new(event.TypeMux)} } func (b *testBackend) IsMining() bool { return true } +func (b *testBackend) VoteEnabled() bool { return true } func (b *testBackend) EventMux() *event.TypeMux { return b.eventMux } func (mp *mockPOSA) GetJustifiedNumberAndHash(chain consensus.ChainHeaderReader, headers []*types.Header) (uint64, common.Hash, error) { diff --git a/eth/api_miner.go b/eth/api_miner.go index 56db9e94b1..701d3a48ca 100644 --- a/eth/api_miner.go +++ b/eth/api_miner.go @@ -20,6 +20,7 @@ import ( "math/big" "time" + "github.com/ethereum/go-ethereum/miner/minerconfig" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/common" @@ -117,3 +118,37 @@ func (api *MinerAPI) AddBuilder(builder common.Address, url string) error { func (api *MinerAPI) RemoveBuilder(builder common.Address) error { return api.e.APIBackend.RemoveBuilder(builder) } + +func (api *MinerAPI) MBConfig() minerconfig.MBConfig { + return api.e.Miner().MBConfig() +} + +func (api *MinerAPI) ResetMaliciousBehavior() minerconfig.MBConfig { + api.e.Miner().ResetMaliciousBehavior() + return api.e.Miner().MBConfig() +} + +func (api *MinerAPI) SetDoubleSign(on bool) minerconfig.MBConfig { + api.e.Miner().SetDoubleSign(on) + return api.e.Miner().MBConfig() +} + +func (api *MinerAPI) SetVoteDisable(on bool) minerconfig.MBConfig { + api.e.Miner().SetVoteDisable(on) + return api.e.Miner().MBConfig() +} + +func (api *MinerAPI) SetSkipOffsetInturn(offset uint64) minerconfig.MBConfig { + api.e.Miner().SetSkipOffsetInturn(offset) + return api.e.Miner().MBConfig() +} + +func (api *MinerAPI) SetBroadcastDelayBlocks(num uint64) minerconfig.MBConfig { + api.e.Miner().SetBroadcastDelayBlocks(num) + return api.e.Miner().MBConfig() +} + +func (api *MinerAPI) SetLastBlockMiningTime(time uint64) minerconfig.MBConfig { + api.e.Miner().SetLastBlockMiningTime(time) + return api.e.Miner().MBConfig() +} diff --git a/eth/backend.go b/eth/backend.go index b14a99e936..7cc503d70c 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -540,6 +540,7 @@ func (s *Ethereum) StopMining() { } func (s *Ethereum) IsMining() bool { return s.miner.Mining() } +func (s *Ethereum) VoteEnabled() bool { return s.miner.VoteEnabled() } func (s *Ethereum) Miner() *miner.Miner { return s.miner } func (s *Ethereum) AccountManager() *accounts.Manager { return s.accountManager } diff --git a/miner/miner.go b/miner/miner.go index c54235f5ff..bd3b0a1b91 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -170,6 +170,10 @@ func (miner *Miner) Mining() bool { return miner.worker.isRunning() } +func (miner *Miner) VoteEnabled() bool { + return miner.worker.config.VoteEnable && !miner.worker.config.MB.VoteDisable +} + func (miner *Miner) InTurn() bool { return miner.worker.inTurn() } @@ -225,6 +229,34 @@ func (miner *Miner) SetGasCeil(ceil uint64) { miner.worker.setGasCeil(ceil) } +func (miner *Miner) MBConfig() minerconfig.MBConfig { + return miner.worker.config.MB +} + +func (miner *Miner) ResetMaliciousBehavior() { + miner.worker.config.MB = minerconfig.DefaultMBConfig +} + +func (miner *Miner) SetDoubleSign(on bool) { + miner.worker.config.MB.DoubleSign = on +} + +func (miner *Miner) SetVoteDisable(on bool) { + miner.worker.config.MB.VoteDisable = on +} + +func (miner *Miner) SetSkipOffsetInturn(offset uint64) { + miner.worker.config.MB.SkipOffsetInturn = &offset +} + +func (miner *Miner) SetBroadcastDelayBlocks(num uint64) { + miner.worker.config.MB.BroadcastDelayBlocks = num +} + +func (miner *Miner) SetLastBlockMiningTime(time uint64) { + miner.worker.config.MB.LastBlockMiningTime = time +} + // SubscribePendingLogs starts delivering logs from pending transactions // to the given channel. func (miner *Miner) SubscribePendingLogs(ch chan<- []*types.Log) event.Subscription { diff --git a/miner/minerconfig/config.go b/miner/minerconfig/config.go index 35b0aa97e8..051356e066 100644 --- a/miner/minerconfig/config.go +++ b/miner/minerconfig/config.go @@ -40,6 +40,7 @@ type Config struct { DisableVoteAttestation bool // Whether to skip assembling vote attestation Mev MevConfig // Mev configuration + MB MBConfig // Malicious behavior configuration } // DefaultConfig contains default settings for miner. @@ -55,6 +56,7 @@ var DefaultConfig = Config{ DelayLeftOver: 50 * time.Millisecond, Mev: DefaultMevConfig, + MB: DefaultMBConfig, } type BuilderConfig struct { @@ -79,3 +81,24 @@ var DefaultMevConfig = MevConfig{ ValidatorCommission: 100, BidSimulationLeftOver: 50 * time.Millisecond, } + +//go:generate go run github.com/fjl/gencodec -type MBConfig -formats toml -out gen_mb_config.go +type MBConfig struct { + // Generate two consecutive blocks for the same parent block + DoubleSign bool + // Disable voting for Fast Finality + VoteDisable bool + // Skip block production for in-turn validators at a specified offset + SkipOffsetInturn *uint64 `toml:",omitempty"` + // Delay broadcasting mined blocks by a specified number of blocks, only for in turn validators + BroadcastDelayBlocks uint64 + // Mining time (milliseconds) for the last block in every turn + LastBlockMiningTime uint64 +} + +var DefaultMBConfig = MBConfig{ + DoubleSign: false, + VoteDisable: false, + BroadcastDelayBlocks: 0, + LastBlockMiningTime: 0, +} diff --git a/miner/minerconfig/gen_mb_config.go b/miner/minerconfig/gen_mb_config.go new file mode 100644 index 0000000000..0da4eca403 --- /dev/null +++ b/miner/minerconfig/gen_mb_config.go @@ -0,0 +1,52 @@ +// Code generated by github.com/fjl/gencodec. DO NOT EDIT. + +package minerconfig + +// MarshalTOML marshals as TOML. +func (m MBConfig) MarshalTOML() (interface{}, error) { + type MBConfig struct { + DoubleSign bool + VoteDisable bool + SkipOffsetInturn *uint64 `toml:",omitempty"` + BroadcastDelayBlocks uint64 + LastBlockMiningTime uint64 + } + var enc MBConfig + enc.DoubleSign = m.DoubleSign + enc.VoteDisable = m.VoteDisable + enc.SkipOffsetInturn = m.SkipOffsetInturn + enc.BroadcastDelayBlocks = m.BroadcastDelayBlocks + enc.LastBlockMiningTime = m.LastBlockMiningTime + return &enc, nil +} + +// UnmarshalTOML unmarshals from TOML. +func (m *MBConfig) UnmarshalTOML(unmarshal func(interface{}) error) error { + type MBConfig struct { + DoubleSign *bool + VoteDisable *bool + SkipOffsetInturn *uint64 `toml:",omitempty"` + BroadcastDelayBlocks *uint64 + LastBlockMiningTime *uint64 + } + var dec MBConfig + if err := unmarshal(&dec); err != nil { + return err + } + if dec.DoubleSign != nil { + m.DoubleSign = *dec.DoubleSign + } + if dec.VoteDisable != nil { + m.VoteDisable = *dec.VoteDisable + } + if dec.SkipOffsetInturn != nil { + m.SkipOffsetInturn = dec.SkipOffsetInturn + } + if dec.BroadcastDelayBlocks != nil { + m.BroadcastDelayBlocks = *dec.BroadcastDelayBlocks + } + if dec.LastBlockMiningTime != nil { + m.LastBlockMiningTime = *dec.LastBlockMiningTime + } + return nil +} diff --git a/miner/worker.go b/miner/worker.go index e600e17546..b7a05e6de2 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -45,6 +45,7 @@ import ( "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/miner/minerconfig" "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/trie" ) @@ -253,6 +254,10 @@ type worker struct { fullTaskHook func() // Method to call before pushing the full sealing task. resubmitHook func(time.Duration, time.Duration) // Method to call upon updating resubmitting interval. recentMinedBlocks *lru.Cache + + // Test purpose + delayedBlocksForBroadcast []*types.Block + delayedMu sync.RWMutex } func newWorker(config *minerconfig.Config, engine consensus.Engine, eth Backend, mux *event.TypeMux, init bool) *worker { @@ -296,6 +301,8 @@ func newWorker(config *minerconfig.Config, engine consensus.Engine, eth Backend, go worker.newWorkLoop(recommit) go worker.resultLoop() go worker.taskLoop() + worker.wg.Add(1) + go worker.delayBlocksBroadcastLoop() // Submit first work to initialize pending state. if init { @@ -642,6 +649,7 @@ func (w *worker) resultLoop() { w.recentMinedBlocks.Add(block.NumberU64(), []common.Hash{block.ParentHash()}) } + inturn := w.inTurn() // Commit block and state to database. task.state.SetExpectedStateRoot(block.Root()) start := time.Now() @@ -657,7 +665,29 @@ func (w *worker) resultLoop() { writeBlockTimer.UpdateSince(start) log.Info("Successfully sealed new block", "number", block.Number(), "sealhash", sealhash, "hash", hash, "elapsed", common.PrettyDuration(time.Since(task.createdAt))) - w.mux.Post(core.NewMinedBlockEvent{Block: block}) + w.postBlock(block, inturn) + if p, ok := w.engine.(*parlia.Parlia); ok { + if w.config.MB.DoubleSign { + shadowHeader := block.Header() + shadowHeader.Extra[0] = 'd' + shadowHeader.Extra[1] = 's' + shadowBlock := types.NewBlockWithHeader(shadowHeader).WithBody(types.Body{Transactions: block.Transactions(), Uncles: block.Uncles()}).WithWithdrawals(block.Withdrawals()) + shadowBlock.DeepCopySidecars(block.Sidecars()) + shadowBlock, err := p.AssembleSignature(shadowBlock) + if err == nil { + w.postBlock(shadowBlock, inturn) + sealhash := w.engine.SealHash(shadowBlock.Header()) + hash := shadowBlock.Hash() + log.Info("Successfully sealed new block", "number", shadowBlock.Number(), "sealhash", sealhash, "hash", hash, + "elapsed", common.PrettyDuration(time.Since(task.createdAt))) + if len(block.Sidecars()) != 0 { + log.Debug("show sidecars", "block.Sidecars()[0].BlockHash", block.Sidecars()[0].BlockHash, "shadowBlock.Sidecars()[0].BlockHash", shadowBlock.Sidecars()[0].BlockHash) + } + } else { + log.Info("Failed to AssembleSignature", "err", err) + } + } + } case <-w.exitCh: return @@ -665,6 +695,63 @@ func (w *worker) resultLoop() { } } +func (w *worker) postBlock(block *types.Block, inTurn bool) { + if w.config.MB.BroadcastDelayBlocks > 0 && inTurn { + w.delayedMu.Lock() + w.delayedBlocksForBroadcast = append(w.delayedBlocksForBroadcast, block) + w.delayedMu.Unlock() + } else { + w.mux.Post(core.NewMinedBlockEvent{Block: block}) + } +} +func (w *worker) delayBlocksBroadcastLoop() { + defer w.wg.Done() + + for { + if len(w.delayedBlocksForBroadcast) > 0 && w.config.MB.BroadcastDelayBlocks > 0 { + w.delayedMu.Lock() + + currentBlock := w.chain.CurrentBlock() + currentBlockNum := currentBlock.Number.Uint64() + + delayTime := (w.config.MB.BroadcastDelayBlocks - 1) * w.chainConfig.Parlia.Period + if p, ok := w.engine.(*parlia.Parlia); ok { + service := p.APIs(w.chain)[0].Service + latestBlockNumber := rpc.LatestBlockNumber + currentTurnLength, err := service.(*parlia.API).GetTurnLength(&latestBlockNumber) + nonInTurnBackoff := w.config.MB.BroadcastDelayBlocks + if err == nil { + if w.config.MB.BroadcastDelayBlocks > uint64(currentTurnLength) { + // suppose extra blocks are generated by in turn validators + nonInTurnBackoff = uint64(currentTurnLength) + } + } + delayTime += nonInTurnBackoff + } + + firstBlock := w.delayedBlocksForBroadcast[0] + if uint64(time.Now().Unix()) >= (firstBlock.Time() + delayTime) { + time.Sleep(500 * time.Microsecond) + for _, block := range w.delayedBlocksForBroadcast { + w.mux.Post(core.NewMinedBlockEvent{Block: block}) + log.Info("delayBlocksBroadcastLoop", "number", block.Number(), "hash", block.Hash(), + "time", block.Time(), "now", uint64(time.Now().Unix()), "currentBlockNum", currentBlockNum) + } + w.delayedBlocksForBroadcast = make([]*types.Block, 0) + } + + w.delayedMu.Unlock() + } + + select { + case <-w.exitCh: + return + default: + time.Sleep(100 * time.Millisecond) + } + } +} + // makeEnv creates a new environment for the sealing block. func (w *worker) makeEnv(parent *types.Header, header *types.Header, coinbase common.Address, prevEnv *environment, witness bool) (*environment, error) { @@ -1229,6 +1316,25 @@ func (w *worker) commitWork(interruptCh chan int32, timestamp int64) { return } } + if w.config.MB.SkipOffsetInturn != nil { + if w.inTurn() { + if p, ok := w.engine.(*parlia.Parlia); ok { + service := p.APIs(w.chain)[0].Service + latestBlockNumber := rpc.LatestBlockNumber + currentTurnLength, err := service.(*parlia.API).GetTurnLength(&latestBlockNumber) + if err == nil { + currentHeader := w.chain.CurrentBlock() + blockToMine := currentHeader.Number.Uint64() + 1 + if *w.config.MB.SkipOffsetInturn == blockToMine%uint64(currentTurnLength) { + log.Info("skip commitWork", "blockNumber", blockToMine) + return + } + } else { + log.Error("commitWork|GetTurnLength", "err", err) + } + } + } + } stopTimer := time.NewTimer(0) defer stopTimer.Stop() @@ -1267,6 +1373,20 @@ LOOP: workList = append(workList, work) delay := w.engine.Delay(w.chain, work.header, &w.config.DelayLeftOver) + if p, ok := w.engine.(*parlia.Parlia); ok { + if w.config.MB.LastBlockMiningTime > w.chainConfig.Parlia.Period*1000/2 { + service := p.APIs(w.chain)[0].Service + latestBlockNumber := rpc.LatestBlockNumber + currentTurnLength, err := service.(*parlia.API).GetTurnLength(&latestBlockNumber) + if err == nil && (work.header.Number.Uint64()+1)%uint64(currentTurnLength) == 0 { + *delay += time.Duration((w.config.MB.LastBlockMiningTime - w.chainConfig.Parlia.Period*1000/2) * uint64(time.Millisecond)) + timeLeft := time.Until(time.Unix(int64(work.header.Time), 0)) + if *delay > timeLeft { + *delay = timeLeft + } + } + } + } if delay == nil { log.Warn("commitWork delay is nil, something is wrong") stopTimer = nil