Skip to content
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
30 changes: 30 additions & 0 deletions consensus/engine/rejected_block.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package engine

import (
"errors"
"fmt"

"github.com/ethereum/go-ethereum/common"
)

// ErrRejectedBlock is returned for a block that consensus must never accept.
var ErrRejectedBlock = errors.New("block rejected by hash")

var rejectedBlockHashes = map[common.Hash]struct{}{
// Shard 0 retains block 92,730,034. Reject its original child so a dirty
// rolled-back database cannot reattach the abandoned branch.
common.HexToHash("0x5de06979a333f20afb8b245a8cf44472dc5bfc7383a57ddee48e1809bcee7c5d"): {},
// Keep the first confirmed malicious shard-0 block rejected as defense in
// depth, including for embedded block references.
common.HexToHash("0x890473cdb9aa8dc5c0bbd54cf20b6d8d84bda60d3dcb2273443d34432d8539e8"): {},
common.HexToHash("0xc936581d391b74a620bf6636519834b14a9a2d4e9a5154867c8407f219d8a878"): {},
}

// ValidateBlockHash rejects an abandoned chain anchor. Descendants cannot
// attach once their anchor is rejected.
func ValidateBlockHash(hash common.Hash) error {
if _, rejected := rejectedBlockHashes[hash]; rejected {
return fmt.Errorf("%w: %s", ErrRejectedBlock, hash.Hex())
}
return nil
}
42 changes: 42 additions & 0 deletions consensus/engine/rejected_block_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package engine

import (
"errors"
"testing"

"github.com/ethereum/go-ethereum/common"
)

func TestValidateBlockHashRejectsAbandonedChainAnchors(t *testing.T) {
tests := []struct {
name string
hash common.Hash
want error
}{
{
name: "reject shard 0 first abandoned child at block 92730035",
hash: common.HexToHash("0x5de06979a333f20afb8b245a8cf44472dc5bfc7383a57ddee48e1809bcee7c5d"),
want: ErrRejectedBlock,
},
{
name: "reject shard 0 first confirmed malicious block at 92730036",
hash: common.HexToHash("0x890473cdb9aa8dc5c0bbd54cf20b6d8d84bda60d3dcb2273443d34432d8539e8"),
want: ErrRejectedBlock,
},
{
name: "reject shard 1 abandoned chain anchor",
hash: common.HexToHash("0xc936581d391b74a620bf6636519834b14a9a2d4e9a5154867c8407f219d8a878"),
want: ErrRejectedBlock,
},
{name: "allow replacement hash", hash: common.HexToHash("0x01")},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := ValidateBlockHash(test.hash)
if !errors.Is(err, test.want) {
t.Fatalf("ValidateBlockHash(%s) error = %v, want %v", test.hash.Hex(), err, test.want)
}
})
}
}
21 changes: 21 additions & 0 deletions consensus/rejected_block_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package consensus

import (
"errors"
"testing"

"github.com/ethereum/go-ethereum/common"
consensusengine "github.com/harmony-one/harmony/consensus/engine"
)

func TestValidateNewBlockRejectsAbandonedChainAnchorBeforeVerifiedCache(t *testing.T) {
hash := common.HexToHash("0x890473cdb9aa8dc5c0bbd54cf20b6d8d84bda60d3dcb2273443d34432d8539e8")
log := NewFBFTLog()
log.verifiedBlocks[hash] = struct{}{}
consensus := &Consensus{fBFTLog: log}

_, err := consensus.validateNewBlock(&FBFTMessage{BlockHash: hash})
if !errors.Is(err, consensusengine.ErrRejectedBlock) {
t.Fatalf("validateNewBlock() error = %v, want %v", err, consensusengine.ErrRejectedBlock)
}
}
14 changes: 14 additions & 0 deletions consensus/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/ethereum/go-ethereum/rlp"

