Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
02a95b5
core: add get all receipts by hash helper
manav2401 Aug 14, 2025
24870b9
eth/protocols/eth: include state-sync tx receipt in eth/69 response
manav2401 Aug 15, 2025
17aeb75
core: remove GetAllReceiptsByHash function
manav2401 Aug 15, 2025
6c1f45c
core/rawdb: remove GetAllReceiptsByHash
manav2401 Aug 15, 2025
e5df450
core: track cache hit/miss for bor receipts
manav2401 Aug 15, 2025
a8818fc
eth/protocols/eth: exclude state-sync in receipt root calculations
manav2401 Aug 16, 2025
d76dac3
eth/protocols/eth: club bor receipt with all tx receipts for a block
manav2401 Aug 16, 2025
0956ff6
eth/protocols/eth: exclude state-sync receipts in root hash calculation
manav2401 Aug 18, 2025
b0d2603
core: extract and save bor receipts during sync
manav2401 Aug 18, 2025
fa463eb
core: assign empty values for missing state-sync receipts
manav2401 Aug 18, 2025
7eda28a
core: write bor receipts in kv db
manav2401 Aug 18, 2025
bb8b8bf
core: check for empty receipts while writing in kv db
manav2401 Aug 18, 2025
7c09388
eth, core: merge state-sync receipt with normal receipts for encoding
manav2401 Aug 27, 2025
066c34d
eth/protocols/eth: add state-sync receipt test for eth/69
manav2401 Aug 27, 2025
cac109f
eth/protocols/eth: remove logs
manav2401 Aug 27, 2025
949cefd
core: check for state-sync using CumulativeGasUsed instead of GasUsed
manav2401 Aug 27, 2025
b4dd6cb
core: update small comment
manav2401 Aug 27, 2025
598e55f
core: move splitReceipts outside InsertReceiptChain
manav2401 Aug 27, 2025
45b9e67
core: decode to pointer of receipt instead of value
manav2401 Aug 27, 2025
0b432a6
core: return nil for empty receipts while splitting
manav2401 Aug 27, 2025
086bd9c
core: add comment for splitReceipts
manav2401 Aug 27, 2025
d2b97b9
core: add tests for split receipts
manav2401 Aug 27, 2025
e730355
core: remove encoded field from TestSplitReceipts
manav2401 Aug 27, 2025
a8da182
Merge branch 'avalkov/upstream-v1.16.1' into manav/state-sync-receipt…
manav2401 Aug 27, 2025
0d4e9df
Merge branch 'avalkov/upstream-v1.16.1' into manav/state-sync-receipt…
manav2401 Sep 3, 2025
b016d4f
core: remove unused function
manav2401 Sep 3, 2025
306364f
eth/protocols/eth: refactor receipt processing to send via p2p
manav2401 Sep 3, 2025
5c32f63
eth/protocols/eth: make copy of receipt packet via re-encoding and de…
manav2401 Sep 3, 2025
2b30564
eth/downloader: fix typecasting issue when processing receipts
manav2401 Sep 3, 2025
128576b
eth, internal: allow snap sync for testing
manav2401 Sep 3, 2025
47b403f
consensus/bor: comment out debug log
manav2401 Sep 3, 2025
406e1f5
core: handle nil receipts while inserting
manav2401 Sep 3, 2025
c1ac861
core/rawdb: exclude bor receipts from range deletion of state history…
manav2401 Sep 29, 2025
a0877e3
core/rawdb: remove logs from tests
manav2401 Sep 29, 2025
51a625a
core/rawdb: small fix in tests
manav2401 Sep 29, 2025
d9c37bb
core: only write non empty bor receipts
manav2401 Sep 29, 2025
69177de
eth/downloader: queue state-sync block receipts
manav2401 Sep 29, 2025
65309a1
Merge branch 'avalkov/upstream-v1.16.1' into manav/state-sync-receipt…
manav2401 Sep 29, 2025
e60f99a
consensus/bor: undo comment
manav2401 Sep 29, 2025
0e3fe02
eth/downloader: add queue test with bor receipts
manav2401 Sep 30, 2025
9abcb6d
core/rawdb: don't return if normal receipts are empty
manav2401 Oct 1, 2025
6e315bb
core/rawdb: enable empty receipt imports to ancient db
manav2401 Oct 1, 2025
3869ad8
core/rawdb: fix misspell
manav2401 Oct 1, 2025
c694492
Merge remote-tracking branch 'upstream/avalkov/upstream-v1.16.1' into…
cffls Oct 3, 2025
dcca57e
eth, core: move IsSprintEndBlock to types package
manav2401 Oct 6, 2025
180832b
core: check for sprint end before split receipts
manav2401 Oct 6, 2025
fd60e69
Merge branch 'manav/state-sync-receipts-in-snap-sync' of https://gith…
manav2401 Oct 6, 2025
d6aae8e
core, eth: handle bor receipts with non-zero cumulative gas used
manav2401 Oct 6, 2025
4b761d3
core: use mock bor config for tests
manav2401 Oct 6, 2025
3ea1285
core: derive cumulative gas used before writing receipts
manav2401 Oct 6, 2025
f65c748
Merge branch 'develop' into manav/state-sync-receipts-in-snap-sync
manav2401 Oct 7, 2025
429fe66
Revert "eth, internal: allow snap sync for testing"
manav2401 Oct 7, 2025
025577c
core: populate fields in bor receipts for ancient db, update tests
manav2401 Oct 7, 2025
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
122 changes: 117 additions & 5 deletions core/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -1659,6 +1659,98 @@ const (
SideStatTy
)

