From e7d0549e02559ff8c16efa8f7f49b5e572813aab Mon Sep 17 00:00:00 2001 From: Daniel Liu <139250065@qq.com> Date: Sat, 18 Jul 2026 10:28:59 +0800 Subject: [PATCH] feat(core,eth): add sanity checks for unbounded block fields #19573 Add SanityCheck helpers for blocks and headers to reject unreasonably large numeric fields and XDPoS header byte fields before they reach fetcher or downloader processing. Apply the checks to propagated blocks, block header responses, and block body uncle headers, including total-difficulty bounds for NewBlock messages. Cover oversized header, uncle, and XDPoS field cases with focused unit tests. --- consensus/XDPoS/utils/utils.go | 5 +- core/types/block.go | 65 ++++++++++++++++++++++++ core/types/block_test.go | 92 ++++++++++++++++++++++++++++++++++ eth/fetcher/fetcher.go | 6 +-- eth/handler.go | 9 ++++ eth/protocol.go | 27 ++++++++++ eth/protocol_test.go | 23 +++++++++ 7 files changed, 222 insertions(+), 5 deletions(-) diff --git a/consensus/XDPoS/utils/utils.go b/consensus/XDPoS/utils/utils.go index 6af811ad153c..624071f68af1 100644 --- a/consensus/XDPoS/utils/utils.go +++ b/consensus/XDPoS/utils/utils.go @@ -76,8 +76,9 @@ func DecodeBytesExtraFields(b []byte, val interface{}) error { if len(b) == 0 { return errors.New("extra field is 0 length") } - // Prevent payload attack, limit the size of extra field to 20k bytes. Normal Extrafield payload is less than 7k bytes. - if len(b) > 20000 { + // Prevent payload attack, limit the size of extra field to 20k bytes. + // Normal Extrafield payload is less than 7k bytes. + if len(b) > 20*1024 { return errors.New("extra field is too long") } diff --git a/core/types/block.go b/core/types/block.go index 4fad656260ea..63e5d661dd94 100644 --- a/core/types/block.go +++ b/core/types/block.go @@ -153,6 +153,15 @@ func (h *Header) HashNoValidator() common.Hash { var headerSize = common.StorageSize(reflect.TypeOf(Header{}).Size()) +const maxHeaderByteFieldSize = 20 * 1024 + +func checkHeaderByteFieldSize(name string, size int) error { + if size <= maxHeaderByteFieldSize { + return nil + } + return fmt.Errorf("too large block %s: size %d exceeds limit %d", name, size, maxHeaderByteFieldSize) +} + // Size returns the approximate memory used by all internal contents. It is used // to approximate and limit the memory consumption of various caches. func (h *Header) Size() common.StorageSize { @@ -163,6 +172,53 @@ func (h *Header) Size() common.StorageSize { return headerSize + common.StorageSize(len(h.Extra)+len(h.Validators)+len(h.Validator)+len(h.Penalties)+(h.Difficulty.BitLen()+h.Number.BitLen()+baseFeeBits)/8) } +// SanityCheck checks a few basic things -- these checks are way beyond what +// any 'sane' production values should hold, and can mainly be used to prevent +// that the unbounded fields are stuffed with junk data to add processing +// overhead +func (h *Header) SanityCheck() error { + if h.Number != nil && !h.Number.IsUint64() { + return fmt.Errorf("too large block number: bitlen %d", h.Number.BitLen()) + } + if h.Difficulty != nil { + if diffLen := h.Difficulty.BitLen(); diffLen > 80 { + return fmt.Errorf("too large block difficulty: bitlen %d", diffLen) + } + } + for _, field := range []struct { + name string + size int + }{ + {name: "extradata", size: len(h.Extra)}, + {name: "validators", size: len(h.Validators)}, + {name: "validator", size: len(h.Validator)}, + {name: "penalties", size: len(h.Penalties)}, + } { + if err := checkHeaderByteFieldSize(field.name, field.size); err != nil { + return err + } + } + if h.BaseFee != nil { + if bfLen := h.BaseFee.BitLen(); bfLen > 256 { + return fmt.Errorf("too large base fee: bitlen %d", bfLen) + } + } + return nil +} + +// SanityCheckHeaders verifies that a list of headers contains only reasonable values. +func SanityCheckHeaders(headers []*Header) error { + for index, header := range headers { + if header == nil { + return fmt.Errorf("nil block header at index %d", index) + } + if err := header.SanityCheck(); err != nil { + return err + } + } + return nil +} + // EmptyBody returns true if there is no additional 'body' to complete the header // that is: no transactions and no uncles. func (h *Header) EmptyBody() bool { @@ -425,6 +481,15 @@ func (b *Block) Size() uint64 { return uint64(c) } +// SanityCheck can be used to prevent that unbounded fields are +// stuffed with junk data to add processing overhead +func (b *Block) SanityCheck() error { + if err := b.header.SanityCheck(); err != nil { + return err + } + return SanityCheckHeaders(b.uncles) +} + type writeCounter uint64 func (c *writeCounter) Write(b []byte) (int, error) { diff --git a/core/types/block_test.go b/core/types/block_test.go index 2afeef96c081..fcda5cf83e1b 100644 --- a/core/types/block_test.go +++ b/core/types/block_test.go @@ -134,6 +134,98 @@ func TestUncleHash(t *testing.T) { } } +func TestHeaderSanityCheckExtraSize(t *testing.T) { + tests := []struct { + name string + extra []byte + wantErr bool + }{ + {name: "limit", extra: bytes.Repeat([]byte{0x01}, maxHeaderByteFieldSize)}, + {name: "over limit", extra: bytes.Repeat([]byte{0x01}, maxHeaderByteFieldSize+1), wantErr: true}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + header := &Header{ + Difficulty: big.NewInt(1), + Number: big.NewInt(1), + Extra: test.extra, + } + err := header.SanityCheck() + if test.wantErr && err == nil { + t.Fatal("expected oversized extra data to fail sanity check") + } + if !test.wantErr && err != nil { + t.Fatalf("expected extra data at limit to pass sanity check: %v", err) + } + }) + } +} + +func TestHeaderSanityCheckXDPoSFieldSizes(t *testing.T) { + tests := []struct { + name string + set func(*Header) + }{ + { + name: "validators", + set: func(header *Header) { + header.Validators = bytes.Repeat([]byte{0x01}, maxHeaderByteFieldSize+1) + }, + }, + { + name: "validator", + set: func(header *Header) { + header.Validator = bytes.Repeat([]byte{0x01}, maxHeaderByteFieldSize+1) + }, + }, + { + name: "penalties", + set: func(header *Header) { + header.Penalties = bytes.Repeat([]byte{0x01}, maxHeaderByteFieldSize+1) + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + header := &Header{ + Difficulty: big.NewInt(1), + Number: big.NewInt(1), + } + test.set(header) + if err := header.SanityCheck(); err == nil { + t.Fatalf("expected oversized %s data to fail sanity check", test.name) + } + }) + } +} + +func TestBlockSanityCheckUncleExtraSize(t *testing.T) { + block := NewBlockWithHeader(&Header{ + Difficulty: big.NewInt(1), + Number: big.NewInt(1), + }).WithBody(Body{ + Uncles: []*Header{{ + Difficulty: big.NewInt(1), + Number: big.NewInt(1), + Extra: bytes.Repeat([]byte{0x01}, maxHeaderByteFieldSize+1), + }}, + }) + + if err := block.SanityCheck(); err == nil { + t.Fatal("expected oversized uncle extra data to fail sanity check") + } +} + +func TestSanityCheckHeadersExtraSize(t *testing.T) { + headers := []*Header{{ + Extra: bytes.Repeat([]byte{0x01}, maxHeaderByteFieldSize+1), + }} + + if err := SanityCheckHeaders(headers); err == nil { + t.Fatal("expected oversized header extra data to fail sanity check") + } +} + var benchBuffer = bytes.NewBuffer(make([]byte, 0, 32000)) func BenchmarkEncodeBlock(b *testing.B) { diff --git a/eth/fetcher/fetcher.go b/eth/fetcher/fetcher.go index 174d5eb9d9f7..1dd6afd3e143 100644 --- a/eth/fetcher/fetcher.go +++ b/eth/fetcher/fetcher.go @@ -767,7 +767,7 @@ func (f *Fetcher) forgetHash(hash common.Hash) { // Remove any pending fetches and decrement the DOS counters if announce := f.fetching[hash]; announce != nil { f.announces[announce.origin]-- - if f.announces[announce.origin] == 0 { + if f.announces[announce.origin] <= 0 { delete(f.announces, announce.origin) } delete(f.fetching, hash) @@ -776,7 +776,7 @@ func (f *Fetcher) forgetHash(hash common.Hash) { // Remove any pending completion requests and decrement the DOS counters for _, announce := range f.fetched[hash] { f.announces[announce.origin]-- - if f.announces[announce.origin] == 0 { + if f.announces[announce.origin] <= 0 { delete(f.announces, announce.origin) } } @@ -785,7 +785,7 @@ func (f *Fetcher) forgetHash(hash common.Hash) { // Remove any pending completions and decrement the DOS counters if announce := f.completing[hash]; announce != nil { f.announces[announce.origin]-- - if f.announces[announce.origin] == 0 { + if f.announces[announce.origin] <= 0 { delete(f.announces, announce.origin) } delete(f.completing, hash) diff --git a/eth/handler.go b/eth/handler.go index 6ce236a7d9b2..f5c2ef6a1348 100644 --- a/eth/handler.go +++ b/eth/handler.go @@ -497,6 +497,9 @@ func (pm *ProtocolManager) handleMsg(p *peer) error { if err := msg.Decode(&headers); err != nil { return errResp(ErrDecode, "msg %v: %v", msg, err) } + if err := types.SanityCheckHeaders(headers); err != nil { + return err + } // If no headers were received, but we're expending a DAO fork check, maybe it's that if len(headers) == 0 && p.forkDrop != nil { // Possibly an empty reply to the fork header checks, sanity check TDs @@ -577,6 +580,9 @@ func (pm *ProtocolManager) handleMsg(p *peer) error { if err := msg.Decode(&request); err != nil { return errResp(ErrDecode, "msg %v: %v", msg, err) } + if err := request.sanityCheck(); err != nil { + return err + } // Deliver them all to the downloader for queuing trasactions := make([][]*types.Transaction, len(request)) uncles := make([][]*types.Header, len(request)) @@ -715,6 +721,9 @@ func (pm *ProtocolManager) handleMsg(p *peer) error { if err := msg.Decode(&request); err != nil { return errResp(ErrDecode, "%v: %v", msg, err) } + if err := request.sanityCheck(); err != nil { + return err + } request.Block.ReceivedAt = msg.ReceivedAt request.Block.ReceivedFrom = p diff --git a/eth/protocol.go b/eth/protocol.go index ca3583647e22..18de1bd71665 100644 --- a/eth/protocol.go +++ b/eth/protocol.go @@ -208,6 +208,20 @@ type newBlockData struct { TD *big.Int } +// sanityCheck verifies that the values are reasonable, as a DoS protection +func (request *newBlockData) sanityCheck() error { + if err := request.Block.SanityCheck(); err != nil { + return err + } + // TD is 34 bits at mainnet block 105048538 + // TD is 32 bits at testnet block 84385810. + // If it becomes 100 million times larger, it will still fit within 100 bits. + if tdlen := request.TD.BitLen(); tdlen > 100 { + return fmt.Errorf("too large block TD: bitlen %d", tdlen) + } + return nil +} + // blockBody represents the data content of a single block. type blockBody struct { Transactions []*types.Transaction // Transactions contained within a block @@ -216,3 +230,16 @@ type blockBody struct { // blockBodiesData is the network packet for block content distribution. type blockBodiesData []*blockBody + +// sanityCheck verifies that the uncle headers are reasonable, as a DoS protection. +func (request blockBodiesData) sanityCheck() error { + for bodyIndex, body := range request { + if body == nil { + return fmt.Errorf("nil block body at index %d", bodyIndex) + } + if err := types.SanityCheckHeaders(body.Uncles); err != nil { + return err + } + } + return nil +} diff --git a/eth/protocol_test.go b/eth/protocol_test.go index c85815150c2e..1fd00039ee5b 100644 --- a/eth/protocol_test.go +++ b/eth/protocol_test.go @@ -17,6 +17,7 @@ package eth import ( + "bytes" "fmt" "sync" "testing" @@ -180,6 +181,28 @@ func testSendTransactions(t *testing.T, protocol int) { wg.Wait() } +func TestBlockBodiesDataSanityCheckUncleExtraSize(t *testing.T) { + request := blockBodiesData{{ + Uncles: []*types.Header{{ + Extra: bytes.Repeat([]byte{0x01}, 20*1024+1), + }}, + }} + + if err := request.sanityCheck(); err == nil { + t.Fatal("expected oversized uncle extra data to fail sanity check") + } +} + +func TestBlockHeadersDataSanityCheckExtraSize(t *testing.T) { + headers := []*types.Header{{ + Extra: bytes.Repeat([]byte{0x01}, 20*1024+1), + }} + + if err := types.SanityCheckHeaders(headers); err == nil { + t.Fatal("expected oversized header extra data to fail sanity check") + } +} + // Tests that the custom union field encoder and decoder works correctly. func TestGetBlockHeadersDataEncodeDecode(t *testing.T) { // Create a "random" hash for testing