Skip to content

Commit 43ae190

Browse files
author
Or Neeman
authored
Fix panic on lightest mode client when requesting a block with unknown hash. (#1631)
Previously, if you use `eth_getBlock` on a lightest mode client, and specify a hash that doesn't correspond to a block, the entire client would crash due to a panic in client_handler.go. This PR fixes that panic, and makes getting a block by an unknown hash return "unknown block", which is the desired behavior and also what you get when you request a block by an unknown number (a number that is higher than the last block's number). This requires modifying the `HeaderRequest` ODR request type to properly work when receiving a response with no headers. This PR modifies it to accept such responses if the request was by hash, but not if it was by number. This opens the door for a malicious LES server to fool a light client into thinking there is no block with this hash, when in fact there is. However, it's simpler and seems preferable to the alternative of querying all one's peers in case some of them are malicious this way, and only returning "unknown block" after querying all of them. It's also preferable to the current status quo of malicious nodes being able to crash a lightest mode client by returning an empty response. Note, too, that requesting the header by hash is only done through the RPC API and is not a common use-pattern by light client users.
1 parent 0458ca3 commit 43ae190

File tree

4 files changed

+28
-6
lines changed

4 files changed

+28
-6
lines changed

les/client_handler.go

+5-3
Original file line numberDiff line numberDiff line change
@@ -320,9 +320,11 @@ func (h *clientHandler) handleMsg(p *serverPeer) error {
320320
headerRequested := h.backend.retriever.sentReqs[resp.ReqID]
321321
h.backend.retriever.lock.RUnlock()
322322
if headerRequested != nil {
323-
contiguousHeaders := h.syncMode != downloader.LightestSync
324-
if _, err := h.fetcher.chain.InsertHeaderChain(headers, 1, contiguousHeaders); err != nil {
325-
return err
323+
if len(headers) != 0 {
324+
contiguousHeaders := h.syncMode != downloader.LightestSync
325+
if _, err := h.fetcher.chain.InsertHeaderChain(headers, 1, contiguousHeaders); err != nil {
326+
return err
327+
}
326328
}
327329
deliverMsg = &Msg{
328330
MsgType: MsgBlockHeaders,

les/odr_requests.go

+15-2
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ func (r *HeaderRequest) CanSend(peer *serverPeer) bool {
146146

147147
// Request sends an ODR request to the LES network (implementation of LesOdrRequest)
148148
func (r *HeaderRequest) Request(reqId uint64, peer *serverPeer) error {
149-
if r.Origin.Hash != (common.Hash{}) {
149+
if r.isByHash() {
150150
peer.Log().Debug("Requesting block header", "hash", r.Origin.Hash)
151151
return peer.requestHeadersByHash(reqId, r.Origin.Hash, 1, 0, false)
152152
} else {
@@ -155,6 +155,11 @@ func (r *HeaderRequest) Request(reqId uint64, peer *serverPeer) error {
155155
}
156156
}
157157

158+
// Whether the request specified the block hash (rather than block number)
159+
func (r *HeaderRequest) isByHash() bool {
160+
return r.Origin.Hash != common.Hash{}
161+
}
162+
158163
// Validate processes an ODR request reply message from the LES network
159164
// returns true and stores results in memory if the message was a valid reply
160165
// to the request (implementation of LesOdrRequest)
@@ -164,7 +169,15 @@ func (r *HeaderRequest) Validate(db ethdb.Database, msg *Msg) error {
164169
return errInvalidMessageType
165170
}
166171
headers := msg.Obj.([]*types.Header)
167-
if len(headers) != 1 {
172+
if len(headers) == 0 && r.isByHash() {
173+
// For requests by number, we only send to peers for which we know the block number
174+
// is within the range of what they have, so if they don't send us the header we reject
175+
// the response and try other peers.
176+
// However, for requests by hash, we have no way of knowing ahead of time whether the peer
177+
// should have it or not (e.g. what if there is no such block?). So we need to accept
178+
// 'no match' as a valid response, to avoid ODR endlessly trying to send to different peers.
179+
return nil
180+
} else if len(headers) != 1 {
168181
return errInvalidEntryCount
169182
}
170183
if r.Origin.Hash != (common.Hash{}) && headers[0].Hash() != r.Origin.Hash {

light/lightchain.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,9 @@ func (lc *LightChain) GetBlockByHash(ctx context.Context, hash common.Hash) (*ty
298298
number := lc.hc.GetBlockNumber(hash)
299299
if number == nil {
300300
header, err := GetHeaderByHash(ctx, lc.odr, hash)
301-
if err != nil {
301+
// Header may be nil, indicating the hash doesn't match any known blocks,
302+
// which is a valid response to the ODR request.
303+
if err != nil || header == nil {
302304
return nil, errors.New("unknown block")
303305
}
304306
return lc.GetBlock(ctx, hash, header.Number.Uint64())

light/odr.go

+5
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,11 @@ type HeaderRequest struct {
131131
// StoreResult handles storing the canonical hash if `InsertHeaderChain` has not already
132132
// This occurs if the total difficulty of the requested header is less than the current known TD.
133133
func (req *HeaderRequest) StoreResult(db ethdb.Database) {
134+
if req.Header == nil {
135+
// A nil header is a valid response when request the header by hash, as it indicates
136+
// no known block with this hash. In this case, there is nothing for us to do here.
137+
return
138+
}
134139
if rawdb.ReadCanonicalHash(db, req.Header.Number.Uint64()) == (common.Hash{}) {
135140
rawdb.WriteCanonicalHash(db, req.Header.Hash(), req.Header.Number.Uint64())
136141
}

0 commit comments

Comments
 (0)