// getReceiptFields given a list of normal receipts returns the tx index, the log index
// and cumulative gas used for populating the bor receipt.
func getReceiptFields(receipts []*types.ReceiptForStorage) (int, int, uint64) {
if len(receipts) == 0 {
return 0, 0, 0
}

logs := 0
for _, receipt := range receipts {
logs += len(receipt.Logs)
}

cumulativeGasUsed := receipts[len(receipts)-1].CumulativeGasUsed

return len(receipts), logs, cumulativeGasUsed
}

// isStateSyncReceiptPresent checks if a state-sync receipt is present in the list of
// receipts or not.
func isStateSyncReceiptPresent(decoded []*types.ReceiptForStorage) bool {
if len(decoded) == 0 {
return false
}

// The state-sync receipt can either have a 0 cumulative gas used (this depends on the remote peer) or
// have the same cumulative gas used as the previous receipt as state-sync transactions uses 0 gas and
// hence they don't contribute to the cumulative gas used value.
if decoded[len(decoded)-1].CumulativeGasUsed == 0 {
return true
}

if len(decoded) >= 2 && decoded[len(decoded)-1].CumulativeGasUsed == decoded[len(decoded)-2].CumulativeGasUsed {
return true
}

return false
}

// splitReceiptsAndDeriveFields separates out the state-sync receipt from the whole receipt list
// of a block and returns the encoded lists back separately. If a state-sync receipt is found, it
// derives the necessary fields and populates them. In case of errors or empty receipt, it returns
// `nil` instead of `rlp.EncodeToBytes(nil)`.
func splitReceiptsAndDeriveFields(receipts rlp.RawValue, number uint64, hash common.Hash, borCfg *params.BorConfig) (rlp.RawValue, rlp.RawValue) {
if receipts == nil {
return nil, nil
}

// Bor receipts can only exist on sprint end blocks. Avoid decoding if possible.
if !types.IsSprintEndBlock(borCfg, number) {
return receipts, nil
}

var decoded []*types.ReceiptForStorage
if err := rlp.DecodeBytes(receipts, &decoded); err != nil {
log.Warn("Failed to decode block receipts", "number", number, "hash", hash, "err", err)
return receipts, nil
}

// Split receipts only if there's a state-sync receipt present
if isStateSyncReceiptPresent(decoded) {
borReceipt := decoded[len(decoded)-1]

// Derive rest of fields for bor receipts before encoding back
txIndex, logIndex, cumulativeGasUsed := getReceiptFields(decoded[:len(decoded)-1])
types.DeriveFieldsForBorLogs(borReceipt.Logs, hash, number, uint(txIndex), uint(logIndex))
borReceipt.Status = types.ReceiptStatusSuccessful
borReceipt.CumulativeGasUsed = cumulativeGasUsed

// Encode the state-sync transaction receipt separately
encodedStateSyncReceipt, err := rlp.EncodeToBytes(borReceipt)
if err != nil {
log.Warn("Failed to encode state-sync receipt", "number", number, "hash", hash, "err", err)
return receipts, nil
}

// If no receipts left, return
if len(decoded[:len(decoded)-1]) == 0 {
return nil, encodedStateSyncReceipt
}

// Encode back the normal (non state-sync) receipts and return
encodedReceipts, err := rlp.EncodeToBytes(decoded[:len(decoded)-1])
if err != nil {
log.Warn("Failed to encode remaining receipts after excluding state-sync receipt", "number", number, "hash", hash, "err", err)
return receipts, encodedStateSyncReceipt
}
return encodedReceipts, encodedStateSyncReceipt
}

return receipts, nil
}