msg_pb "github.com/harmony-one/harmony/api/proto/message"
consensusengine "github.com/harmony-one/harmony/consensus/engine"
"github.com/harmony-one/harmony/consensus/signature"
"github.com/harmony-one/harmony/core/types"
"github.com/harmony-one/harmony/crypto/bls"
Expand All @@ -26,6 +27,16 @@ func (consensus *Consensus) onAnnounce(msg *msg_pb.Message) {
Msg("[OnAnnounce] Unparseable leader message")
return
}
// Reject an abandoned block before it can enter the FBFT log or cause this
// validator to sign PREPARE. validateNewBlock repeats this check for every
// other entry point.
if err := consensusengine.ValidateBlockHash(recvMsg.BlockHash); err != nil {
consensus.getLogger().Warn().Err(err).
Uint64("MsgBlockNum", recvMsg.BlockNum).
Str("MsgBlockHash", recvMsg.BlockHash.Hex()).
Msg("[OnAnnounce] Rejected block")
return
}

// NOTE let it handle its own logs
if !consensus.onAnnounceSanityChecks(recvMsg) {
Expand Down Expand Up @@ -86,6 +97,9 @@ func (consensus *Consensus) ValidateNewBlock(recvMsg *FBFTMessage) (*types.Block
return consensus.validateNewBlock(recvMsg)
}
func (consensus *Consensus) validateNewBlock(recvMsg *FBFTMessage) (*types.Block, error) {
if err := consensusengine.ValidateBlockHash(recvMsg.BlockHash); err != nil {
return nil, err
}
if consensus.fBFTLog.IsBlockVerified(recvMsg.BlockHash) {
var blockObj *types.Block

Expand Down
3 changes: 3 additions & 0 deletions core/block_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ func NewBlockValidator(blockchain BlockChain) *BlockValidator {
// ValidateBody verifies the block header's transaction root.
// The headers are assumed to be already validated at this point.
func (v *BlockValidator) ValidateBody(block *types.Block) error {
if err := validateBlockHashes(block); err != nil {
return err
}
// Check whether the block's known, and if not, that it's linkable
if v.bc.HasBlockAndState(block.Hash(), block.NumberU64()) {
return errors.WithMessage(ErrKnownBlock, "validate body: has block and state")
Expand Down
21 changes: 21 additions & 0 deletions core/blockchain_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -951,6 +951,9 @@ func (bc *BlockChainImpl) ExportN(w io.Writer, first uint64, last uint64) error
}

func (bc *BlockChainImpl) WriteHeadBlock(block *types.Block) error {
if err := validateBlockHashes(block); err != nil {
return err
}
return bc.writeHeadBlock(block)
}

Expand Down Expand Up @@ -1008,6 +1011,9 @@ func (bc *BlockChainImpl) writeHeadBlock(block *types.Block) error {

// tikvFastForward writes a new head block in tikv mode, used for reader node or follower writer node
func (bc *BlockChainImpl) tikvFastForward(block *types.Block, logs []*types.Log) error {
if err := validateBlockHashes(block); err != nil {
return err
}
bc.currentBlock.Store(block)
headBlockGauge.Update(int64(block.NumberU64()))

Expand Down Expand Up @@ -1439,6 +1445,9 @@ func (bc *BlockChainImpl) InsertReceiptChain(blockChain types.Blocks, receiptCha
batch = bc.db.NewBatch()
)
for i, block := range blockChain {
if err := validateBlockHashes(block); err != nil {
return i, err
}
receipts := receiptChain[i]
// Short circuit insertion if shutting down or processing failed
if atomic.LoadInt32(&bc.procInterrupt) == 1 {
Expand Down Expand Up @@ -1523,6 +1532,9 @@ func (bc *BlockChainImpl) InsertReceiptChain(blockChain types.Blocks, receiptCha
var lastWrite uint64

func (bc *BlockChainImpl) WriteBlockWithoutState(block *types.Block) (err error) {
if err := validateBlockHashes(block); err != nil {
return err
}
bc.chainmu.Lock()
defer bc.chainmu.Unlock()

Expand All @@ -1540,6 +1552,9 @@ func (bc *BlockChainImpl) WriteBlockWithState(
paid reward.Reader,
state *state.DB,
) (status WriteStatus, err error) {
if err := validateBlockHashes(block); err != nil {
return NonStatTy, err
}
currentBlock := bc.CurrentBlock()
if currentBlock == nil {
return NonStatTy, errors.New("Current block is nil")
Expand Down Expand Up @@ -1688,6 +1703,12 @@ func (bc *BlockChainImpl) GetMaxGarbageCollectedBlockNumber() int64 {
}

func (bc *BlockChainImpl) InsertChain(chain types.Blocks, verifyHeaders bool) (int, error) {
for i, block := range chain {
if err := validateBlockHashes(block); err != nil {
return i, err
}
}

// if in tikv mode, writer node need preempt master or come be a follower
if bc.isInitTiKV() && !bc.tikvPreemptMaster(bc.rangeBlock(chain)) {
return len(chain), nil
Expand Down
6 changes: 6 additions & 0 deletions core/epochchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ func (bc *EpochChain) InsertChain(blocks types.Blocks, _ bool) (int, error) {
<-bc.mu
}()
for i, block := range blocks {
if err := validateBlockHashes(block); err != nil {
return i, err
}
if !block.IsLastBlockInEpoch() {
return i, ErrNotLastBlockInEpoch
}
Expand Down Expand Up @@ -282,6 +285,9 @@ func (bc *EpochChain) writeShardStateBytes(db rawdb.DatabaseWriter,

// WriteHeadBlock writes a new head block.
func (bc *EpochChain) WriteHeadBlock(block *types.Block) error {
if err := validateBlockHashes(block); err != nil {
return err
}
batch := bc.db.NewBatch()
se, err := bc.writeHeadBlock(batch, block)
if err != nil {
Expand Down
3 changes: 3 additions & 0 deletions core/offchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ func (bc *BlockChainImpl) CommitOffChainData(
payout reward.Reader,
state *state.DB,
) (status WriteStatus, err error) {
if err := validateBlockHashes(block); err != nil {
return NonStatTy, err
}
// Write receipts of the block
if err := rawdb.WriteReceipts(batch, block.Hash(), block.NumberU64(), receipts); err != nil {
return NonStatTy, err
Expand Down
41 changes: 41 additions & 0 deletions core/rejected_block.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package core

import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/rlp"
blockv0 "github.com/harmony-one/harmony/block/v0"
consensus_engine "github.com/harmony-one/harmony/consensus/engine"
"github.com/harmony-one/harmony/core/types"
)

func validateBlockHashes(block *types.Block) error {
return validateBlockHashesWith(block, consensus_engine.ValidateBlockHash)
}

func validateBlockHashesWith(block *types.Block, validateHash func(common.Hash) error) error {
if err := validateHash(block.Hash()); err != nil {
return err
}

_, isV0 := block.Header().Header.(*blockv0.Header)
if !isV0 {
encoded := block.Header().CrossLinks()
var crossLinks types.CrossLinks
if len(encoded) > 0 && rlp.DecodeBytes(encoded, &crossLinks) == nil {
for i := range crossLinks {
if err := validateHash(crossLinks[i].Hash()); err != nil {
return err
}
}
}
}

for _, proof := range block.IncomingReceipts() {
if proof != nil && proof.Header != nil {
if err := validateHash(proof.Header.Hash()); err != nil {
return err
}
}
}
return nil
}
76 changes: 76 additions & 0 deletions core/rejected_block_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package core

import (
"errors"
"math/big"
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/rlp"
blockfactory "github.com/harmony-one/harmony/block/factory"
"github.com/harmony-one/harmony/consensus/engine"
"github.com/harmony-one/harmony/core/types"
)

const rejectedShard1BlockHash = "0xc936581d391b74a620bf6636519834b14a9a2d4e9a5154867c8407f219d8a878"

func TestValidateBlockHashesRejectsEmbeddedCrossLink(t *testing.T) {
block := blockWithRejectedCrossLink(t)

if err := validateBlockHashes(block); !errors.Is(err, engine.ErrRejectedBlock) {
t.Fatalf("validateBlockHashes() error = %v, want %v", err, engine.ErrRejectedBlock)
}
}

func TestWriteBlockWithoutStateRejectsEmbeddedCrossLinkBeforeDatabaseWrite(t *testing.T) {
block := blockWithRejectedCrossLink(t)
var chain *BlockChainImpl

if err := chain.WriteBlockWithoutState(block); !errors.Is(err, engine.ErrRejectedBlock) {
t.Fatalf("WriteBlockWithoutState() error = %v, want %v", err, engine.ErrRejectedBlock)
}
}

func blockWithRejectedCrossLink(t *testing.T) *types.Block {
t.Helper()
crossLinks := types.CrossLinks{{
ShardIDF: 1,
BlockNumberF: big.NewInt(94978279),
ViewIDF: new(big.Int),
HashF: common.HexToHash(rejectedShard1BlockHash),
EpochF: new(big.Int),
}}
encoded, err := rlp.EncodeToBytes(crossLinks)
if err != nil {
t.Fatal(err)
}
header := blockfactory.NewTestHeader().With().CrossLinks(encoded).Header()
return types.NewBlock(header, nil, nil, nil, nil, nil)
}

func TestValidateBlockHashesRejectsIncomingReceiptSource(t *testing.T) {
rejectedHeader := blockfactory.NewTestHeader().With().Extra([]byte("rejected source header")).Header()
block := types.NewBlock(
blockfactory.NewTestHeader(), nil, nil, nil,
[]*types.CXReceiptsProof{{Header: rejectedHeader}}, nil,
)

validateHash := func(hash common.Hash) error {
if hash == rejectedHeader.Hash() {
return engine.ErrRejectedBlock
}
return nil
}
if err := validateBlockHashesWith(block, validateHash); !errors.Is(err, engine.ErrRejectedBlock) {
t.Fatalf("validateBlockHashes() error = %v, want %v", err, engine.ErrRejectedBlock)
}
}

func TestValidateBlockHashesLeavesMalformedCrossLinksToSemanticValidation(t *testing.T) {
header := blockfactory.NewTestHeader().With().CrossLinks([]byte("not rlp")).Header()
block := types.NewBlock(header, nil, nil, nil, nil, nil)

if err := validateBlockHashes(block); err != nil {
t.Fatalf("validateBlockHashes() error = %v, want nil", err)
}
}
9 changes: 9 additions & 0 deletions internal/chain/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ func NewEngine() *engineImpl {
// VerifyHeader checks whether a header conforms to the consensus rules of the bft engine.
// Note that each block header contains the bls signature of the parent block
func (e *engineImpl) VerifyHeader(chain engine.ChainReader, header *block.Header, seal bool) error {
if err := engine.ValidateBlockHash(header.Hash()); err != nil {
return err
}
parentHeader := chain.GetHeader(header.ParentHash(), header.Number().Uint64()-1)
if parentHeader == nil {
return engine.ErrUnknownAncestor
Expand Down Expand Up @@ -636,6 +639,9 @@ func applySlashes(
// i.e. this header verification api is more flexible since the caller specifies which commit signature and bitmap to use
// for verifying the block header, which is necessary for cross-shard block header verification. Example of such is cross-shard transaction.
func (e *engineImpl) VerifyHeaderSignature(chain engine.ChainReader, header *block.Header, commitSig bls_cosi.SerializedSignature, commitBitmap []byte) error {
if err := engine.ValidateBlockHash(header.Hash()); err != nil {
return err
}
if chain.CurrentHeader().Number().Uint64() <= uint64(1) {
return nil
}
Expand All @@ -647,6 +653,9 @@ func (e *engineImpl) VerifyHeaderSignature(chain engine.ChainReader, header *blo

// VerifyCrossLink verifies the signature of the given CrossLink.
func (e *engineImpl) VerifyCrossLink(chain engine.ChainReader, cl types.CrossLink) error {
if err := engine.ValidateBlockHash(cl.Hash()); err != nil {
return err
}
if cl.BlockNum() <= 1 {
return errors.New("crossLink BlockNumber should greater than 1")
}
Expand Down
Loading