diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 9ed921c24f..9d1c3ae95b 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -172,6 +172,7 @@ var ( configFileFlag, utils.BlockAmountReserved, utils.CheckSnapshotWithMPT, + utils.EnableDoubleSignMonitorFlag, } rpcFlags = []cli.Flag{ diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index c48c1bcfce..b8ccb9317b 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -900,6 +900,11 @@ var ( Name: "check-snapshot-with-mpt", Usage: "Enable checking between snapshot and MPT ", } + + EnableDoubleSignMonitorFlag = cli.BoolFlag{ + Name: "monitor.doublesign", + Usage: "Enable double sign monitor to check whether any validator signs multiple blocks", + } ) // MakeDataDir retrieves the currently requested data directory, terminating @@ -1166,6 +1171,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) { + if ctx.GlobalBool(EnableDoubleSignMonitorFlag.Name) { + cfg.EnableDoubleSignMonitor = true + } +} + // MakeDatabaseHandles raises out the number of allowed file handles per process // for Geth and returns half of the allowance to assign to the database. func MakeDatabaseHandles() int { @@ -1328,6 +1341,7 @@ func SetNodeConfig(ctx *cli.Context, cfg *node.Config) { setNodeUserIdent(ctx, cfg) setDataDir(ctx, cfg) setSmartCard(ctx, cfg) + setMonitor(ctx, cfg) if ctx.GlobalIsSet(ExternalSignerFlag.Name) { cfg.ExternalSigner = ctx.GlobalString(ExternalSignerFlag.Name) diff --git a/consensus/beacon/consensus.go b/consensus/beacon/consensus.go index cd493f3d65..0e96e87426 100644 --- a/consensus/beacon/consensus.go +++ b/consensus/beacon/consensus.go @@ -171,10 +171,11 @@ func (beacon *Beacon) VerifyUncles(chain consensus.ChainReader, block *types.Blo // verifyHeader checks whether a header conforms to the consensus rules of the // stock Ethereum consensus engine. The difference between the beacon and classic is // (a) The following fields are expected to be constants: -// - difficulty is expected to be 0 -// - nonce is expected to be 0 -// - unclehash is expected to be Hash(emptyHeader) +// - difficulty is expected to be 0 +// - nonce is expected to be 0 +// - unclehash is expected to be Hash(emptyHeader) // to be the desired constants +// // (b) the timestamp is not verified anymore // (c) the extradata is limited to 32 bytes func (beacon *Beacon) verifyHeader(chain consensus.ChainHeaderReader, header, parent *types.Header) error { @@ -337,6 +338,11 @@ func (beacon *Beacon) Close() error { return beacon.ethone.Close() } +// ExtraSeal returns fixed number of extra-data suffix bytes reserved for signer seal +func (beacon *Beacon) ExtraSeal() int { + return 0 +} + // IsPoSHeader reports the header belongs to the PoS-stage with some special fields. // This function is not suitable for a part of APIs like Prepare or CalcDifficulty // because the header difficulty is not set yet. diff --git a/consensus/clique/clique.go b/consensus/clique/clique.go index 75ed916a86..f39ca7b24d 100644 --- a/consensus/clique/clique.go +++ b/consensus/clique/clique.go @@ -595,6 +595,11 @@ func (c *Clique) Delay(chain consensus.ChainReader, header *types.Header) *time. return nil } +// ExtraSeal returns fixed number of extra-data suffix bytes reserved for signer seal +func (c *Clique) ExtraSeal() int { + return extraSeal +} + // Seal implements consensus.Engine, attempting to create a sealed block using // the local signing credentials. func (c *Clique) Seal(chain consensus.ChainHeaderReader, block *types.Block, results chan<- *types.Block, stop <-chan struct{}) error { diff --git a/consensus/consensus.go b/consensus/consensus.go index c3e7b4870a..db4f9c73e4 100644 --- a/consensus/consensus.go +++ b/consensus/consensus.go @@ -118,6 +118,8 @@ type Engine interface { // SealHash returns the hash of a block prior to it being sealed. SealHash(header *types.Header) common.Hash + ExtraSeal() int + // CalcDifficulty is the difficulty adjustment algorithm. It returns the difficulty // that a new block should have. CalcDifficulty(chain ChainHeaderReader, time uint64, parent *types.Header) *big.Int diff --git a/consensus/ethash/consensus.go b/consensus/ethash/consensus.go index be6085c713..4b8809c327 100644 --- a/consensus/ethash/consensus.go +++ b/consensus/ethash/consensus.go @@ -614,6 +614,11 @@ func (ethash *Ethash) Delay(_ consensus.ChainReader, _ *types.Header) *time.Dura return nil } +// ExtraSeal returns fixed number of extra-data suffix bytes reserved for signer seal +func (ethash *Ethash) ExtraSeal() int { + return 0 +} + // SealHash returns the hash of a block prior to it being sealed. func (ethash *Ethash) SealHash(header *types.Header) (hash common.Hash) { hasher := sha3.NewLegacyKeccak256() diff --git a/consensus/parlia/parlia.go b/consensus/parlia/parlia.go index fee0fe1293..1b8858ec84 100644 --- a/consensus/parlia/parlia.go +++ b/consensus/parlia/parlia.go @@ -808,6 +808,11 @@ func (p *Parlia) Delay(chain consensus.ChainReader, header *types.Header) *time. return &delay } +// ExtraSeal returns fixed number of extra-data suffix bytes reserved for signer seal +func (p *Parlia) ExtraSeal() int { + return extraSeal +} + // 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 { diff --git a/core/blockchain.go b/core/blockchain.go index 572b80bab1..1916fc3fd0 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -34,6 +34,7 @@ import ( "github.com/ethereum/go-ethereum/common/mclock" "github.com/ethereum/go-ethereum/common/prque" "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/core/monitor" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/state/snapshot" @@ -258,6 +259,9 @@ type BlockChain struct { shouldPreserve func(*types.Block) bool // Function used to determine whether should preserve the given block. terminateInsert func(common.Hash, uint64) bool // Testing hook used to terminate ancient receipt chain insertion. + + // monitor + doubleSignMonitor *monitor.DoubleSignMonitor } // NewBlockChain returns a fully initialised block chain using information @@ -504,6 +508,13 @@ func NewBlockChain(db ethdb.Database, cacheConfig *CacheConfig, chainConfig *par bc.wg.Add(1) go bc.rewindInvalidHeaderBlockLoop() } + + if bc.doubleSignMonitor != nil { + bc.wg.Add(1) + go bc.startDoubleSignMonitor() + + } + return bc, nil } @@ -2566,6 +2577,29 @@ func (bc *BlockChain) trustedDiffLayerLoop() { } } +func (bc *BlockChain) startDoubleSignMonitor() { + eventChan := make(chan ChainHeadEvent, monitor.MaxCacheHeader) + sub := bc.SubscribeChainHeadEvent(eventChan) + headerChan := make(chan *types.Header, monitor.MaxCacheHeader) + defer func() { + sub.Unsubscribe() + bc.doubleSignMonitor.Close() + close(eventChan) + close(headerChan) + bc.wg.Done() + }() + + go bc.doubleSignMonitor.Start(headerChan) + for { + select { + case event := <-eventChan: + headerChan <- event.Block.Header() + case <-bc.quit: + return + } + } +} + func (bc *BlockChain) GetUnTrustedDiffLayer(blockHash common.Hash, pid string) *types.DiffLayer { bc.diffMux.RLock() defer bc.diffMux.RUnlock() @@ -2972,6 +3006,11 @@ func EnableBlockValidator(chainConfig *params.ChainConfig, engine consensus.Engi } } +func EnableDoubleSignChecker(bc *BlockChain) (*BlockChain, error) { + bc.doubleSignMonitor = monitor.NewDoubleSignMonitor(bc.engine.ExtraSeal(), bc.engine.SealHash) + return bc, nil +} + func (bc *BlockChain) GetVerifyResult(blockNumber uint64, blockHash common.Hash, diffHash common.Hash) *VerifyResult { var res VerifyResult res.BlockNumber = blockNumber diff --git a/core/monitor/double_sign_mointor.go b/core/monitor/double_sign_mointor.go new file mode 100644 index 0000000000..6d72880527 --- /dev/null +++ b/core/monitor/double_sign_mointor.go @@ -0,0 +1,149 @@ +package monitor + +import ( + "bytes" + "errors" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/prque" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/crypto/secp256k1" + "github.com/ethereum/go-ethereum/log" +) + +const ( + MaxCacheHeader = 100 +) + +func NewDoubleSignMonitor( + extraSeal int, + sealHash func(header *types.Header) (hash common.Hash), +) *DoubleSignMonitor { + return &DoubleSignMonitor{ + sealHash: sealHash, + extraSeal: extraSeal, + headerNumbers: prque.New(nil), + headers: make(map[uint64]*types.Header, MaxCacheHeader), + quit: make(chan struct{}), + } +} + +type DoubleSignMonitor struct { + extraSeal int + sealHash func(header *types.Header) (hash common.Hash) + headerNumbers *prque.Prque + headers map[uint64]*types.Header + quit chan struct{} +} + +func (m *DoubleSignMonitor) getSignature(h *types.Header) ([]byte, error) { + if len(h.Extra) < m.extraSeal { + return nil, errors.New("extra-data 65 byte signature suffix missing") + } + signature := h.Extra[len(h.Extra)-m.extraSeal:] + return signature, nil +} + +func (m *DoubleSignMonitor) extractSignerFromHeader(h *types.Header) (signer common.Address, err error) { + signature, err := m.getSignature(h) + if err != nil { + return + } + pubKey, err := secp256k1.RecoverPubkey(m.sealHash(h).Bytes(), signature) + if err != nil { + return + } + copy(signer[:], crypto.Keccak256(pubKey[1:])[12:]) + return +} + +func (m *DoubleSignMonitor) isDoubleSignHeaders(h1, h2 *types.Header) (bool, []byte, []byte, error) { + if h1 == nil || h2 == nil { + return false, nil, nil, nil + } + if h1.Number.Cmp(h2.Number) != 0 { + return false, nil, nil, nil + } + if bytes.Equal(h1.ParentHash[:], h2.ParentHash[:]) { + return false, nil, nil, nil + } + signature1, err := m.getSignature(h1) + if err != nil { + return false, nil, nil, err + } + signature2, err := m.getSignature(h2) + if err != nil { + return false, nil, nil, err + } + if bytes.Equal(signature1, signature2) { + return false, signature1, signature2, nil + } + + signer1, err := m.extractSignerFromHeader(h1) + if err != nil { + return false, signature1, signature2, err + } + signer2, err := m.extractSignerFromHeader(h2) + if err != nil { + return false, signature1, signature2, err + } + if !bytes.Equal(signer1.Bytes(), signer2.Bytes()) { + return false, signature1, signature2, nil + } + + return true, signature1, signature2, nil +} + +func (m *DoubleSignMonitor) deleteOldHeader() { + v, _ := m.headerNumbers.Pop() + h := v.(*types.Header) + delete(m.headers, h.Number.Uint64()) +} + +func (m *DoubleSignMonitor) checkHeader(h *types.Header) (bool, *types.Header, []byte, []byte, error) { + h2, exist := m.headers[h.Number.Uint64()] + if !exist { + if m.headerNumbers.Size() > MaxCacheHeader { + m.deleteOldHeader() + } + m.headers[h.Number.Uint64()] = h + m.headerNumbers.Push(h, -h.Number.Int64()) + return false, nil, nil, nil, nil + } + + isDoubleSign, s1, s2, err := m.isDoubleSignHeaders(h, h2) + if err != nil { + return false, nil, s1, s2, err + } + if isDoubleSign { + return true, h2, s1, s2, nil + } + + return false, nil, s1, s2, nil +} + +func (m *DoubleSignMonitor) Start(ch <-chan *types.Header) { + for { + select { + case h := <-ch: + isDoubleSign, h2, s1, s2, err := m.checkHeader(h) + if err != nil { + log.Error("check double sign header error", "err", err) + continue + } + if isDoubleSign { + // found a double sign header + log.Error("found a double sign header", "number", h.Number.Uint64(), + "first_hash", h.Hash(), "first_miner", h.Coinbase, "first_signature", s1, + "second_hash", h2.Hash(), "second_miner", h2.Coinbase, "second_signature", s2) + } + case <-m.quit: + return + } + } +} + +func (m *DoubleSignMonitor) Close() { + close(m.quit) +} diff --git a/eth/backend.go b/eth/backend.go index e9c077e6a7..fc0ca6534c 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -221,6 +221,9 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { if config.PersistDiff { bcOps = append(bcOps, core.EnablePersistDiff(config.DiffBlock)) } + if stack.Config().EnableDoubleSignMonitor { + bcOps = append(bcOps, core.EnableDoubleSignChecker) + } peers := newPeerSet() bcOps = append(bcOps, core.EnableBlockValidator(chainConfig, eth.engine, config.TriesVerifyMode, peers)) diff --git a/node/config.go b/node/config.go index 3c8c541420..2e37c23532 100644 --- a/node/config.go +++ b/node/config.go @@ -201,6 +201,9 @@ type Config struct { // AllowUnprotectedTxs allows non EIP-155 protected transactions to be send over RPC. AllowUnprotectedTxs bool `toml:",omitempty"` + + // EnableDoubleSignMonitor is a flag that whether to enable the double signature checker + EnableDoubleSignMonitor bool `toml:",omitempty"` } // IPCEndpoint resolves an IPC endpoint based on a configured value, taking into