// InsertReceiptChain attempts to complete an already existing header chain with
// transaction and receipt data.
func (bc *BlockChain) InsertReceiptChain(blockChain types.Blocks, receiptChain []rlp.RawValue, ancientLimit uint64) (int, error) {
Expand Down Expand Up @@ -1763,14 +1855,15 @@ func (bc *BlockChain) InsertReceiptChain(blockChain types.Blocks, receiptChain [
log.Info("Wrote genesis to ancients")
}
}
// BOR: Retrieve all the bor receipts and also maintain the array of headers
// for bor specific reorg check.
borReceipts := []rlp.RawValue{}

var headers []*types.Header
// Separate out bor receipts (i.e. receipts of state-sync transactions)
var borReceipts = make([]rlp.RawValue, len(receiptChain))
for i, receipts := range receiptChain {
receiptChain[i], borReceipts[i] = splitReceiptsAndDeriveFields(receipts, blockChain[i].NumberU64(), blockChain[i].Hash(), bc.chainConfig.Bor)
}

var headers []*types.Header
for _, block := range blockChain {
borReceipts = append(borReceipts, bc.GetBorReceiptRLPByHash(block.Hash()))
headers = append(headers, block.Header())
}

Expand Down Expand Up @@ -1798,8 +1891,14 @@ func (bc *BlockChain) InsertReceiptChain(blockChain types.Blocks, receiptChain [
for i, block := range blockChain {
if bc.txIndexer == nil || bc.txIndexer.limit == 0 || ancientLimit <= bc.txIndexer.limit || block.NumberU64() >= ancientLimit-bc.txIndexer.limit {
rawdb.WriteTxLookupEntriesByBlock(batch, block)
if len(borReceipts[i]) > 0 {
rawdb.WriteBorTxLookupEntry(batch, block.Hash(), block.NumberU64())
}
} else if rawdb.ReadTxIndexTail(bc.db) != nil {
rawdb.WriteTxLookupEntriesByBlock(batch, block)
if len(borReceipts[i]) > 0 {
rawdb.WriteBorTxLookupEntry(batch, block.Hash(), block.NumberU64())
}
}

stats.processed++
Expand Down Expand Up @@ -1897,11 +1996,24 @@ func (bc *BlockChain) InsertReceiptChain(blockChain types.Blocks, receiptChain [
skipPresenceCheck = true
}
}

// Separate out bor receipts (i.e. receipts of state-sync transactions)
var borReceiptRaw rlp.RawValue
receiptChain[i], borReceiptRaw = splitReceiptsAndDeriveFields(receiptChain[i], block.NumberU64(), block.Hash(), bc.chainConfig.Bor)

// Write all the data out into the database
rawdb.WriteCanonicalHash(batch, block.Hash(), block.NumberU64())
rawdb.WriteBlock(batch, block)
rawdb.WriteRawReceipts(batch, block.Hash(), block.NumberU64(), receiptChain[i])

var borReceipt types.ReceiptForStorage
if len(borReceiptRaw) > 0 {
if err := rlp.DecodeBytes(borReceiptRaw, &borReceipt); err == nil {
rawdb.WriteBorReceipt(batch, block.Hash(), block.NumberU64(), &borReceipt)
rawdb.WriteBorTxLookupEntry(batch, block.Hash(), block.NumberU64())
}
}

// Write everything belongs to the blocks into the database. So that
// we can ensure all components of body is completed(body, receipts)
// except transaction indexes(will be created once sync is finished).
Expand Down
103 changes: 103 additions & 0 deletions core/blockchain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import (
"github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie"
"github.com/holiman/uint256"
"github.com/stretchr/testify/require"
)

// So we can deterministically seed different blockchains
Expand Down Expand Up @@ -5476,3 +5477,105 @@ func TestStatelessSetHeadBeyondRoot(t *testing.T) {
}
}
}

// TestSplitReceiptsAndDeriveFields checks if normal and state-sync receipts are split correctly
// before inserting to database. It also checks if state-sync receipt fields are derived correctly.
func TestSplitReceiptsAndDeriveFields(t *testing.T) {
var tests = []struct {
name string
normalReceipts []*types.ReceiptForStorage
stateSyncReceipt *types.ReceiptForStorage
}{
// Both nil
{
name: "both normal and state-sync receipt is nil",
normalReceipts: nil,
stateSyncReceipt: nil,
},
// Normal receipt empty and state-sync nil
{
name: "empty normal receipt and nil state-sync receipt",
normalReceipts: []*types.ReceiptForStorage{},
stateSyncReceipt: nil,
},
// Only normal receipt (single)
{
name: "state-sync receipt is nil, only single normal receipt present",
normalReceipts: []*types.ReceiptForStorage{{CumulativeGasUsed: 555, Status: 1, Logs: nil}},
stateSyncReceipt: nil,
},
// Only normal receipt (multiple)
{
name: "state-sync receipt is nil, multiple normal receipts present",
normalReceipts: []*types.ReceiptForStorage{{CumulativeGasUsed: 555, Status: 1, Logs: nil}, {CumulativeGasUsed: 666, Status: 1, Logs: nil}},
stateSyncReceipt: nil,
},
// Only state-sync receipt
{
name: "normal receipt is nil, only state-sync receipt present",
normalReceipts: nil,
stateSyncReceipt: &types.ReceiptForStorage{CumulativeGasUsed: 0, Status: 1, Logs: nil, Type: 0},
},
// Normal + state-sync receipts (single)
{
name: "single normal and state-sync receipt present",
normalReceipts: []*types.ReceiptForStorage{{CumulativeGasUsed: 555, Status: 1, Logs: nil}},
stateSyncReceipt: &types.ReceiptForStorage{CumulativeGasUsed: 0, Status: 1, Logs: nil, Type: 0},
},
// Normal + state-sync receipts (multiple)
{
name: "multiple normal and single state-sync receipt present",
normalReceipts: []*types.ReceiptForStorage{{CumulativeGasUsed: 555, Status: 1, Logs: nil}, {CumulativeGasUsed: 666, Status: 1, Logs: nil}, {CumulativeGasUsed: 777, Status: 1, Logs: nil}},
stateSyncReceipt: &types.ReceiptForStorage{CumulativeGasUsed: 0, Status: 1, Logs: nil, Type: 0},
},
// Normal + state-sync receipts (multiple) with non-zero cumulative gas used
{
name: "multiple normal and single state-sync receipt present with non-zero cumulative gas used",
normalReceipts: []*types.ReceiptForStorage{{CumulativeGasUsed: 555, Status: 1, Logs: nil}, {CumulativeGasUsed: 666, Status: 1, Logs: nil}, {CumulativeGasUsed: 777, Status: 1, Logs: nil}},
stateSyncReceipt: &types.ReceiptForStorage{CumulativeGasUsed: 777, Status: 1, Logs: nil, Type: 0},
},
}

// Create a mock config with sprint length as 1 (so that split receipts will run for all test cases)
mockBorCfg := params.BorConfig{
Sprint: map[string]uint64{"0": 1},
}

for _, test := range tests {
// Individually encode receipts for comparing with values after splitting
var normalEncoded []byte = nil
if len(test.normalReceipts) > 0 {
normalEncoded, _ = rlp.EncodeToBytes(test.normalReceipts)
}

// For state-sync receipts, create a copy and populate remaining fields which will be
// used to compare later with the output. Don't populate the original test object as
// the `splitReceiptsAndDeriveFields` should populate values in it.
stateSyncReceipt := test.stateSyncReceipt
var stateSyncEncoded []byte = nil
if test.stateSyncReceipt != nil {
_, _, cumulativeGasUsed := getReceiptFields(test.normalReceipts)
stateSyncReceipt.CumulativeGasUsed = cumulativeGasUsed
stateSyncEncoded, _ = rlp.EncodeToBytes(stateSyncReceipt)
}

// Merge both and encode the final list. Use the receipts in test object.
var allReceipts = make([]*types.ReceiptForStorage, 0)
if test.normalReceipts != nil {
allReceipts = append(allReceipts, test.normalReceipts...)
}
if test.stateSyncReceipt != nil {
allReceipts = append(allReceipts, test.stateSyncReceipt)
}
encoded, _ := rlp.EncodeToBytes(allReceipts)
if len(allReceipts) == 0 {
// Skip encoding, instead just set nil
encoded = nil
}

// Split receipts and assert if the individual list match with the expected receipt data or not
normal, stateSync := splitReceiptsAndDeriveFields(encoded, 0, common.Hash{}, &mockBorCfg)
require.Equal(t, rlp.RawValue(normalEncoded), normal, fmt.Sprintf("case: %s, normal receipts mismatch, got: %v, expected: %v", test.name, normal, normalEncoded))
require.Equal(t, rlp.RawValue(stateSyncEncoded), stateSync, fmt.Sprintf("case: %s, state-sync receipts mismatch, got: %v, expected: %v", test.name, stateSync, stateSyncEncoded))
}
}
12 changes: 12 additions & 0 deletions core/bor_blockchain.go → core/bor_blockchain_reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,24 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/metrics"
"github.com/ethereum/go-ethereum/rlp"
)

var (
borReceiptsCacheHit = metrics.NewRegisteredGauge("bor/receipts/cache/hit", nil)
borReceiptsCacheMiss = metrics.NewRegisteredGauge("bor/receipts/cache/miss", nil)
borReceiptsRLPCacheHit = metrics.NewRegisteredGauge("bor/rlpreceipts/cache/hit", nil)
borReceiptsRLPCacheMiss = metrics.NewRegisteredGauge("bor/rlpreceipts/cache/miss", nil)
)

// GetBorReceiptByHash retrieves the bor block receipt in a given block.
func (bc *BlockChain) GetBorReceiptByHash(hash common.Hash) *types.Receipt {
if receipt, ok := bc.borReceiptsCache.Get(hash); ok {
borReceiptsCacheHit.Update(1)
return receipt
}
borReceiptsCacheMiss.Update(1)

// read header from hash
number := rawdb.ReadHeaderNumber(bc.db, hash)
Expand All @@ -34,8 +44,10 @@ func (bc *BlockChain) GetBorReceiptByHash(hash common.Hash) *types.Receipt {
// GetBorReceiptRLPByHash retrieves the bor block receipt RLP in a given block.
func (bc *BlockChain) GetBorReceiptRLPByHash(hash common.Hash) rlp.RawValue {
if receiptRLP, ok := bc.borReceiptsRLPCache.Get(hash); ok {
borReceiptsRLPCacheHit.Update(1)
return receiptRLP
}
borReceiptsRLPCacheMiss.Update(1)

// read header from hash
number := rawdb.ReadHeaderNumber(bc.db, hash)
Expand Down
28 changes: 24 additions & 4 deletions core/rawdb/accessors_history.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,20 +160,40 @@ func increaseKey(key []byte) []byte {
//
// Note, this method assumes the storage space with prefix `StateHistoryIndexPrefix`
// is exclusively occupied by the history indexing data!
//
// Note, as bor receipts and tx lookup are also stored with prefix "matic-bor", it needs
// to be excluded from range deletion.
func DeleteStateHistoryIndex(db ethdb.KeyValueRangeDeleter) {
start := StateHistoryIndexPrefix
limit := increaseKey(bytes.Clone(StateHistoryIndexPrefix))
// We split the delete range queries into 2 parts to ensure that bor receipts related data isn't deleted.
// 1. `StateHistoryIndexPrefix` (i.e. "m") to "matic-bor" key (exclusive)
// 2. Immediate next key after "matic-bor" to next key after `StateHistoryIndexPrefix` (i.e. "n")
start1 := StateHistoryIndexPrefix
limit1 := []byte("matic-bor")
start2 := increaseKey(bytes.Clone(limit1))
limit2 := increaseKey(bytes.Clone(StateHistoryIndexPrefix))

// Try to remove the data in the range by a loop, as the leveldb
// doesn't support the native range deletion.
for {
err := db.DeleteRange(start, limit)
err := db.DeleteRange(start1, limit1)
if err == nil {
break
}
if errors.Is(err, ethdb.ErrTooManyKeys) {
continue
}
log.Crit("Failed to delete history index range 1", "err", err)
}

// Try removing for second range
for {
err := db.DeleteRange(start2, limit2)
if err == nil {
return
}
if errors.Is(err, ethdb.ErrTooManyKeys) {
continue
}
log.Crit("Failed to delete history index range", "err", err)
log.Crit("Failed to delete history index range 2", "err", err)
}
}
Loading
Loading