diff --git a/core/blockchain.go b/core/blockchain.go index 52cf25d22e..d449fee71e 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -1718,6 +1718,12 @@ func splitReceiptsAndDeriveFields(receipts rlp.RawValue, number uint64, hash com return nil, nil } + // After the state-sync HF, no need to split receipts as all receipts for a block + // are stored together (i.e. under same key). + if borCfg.IsStateSync(big.NewInt(int64(number))) { + return receipts, nil + } + // Bor receipts can only exist on sprint end blocks. Avoid decoding if possible. if !types.IsSprintEndBlock(borCfg, number) { return receipts, nil diff --git a/eth/downloader/bor_fetchers_concurrent_receipts.go b/eth/downloader/bor_fetchers_concurrent_receipts.go index fe556fe7d6..f50f4666de 100644 --- a/eth/downloader/bor_fetchers_concurrent_receipts.go +++ b/eth/downloader/bor_fetchers_concurrent_receipts.go @@ -92,10 +92,15 @@ func (q *receiptQueue) request(peer *peerConnection, req *fetchRequest, resCh ch // deliver is responsible for taking a generic response packet from the concurrent // fetcher, unpacking the receipt data and delivering it to the downloader's queue. func (q *receiptQueue) deliver(peer *peerConnection, packet *eth.Response) (int, error) { - receipts := *packet.Res.(*eth.ReceiptsRLPResponse) - hashes := packet.Meta.([]common.Hash) // {receipt hashes} + // We're expecting a full decoded receipt list instead of encoded receipts for storage + // we used to have earlier. + receipts, getReceiptListHashes := eth.EncodeReceiptsAndPrepareHasher(packet.Res, q.queue.borConfig) + if receipts == nil || getReceiptListHashes == nil { + peer.log.Warn("Unknown receipt list type, discarding packet") + return 0, nil + } - accepted, err := q.queue.DeliverReceipts(peer.id, receipts, hashes) + accepted, err := q.queue.DeliverReceipts(peer.id, receipts, getReceiptListHashes) switch { case err == nil && len(receipts) == 0: diff --git a/eth/downloader/queue.go b/eth/downloader/queue.go index 347032dbd3..75a02a53b0 100644 --- a/eth/downloader/queue.go +++ b/eth/downloader/queue.go @@ -22,6 +22,7 @@ package downloader import ( "errors" "fmt" + "math/big" "sync" "sync/atomic" "time" @@ -1034,12 +1035,13 @@ func (q *queue) DeliverBodies(id string, txLists [][]*types.Transaction, txListH // DeliverReceipts injects a receipt retrieval response into the results queue. // The method returns the number of transaction receipts accepted from the delivery // and also wakes any threads waiting for data delivery. -func (q *queue) DeliverReceipts(id string, receiptList []rlp.RawValue, receiptListHashes []common.Hash) (int, error) { +func (q *queue) DeliverReceipts(id string, receiptList []rlp.RawValue, getReceiptListHash func(int, *big.Int) common.Hash) (int, error) { q.lock.Lock() defer q.lock.Unlock() validate := func(index int, header *types.Header) error { - if receiptListHashes[index] != header.ReceiptHash { + receiptListHash := getReceiptListHash(index, header.Number) + if receiptListHash != header.ReceiptHash { return errInvalidReceipt } diff --git a/eth/downloader/queue_test.go b/eth/downloader/queue_test.go index 59e82d97a0..59731732bf 100644 --- a/eth/downloader/queue_test.go +++ b/eth/downloader/queue_test.go @@ -470,12 +470,11 @@ func XTestDelivery(t *testing.T) { } hasher := trie.NewStackTrie(nil) - hashes := make([]common.Hash, len(rcs)) - - for i, receipt := range rcs { - hashes[i] = types.DeriveSha(receipt, hasher) + getHashes := func(index int, number *big.Int) common.Hash { + return types.DeriveSha(rcs[index], hasher) } - _, err := q.DeliverReceipts(peer.id, types.EncodeBlockReceiptLists(rcs), hashes) + + _, err := q.DeliverReceipts(peer.id, types.EncodeBlockReceiptLists(rcs), getHashes) if err != nil { fmt.Printf("delivered %d receipts %v\n", len(rcs), err) } diff --git a/eth/protocols/eth/handlers.go b/eth/protocols/eth/handlers.go index 312bb9b230..d225942927 100644 --- a/eth/protocols/eth/handlers.go +++ b/eth/protocols/eth/handlers.go @@ -19,13 +19,16 @@ package eth import ( "encoding/json" "fmt" + "math/big" "time" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/p2p/tracker" + "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/trie" ) @@ -336,18 +339,49 @@ func ServiceGetReceiptsQuery69(chain *core.BlockChain, query GetReceiptsRequest) bytes int receipts []rlp.RawValue ) + borCfg := chain.Config().Bor for lookups, hash := range query { if bytes >= softResponseLimit || len(receipts) >= maxReceiptsServe || lookups >= 2*maxReceiptsServe { break } - // In order to include state-sync transaction receipts, which resides in a separate - // table in db, we need to separately fetch normal and state-sync transaction receipts. - // Later, decode them, merge them into a single unit and re-encode the final list to - // be sent over p2p. + number := rawdb.ReadHeaderNumber(chain.DB(), hash) + if number == nil { + continue + } + + // If we're past the state-sync hardfork, state-sync receipts (if present) are stored + // with normal block receipts so no special handling needed. + if borCfg != nil && borCfg.IsStateSync(big.NewInt(int64(*number))) { + allReceipts := chain.GetReceiptsRLP(hash) + if allReceipts == nil { + if header := chain.GetHeaderByHash(hash); header == nil || header.ReceiptHash != types.EmptyRootHash { + continue + } + } + body := chain.GetBodyRLP(hash) + if body == nil { + continue + } + // Noop as no special handling is needed + isStateSyncReceipt := func(index int) bool { + return false + } + results, err := blockReceiptsToNetwork69(allReceipts, body, isStateSyncReceipt) + if err != nil { + log.Error("Error in block receipts conversion", "hash", hash, "err", err) + continue + } + + receipts = append(receipts, results) + bytes += len(results) + continue + } - // Fetch receipts of normal evm transactions + // Before state-sync HF, we need to fetch state-sync receipts separately along with fetching + // block receipts. Upon fetching, decode them, merge them into a single unit and re-encode + // the final list to be sent over p2p. normalReceipts := chain.GetReceiptsRLP(hash) var normalReceiptsDecoded []*types.ReceiptForStorage if normalReceipts != nil { @@ -374,10 +408,6 @@ func ServiceGetReceiptsQuery69(chain *core.BlockChain, query GetReceiptsRequest) if header := chain.GetHeaderByHash(hash); header == nil || header.ReceiptHash != types.EmptyRootHash { continue } - - // Append an empty entry for this block and continue - receipts = append(receipts, nil) - continue } // Track existence of bor receipts for encoding @@ -534,19 +564,6 @@ func handleReceipts[L ReceiptsList](backend Backend, msg Decoder, peer *Peer) er return err } - // The response in p2p packet can only be consumed once. As we need a copy of receipt - // to exclude state-sync transaction receipt from receipt root calculation, encode - // and decode the response back. - packet, err := rlp.EncodeToBytes(res) - if err != nil { - return fmt.Errorf("failed to re-encode receipt packet for making copy: %w", err) - } - - resWithoutStateSync := new(ReceiptsPacket[L]) - if err := rlp.DecodeBytes(packet, resWithoutStateSync); err != nil { - return fmt.Errorf("failed to decode re-encoded receipt packet for making copy: %w", err) - } - // Assign temporary hashing buffer to each list item, the same buffer is shared // between all receipt list instances. buffers := new(receiptListBuffers) @@ -554,30 +571,70 @@ func handleReceipts[L ReceiptsList](backend Backend, msg Decoder, peer *Peer) er res.List[i].setBuffers(buffers) } + // The `metadata` function below was used earlier to calculate `ReceiptHash` which is further + // used to validate against `header.ReceiptHash`. By default, state-sync receipts (which are + // appended at the end of list for a block) are excluded from the `ReceiptHash` calculation. + // After the state-sync hard fork, they should be included in the calculation. We don't have + // access to block number here so we can't determine whether to exclude or not. Instead, just + // ignore the `metadata` function and pass on the whole receipt list as is. The receipt queue + // handler which has access to block number will take care of the exclusion if needed. metadata := func() interface{} { - hasher := trie.NewStackTrie(nil) - hashes := make([]common.Hash, len(resWithoutStateSync.List)) - for i := range resWithoutStateSync.List { - // The receipt root of a block doesn't include receipts from state-sync - // transactions specific to polygon. Exclude them for calculating the - // hashes of all receipts. - resWithoutStateSync.List[i].ExcludeStateSyncReceipt() - hashes[i] = types.DeriveSha(resWithoutStateSync.List[i], hasher) - } - - return hashes - } - var enc ReceiptsRLPResponse - for i := range res.List { - enc = append(enc, res.List[i].EncodeForStorage()) + return nil } + + // Assign the decoded receipt list to the result of `Response` packet. return peer.dispatchResponse(&Response{ id: res.RequestId, code: ReceiptsMsg, - Res: &enc, + Res: &res.List, }, metadata) } +// EncodeReceiptsAndPrepareHasher encodes a list of receipts to the storage format (does not +// include TxType field). It also returns a function which calculates `ReceiptHash` of a receipt list +// based on the whether we've crossed the hardfork or not. +func EncodeReceiptsAndPrepareHasher(packet interface{}, borCfg *params.BorConfig) (ReceiptsRLPResponse, func(int, *big.Int) common.Hash) { + // Extract receipts based on type. Add/remove support for new types here as needed. + var ( + receipts ReceiptsRLPResponse + getReceiptListHashes func(int, *big.Int) common.Hash + ) + switch packet := packet.(type) { + case []*ReceiptList68: + receiptList := packet + receipts, getReceiptListHashes = encodeReceiptsAndPrepareHasher(receiptList, borCfg) + case []*ReceiptList69: + receiptList := packet + receipts, getReceiptListHashes = encodeReceiptsAndPrepareHasher(receiptList, borCfg) + default: + // This shouldn't happen unless there's a bug in identifying type of receipt list + // or there's a new type which isn't handled here. + return nil, nil + } + return receipts, getReceiptListHashes +} + +// encodeReceiptsAndPrepareHasher is an internal generic function for all receipt types +func encodeReceiptsAndPrepareHasher[L ReceiptsList](receipts []L, borCfg *params.BorConfig) (ReceiptsRLPResponse, func(int, *big.Int) common.Hash) { + var encodedReceipts ReceiptsRLPResponse = make(ReceiptsRLPResponse, len(receipts)) + for i := range receipts { + encodedReceipts[i] = receipts[i].EncodeForStorage() + } + + hasher := trie.NewStackTrie(nil) + calculateReceiptHashes := func(index int, number *big.Int) common.Hash { + // Don't exclude state-sync receipts for post hardfork blocks + if borCfg.IsStateSync(number) { + return types.DeriveSha(receipts[index], hasher) + } else { + receipts[index].ExcludeStateSyncReceipt() + return types.DeriveSha(receipts[index], hasher) + } + } + + return encodedReceipts, calculateReceiptHashes +} + func handleNewPooledTransactionHashes(backend Backend, msg Decoder, peer *Peer) error { // New transaction announcement arrived, make sure we have // a valid and fresh chain to handle them diff --git a/eth/protocols/eth/receipt.go b/eth/protocols/eth/receipt.go index dc92049838..df98907933 100644 --- a/eth/protocols/eth/receipt.go +++ b/eth/protocols/eth/receipt.go @@ -457,7 +457,10 @@ func blockReceiptsToNetwork69(blockReceipts, blockBody rlp.RawValue, isStateSync content, _, _ := rlp.SplitList(it.Value()) receiptList := enc.List() if isStateSyncReceipt(i) { - enc.WriteUint64(uint64(0)) // TxType is always 0 for state-sync transactions + // TxType is always 0 for state-sync transactions before state-sync hardfork. Post + // hardfork, they will be part of normal block receipts and body so no special + // handling needed. + enc.WriteUint64(uint64(0)) } else { txType, _ := nextTxType() enc.WriteUint64(uint64(txType)) diff --git a/eth/protocols/eth/receipt_test.go b/eth/protocols/eth/receipt_test.go index 4d6b528245..d5ed06603f 100644 --- a/eth/protocols/eth/receipt_test.go +++ b/eth/protocols/eth/receipt_test.go @@ -18,10 +18,12 @@ package eth import ( "bytes" + "math/big" "testing" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/trie" ) @@ -66,10 +68,11 @@ var receiptsTests = []struct { } var stateSyncReceiptsTests = []struct { - normalReceipts []types.ReceiptForStorage - stateSyncReceipt *types.ReceiptForStorage - txs []*types.Transaction - root common.Hash + normalReceipts []types.ReceiptForStorage + stateSyncReceipt *types.ReceiptForStorage + txs []*types.Transaction + rootWithoutStateSync common.Hash + rootWithStateSync common.Hash }{ // Only state-sync receipt { @@ -89,6 +92,12 @@ var stateSyncReceiptsTests = []struct { stateSyncReceipt: &types.ReceiptForStorage{CumulativeGasUsed: 555, Status: 1, Logs: nil, Type: 0}, txs: []*types.Transaction{types.NewTx(&types.LegacyTx{}), types.NewBorTransaction()}, }, + // Multiple normal + state-sync receipts with non-zero cumulative gas used for state-sync receipt + { + normalReceipts: []types.ReceiptForStorage{{CumulativeGasUsed: 555, Status: 1, Logs: nil}, {CumulativeGasUsed: 666, Status: 1, Logs: nil}}, + stateSyncReceipt: &types.ReceiptForStorage{CumulativeGasUsed: 666, Status: 1, Logs: nil, Type: 0}, + txs: []*types.Transaction{types.NewTx(&types.LegacyTx{}), types.NewBorTransaction()}, + }, } func init() { @@ -108,6 +117,8 @@ func init() { receiptsTests[i].root = types.DeriveSha(receipts, trie.NewStackTrie(nil)) } + // Duplicate tests for pre and post hardfork scenarios. + stateSyncReceiptsTests = append(stateSyncReceiptsTests, stateSyncReceiptsTests...) for i := range stateSyncReceiptsTests { // derive basic fields for normal receipts skipping the state-sync receipts for j := range stateSyncReceiptsTests[i].normalReceipts { @@ -121,7 +132,13 @@ func init() { r := types.Receipt(sr) receipts[j] = &r } - stateSyncReceiptsTests[i].root = types.DeriveSha(receipts, trie.NewStackTrie(nil)) + // Compute the root excluding state-sync receipt (pre hardfork case) + stateSyncReceiptsTests[i].rootWithoutStateSync = types.DeriveSha(receipts, trie.NewStackTrie(nil)) + + // Append the state-sync receipt and compute the root (post hardfork case) + r := types.Receipt(*stateSyncReceiptsTests[i].stateSyncReceipt) + receipts = append(receipts, &r) + stateSyncReceiptsTests[i].rootWithStateSync = types.DeriveSha(receipts, trie.NewStackTrie(nil)) } } @@ -167,10 +184,17 @@ func TestReceiptList69(t *testing.T) { } } -func TestStateSyncReceiptList69(t *testing.T) { +// TestReceiptList69_WithStateSync tests encoding/decoding of ReceiptList69 with state-sync +// receipts included. It tests each case independently and always excludes state-sync receipt +// from root hash calculations. +func TestReceiptList69_WithStateSync(t *testing.T) { // The tests tries to replicate behaviour of how the getReceipts query would // handle normal and state-sync receipts. for i, test := range stateSyncReceiptsTests { + // Cases are duplicated (for a different test). Skip re-running again. + if i == len(stateSyncReceiptsTests)/2 { + break + } // Track existence of bor receipts for encoding var isBorReceiptPresent bool @@ -225,8 +249,115 @@ func TestStateSyncReceiptList69(t *testing.T) { // compute root hash from ReceiptList69 and compare. responseHash := types.DeriveSha(&rl, trie.NewStackTrie(nil)) - if responseHash != test.root { - t.Fatalf("test[%d]: wrong root hash from ReceiptList69\nhave: %v\nwant: %v", i, responseHash, test.root) + if responseHash != test.rootWithoutStateSync { + t.Fatalf("test[%d]: wrong root hash from ReceiptList69\nhave: %v\nwant: %v", i, responseHash, test.rootWithoutStateSync) + } + } +} + +// TestReceiptList69_WithStateSync_e2e is an end-to-end test which simulates how receipts +// are encoded and decoded in p2p handlers. Instead of testing case by case, it assembles +// all receipts (assuming they belong to different consecutive blocks) into a single p2p +// packet and decodes the whole packet back. It also tests the behaviour of state-sync +// hardfork activation after which state-sync receipts are included in the `ReceiptHash` +// calculation. +func TestReceiptList69_WithStateSync_e2e(t *testing.T) { + var receipts []rlp.RawValue + var encodedReceipts [][]byte + + // Assemble all receipts from all cases into a single p2p packet + for i, test := range stateSyncReceiptsTests { + // Track existence of bor receipts for encoding + var isBorReceiptPresent bool + + // Merge both receipts + var blockReceipts = make([]types.ReceiptForStorage, 0) + if test.normalReceipts != nil { + blockReceipts = append(blockReceipts, test.normalReceipts...) + } + if test.stateSyncReceipt != nil { + blockReceipts = append(blockReceipts, *test.stateSyncReceipt) + isBorReceiptPresent = true + } + + // isStateSyncReceipt denotes whether a receipt belongs to state-sync transaction or not + isStateSyncReceipt := func(index int) bool { + // If bor receipt is present, it will always be at the end of list + if isBorReceiptPresent && index == len(blockReceipts)-1 { + return true + } + return false + } + + // encode receipts from types.ReceiptForStorage object. + canonDB, _ := rlp.EncodeToBytes(blockReceipts) + encodedReceipts = append(encodedReceipts, canonDB) + + // encode block body from types object. + blockBody := types.Body{Transactions: test.txs} + canonBody, _ := rlp.EncodeToBytes(blockBody) + + // convert from storage encoding to network encoding + network, err := blockReceiptsToNetwork69(canonDB, canonBody, isStateSyncReceipt) + if err != nil { + t.Fatalf("test[%d]: blockReceiptsToNetwork69 error: %v", i, err) + } + + receipts = append(receipts, network) + } + + // Create a p2p packet containing all receipts (in network69 format). + // The p2p packet is written to a buffer stream and is read back again + // in same way via an interface. We can directly use the rlp encode + // and decode bytes function to replicate same behaviour. + packet := &ReceiptsRLPPacket{ + RequestId: 0, + ReceiptsRLPResponse: receipts, + } + encodedPacket, _ := rlp.EncodeToBytes(packet) + + // Receiver side which decodes the packet + res := new(ReceiptsPacket[*ReceiptList69]) + rlp.DecodeBytes(encodedPacket, res) + + // Borrowed from `handleReceipts` + // Assign temporary hashing buffer to each list item, the same buffer is shared + // between all receipt list instances. + buffers := new(receiptListBuffers) + for i := range res.List { + res.List[i].setBuffers(buffers) + } + + // For 8 cases, test 4 pre hardfork and 4 post hardfork scenarios + borCfg := ¶ms.BorConfig{ + StateSyncBlock: big.NewInt(4), + } + // Simulating the way packet is delivered to the receipt queue + deliver := func(packet interface{}) (ReceiptsRLPResponse, func(int, *big.Int) common.Hash) { + return EncodeReceiptsAndPrepareHasher(packet, borCfg) + } + + receipts, getReceiptListHashes := deliver(res.List) + if receipts == nil || getReceiptListHashes == nil { + t.Fatalf("EncodeReceiptsAndPrepareHasher failed, invalid packet") + } + + // Check if the receipts on the receiver side match with what was sent + for i, r := range receipts { + if !bytes.Equal(r, encodedReceipts[i]) { + t.Fatalf("re-encoded receipts not equal\nhave: %x\nwant: %x", r, encodedReceipts[i]) + } + } + + // Check if the `ReceiptHash` calculations matches + for i := 0; i < len(receipts); i++ { + got := getReceiptListHashes(i, big.NewInt(int64(i))) + expected := stateSyncReceiptsTests[i].rootWithoutStateSync + if borCfg.IsStateSync(big.NewInt(int64(i))) { + expected = stateSyncReceiptsTests[i].rootWithStateSync + } + if got != expected { + t.Fatalf("wrong root hash from ReceiptList69\nhave: %v\nwant: %v", got, expected) } } } diff --git a/tests/bor/bor_test.go b/tests/bor/bor_test.go index 3e3c9843be..0be7a89e96 100644 --- a/tests/bor/bor_test.go +++ b/tests/bor/bor_test.go @@ -424,7 +424,7 @@ func TestInsertingSpanSizeBlocks(t *testing.T) { } } -func TestFetchStateSyncEvents(t *testing.T) { +func TestFetchStateSyncEvents_PreStateSyncHF(t *testing.T) { t.Parallel() log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelInfo, true))) fdlimit.Raise(2048) @@ -495,6 +495,103 @@ func TestFetchStateSyncEvents(t *testing.T) { validateStateSyncEvents(t, eventRecords, chain.GetStateSync()) insertNewBlock(t, chain, block) + + // TODO: Ideally bor receipts should be present but as all state-sync events fails in this + // test, no logs are generated and hence empty receipts aren't written. Fix this. + + // Ensure bor receipts are stored correctly + // borReceipt := chain.GetBorReceiptByHash(block.Hash()) + // t.Log(borReceipt) + // require.NotNil(t, borReceipt, "bor receipt expected but found nil") + // require.Equal(t, uint8(0), borReceipt.Type, "bor receipt should have type 0") + + receipts := chain.GetReceiptsByHash(block.Hash()) + require.Equal(t, 0, len(receipts), "no normal receipts should be found") +} + +func TestFetchStateSyncEvents_PostStateSyncHF(t *testing.T) { + t.Parallel() + log.SetDefault(log.NewLogger(log.NewTerminalHandlerWithLevel(os.Stderr, log.LevelInfo, true))) + fdlimit.Raise(2048) + + stateSyncConfirmationDelay := int64(128) + updateGenesis := func(gen *core.Genesis) { + gen.Config.Bor.StateSyncConfirmationDelay = map[string]uint64{"0": uint64(stateSyncConfirmationDelay)} + gen.Config.Bor.Sprint = map[string]uint64{"0": sprintSize} + gen.Config.Bor.StateSyncBlock = big.NewInt(0) // enable the HF from genesis + } + init := buildEthereumInstance(t, rawdb.NewMemoryDatabase(), updateGenesis) + chain := init.ethereum.BlockChain() + engine := init.ethereum.Engine() + _bor := engine.(*bor.Bor) + defer _bor.Close() + + // Insert blocks for 0th sprint + block := init.genesis.ToBlock() + + // Create a mock span 0 + span0 := createMockSpan(addr, chain.Config().ChainID.String()) + borValSet := borSpan.ConvertHeimdallValSetToBorValSet(span0.ValidatorSet) + currentValidators := borValSet.Validators + + // Load mock span 0 + res := loadSpanFromFile(t) + + // reate mock bor spanner + spanner := getMockedSpanner(t, currentValidators) + _bor.SetSpanner(spanner) + + // Create mock heimdall client + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + h := createMockHeimdall(ctrl, &span0, res) + + // Mock state sync events + fromID := uint64(1) + // at # sprintSize, events are fetched for [fromID, (block-sprint).Time]) + // as indore hf is enabled, we need to consider the stateSyncConfirmationDelay and + // we need to predict the time of 4th block (i.e. the sprint end block) to calculate + // the correct value of to. As per the config, non sprint end primary blocks take + // 1s and sprint end ones take 6s. This leads to 3*1 + 6 = 9s of added time from genesis. + to := int64(chain.GetHeaderByNumber(0).Time) + 9 - stateSyncConfirmationDelay + eventCount := 50 + + sample := getSampleEventRecord(t) + sample.Time = time.Unix(to-int64(eventCount+1), 0) // Last event.Time will be just < to + eventRecords := generateFakeStateSyncEvents(sample, eventCount) + + h.EXPECT().StateSyncEvents(gomock.Any(), fromID, to).Return(eventRecords, nil).AnyTimes() + h.EXPECT().GetLatestSpan(gomock.Any()).Return(nil, fmt.Errorf("span not found")).AnyTimes() + _bor.SetHeimdallClient(h) + + // Insert sprintSize # of blocks so that span is fetched at the start of a new sprint + for i := uint64(1); i < sprintSize; i++ { + if IsSpanEnd(i) { + currentValidators = borValSet.Validators + } + + block = buildNextBlock(t, _bor, chain, block, nil, init.genesis.Config.Bor, nil, currentValidators, false) + insertNewBlock(t, chain, block) + } + + block = buildNextBlock(t, _bor, chain, block, nil, init.genesis.Config.Bor, nil, borValSet.Validators, false) + insertNewBlock(t, chain, block) + + // Fetch the last block and check if state-sync tx and receipts are available + lastBlock := chain.GetBlockByNumber(block.NumberU64()) + txs := lastBlock.Transactions() + require.Equal(t, 1, len(txs), "state-sync tx should be part of block body") + require.Equal(t, uint8(types.StateSyncTxType), txs[0].Type(), "transaction should be of state-sync type") + + receipts := chain.GetReceiptsByHash(lastBlock.Hash()) + require.Equal(t, 1, len(receipts), "state-sync receipt should be stored against this block") + require.Equal(t, uint8(types.StateSyncTxType), receipts[0].Type, "receipt should be of state-sync type") + require.Equal(t, txs[0].Hash(), receipts[0].TxHash, "state-sync receipt hash should have correct tx hash") + + // Confirm if bor receipts are not stored separately + borReceipt := chain.GetBorReceiptByHash(lastBlock.Hash()) + require.Nil(t, borReceipt, "bor receipt should not be stored separately") } func validateStateSyncEvents(t *testing.T, expected []*clerk.EventRecordWithTime, got []*types.StateSyncData) {