core/filtermaps: implement log indexer as core.Indexer

This commit is contained in:
zsfelfoldi 2025-07-28 23:41:15 +02:00
parent 8ce710a354
commit 4a8eaeb1d1
33 changed files with 3774 additions and 4046 deletions

View file

@ -27,7 +27,11 @@ type Range[T uint32 | uint64] struct {
// NewRange creates a new range based of first element and number of elements.
func NewRange[T uint32 | uint64](first, count T) Range[T] {
return Range[T]{first, first + count}
afterLast := first + count
if afterLast < first {
panic("range overflow")
}
return Range[T]{first, afterLast}
}
// First returns the first element of the range.
@ -97,6 +101,12 @@ func (r Range[T]) Intersection(q Range[T]) Range[T] {
// Union returns the union of two ranges. Panics for gapped ranges.
func (r Range[T]) Union(q Range[T]) Range[T] {
if r.IsEmpty() {
return q
}
if q.IsEmpty() {
return r
}
if max(r.first, q.first) > min(r.afterLast, q.afterLast) {
panic("cannot create union; gap between ranges")
}

View file

@ -32,12 +32,11 @@ type blockchain interface {
GetRawReceipts(hash common.Hash, number uint64) types.Receipts
}
// ChainView represents an immutable view of a chain with a block id and a set
// ChainView represents an immutable view of a chain with a block hash and a set
// of receipts associated to each block number and a block hash associated with
// all block numbers except the head block. This is because in the future
// ChainView might represent a view where the head block is currently being
// created. Block id is a unique identifier that can also be calculated for the
// head block.
// created.
// Note that the view's head does not have to be the current canonical head
// of the underlying blockchain, it should only possess the block headers
// and receipts up until the expected chain view head.
@ -79,21 +78,6 @@ func (cv *ChainView) BlockHash(number uint64) common.Hash {
return cv.blockHash(number)
}
// BlockId returns the unique block id belonging to the given block number.
// Note that it is currently equal to the block hash. In the future it might
// be a different id for future blocks if the log index root becomes part of
// consensus and therefore rendering the index with the new head will happen
// before the hash of that new head is available.
func (cv *ChainView) BlockId(number uint64) common.Hash {
cv.lock.Lock()
defer cv.lock.Unlock()
if number > cv.headNumber {
panic("invalid block number")
}
return cv.blockHash(number)
}
// Header returns the block header at the given block number.
func (cv *ChainView) Header(number uint64) *types.Header {
return cv.chain.GetHeader(cv.BlockHash(number), number)
@ -128,7 +112,7 @@ func (cv *ChainView) SharedRange(cv2 *ChainView) common.Range[uint64] {
return common.Range[uint64]{}
}
sharedLen := min(cv.headNumber, cv2.headNumber) + 1
for sharedLen > 0 && cv.BlockId(sharedLen-1) != cv2.BlockId(sharedLen-1) {
for sharedLen > 0 && cv.BlockHash(sharedLen-1) != cv2.BlockHash(sharedLen-1) {
sharedLen--
}
return common.NewRange(0, sharedLen)
@ -139,7 +123,7 @@ func equalViews(cv1, cv2 *ChainView) bool {
if cv1 == nil || cv2 == nil {
return false
}
return cv1.headNumber == cv2.headNumber && cv1.BlockId(cv1.headNumber) == cv2.BlockId(cv2.headNumber)
return cv1.headNumber == cv2.headNumber && cv1.BlockHash(cv1.headNumber) == cv2.BlockHash(cv2.headNumber)
}
// matchViews returns true if the two chain views are equivalent up until the
@ -152,12 +136,7 @@ func matchViews(cv1, cv2 *ChainView, number uint64) bool {
if cv1.headNumber < number || cv2.headNumber < number {
return false
}
var h1, h2 common.Hash
if number == cv1.headNumber || number == cv2.headNumber {
h1, h2 = cv1.BlockId(number), cv2.BlockId(number)
} else {
h1, h2 = cv1.BlockHash(number), cv2.BlockHash(number)
}
h1, h2 := cv1.BlockHash(number), cv2.BlockHash(number)
return h1 == h2 && h1 != common.Hash{}
}

View file

@ -33,7 +33,7 @@ type checkpointList []epochCheckpoint
// be initialized at the epoch boundary.
type epochCheckpoint struct {
BlockNumber uint64 // block that generated the last log value of the given epoch
BlockId common.Hash
BlockHash common.Hash
FirstIndex uint64 // first log value index of the given block
}

View file

@ -1,28 +1,28 @@
[
{"blockNumber": 814410, "blockId": "0x6c38f0d4ff2c23ae187f581cebb0963c0ec2dbf051b289de1582c369c98a3244", "firstIndex": 67107349},
{"blockNumber": 914268, "blockId": "0xeff161573a11eb6e2ab930674a5245b2c5ffc5e9e63093503344ec6fa60578a3", "firstIndex": 134216064},
{"blockNumber": 1048866, "blockId": "0xebd3b95415dad9ab7f2b1d25e48c791e299063c09427bbde54217029c3e215ad", "firstIndex": 201325914},
{"blockNumber": 1144433, "blockId": "0x0c895438ef12a2c835b4372c209e40a499ba2b18ed0e658846813784de391392", "firstIndex": 268434935},
{"blockNumber": 1230406, "blockId": "0xd3512e7241efc9853e46b1ad565403d302f62457b6dd84917c31ff375fabf487", "firstIndex": 335544096},
{"blockNumber": 1309104, "blockId": "0xcf108c6e002e7a5657bf7587adde03edf5f20d03e95b66a194eb8080daaf4f97", "firstIndex": 402653143},
{"blockNumber": 1380499, "blockId": "0x984b0f8b6bf06f1240bbca8c1df9bef3880bedafd5b5c2a55cbc2f64c9efb974", "firstIndex": 469759822},
{"blockNumber": 1476950, "blockId": "0x8323b528bb8d80a96b172de92452be0f45078a9ca311cb4be6675f089e25a426", "firstIndex": 536870335},
{"blockNumber": 1533506, "blockId": "0x737dc416070aa3b522a0c2ab813a70c1820f062c718f27597394f309f0004106", "firstIndex": 603979618},
{"blockNumber": 1613764, "blockId": "0x9d6a5a505afdda86b1ebcc2b10cb687a1f89c2e2b74315945a3ddc6a0b3c9cd6", "firstIndex": 671088253},
{"blockNumber": 1719073, "blockId": "0x17bf6a51d9c55a908c3e9e652f80b2aa464b049c697abf3bd5a537e461aff0b9", "firstIndex": 738197366},
{"blockNumber": 1973133, "blockId": "0x14b288d70e688de3334d09283670301aacfed21c193da8a56fd63767f8177705", "firstIndex": 805305624},
{"blockNumber": 2274761, "blockId": "0xf02790b273b04219e001d2fcc93e8e47708cb228364e96ad754bc8050d08a9aa", "firstIndex": 872414871},
{"blockNumber": 2530470, "blockId": "0x157ea98b1a7e66dd3f045da8e25219800671f9a000211d3d94006efd33d89a80", "firstIndex": 939523234},
{"blockNumber": 2781706, "blockId": "0xa723663a584e280700d2302eba03d1b3b6549333db70c6152576d33e2911f594", "firstIndex": 1006632936},
{"blockNumber": 3101669, "blockId": "0xa6e66a18fed64d33178c7088f273c404be597662c34f6e6884cf2e24f3ea4ace", "firstIndex": 1073741353},
{"blockNumber": 3221725, "blockId": "0xe771f897dece48b1583cc1d1d10de8015da57407eb1fdf239fdbe46eaab85143", "firstIndex": 1140850137},
{"blockNumber": 3357164, "blockId": "0x6252d0aa54c79623b0680069c88d7b5c47983f0d5c4845b6c811b8d9b5e8ff3c", "firstIndex": 1207959453},
{"blockNumber": 3447019, "blockId": "0xeb7d585e1e063f3cc05ed399fbf6c2df63c271f62f030acb804e9fb1e74b6dc1", "firstIndex": 1275067542},
{"blockNumber": 3546397, "blockId": "0xdabdef7defa4281180a57c5af121877b82274f15ccf074ea0096146f4c246df2", "firstIndex": 1342176778},
{"blockNumber": 3867885, "blockId": "0x8be069dd7a3e2ffb869ee164d11b28555233d2510b134ab9d5484fdae55d2225", "firstIndex": 1409285539},
{"blockNumber": 3935446, "blockId": "0xc91a61bc215bbcccc3020c62e5c8153162df0d8bcc59813d74671b2d24903ed7", "firstIndex": 1476394742},
{"blockNumber": 3989508, "blockId": "0xc85dec36a767e44237842ef51915944c2a49780c8c394a3aa6cfb013c99cf58b", "firstIndex": 1543503452},
{"blockNumber": 4057078, "blockId": "0xccdb79f6705629cb6ab1667a1244938f60911236549143fcff23a3989213e67e", "firstIndex": 1610612030},
{"blockNumber": 4126499, "blockId": "0x92f2ef21fc911e87e81e38373d5f2915587b9648a0ab3cf4fcfe3e5aaffe7b85", "firstIndex": 1677720416},
{"blockNumber": 4239335, "blockId": "0x64fbd22965eb583a584552b7edb9b7ce26fb6aad247c1063d0d5a4d11cbcc58c", "firstIndex": 1744830176}
{"blockNumber": 814410, "blockHash": "0x6c38f0d4ff2c23ae187f581cebb0963c0ec2dbf051b289de1582c369c98a3244", "firstIndex": 67107349},
{"blockNumber": 914268, "blockHash": "0xeff161573a11eb6e2ab930674a5245b2c5ffc5e9e63093503344ec6fa60578a3", "firstIndex": 134216064},
{"blockNumber": 1048866, "blockHash": "0xebd3b95415dad9ab7f2b1d25e48c791e299063c09427bbde54217029c3e215ad", "firstIndex": 201325914},
{"blockNumber": 1144433, "blockHash": "0x0c895438ef12a2c835b4372c209e40a499ba2b18ed0e658846813784de391392", "firstIndex": 268434935},
{"blockNumber": 1230406, "blockHash": "0xd3512e7241efc9853e46b1ad565403d302f62457b6dd84917c31ff375fabf487", "firstIndex": 335544096},
{"blockNumber": 1309104, "blockHash": "0xcf108c6e002e7a5657bf7587adde03edf5f20d03e95b66a194eb8080daaf4f97", "firstIndex": 402653143},
{"blockNumber": 1380499, "blockHash": "0x984b0f8b6bf06f1240bbca8c1df9bef3880bedafd5b5c2a55cbc2f64c9efb974", "firstIndex": 469759822},
{"blockNumber": 1476950, "blockHash": "0x8323b528bb8d80a96b172de92452be0f45078a9ca311cb4be6675f089e25a426", "firstIndex": 536870335},
{"blockNumber": 1533506, "blockHash": "0x737dc416070aa3b522a0c2ab813a70c1820f062c718f27597394f309f0004106", "firstIndex": 603979618},
{"blockNumber": 1613764, "blockHash": "0x9d6a5a505afdda86b1ebcc2b10cb687a1f89c2e2b74315945a3ddc6a0b3c9cd6", "firstIndex": 671088253},
{"blockNumber": 1719073, "blockHash": "0x17bf6a51d9c55a908c3e9e652f80b2aa464b049c697abf3bd5a537e461aff0b9", "firstIndex": 738197366},
{"blockNumber": 1973133, "blockHash": "0x14b288d70e688de3334d09283670301aacfed21c193da8a56fd63767f8177705", "firstIndex": 805305624},
{"blockNumber": 2274761, "blockHash": "0xf02790b273b04219e001d2fcc93e8e47708cb228364e96ad754bc8050d08a9aa", "firstIndex": 872414871},
{"blockNumber": 2530470, "blockHash": "0x157ea98b1a7e66dd3f045da8e25219800671f9a000211d3d94006efd33d89a80", "firstIndex": 939523234},
{"blockNumber": 2781706, "blockHash": "0xa723663a584e280700d2302eba03d1b3b6549333db70c6152576d33e2911f594", "firstIndex": 1006632936},
{"blockNumber": 3101669, "blockHash": "0xa6e66a18fed64d33178c7088f273c404be597662c34f6e6884cf2e24f3ea4ace", "firstIndex": 1073741353},
{"blockNumber": 3221725, "blockHash": "0xe771f897dece48b1583cc1d1d10de8015da57407eb1fdf239fdbe46eaab85143", "firstIndex": 1140850137},
{"blockNumber": 3357164, "blockHash": "0x6252d0aa54c79623b0680069c88d7b5c47983f0d5c4845b6c811b8d9b5e8ff3c", "firstIndex": 1207959453},
{"blockNumber": 3447019, "blockHash": "0xeb7d585e1e063f3cc05ed399fbf6c2df63c271f62f030acb804e9fb1e74b6dc1", "firstIndex": 1275067542},
{"blockNumber": 3546397, "blockHash": "0xdabdef7defa4281180a57c5af121877b82274f15ccf074ea0096146f4c246df2", "firstIndex": 1342176778},
{"blockNumber": 3867885, "blockHash": "0x8be069dd7a3e2ffb869ee164d11b28555233d2510b134ab9d5484fdae55d2225", "firstIndex": 1409285539},
{"blockNumber": 3935446, "blockHash": "0xc91a61bc215bbcccc3020c62e5c8153162df0d8bcc59813d74671b2d24903ed7", "firstIndex": 1476394742},
{"blockNumber": 3989508, "blockHash": "0xc85dec36a767e44237842ef51915944c2a49780c8c394a3aa6cfb013c99cf58b", "firstIndex": 1543503452},
{"blockNumber": 4057078, "blockHash": "0xccdb79f6705629cb6ab1667a1244938f60911236549143fcff23a3989213e67e", "firstIndex": 1610612030},
{"blockNumber": 4126499, "blockHash": "0x92f2ef21fc911e87e81e38373d5f2915587b9648a0ab3cf4fcfe3e5aaffe7b85", "firstIndex": 1677720416},
{"blockNumber": 4239335, "blockHash": "0x64fbd22965eb583a584552b7edb9b7ce26fb6aad247c1063d0d5a4d11cbcc58c", "firstIndex": 1744830176}
]

View file

@ -1,292 +1,292 @@
[
{"blockNumber": 4166212, "blockId": "0xd94b724fc1c7dceb3251b51b81f7a0f3220ae5b9add1e62917004f14f2a3532c", "firstIndex": 67108840},
{"blockNumber": 4513996, "blockId": "0xc0fd25fef5888609d05fa8c5620d8e18c31cdd675dd4ee926ff966668f0e5d76", "firstIndex": 134217480},
{"blockNumber": 4817399, "blockId": "0x8573c3166fb7f716e97cf6109d382f86441238e0b85828944a072ae8583a6cfa", "firstIndex": 201326547},
{"blockNumber": 5087706, "blockId": "0xcd2dfae45901e299b25faa04c10df60983d5c0a3d0e478e789c7aeec980cf691", "firstIndex": 268435422},
{"blockNumber": 5306085, "blockId": "0xe8026984c85873f2b24018a7e0bdf8c2df136b2fc49666b4addf0d2e067890da", "firstIndex": 335544018},
{"blockNumber": 5509898, "blockId": "0x41bc63dc8184f9a7d4b9a5a554e00eee32fee60ccfa2339264be11270ac28de1", "firstIndex": 402652789},
{"blockNumber": 5670367, "blockId": "0xe61c2ba463f2f458817ed1bf0ff9fb9d07e9d7354f46b48175b2d784b817bf3b", "firstIndex": 469761896},
{"blockNumber": 5826113, "blockId": "0xfbbb1fc5e03a5562bf6b7f1799d7a572d9ef66c7fd0e0b9f4fed7262767b5c86", "firstIndex": 536870852},
{"blockNumber": 5953008, "blockId": "0x6bbdccd094928e846de3c615a33f2952e3259afaa1929f4ee241a56907d0593b", "firstIndex": 603979150},
{"blockNumber": 6102812, "blockId": "0x2403f2502088a1fa26c1294f5245032fe3b7f3a8bb14c12d0ba8d577156bbc1b", "firstIndex": 671087962},
{"blockNumber": 6276672, "blockId": "0xeaa05a0e0574fbb164244628a7d14d9d47f1692941098aa737059d44104401df", "firstIndex": 738197399},
{"blockNumber": 6448662, "blockId": "0xe6f097fa18a2cef425514e863659e42e1aa8d74df49cb0bed5877e82a08d1f84", "firstIndex": 805306056},
{"blockNumber": 6655925, "blockId": "0xee5c9d87b06dc0c2e850fead07bcd13f85d45ad6b5a6e7ebee53cc8618772786", "firstIndex": 872415229},
{"blockNumber": 6873878, "blockId": "0x9a0ea103146da21fa1d9cab7ff609ec2c5e7f1856288a1f744239588e33904c2", "firstIndex": 939524080},
{"blockNumber": 7080893, "blockId": "0x35ad45055d7a1a3ef94efeb05e4122757bcc126ff1bc2b4ac72dc78ab3c0fd75", "firstIndex": 1006632327},
{"blockNumber": 7266955, "blockId": "0x7b80f70520f2c16eb5890f4963af8f60446f50af82b589ceaf009d09e704b302", "firstIndex": 1073741608},
{"blockNumber": 7466649, "blockId": "0xea8980b5c692c729013f9cc4c9f66e94d5ef2c5caad23f370be0953b6d85f32c", "firstIndex": 1140850335},
{"blockNumber": 7661736, "blockId": "0x615cdb03dbf29a22d59bbc769c03f3647c2c9abb7d1767f3f6529708209e7073", "firstIndex": 1207959232},
{"blockNumber": 7834494, "blockId": "0x05b7dd13e2eb8a833e4b278f9151ca01b294ae5fc5617769422ec85ffb446215", "firstIndex": 1275068098},
{"blockNumber": 7989998, "blockId": "0x4b5026add7f2fc2c02993b82c61bceba1b75cfcb8a78c5112cc2832bc0413be6", "firstIndex": 1342177025},
{"blockNumber": 8142950, "blockId": "0x9f56b7753a0e9964973f3e096e1385215e3aef232b448359e5bec05046b016a3", "firstIndex": 1409285545},
{"blockNumber": 8297598, "blockId": "0x2ddfeb161fe34ff73c4a995bb94256c3b522b39348169edb97bb3d876ccdf77d", "firstIndex": 1476394898},
{"blockNumber": 8465061, "blockId": "0xfdbe5435da5ae9fb34a9154e5ebe515afac9a128e383f798f1e69952be404fcd", "firstIndex": 1543503805},
{"blockNumber": 8655740, "blockId": "0x305137325a39a44aba997e214c05f32d965658f7e3fe20322f2e57e72f239977", "firstIndex": 1610612406},
{"blockNumber": 8807102, "blockId": "0xc32f9fd9a43d2b502dbcc4a683ffe324fca9a871753478bcb1f4fb9573da96a5", "firstIndex": 1677721343},
{"blockNumber": 8911110, "blockId": "0x806eabee7dc429b57afcb6c0b72b61728b4f13c62ee9942f1813790463f9ab5b", "firstIndex": 1744830172},
{"blockNumber": 8960262, "blockId": "0xa69f24032e9d5761565e726d484926a0932c6ca7fdb4c04a55bfc5e8f5879c4d", "firstIndex": 1811938729},
{"blockNumber": 9085906, "blockId": "0x922b4d6aeca64517f5048fa1ffa98d3e813bc415716a1763d07cf2bdea236896", "firstIndex": 1879048062},
{"blockNumber": 9230838, "blockId": "0xd892f513c48c95a859ae59c4e00ec4be0d684c549456c90e9c36669c11092f50", "firstIndex": 1946156580},
{"blockNumber": 9390449, "blockId": "0xcc0ced78fa66b570f338d129794d574560a23958e28d0d5288003d4542cec734", "firstIndex": 2013264376},
{"blockNumber": 9515554, "blockId": "0xc06d04737813c3e2c8e910b2d4543c5c684c7bfe235bb501445f68fd0663f3d9", "firstIndex": 2080374749},
{"blockNumber": 9659367, "blockId": "0x765fa33d9418b1ed648e88b497b4bf4aa66990e6413dfc155525cda61e3cc813", "firstIndex": 2147483344},
{"blockNumber": 9793940, "blockId": "0x9527dcbd85a100c51aee4ba90828fee23312f5f041859f3416a54fd87a437ce2", "firstIndex": 2214591669},
{"blockNumber": 9923756, "blockId": "0xc4da5fdd2de077459fe84f721d7fc01ef69683e03e634b0c5443186486792246", "firstIndex": 2281700949},
{"blockNumber": 10042325, "blockId": "0x6f554f195d20e4ba1262f73cf2c1cd44b14a71befe5ef0a5318fc309f8cde9fa", "firstIndex": 2348810131},
{"blockNumber": 10168768, "blockId": "0xdcdf88ec2c197d552343c80d01375a35d6462aff39c5bd976160f7176d2add64", "firstIndex": 2415918723},
{"blockNumber": 10289467, "blockId": "0x5ece23babcf6b8838695b891b044c3dcf8120cdc15acd83018e96ffc590f6cd0", "firstIndex": 2483027720},
{"blockNumber": 10386601, "blockId": "0xd398ee129aea1247019fa39dc2f5083819687d8f5ad17ec39ba1803d178fbcda", "firstIndex": 2550136043},
{"blockNumber": 10479586, "blockId": "0x61c1cccd60a546eacc29f2ae2a6facab402b4f35e2986c6629b302e6b1ebca3e", "firstIndex": 2617245577},
{"blockNumber": 10562579, "blockId": "0x14fd6a079050e3e110140372605c050945d6db03746fa5b84a8718cb2a0e9b86", "firstIndex": 2684353802},
{"blockNumber": 10641438, "blockId": "0x1998dcae09111b2c1bf6d0592e2f0b4a0143fe7db7115ff94b2b73b74542cee4", "firstIndex": 2751463334},
{"blockNumber": 10717094, "blockId": "0x9ce45a150bb6bd13687d0692c86716911ffc6aff5831a03f4015bdffb85f5093", "firstIndex": 2818571144},
{"blockNumber": 10784494, "blockId": "0xf65c936cf578e673696477f7db7189b47222fdf7ed13096564b9d47b15c86eb9", "firstIndex": 2885680709},
{"blockNumber": 10848591, "blockId": "0x84ea8e9d5789302ebe88888115488f91cfb75fd89e282dfbd29ba9d6225ab0ca", "firstIndex": 2952789491},
{"blockNumber": 10909102, "blockId": "0x22275047f643f4a3b82c349274a53d56a00da25beac89655877fb8cbfa3182c8", "firstIndex": 3019898024},
{"blockNumber": 10972828, "blockId": "0x38e8acfec19bb53ab9fe70dd3577ccd8bd7c41b4d468b8f099a19bf06ce56518", "firstIndex": 3087006561},
{"blockNumber": 11036532, "blockId": "0x478dac03f520e987a6e845cac1b3a756d69e4f5257f20758b8fc38456e7b6884", "firstIndex": 3154116463},
{"blockNumber": 11102457, "blockId": "0x1a36616b03351ebe481330fc86578f57d9da5c0dda59f4406d49b53a2fa9b6f6", "firstIndex": 3221224961},
{"blockNumber": 11168092, "blockId": "0xb23af483ca6dbc2fb8d2fada1f8189de2e155758ed9ecbbb20c234a30c7e093c", "firstIndex": 3288333510},
{"blockNumber": 11233640, "blockId": "0xdc4c80d8370823773425b31e70b3af59582b556514857d1e9a9b4c53b880a424", "firstIndex": 3355442901},
{"blockNumber": 11300454, "blockId": "0xdbd4d8e9c1756093037f431330db4604e5a3e67bb74f425f667633d5298fe34c", "firstIndex": 3422551962},
{"blockNumber": 11367550, "blockId": "0x9e00816bd5b39353657a99421d278091599a39f246bbf8c0898f83e6d70b7c7c", "firstIndex": 3489660743},
{"blockNumber": 11431820, "blockId": "0xd32d5e5871ce71acc9dcc7cf9d8cb9d964646182529c327a0ddf617814e27d74", "firstIndex": 3556769150},
{"blockNumber": 11497031, "blockId": "0xed823054d4b293294e3301d3131e2ae9f0ce63c9f07d2d80fc144e42d535d61d", "firstIndex": 3623878513},
{"blockNumber": 11560027, "blockId": "0x7098177efe77cfb5912dc63069fe366033111041ca5c539907a89e439c861d8f", "firstIndex": 3690987092},
{"blockNumber": 11625049, "blockId": "0x61fccb47104a076b905d390498a1b38c317348bb1567e69aa1b670bed7a6a13f", "firstIndex": 3758096043},
{"blockNumber": 11690298, "blockId": "0xaf6f9e9c92db9e155f1ffb35eb68765c09de1206c353c9a56a5d5fa53e2d88d2", "firstIndex": 3825204430},
{"blockNumber": 11755003, "blockId": "0xf9efb54f08a2b59086a607541005928f1bda54fa2c46fcc48352fa6aa461c03a", "firstIndex": 3892313541},
{"blockNumber": 11819591, "blockId": "0xa551deb3bd2ec0ca2683cb76adb8fb295d171a3787aa5ff071bd1fa94d3af82e", "firstIndex": 3959422322},
{"blockNumber": 11883278, "blockId": "0x60c7a359d4aeb09b63b64487fa7794aee87c41b11446f69ba83e24d5bce6947f", "firstIndex": 4026531239},
{"blockNumber": 11948436, "blockId": "0x1c46fb22653e5f9eb9503fe532151519845d05b121291766715973e027eb9c0c", "firstIndex": 4093639741},
{"blockNumber": 12013084, "blockId": "0x0b8d1da9e70c4494ba61190390c891230846e69d9b0be605df8b04c4199ffd1b", "firstIndex": 4160749561},
{"blockNumber": 12078612, "blockId": "0xb049af260be25c6153b67c227689b0556d8fb7d6033c3f61b8218f5c4ac5917a", "firstIndex": 4227857310},
{"blockNumber": 12143549, "blockId": "0x685dce41efbe2bab0ac9a4b9026d9e5faa3e01ae4637c8181cd2f1eac326fe6a", "firstIndex": 4294966232},
{"blockNumber": 12207918, "blockId": "0x53644ea8f781c6270f1136ed4bbcb956c98f12a4793c2f56f49b3105282b13ea", "firstIndex": 4362075410},
{"blockNumber": 12272370, "blockId": "0x503523e1a01cb7d68be0eb22f36280c0ee7a1fc802061a704e326fbb8efcfe27", "firstIndex": 4429184831},
{"blockNumber": 12329342, "blockId": "0xe6a1bf78dd827def078e23c768cf6e96666ee06b5fdc113380a4769d85f4bd33", "firstIndex": 4496293151},
{"blockNumber": 12382311, "blockId": "0x352fe793c16b1d8aea195af86ee9aec3dbaa83e7a32872a0d222a77e96fc0e57", "firstIndex": 4563402486},
{"blockNumber": 12436970, "blockId": "0x90276349edf0941bb3327dc1d749d0371fe550eaddafb5141fcacdd281de0976", "firstIndex": 4630510187},
{"blockNumber": 12489954, "blockId": "0x71894b1e1608aeba777662a6055e601ce6f17950dbe3844c3601e0f88cbd5848", "firstIndex": 4697619586},
{"blockNumber": 12541655, "blockId": "0xc34c7299041df72a81c4b86e81639d2dfcebfae603e287fc143881a636ce1188", "firstIndex": 4764728948},
{"blockNumber": 12597319, "blockId": "0x4c4dc71d8d2f1223ad0469ddfe09fa8934f64e3712ed83708d6e1b9c64eceebb", "firstIndex": 4831837390},
{"blockNumber": 12651863, "blockId": "0x406a642c8364fb61cbd896512a2d19ecdba3932ab09ac67cf48d91aa1a03f24e", "firstIndex": 4898946362},
{"blockNumber": 12706391, "blockId": "0xa7fc3739898f7ff5733896313b248fb0b306a132681609b8b9e2671b42dac3a1", "firstIndex": 4966054748},
{"blockNumber": 12762840, "blockId": "0x5a1048966dfa55a48248517c45683ce342671bf544117d2bac13d8e343563136", "firstIndex": 5033164303},
{"blockNumber": 12816597, "blockId": "0x97f5b87f422870a8ffeff70c12f4377dde96d248aa912cb64ebfaea51dc086b6", "firstIndex": 5100272541},
{"blockNumber": 12872315, "blockId": "0x432f7df7973fdea4e5abe2c09b634ca3b724e37a19d5ec23ca5810e4854c6b33", "firstIndex": 5167382243},
{"blockNumber": 12929620, "blockId": "0xa4524e3b4c5dadac91df2f29696532cfed377f47469fe7cfabf10baac945eea2", "firstIndex": 5234490748},
{"blockNumber": 12988653, "blockId": "0x9be70e31aeea164c370d8d183a1af45120693b13d360edc78214535c40ef1d63", "firstIndex": 5301600248},
{"blockNumber": 13049074, "blockId": "0x3076857bd17959c2a3df18357eab3ecbb7ca25457785b07074d2e885eb566fc2", "firstIndex": 5368707746},
{"blockNumber": 13108823, "blockId": "0x67b2764f6b53779e3d683def2c7659c6d92814152574073d24f10cd67200a22b", "firstIndex": 5435817680},
{"blockNumber": 13175389, "blockId": "0x0d41e34dbca58dec45e0509b6cbf027b2377220845a85caebbf3a14b8505e8ce", "firstIndex": 5502925085},
{"blockNumber": 13237624, "blockId": "0x723e0818d54ab1b1c53efc2f2f448c16c1f943af2033575c612c3c71228d4ddf", "firstIndex": 5570034948},
{"blockNumber": 13298658, "blockId": "0xd7f9f477b20bcf1379ccaaae701c9658a02b177861e0af9e873c63fe8b854f4b", "firstIndex": 5637144562},
{"blockNumber": 13361166, "blockId": "0x9f9e4130093269d59045f91344a13eb2786023e02099f53dd44854d50a7f5ffb", "firstIndex": 5704253260},
{"blockNumber": 13421716, "blockId": "0x28752f35ad2adeba342ef6aa51bca1c0abc757c4f712cf620c8a5e6a8576636e", "firstIndex": 5771361775},
{"blockNumber": 13480516, "blockId": "0x43f4ecf24c17b7a77621e2ca09c88c53b3407fb018f4bb3a032c212015b4e18d", "firstIndex": 5838469616},
{"blockNumber": 13535688, "blockId": "0x99ffcd9982219a4be45fa5f15622b288afe4f85da536956d39dccac1ec416444", "firstIndex": 5905579739},
{"blockNumber": 13590487, "blockId": "0xff7b74464a318037e0670c0be7a5b1b79418d1a42c7a3587c46adc1ef720c819", "firstIndex": 5972688013},
{"blockNumber": 13646596, "blockId": "0xd555ce5f2b65ffeec9b9e3e75a56f5f23d7353a1014bcc861efd7df6e5dc2c92", "firstIndex": 6039797525},
{"blockNumber": 13702902, "blockId": "0x3e8feab590c6631c63931fee83bf7e10234890bdf377b1b374fb70221b9ac9c0", "firstIndex": 6106905641},
{"blockNumber": 13760908, "blockId": "0x18e96fa585c57aad1f2a8dc9639d9f4fe53c3f38894862471b7cec17f97f3cb1", "firstIndex": 6174014330},
{"blockNumber": 13819340, "blockId": "0x25384bf2cef178e30c4d9e0abae59357055241fc715133b2bb078e8278e5296a", "firstIndex": 6241123906},
{"blockNumber": 13877817, "blockId": "0x4a84a0a7035dbfed0e1281fb1df162a195f2c516686a2e2a6ad605e6f2b3cf57", "firstIndex": 6308232690},
{"blockNumber": 13935260, "blockId": "0x6c14a8e563614edbad66698ebe911e997e27844f9d33c7069795379540e71a44", "firstIndex": 6375341148},
{"blockNumber": 13993914, "blockId": "0xc0991d1bc431c5215fb0218b0b541f2b4fae0994775e5eddb016965fe5b4a0c7", "firstIndex": 6442448947},
{"blockNumber": 14051007, "blockId": "0x854803ce364b6fe4446800e74448a5ee883e7d472e8a0347f1ad13bd347fa69f", "firstIndex": 6509558964},
{"blockNumber": 14109063, "blockId": "0xa3fadce7ab18a5fedcfed9e3b1f0cf2fb32151172c362327c602343bc26dde8f", "firstIndex": 6576668017},
{"blockNumber": 14166701, "blockId": "0xaa2443a38e2b0d6b01cf4b73de54d2b7e92c99475bd0802beebd86845c126d1b", "firstIndex": 6643774648},
{"blockNumber": 14222367, "blockId": "0xa27c78134c1fbf432c8caee7c9d2ef323fdde519a065804c9c8da6ca1615e04f", "firstIndex": 6710886238},
{"blockNumber": 14277029, "blockId": "0x5d0ed08b65db01e382c339dcb2715b2927fb472b0c1f4256841c386d785c3f3a", "firstIndex": 6777995247},
{"blockNumber": 14330955, "blockId": "0x3e5014d82c416549acfe2e26b13ff5ddf29ad74943ef5b94a65eca0a4d72d82e", "firstIndex": 6845102969},
{"blockNumber": 14384139, "blockId": "0x720e1fd687292019c3fa1812c39c73d20fde783c0196f43378a51cb4e0b46d65", "firstIndex": 6912212078},
{"blockNumber": 14437534, "blockId": "0xe4342ab6f363066d9c2514a870241ceb63df232d33659fdb3928faf433360bb4", "firstIndex": 6979321493},
{"blockNumber": 14489073, "blockId": "0xeb2f9923d816e773460eccfa42eb4ff2bbfc353b756ba274e98dc85c64ab1536", "firstIndex": 7046428879},
{"blockNumber": 14541559, "blockId": "0x55cc33721fa9022964f47c30f4146fee7cc80f5b4158d7ecea0ff661cbba3487", "firstIndex": 7113538857},
{"blockNumber": 14594432, "blockId": "0x676441bdc1ec430a9fbc05e56a0437a475606a253f9fdffc86f5f6286c159b5d", "firstIndex": 7180647956},
{"blockNumber": 14644942, "blockId": "0x03232a1d3c9b2f1f0c2a4b780c83bce66238d9abdeed71c2a8af9e97a53011b0", "firstIndex": 7247754981},
{"blockNumber": 14695793, "blockId": "0x6017f6d1d79188692e1a1a18de5f93b83b97905c8ff5bed5cef4b2e9f4d4d41b", "firstIndex": 7314863646},
{"blockNumber": 14746186, "blockId": "0x332c8ac0b2960fb1627407d45443201a0f6de7421b52af077b32fa6e018e5401", "firstIndex": 7381974499},
{"blockNumber": 14798403, "blockId": "0x4507ae1d9f27a3eada874dcf04112a5d4ee7155701afd3fb857fe58201a2dede", "firstIndex": 7449082752},
{"blockNumber": 14848093, "blockId": "0xff98c820c66c3beaaa3b195158c94f099148573a7f032295ba7156c13f78eaa8", "firstIndex": 7516191978},
{"blockNumber": 14897508, "blockId": "0x49c31d3433b197585f733d7c382ae16d9d78bf59c333a9b76362cc6c5fed270c", "firstIndex": 7583300663},
{"blockNumber": 14950638, "blockId": "0xf1a1609206788977841927271808fe1c42d059026385d025bf4cea0c0a05966b", "firstIndex": 7650408992},
{"blockNumber": 15003958, "blockId": "0xf9e6b41108d907672f29ba1df97ed83aca2ba933335e6b5ccfb5d4001807b33f", "firstIndex": 7717519105},
{"blockNumber": 15056783, "blockId": "0x126a5a5c22e878a4c42bea890d48cfe00edf7fbc27d805c88a5269540bc94a42", "firstIndex": 7784628173},
{"blockNumber": 15108163, "blockId": "0x5f8494ccacefce9fef8a95a52247f2670ed30bb5a3b3deb85c645e72c115d506", "firstIndex": 7851735261},
{"blockNumber": 15159385, "blockId": "0x0fed6c5aafb53e9cdb0d8620c3eef9b619fb3ae9559296618f634eddae33f0c6", "firstIndex": 7918844299},
{"blockNumber": 15210872, "blockId": "0x37bdb94b57044b67fbd9b9a7cdc728fcfd2066cf1904cab478fd42d41ec6e1f0", "firstIndex": 7985954119},
{"blockNumber": 15264226, "blockId": "0xc173ad8a791e2293efd8e0b2c9f088fac49233aabb1e72db6a045aa9904f4728", "firstIndex": 8053061840},
{"blockNumber": 15315693, "blockId": "0x985cf97bf28f0becb2b16abfe629a5f520596e45fc23f4936bc2f6298b8145f9", "firstIndex": 8120172048},
{"blockNumber": 15364606, "blockId": "0x593362ef8a82140f3dd3ff13d0ce50287dcb0b8b3d31ffcc7813c6be16f7438d", "firstIndex": 8187281275},
{"blockNumber": 15412727, "blockId": "0x5a6ff10acc48e9e8bcf963b042012ea094628b9488b8bf569783b3297c67eedd", "firstIndex": 8254388575},
{"blockNumber": 15462642, "blockId": "0x137f15c5d112b0fdc6e4c84393032030646122c3b0d86286143a1ab8bfacbac4", "firstIndex": 8321497214},
{"blockNumber": 15509781, "blockId": "0xe58a8ca223b7c03755d046a6212bc33fa9814f73efe52804d8eea7ae4b4bd7f9", "firstIndex": 8388606496},
{"blockNumber": 15558625, "blockId": "0x4127d527b58c7685a1237d228508ea77d40f33dbeffb5cc30e47e83a4b6152a5", "firstIndex": 8455714910},
{"blockNumber": 15604075, "blockId": "0x7d1c1bccfe5c23a1edd81246e6e55277f271cffef30bde22c9d1202f764dd96f", "firstIndex": 8522823324},
{"blockNumber": 15651742, "blockId": "0x732e5203a504fd97510325c1fb876476ca132831e78182367cd4b4efae1232c3", "firstIndex": 8589934155},
{"blockNumber": 15700810, "blockId": "0xe031decb5e42a69dafcc814441227085508cf51c9c77f35d06487db19bd05a6a", "firstIndex": 8657041374},
{"blockNumber": 15762855, "blockId": "0xbdff9b1e193c44f35bf38945d2523c74ef59c4f352efc93fad02ef34eef1c5f0", "firstIndex": 8724150924},
{"blockNumber": 15814304, "blockId": "0x524a226d9f48e37db00e252c7f6a1350d1829d38fe83c756021cc11c56d3b2dd", "firstIndex": 8791259373},
{"blockNumber": 15865514, "blockId": "0x4e4ad46f3ae7fb2a2e7cb94aa804929cb7353c79c78cc2cbcee710da5f0ec4b5", "firstIndex": 8858369571},
{"blockNumber": 15920411, "blockId": "0xa39e4cbd8e84b427e44b8d06b5b1149652f3af5fa88633b802665b8677772bc5", "firstIndex": 8925477782},
{"blockNumber": 15974193, "blockId": "0x4a3f82935e6fcda6ce7636001989b65b79422d362e60833e01185a7960b2fa55", "firstIndex": 8992587369},
{"blockNumber": 16032739, "blockId": "0x7f50bb76dfd01125e0a4a7595106e43329eb8bd02e819be2ade4248a403c714a", "firstIndex": 9059696611},
{"blockNumber": 16090896, "blockId": "0xdc4db130973ef793d2223844e35122a2477e642d86c69616fb3604d2a79805c4", "firstIndex": 9126804425},
{"blockNumber": 16150783, "blockId": "0xb46294fe1cbcbbdff7309ebf0bf481e78e2589d386b95fa4c781c437e02e0aec", "firstIndex": 9193912139},
{"blockNumber": 16207278, "blockId": "0x8a3af0387572b56989bb6c98d8860e07b5cdefb2ebe7b1dc58be07c24a53e056", "firstIndex": 9261022215},
{"blockNumber": 16262389, "blockId": "0xec5a03d88cfc1bf8c278ddce7ace5032c3f7ed5032b4575df54779ec1abca131", "firstIndex": 9328130680},
{"blockNumber": 16319525, "blockId": "0x74ae702375e242205fe4986448b0ee494d4a38ccc813981489c4b0488e58a949", "firstIndex": 9395240779},
{"blockNumber": 16373441, "blockId": "0x5f404555cf45c547ef0fc044f2701bc7a28ce1e432de06e0264dbfd1c17447d0", "firstIndex": 9462347671},
{"blockNumber": 16423343, "blockId": "0xe4af890b6c4dd3b10af2e6c08f14563bd2a3121b22a3cc6a0db29d438c1dcc59", "firstIndex": 9529457704},
{"blockNumber": 16474687, "blockId": "0x019a8b374cf150fcda1c8af2a3e7ba63603ae81744d6343897c4b17ee58411d5", "firstIndex": 9596566169},
{"blockNumber": 16525504, "blockId": "0xad394ce7e84e1c904cc5dd6dabd51b1a575c0b2fb006f10f272875b7f921904e", "firstIndex": 9663675287},
{"blockNumber": 16574051, "blockId": "0x098e2dca2eda0d700aa3127bdfb679ce16e3ca297a305978c6ecfff044269511", "firstIndex": 9730784563},
{"blockNumber": 16622457, "blockId": "0x8627f1237731c1127116898724f1b6420b8e7bcd8e18a0ea568f192bbd2cd55d", "firstIndex": 9797893241},
{"blockNumber": 16672410, "blockId": "0x3e32b46faa670b191b89bea84a68022d5ea8b90008b388a948ff27f871d17292", "firstIndex": 9865002964},
{"blockNumber": 16719964, "blockId": "0x1668c3ea6cb256f306e93851cc2c00612f85df5cf7b5527ffce14c89088d7fdf", "firstIndex": 9932111708},
{"blockNumber": 16769014, "blockId": "0xed9a6f88e47b5bfe4d5bc92e1c1bfeb252f716f0990ffa1f907655c0f5e3a365", "firstIndex": 9999220312},
{"blockNumber": 16818134, "blockId": "0x93504ba617500698a1b46f3110cd26827b8efe468190b2a91d3595784e34a1b6", "firstIndex": 10066329167},
{"blockNumber": 16869369, "blockId": "0x94ca4b395a5c36c4e9d07cbb08271f0680455af4e4659906b647e16abebd7d4e", "firstIndex": 10133438415},
{"blockNumber": 16921428, "blockId": "0x66650775bff4cb95de56099c10b06f3dfe302a731b391f406d1aa551121308ba", "firstIndex": 10200547148},
{"blockNumber": 16974677, "blockId": "0x58d5fc4cd45047dac10d55a036e0a4448faacc1041b29c4c7fb41a6e9394c3ff", "firstIndex": 10267655623},
{"blockNumber": 17031050, "blockId": "0x9d15b831979e95fc4149ecae46478cac1e54470b277d57d7102d19a72f28eeba", "firstIndex": 10334764441},
{"blockNumber": 17086163, "blockId": "0x978ff48863fcdb50b274ea4220e7635c13c41bae91912db9e24205fb917fde85", "firstIndex": 10401873176},
{"blockNumber": 17141086, "blockId": "0x4084d442499893361731f17534944d260cccd8e07d41c52d7160910555770b90", "firstIndex": 10468982462},
{"blockNumber": 17190735, "blockId": "0x42ed86fb42def23870df6ec23204f557669ec7cd8de59c9f3ef9ab90704e4b11", "firstIndex": 10536090726},
{"blockNumber": 17237026, "blockId": "0x9a49d943f82a7335f8049df4d953080f25920e3b098f6a2d872520166b225961", "firstIndex": 10603199938},
{"blockNumber": 17287007, "blockId": "0xac3d189676d24eadc62adcf97d8730a001a7766fe3ac27b18e745ca9727f6c46", "firstIndex": 10670308186},
{"blockNumber": 17338498, "blockId": "0xe50e251aa0a3209fa8a0030b237cf8357b08eeb80492a432c8aa5969a820ee42", "firstIndex": 10737417623},
{"blockNumber": 17389178, "blockId": "0x1e18cedcfa6c81bc845c9136fc92f9f27ca8fff8847289bb139f374f0f6ba733", "firstIndex": 10804526769},
{"blockNumber": 17442038, "blockId": "0x07f47ebbee99938fb96beb1ffadf8266f71d0d73f393172f0e24facd40d45c97", "firstIndex": 10871635940},
{"blockNumber": 17497589, "blockId": "0x247a5e852e36e46f7897ad1875e93d810ff4434bb835b469afae3a3d3d4a9c9a", "firstIndex": 10938744251},
{"blockNumber": 17554111, "blockId": "0x420e420a1b599e4dc3bd115652b4d6f5eedc391086ef064f97a9cbffc02b62f9", "firstIndex": 11005853686},
{"blockNumber": 17608282, "blockId": "0xb2ba644d3593d3c832f932f14ffec532e26e9e1cdcd8fbe7477f46e238c6bffd", "firstIndex": 11072962303},
{"blockNumber": 17664091, "blockId": "0x6d0bfff8145652a617bfe03ab87e3af09219cdb822c9c214b42b16f20742c1eb", "firstIndex": 11140070260},
{"blockNumber": 17714917, "blockId": "0x61509544b5174d3ff5d48816ff54f2929c8a400ef21454116056baa136b607b6", "firstIndex": 11207179013},
{"blockNumber": 17763850, "blockId": "0x8da83012621f42670601406c902ce6e48e77c6285dcc6983e108e69b3a8d5d2d", "firstIndex": 11274288732},
{"blockNumber": 17814186, "blockId": "0xc37bda200856589c220dc206e31186d6c12761fd193f5bc7123cf331fbbed7f5", "firstIndex": 11341397343},
{"blockNumber": 17864461, "blockId": "0x99af43893dc9f54c55abbdbd7b000e27248145371e9b00db116dfb961b36749c", "firstIndex": 11408506122},
{"blockNumber": 17913212, "blockId": "0x9b642236d63d756fcd3404713bc6abe97804b46a9e665ae5f1d9aa48e693fe78", "firstIndex": 11475614347},
{"blockNumber": 17961497, "blockId": "0xaec9b1e851cf51646673ec7698e1d5964d44eb04729ad11385f6ae84d497070a", "firstIndex": 11542723713},
{"blockNumber": 18010859, "blockId": "0xb15fb7fc308516cad5f1dadfb7dd92fd3fe612308f67154cf2a241990aa15bc4", "firstIndex": 11609833204},
{"blockNumber": 18061052, "blockId": "0xd8ea94b8b8a7380d21dc433abb69973f3de2eeb339f4aaffdb79f85a5d86929d", "firstIndex": 11676938565},
{"blockNumber": 18111466, "blockId": "0x5b5cc6d182bffaf06692a7efda8cad5d4742fdbce5c1146b3be3c5be10ea046a", "firstIndex": 11744048539},
{"blockNumber": 18166001, "blockId": "0x901d8401c795542c75069cd917781fa587ad416da0a5298f144fae2f14305705", "firstIndex": 11811159949},
{"blockNumber": 18218505, "blockId": "0x5894285023f34ec6fd501cd90b605e118b9688ca7c01082289bf69a6987142bc", "firstIndex": 11878268395},
{"blockNumber": 18271037, "blockId": "0x3f0fce315fd993d966b6c599ebd360462466e67046d512dc430ac07f75243bae", "firstIndex": 11945377050},
{"blockNumber": 18322778, "blockId": "0xfa6536b3b06e7b579e53a8d01368182808c950586e39e12485be3145c3acbf43", "firstIndex": 12012486036},
{"blockNumber": 18372240, "blockId": "0x5531859d5b8a4093020e2fb971808d0eeaddd7d61dc58d70f34923081f97336f", "firstIndex": 12079595358},
{"blockNumber": 18421630, "blockId": "0xfeadff676fe8f04d0362b77ea2d7c3ea59b05d68cc6b2cc95cfe751558b1bfa7", "firstIndex": 12146703872},
{"blockNumber": 18471472, "blockId": "0x352b0a17e4160d334b876eb180d686fd67c7ddc40a78219b0bc4b59f5ae5912d", "firstIndex": 12213813174},
{"blockNumber": 18522088, "blockId": "0xf7d408309246089531b47c787049c7ffd2820c1b87b4804033a458bc193a3432", "firstIndex": 12280921181},
{"blockNumber": 18572725, "blockId": "0x44126d4a5b43594b09d26e0b0a5dc859c074865cb9b11ab78c80f28406fd0240", "firstIndex": 12348029615},
{"blockNumber": 18623223, "blockId": "0xd3aec1f98cea30e870d0f530194e9a72e225eb45eefe435e38da5967eb2fa470", "firstIndex": 12415139522},
{"blockNumber": 18675228, "blockId": "0x24deaef2508038bddf8e9020f95128ae34eb1786f786c3f98df0bdf95f3e97cc", "firstIndex": 12482248123},
{"blockNumber": 18725495, "blockId": "0x2e5a7edd7337b20e862741e116680a472089426dbc2b0eb5f1c984f8d6da935b", "firstIndex": 12549357121},
{"blockNumber": 18778174, "blockId": "0xe734a1084fa5eeff427bba8e8d3623181a30dd9bad128fc22de9b15dffd0e549", "firstIndex": 12616466131},
{"blockNumber": 18834807, "blockId": "0x0924a55ef4f9c2605261433c5b236d47645484efa44b34dc9f15b7c2e6b47a74", "firstIndex": 12683575000},
{"blockNumber": 18883054, "blockId": "0xef638ec491a1cf685b22bf4072e41710440d6133a53e31250762825247d3eb64", "firstIndex": 12750683712},
{"blockNumber": 18933405, "blockId": "0x6e9eefc2e121d13e6c04544ea370fe8543afeb36fef3a8e2ea68afb079738c8c", "firstIndex": 12817792510},
{"blockNumber": 18988033, "blockId": "0x4c98c0dbb6a7217609c4b277fe7ff9e2c32c8d000ee628a3f37b2b296e1ca421", "firstIndex": 12884901389},
{"blockNumber": 19041096, "blockId": "0x61a219a23d111cd07861ff47b89d4b2840c0ab5421d5d55a6f1fe43da3f33b98", "firstIndex": 12952010147},
{"blockNumber": 19088914, "blockId": "0xefb26ba58447e50e9b820e0b0d16c08da45793a824af12a2e1f38b6f46b7e36a", "firstIndex": 13019118891},
{"blockNumber": 19140413, "blockId": "0x91dbb13623fc4d5a4e462dfa0a03c92bf5301948dd4b2c3e5fddd2bc88856d47", "firstIndex": 13086228137},
{"blockNumber": 19191876, "blockId": "0xdd7f97af7ad206b2636656ed4722fe1c47c6ba9fa5531066785acbe4f8d4c4ad", "firstIndex": 13153336551},
{"blockNumber": 19237651, "blockId": "0xf1e875f314314abe3654b667c6443c772dd67c525c52877b06515ea8fc5a391b", "firstIndex": 13220444812},
{"blockNumber": 19291032, "blockId": "0x9eda269ac47957b619d2a8c408b5efc8cfb168a46bfde351f43325fa337fce47", "firstIndex": 13287553967},
{"blockNumber": 19344225, "blockId": "0x62859b78d45f589f679fc6a3bf266a2e0aa906fd61d787755ec51cfadc05eaf6", "firstIndex": 13354663338},
{"blockNumber": 19394709, "blockId": "0x7cda3d193bcfe8ddbc89b85e98c814dbbe31a54ccab52ab7738ecd003061f3c8", "firstIndex": 13421772549},
{"blockNumber": 19442850, "blockId": "0xd95f3f8bab5b91c3e1f848c9455936850bf92cf6a5086b817f6f81031bc0f81e", "firstIndex": 13488880704},
{"blockNumber": 19488177, "blockId": "0x3a1a9623ee332d6decada9359cf84eeafb81fffa997d6cf4f3efcc92e98e9310", "firstIndex": 13555989570},
{"blockNumber": 19534365, "blockId": "0x5c7a8ee7cbba4894ea412cc335895a9a7d3369d35779bcdc97a6c24b6a81a7ba", "firstIndex": 13623097446},
{"blockNumber": 19579409, "blockId": "0x4280f5f76cb0f9947f9da1cb9de6c4b6e65b37b3a3c174f5fb29d360cf389ffb", "firstIndex": 13690206790},
{"blockNumber": 19621406, "blockId": "0x0a98ca72e97fd295c7c73263b17185d74dbe427f41c616a5d5454faa5a1c63a0", "firstIndex": 13757316653},
{"blockNumber": 19664868, "blockId": "0xc8c53d89b7a849861f3daaf9503e6397a0340cecb085850c3636bee6ea2b7189", "firstIndex": 13824424608},
{"blockNumber": 19709011, "blockId": "0xf65babc88ef6a9eb2ffce9a6844adf895995f65a2facd1956f5df17ca68dbf94", "firstIndex": 13891534141},
{"blockNumber": 19755170, "blockId": "0xf903882449eeb86d69af33b6797099275e82d8787334a02a42a2168dbd739b95", "firstIndex": 13958642627},
{"blockNumber": 19803666, "blockId": "0xa8a975f570db082a3771790bb1fe405667dde4674ff49e31092687fa92727e36", "firstIndex": 14025752433},
{"blockNumber": 19847730, "blockId": "0x89d479a4b4d9faa7af3e3f61d13a7a9c80a11c4adf7cd018859eb0ddba30bdd5", "firstIndex": 14092860598},
{"blockNumber": 19894138, "blockId": "0x740278e6dc79cfd2b030780868a1b8e1520dbae0e8d3ee0a3ef43c84c83c6e0a", "firstIndex": 14159952129},
{"blockNumber": 19938334, "blockId": "0x05ae7ce9813235c19baa57658bc5c8e160130328a1fd2cc58d2baf2d9a4ae097", "firstIndex": 14227078393},
{"blockNumber": 19985138, "blockId": "0x511c8876a93a77fa473ae0d29e25d8163162c09b7212f506be0ea73840ea3971", "firstIndex": 14294187397},
{"blockNumber": 20028221, "blockId": "0x64379baedf429cfa65df076142a7d9909d7e369d6aa6b8e161bb686944d169ff", "firstIndex": 14361296446},
{"blockNumber": 20071555, "blockId": "0x4f43d8b1091d2bd0c6b080ce51bce08aae6be605e00889be3cdd17cb80614ff8", "firstIndex": 14428405409},
{"blockNumber": 20113611, "blockId": "0xebbf87409ef6732a53513414926b0aa998a7269f9e9717a3acbe96a33c2de937", "firstIndex": 14495514255},
{"blockNumber": 20156628, "blockId": "0x70218108c0669f42c4ac1afff80947cf4a0a2fd8e9300b31d45bbe330a5159c9", "firstIndex": 14562622257},
{"blockNumber": 20199886, "blockId": "0x1d6aaa4b041eb0cc9caed34347bb943e50a139caf8998d781ff4f06d4aa727dc", "firstIndex": 14629732017},
{"blockNumber": 20244148, "blockId": "0x37fed57b6945a815ec56c84c6261e90cf9f3883d9cbffc62b70d9722c1f0f363", "firstIndex": 14696839730},
{"blockNumber": 20288249, "blockId": "0x1d9fe664431e093c4ef6139ad35fea767a2e66825e605cfea9e91d1738665619", "firstIndex": 14763948803},
{"blockNumber": 20333358, "blockId": "0x1ac7c1362791507057b7e87d7118b4f61c2919c8ebdb498ecf7d64a59ba3d2e2", "firstIndex": 14831058814},
{"blockNumber": 20376855, "blockId": "0xf6a19eac8a402f591079a4aa7ff57349c350ec96d683a031e4d85c05bc0541b2", "firstIndex": 14898167076},
{"blockNumber": 20421474, "blockId": "0xe8767aa7516b632dc8c0fed3a366273ef14bd7b8c2212998b7105fb80b4c0d9b", "firstIndex": 14965276285},
{"blockNumber": 20466974, "blockId": "0xafa2d2e6c9594762c163c339069cb15aa9fd27d301268892e12f99a3f2912ade", "firstIndex": 15032384674},
{"blockNumber": 20512183, "blockId": "0xc1c51f9372d781d7ac0a1a7aa052503bbde3e1706bf65f225b0f6a6eed93d055", "firstIndex": 15099493832},
{"blockNumber": 20557251, "blockId": "0xa905087c15e2b12b19cd23f20089b471cd9815dcff73842d2043e13a276fcc55", "firstIndex": 15166601921},
{"blockNumber": 20595654, "blockId": "0x9e7772dd946f65e7569f34f6896d8e597ac8ff88700b871de21ff578c0c995e6", "firstIndex": 15233711474},
{"blockNumber": 20638340, "blockId": "0xc08b46af83db6f0e5d1acad7020bacd59d0f18df72651d32c9b60edd0351a886", "firstIndex": 15300820584},
{"blockNumber": 20683359, "blockId": "0x7ec93a1f1d211c24d93ce7e344634efef680a6388374cf3831d250ecd63711f9", "firstIndex": 15367929168},
{"blockNumber": 20728401, "blockId": "0x4ad4124c2595abcf4477cac4eb70c0c9372bbd48f2066c7ac5b8e9f94817ab48", "firstIndex": 15435038303},
{"blockNumber": 20771272, "blockId": "0x68fd6b12fdb4add8b2957105a21b571c7dcb114c6c2ca02f5fa40730a108f864", "firstIndex": 15502146509},
{"blockNumber": 20814862, "blockId": "0x4e7f640bf3c135b2442ca4c744023021af163e8d45e8d776fb7051c49d79fcff", "firstIndex": 15569256173},
{"blockNumber": 20857627, "blockId": "0x9b023bffed12530b5a68db263bb621a147e0cf02b30ec39a82b82a1fdeb7ca36", "firstIndex": 15636364955},
{"blockNumber": 20896662, "blockId": "0x8c3adf694b8520ed1edd38282b2f58cf383426176628b5be6330a56882e2a07e", "firstIndex": 15703473445},
{"blockNumber": 20939156, "blockId": "0x0649e2559f767da945a674c9d6c700628109e18d0757ad33496a228a827413a4", "firstIndex": 15770581979},
{"blockNumber": 20981054, "blockId": "0xebf0ecca671b833f77df70106cf1171d4dd3fa0f062607e4845569073997f1cd", "firstIndex": 15837691319},
{"blockNumber": 21022980, "blockId": "0x07479520c08a727c42e3616aca7fc4435773f4ef277d84369190263159606d71", "firstIndex": 15904799391},
{"blockNumber": 21068139, "blockId": "0x669cfd03b293a61815588cb9cef928a512d328c1fe9191fe6b281573f429f8b8", "firstIndex": 15971908604},
{"blockNumber": 21112368, "blockId": "0x98957aedaf8b0483be208e8d16a3e6d73ff217e732f3649650671c92c8fb57a5", "firstIndex": 16039007815},
{"blockNumber": 21155756, "blockId": "0xc301e40988fbe19b1254ab3abe2f78fe0cd35259898be02476179a041926f777", "firstIndex": 16106127130},
{"blockNumber": 21200717, "blockId": "0x8d35b199ee65c5ccf4ca0fc22b50282850cd88b72580679f1568eea67d3431e8", "firstIndex": 16173235593},
{"blockNumber": 21244396, "blockId": "0xe7ec084eb8a7125fe26c4ef64fdbb3e0a727b2d882fb1e4fdad1d7fa3dc55fd2", "firstIndex": 16240343803},
{"blockNumber": 21290031, "blockId": "0x8bb31b883d30875b7ed8a01c35a6f51e603b8d974c4e74af20fb9c723e5ac5df", "firstIndex": 16307452772},
{"blockNumber": 21335893, "blockId": "0xd0976b7c946a7556e607e47a02d2cfcccd259508e2828b9c4faa5c7105a597a8", "firstIndex": 16374562657},
{"blockNumber": 21378611, "blockId": "0x72d42953f013aeb7c84bb22c9e07828b92f73885b20c208af612874df8fe85ed", "firstIndex": 16441671192},
{"blockNumber": 21421343, "blockId": "0x663b3cddcf3f7cfa59ed29b4894e43fa784c07576cfec897ff1a3838235ecb50", "firstIndex": 16508779766},
{"blockNumber": 21466598, "blockId": "0xe04ebd60289dbcc09a27e053e7b240a59e928e2a76a85a6fa4044ee387c46d18", "firstIndex": 16575888836},
{"blockNumber": 21511717, "blockId": "0x5ea9863a0b504f381010ff383d60a56390ba9cef162d9d5a077b1884d2adac98", "firstIndex": 16642998234},
{"blockNumber": 21550050, "blockId": "0x2a7b3e635c80521887ca3ebda04541090ec720dd817cd06fcba61e7ae0f78a4f", "firstIndex": 16710106733},
{"blockNumber": 21592440, "blockId": "0xc7095268d0d53cb5a069c2ea7107cc42172bb1b817f04154938e6b52bdbb369c", "firstIndex": 16777215261},
{"blockNumber": 21636007, "blockId": "0x55f463b932053ebfc02728a12c2cdfb2cb4700a115cde686c9ab22bb4c336ecb", "firstIndex": 16844324844},
{"blockNumber": 21680799, "blockId": "0x4521d473aa7f5150f920c66fe639b20f30cb373b32e425fcb7dcce9803b506be", "firstIndex": 16911431255},
{"blockNumber": 21725330, "blockId": "0x793876007ce3594df603628766b139a02273ceee43d5b3ca2d0ca2f13a5fc5d9", "firstIndex": 16978541626},
{"blockNumber": 21771310, "blockId": "0xd0db23bb28d6e39917348b1879dd560c73354ea6d7d0bbee0b7a8fcbcede1091", "firstIndex": 17045650902},
{"blockNumber": 21811348, "blockId": "0x8165948c08db34aeb8cc2a3e331a00af2d09fbff9c466019fb2f2892ca3f7b71", "firstIndex": 17112760024},
{"blockNumber": 21851615, "blockId": "0x3083e8f4fbb4d726ef19b1a5bce20e639a0418eac384f9967f24c7697a7e2dfc", "firstIndex": 17179868870},
{"blockNumber": 21894041, "blockId": "0x9b0776a020374e4985f6a7e1199596bedd387ff085f65224d724ab11955c56fe", "firstIndex": 17246977342},
{"blockNumber": 21932218, "blockId": "0xe9c50eddc21759d3ab790e2babba56f4b8d38eb739f1b561bc337a1ba6e15393", "firstIndex": 17314085541},
{"blockNumber": 21959188, "blockId": "0x6b9c482cc4822af2ad62a359b923062556ab6a046d1a39a8243b764848690601", "firstIndex": 17381193886},
{"blockNumber": 21994911, "blockId": "0x0d99ef9dd42dd1c62fc5249eb4f618e7672ab93fa0ba7545c77360371cf972e5", "firstIndex": 17448302795},
{"blockNumber": 22026007, "blockId": "0xf7cdc7f6694f2c2ed31fa8a607f65cfa59d0dd7d7424ab5c017f959ae2982c71", "firstIndex": 17515413036},
{"blockNumber": 22059890, "blockId": "0x82b768a0dddefda2eefd3a33596ea2be075312f1dd4b01f6b0d377faca2af98b", "firstIndex": 17582521768},
{"blockNumber": 22090784, "blockId": "0xf97c2eaf9a550360ac24000c0ff17ffa388a2bdd6f73f2f36718e332edfa107a", "firstIndex": 17649630983},
{"blockNumber": 22121157, "blockId": "0xa790025235db782e899f23d8b09663ec2d74ec149e4125d62989f98829b08e2d", "firstIndex": 17716724973},
{"blockNumber": 22148056, "blockId": "0xbe25ac4f1bdd89a7db5782bf1157e6c4378d80220d67a029397853ef16cd1a4c", "firstIndex": 17783838366},
{"blockNumber": 22168652, "blockId": "0x6ae43618c915e636794e2cc2d75dde9992766881c7405fe6479c045ed4bee57e", "firstIndex": 17850956277},
{"blockNumber": 22190975, "blockId": "0x9437121647899a4b7b84d67fbea7cc6ff967481c2eab4328ccd86e2cefe19420", "firstIndex": 17918066140},
{"blockNumber": 22234357, "blockId": "0x036030830134f9224160d5a0b62da35ec7813dc8855d554bd22e9d38545243ed", "firstIndex": 17985175075},
{"blockNumber": 22276736, "blockId": "0x5ceb96d98aa1b4c1c2f2fa253ae9cdb1b04e0420c11bf795400e8762c0a1635c", "firstIndex": 18052284344},
{"blockNumber": 22321282, "blockId": "0x8a601ebf6a757020c6d43a978f0bd2c150c4acc1ffdd50c7ee88afc78b0c11f8", "firstIndex": 18119392242},
{"blockNumber": 22349231, "blockId": "0xb751c026a92ba5be95ad7ea4e2729a175b0d0e11a4c108f47cab232b4715d1a2", "firstIndex": 18186501218},
{"blockNumber": 22377469, "blockId": "0xa47916860a22f7e26761ec2d7f717410791bd3ed0237b2f6266750214c7bbf08", "firstIndex": 18253610249},
{"blockNumber": 22422685, "blockId": "0x8beaee39603af55fad222730f556c840c41cd76a5eef0bad367ac94d3b86c7aa", "firstIndex": 18320716377},
{"blockNumber": 22462378, "blockId": "0x6dba9c5d2949f5a6a072267b590e8b15e6fb157a0fc22719387f1fd6bfcd8d5d", "firstIndex": 18387828426},
{"blockNumber": 22500185, "blockId": "0x2484c380df0a8f7edfdf8d917570d23fab8499aea80c35b6cf4e5fe1e34106e9", "firstIndex": 18454936227},
{"blockNumber": 22539624, "blockId": "0xd418071906803d25afc3842a6a6468ad3b5fea27107b314ce4e2ccf08b478acf", "firstIndex": 18522044531},
{"blockNumber": 22577021, "blockId": "0xff222982693f3ff60d2097822171f80a6ddd979080aeb7e995bfb1b973497c84", "firstIndex": 18589154438},
{"blockNumber": 22614525, "blockId": "0x9868da1fea2ffca3f67e35570f02eb5707b27f6967ea4a109eb4ddbf24566efd", "firstIndex": 18656264174},
{"blockNumber": 22652848, "blockId": "0x060a911da11ab0f1dda307f5196e622d23901d198925749e70ab58a439477c5a", "firstIndex": 18723372617},
{"blockNumber": 22692432, "blockId": "0x6a937f2c283aba8c778c1f5ef340b225fd820f8a7dfa6f24f5fe541994f32f2d", "firstIndex": 18790480232},
{"blockNumber": 22731200, "blockId": "0x00d57a9e7a2dad252436fe9f0382c6a8860d301a9f9ffe6d7ac64c82b95300f8", "firstIndex": 18857590076},
{"blockNumber": 22769000, "blockId": "0xa48db20307c19c373ef2d31d85088ea14b8df0450491c31982504c87b04edbc0", "firstIndex": 18924699130},
{"blockNumber": 22808126, "blockId": "0x1419c64ff003edca0586f1c8ec3063da5c54c57ff826cfb34bc866cc18949653", "firstIndex": 18991807807},
{"blockNumber": 22845231, "blockId": "0x691f87217e61c5d7ae9ad53a44d30e1ab6b1cc3f2b689b9fbf7c38fbacacfe3e", "firstIndex": 19058917062},
{"blockNumber": 22884189, "blockId": "0x7f102d44c0ea7803f5b0e1a98a6abf0e8383eb99fb114d6f7b4591753ce8bba3", "firstIndex": 19126024122},
{"blockNumber": 22920923, "blockId": "0x04fe6179495016fc3fe56d8ef5311c360a5761a898262173849c3494fdd73d92", "firstIndex": 19193134595},
{"blockNumber": 22958100, "blockId": "0xe38e0ff7b0c4065ca42ea577bc32f2566ca46f2ddeedcc4bc1f8fb00e7f26329", "firstIndex": 19260242424},
{"blockNumber": 22988600, "blockId": "0x04ca74758b22e0ea54b8c992022ff21c16a2af9c45144c3b0f80de921a7eee82", "firstIndex": 19327351273},
{"blockNumber": 23018392, "blockId": "0x61cc979b00bc97b48356f986a5b9ec997d674bc904c2a2e4b0f17de08e50b3bb", "firstIndex": 19394459627},
{"blockNumber": 23048524, "blockId": "0x489de15d95739ede4ab15e8b5151d80d4dc85ae10e7be800b1a4723094a678df", "firstIndex": 19461570073}
{"blockNumber": 4166212, "blockHash": "0xd94b724fc1c7dceb3251b51b81f7a0f3220ae5b9add1e62917004f14f2a3532c", "firstIndex": 67108840},
{"blockNumber": 4513996, "blockHash": "0xc0fd25fef5888609d05fa8c5620d8e18c31cdd675dd4ee926ff966668f0e5d76", "firstIndex": 134217480},
{"blockNumber": 4817399, "blockHash": "0x8573c3166fb7f716e97cf6109d382f86441238e0b85828944a072ae8583a6cfa", "firstIndex": 201326547},
{"blockNumber": 5087706, "blockHash": "0xcd2dfae45901e299b25faa04c10df60983d5c0a3d0e478e789c7aeec980cf691", "firstIndex": 268435422},
{"blockNumber": 5306085, "blockHash": "0xe8026984c85873f2b24018a7e0bdf8c2df136b2fc49666b4addf0d2e067890da", "firstIndex": 335544018},
{"blockNumber": 5509898, "blockHash": "0x41bc63dc8184f9a7d4b9a5a554e00eee32fee60ccfa2339264be11270ac28de1", "firstIndex": 402652789},
{"blockNumber": 5670367, "blockHash": "0xe61c2ba463f2f458817ed1bf0ff9fb9d07e9d7354f46b48175b2d784b817bf3b", "firstIndex": 469761896},
{"blockNumber": 5826113, "blockHash": "0xfbbb1fc5e03a5562bf6b7f1799d7a572d9ef66c7fd0e0b9f4fed7262767b5c86", "firstIndex": 536870852},
{"blockNumber": 5953008, "blockHash": "0x6bbdccd094928e846de3c615a33f2952e3259afaa1929f4ee241a56907d0593b", "firstIndex": 603979150},
{"blockNumber": 6102812, "blockHash": "0x2403f2502088a1fa26c1294f5245032fe3b7f3a8bb14c12d0ba8d577156bbc1b", "firstIndex": 671087962},
{"blockNumber": 6276672, "blockHash": "0xeaa05a0e0574fbb164244628a7d14d9d47f1692941098aa737059d44104401df", "firstIndex": 738197399},
{"blockNumber": 6448662, "blockHash": "0xe6f097fa18a2cef425514e863659e42e1aa8d74df49cb0bed5877e82a08d1f84", "firstIndex": 805306056},
{"blockNumber": 6655925, "blockHash": "0xee5c9d87b06dc0c2e850fead07bcd13f85d45ad6b5a6e7ebee53cc8618772786", "firstIndex": 872415229},
{"blockNumber": 6873878, "blockHash": "0x9a0ea103146da21fa1d9cab7ff609ec2c5e7f1856288a1f744239588e33904c2", "firstIndex": 939524080},
{"blockNumber": 7080893, "blockHash": "0x35ad45055d7a1a3ef94efeb05e4122757bcc126ff1bc2b4ac72dc78ab3c0fd75", "firstIndex": 1006632327},
{"blockNumber": 7266955, "blockHash": "0x7b80f70520f2c16eb5890f4963af8f60446f50af82b589ceaf009d09e704b302", "firstIndex": 1073741608},
{"blockNumber": 7466649, "blockHash": "0xea8980b5c692c729013f9cc4c9f66e94d5ef2c5caad23f370be0953b6d85f32c", "firstIndex": 1140850335},
{"blockNumber": 7661736, "blockHash": "0x615cdb03dbf29a22d59bbc769c03f3647c2c9abb7d1767f3f6529708209e7073", "firstIndex": 1207959232},
{"blockNumber": 7834494, "blockHash": "0x05b7dd13e2eb8a833e4b278f9151ca01b294ae5fc5617769422ec85ffb446215", "firstIndex": 1275068098},
{"blockNumber": 7989998, "blockHash": "0x4b5026add7f2fc2c02993b82c61bceba1b75cfcb8a78c5112cc2832bc0413be6", "firstIndex": 1342177025},
{"blockNumber": 8142950, "blockHash": "0x9f56b7753a0e9964973f3e096e1385215e3aef232b448359e5bec05046b016a3", "firstIndex": 1409285545},
{"blockNumber": 8297598, "blockHash": "0x2ddfeb161fe34ff73c4a995bb94256c3b522b39348169edb97bb3d876ccdf77d", "firstIndex": 1476394898},
{"blockNumber": 8465061, "blockHash": "0xfdbe5435da5ae9fb34a9154e5ebe515afac9a128e383f798f1e69952be404fcd", "firstIndex": 1543503805},
{"blockNumber": 8655740, "blockHash": "0x305137325a39a44aba997e214c05f32d965658f7e3fe20322f2e57e72f239977", "firstIndex": 1610612406},
{"blockNumber": 8807102, "blockHash": "0xc32f9fd9a43d2b502dbcc4a683ffe324fca9a871753478bcb1f4fb9573da96a5", "firstIndex": 1677721343},
{"blockNumber": 8911110, "blockHash": "0x806eabee7dc429b57afcb6c0b72b61728b4f13c62ee9942f1813790463f9ab5b", "firstIndex": 1744830172},
{"blockNumber": 8960262, "blockHash": "0xa69f24032e9d5761565e726d484926a0932c6ca7fdb4c04a55bfc5e8f5879c4d", "firstIndex": 1811938729},
{"blockNumber": 9085906, "blockHash": "0x922b4d6aeca64517f5048fa1ffa98d3e813bc415716a1763d07cf2bdea236896", "firstIndex": 1879048062},
{"blockNumber": 9230838, "blockHash": "0xd892f513c48c95a859ae59c4e00ec4be0d684c549456c90e9c36669c11092f50", "firstIndex": 1946156580},
{"blockNumber": 9390449, "blockHash": "0xcc0ced78fa66b570f338d129794d574560a23958e28d0d5288003d4542cec734", "firstIndex": 2013264376},
{"blockNumber": 9515554, "blockHash": "0xc06d04737813c3e2c8e910b2d4543c5c684c7bfe235bb501445f68fd0663f3d9", "firstIndex": 2080374749},
{"blockNumber": 9659367, "blockHash": "0x765fa33d9418b1ed648e88b497b4bf4aa66990e6413dfc155525cda61e3cc813", "firstIndex": 2147483344},
{"blockNumber": 9793940, "blockHash": "0x9527dcbd85a100c51aee4ba90828fee23312f5f041859f3416a54fd87a437ce2", "firstIndex": 2214591669},
{"blockNumber": 9923756, "blockHash": "0xc4da5fdd2de077459fe84f721d7fc01ef69683e03e634b0c5443186486792246", "firstIndex": 2281700949},
{"blockNumber": 10042325, "blockHash": "0x6f554f195d20e4ba1262f73cf2c1cd44b14a71befe5ef0a5318fc309f8cde9fa", "firstIndex": 2348810131},
{"blockNumber": 10168768, "blockHash": "0xdcdf88ec2c197d552343c80d01375a35d6462aff39c5bd976160f7176d2add64", "firstIndex": 2415918723},
{"blockNumber": 10289467, "blockHash": "0x5ece23babcf6b8838695b891b044c3dcf8120cdc15acd83018e96ffc590f6cd0", "firstIndex": 2483027720},
{"blockNumber": 10386601, "blockHash": "0xd398ee129aea1247019fa39dc2f5083819687d8f5ad17ec39ba1803d178fbcda", "firstIndex": 2550136043},
{"blockNumber": 10479586, "blockHash": "0x61c1cccd60a546eacc29f2ae2a6facab402b4f35e2986c6629b302e6b1ebca3e", "firstIndex": 2617245577},
{"blockNumber": 10562579, "blockHash": "0x14fd6a079050e3e110140372605c050945d6db03746fa5b84a8718cb2a0e9b86", "firstIndex": 2684353802},
{"blockNumber": 10641438, "blockHash": "0x1998dcae09111b2c1bf6d0592e2f0b4a0143fe7db7115ff94b2b73b74542cee4", "firstIndex": 2751463334},
{"blockNumber": 10717094, "blockHash": "0x9ce45a150bb6bd13687d0692c86716911ffc6aff5831a03f4015bdffb85f5093", "firstIndex": 2818571144},
{"blockNumber": 10784494, "blockHash": "0xf65c936cf578e673696477f7db7189b47222fdf7ed13096564b9d47b15c86eb9", "firstIndex": 2885680709},
{"blockNumber": 10848591, "blockHash": "0x84ea8e9d5789302ebe88888115488f91cfb75fd89e282dfbd29ba9d6225ab0ca", "firstIndex": 2952789491},
{"blockNumber": 10909102, "blockHash": "0x22275047f643f4a3b82c349274a53d56a00da25beac89655877fb8cbfa3182c8", "firstIndex": 3019898024},
{"blockNumber": 10972828, "blockHash": "0x38e8acfec19bb53ab9fe70dd3577ccd8bd7c41b4d468b8f099a19bf06ce56518", "firstIndex": 3087006561},
{"blockNumber": 11036532, "blockHash": "0x478dac03f520e987a6e845cac1b3a756d69e4f5257f20758b8fc38456e7b6884", "firstIndex": 3154116463},
{"blockNumber": 11102457, "blockHash": "0x1a36616b03351ebe481330fc86578f57d9da5c0dda59f4406d49b53a2fa9b6f6", "firstIndex": 3221224961},
{"blockNumber": 11168092, "blockHash": "0xb23af483ca6dbc2fb8d2fada1f8189de2e155758ed9ecbbb20c234a30c7e093c", "firstIndex": 3288333510},
{"blockNumber": 11233640, "blockHash": "0xdc4c80d8370823773425b31e70b3af59582b556514857d1e9a9b4c53b880a424", "firstIndex": 3355442901},
{"blockNumber": 11300454, "blockHash": "0xdbd4d8e9c1756093037f431330db4604e5a3e67bb74f425f667633d5298fe34c", "firstIndex": 3422551962},
{"blockNumber": 11367550, "blockHash": "0x9e00816bd5b39353657a99421d278091599a39f246bbf8c0898f83e6d70b7c7c", "firstIndex": 3489660743},
{"blockNumber": 11431820, "blockHash": "0xd32d5e5871ce71acc9dcc7cf9d8cb9d964646182529c327a0ddf617814e27d74", "firstIndex": 3556769150},
{"blockNumber": 11497031, "blockHash": "0xed823054d4b293294e3301d3131e2ae9f0ce63c9f07d2d80fc144e42d535d61d", "firstIndex": 3623878513},
{"blockNumber": 11560027, "blockHash": "0x7098177efe77cfb5912dc63069fe366033111041ca5c539907a89e439c861d8f", "firstIndex": 3690987092},
{"blockNumber": 11625049, "blockHash": "0x61fccb47104a076b905d390498a1b38c317348bb1567e69aa1b670bed7a6a13f", "firstIndex": 3758096043},
{"blockNumber": 11690298, "blockHash": "0xaf6f9e9c92db9e155f1ffb35eb68765c09de1206c353c9a56a5d5fa53e2d88d2", "firstIndex": 3825204430},
{"blockNumber": 11755003, "blockHash": "0xf9efb54f08a2b59086a607541005928f1bda54fa2c46fcc48352fa6aa461c03a", "firstIndex": 3892313541},
{"blockNumber": 11819591, "blockHash": "0xa551deb3bd2ec0ca2683cb76adb8fb295d171a3787aa5ff071bd1fa94d3af82e", "firstIndex": 3959422322},
{"blockNumber": 11883278, "blockHash": "0x60c7a359d4aeb09b63b64487fa7794aee87c41b11446f69ba83e24d5bce6947f", "firstIndex": 4026531239},
{"blockNumber": 11948436, "blockHash": "0x1c46fb22653e5f9eb9503fe532151519845d05b121291766715973e027eb9c0c", "firstIndex": 4093639741},
{"blockNumber": 12013084, "blockHash": "0x0b8d1da9e70c4494ba61190390c891230846e69d9b0be605df8b04c4199ffd1b", "firstIndex": 4160749561},
{"blockNumber": 12078612, "blockHash": "0xb049af260be25c6153b67c227689b0556d8fb7d6033c3f61b8218f5c4ac5917a", "firstIndex": 4227857310},
{"blockNumber": 12143549, "blockHash": "0x685dce41efbe2bab0ac9a4b9026d9e5faa3e01ae4637c8181cd2f1eac326fe6a", "firstIndex": 4294966232},
{"blockNumber": 12207918, "blockHash": "0x53644ea8f781c6270f1136ed4bbcb956c98f12a4793c2f56f49b3105282b13ea", "firstIndex": 4362075410},
{"blockNumber": 12272370, "blockHash": "0x503523e1a01cb7d68be0eb22f36280c0ee7a1fc802061a704e326fbb8efcfe27", "firstIndex": 4429184831},
{"blockNumber": 12329342, "blockHash": "0xe6a1bf78dd827def078e23c768cf6e96666ee06b5fdc113380a4769d85f4bd33", "firstIndex": 4496293151},
{"blockNumber": 12382311, "blockHash": "0x352fe793c16b1d8aea195af86ee9aec3dbaa83e7a32872a0d222a77e96fc0e57", "firstIndex": 4563402486},
{"blockNumber": 12436970, "blockHash": "0x90276349edf0941bb3327dc1d749d0371fe550eaddafb5141fcacdd281de0976", "firstIndex": 4630510187},
{"blockNumber": 12489954, "blockHash": "0x71894b1e1608aeba777662a6055e601ce6f17950dbe3844c3601e0f88cbd5848", "firstIndex": 4697619586},
{"blockNumber": 12541655, "blockHash": "0xc34c7299041df72a81c4b86e81639d2dfcebfae603e287fc143881a636ce1188", "firstIndex": 4764728948},
{"blockNumber": 12597319, "blockHash": "0x4c4dc71d8d2f1223ad0469ddfe09fa8934f64e3712ed83708d6e1b9c64eceebb", "firstIndex": 4831837390},
{"blockNumber": 12651863, "blockHash": "0x406a642c8364fb61cbd896512a2d19ecdba3932ab09ac67cf48d91aa1a03f24e", "firstIndex": 4898946362},
{"blockNumber": 12706391, "blockHash": "0xa7fc3739898f7ff5733896313b248fb0b306a132681609b8b9e2671b42dac3a1", "firstIndex": 4966054748},
{"blockNumber": 12762840, "blockHash": "0x5a1048966dfa55a48248517c45683ce342671bf544117d2bac13d8e343563136", "firstIndex": 5033164303},
{"blockNumber": 12816597, "blockHash": "0x97f5b87f422870a8ffeff70c12f4377dde96d248aa912cb64ebfaea51dc086b6", "firstIndex": 5100272541},
{"blockNumber": 12872315, "blockHash": "0x432f7df7973fdea4e5abe2c09b634ca3b724e37a19d5ec23ca5810e4854c6b33", "firstIndex": 5167382243},
{"blockNumber": 12929620, "blockHash": "0xa4524e3b4c5dadac91df2f29696532cfed377f47469fe7cfabf10baac945eea2", "firstIndex": 5234490748},
{"blockNumber": 12988653, "blockHash": "0x9be70e31aeea164c370d8d183a1af45120693b13d360edc78214535c40ef1d63", "firstIndex": 5301600248},
{"blockNumber": 13049074, "blockHash": "0x3076857bd17959c2a3df18357eab3ecbb7ca25457785b07074d2e885eb566fc2", "firstIndex": 5368707746},
{"blockNumber": 13108823, "blockHash": "0x67b2764f6b53779e3d683def2c7659c6d92814152574073d24f10cd67200a22b", "firstIndex": 5435817680},
{"blockNumber": 13175389, "blockHash": "0x0d41e34dbca58dec45e0509b6cbf027b2377220845a85caebbf3a14b8505e8ce", "firstIndex": 5502925085},
{"blockNumber": 13237624, "blockHash": "0x723e0818d54ab1b1c53efc2f2f448c16c1f943af2033575c612c3c71228d4ddf", "firstIndex": 5570034948},
{"blockNumber": 13298658, "blockHash": "0xd7f9f477b20bcf1379ccaaae701c9658a02b177861e0af9e873c63fe8b854f4b", "firstIndex": 5637144562},
{"blockNumber": 13361166, "blockHash": "0x9f9e4130093269d59045f91344a13eb2786023e02099f53dd44854d50a7f5ffb", "firstIndex": 5704253260},
{"blockNumber": 13421716, "blockHash": "0x28752f35ad2adeba342ef6aa51bca1c0abc757c4f712cf620c8a5e6a8576636e", "firstIndex": 5771361775},
{"blockNumber": 13480516, "blockHash": "0x43f4ecf24c17b7a77621e2ca09c88c53b3407fb018f4bb3a032c212015b4e18d", "firstIndex": 5838469616},
{"blockNumber": 13535688, "blockHash": "0x99ffcd9982219a4be45fa5f15622b288afe4f85da536956d39dccac1ec416444", "firstIndex": 5905579739},
{"blockNumber": 13590487, "blockHash": "0xff7b74464a318037e0670c0be7a5b1b79418d1a42c7a3587c46adc1ef720c819", "firstIndex": 5972688013},
{"blockNumber": 13646596, "blockHash": "0xd555ce5f2b65ffeec9b9e3e75a56f5f23d7353a1014bcc861efd7df6e5dc2c92", "firstIndex": 6039797525},
{"blockNumber": 13702902, "blockHash": "0x3e8feab590c6631c63931fee83bf7e10234890bdf377b1b374fb70221b9ac9c0", "firstIndex": 6106905641},
{"blockNumber": 13760908, "blockHash": "0x18e96fa585c57aad1f2a8dc9639d9f4fe53c3f38894862471b7cec17f97f3cb1", "firstIndex": 6174014330},
{"blockNumber": 13819340, "blockHash": "0x25384bf2cef178e30c4d9e0abae59357055241fc715133b2bb078e8278e5296a", "firstIndex": 6241123906},
{"blockNumber": 13877817, "blockHash": "0x4a84a0a7035dbfed0e1281fb1df162a195f2c516686a2e2a6ad605e6f2b3cf57", "firstIndex": 6308232690},
{"blockNumber": 13935260, "blockHash": "0x6c14a8e563614edbad66698ebe911e997e27844f9d33c7069795379540e71a44", "firstIndex": 6375341148},
{"blockNumber": 13993914, "blockHash": "0xc0991d1bc431c5215fb0218b0b541f2b4fae0994775e5eddb016965fe5b4a0c7", "firstIndex": 6442448947},
{"blockNumber": 14051007, "blockHash": "0x854803ce364b6fe4446800e74448a5ee883e7d472e8a0347f1ad13bd347fa69f", "firstIndex": 6509558964},
{"blockNumber": 14109063, "blockHash": "0xa3fadce7ab18a5fedcfed9e3b1f0cf2fb32151172c362327c602343bc26dde8f", "firstIndex": 6576668017},
{"blockNumber": 14166701, "blockHash": "0xaa2443a38e2b0d6b01cf4b73de54d2b7e92c99475bd0802beebd86845c126d1b", "firstIndex": 6643774648},
{"blockNumber": 14222367, "blockHash": "0xa27c78134c1fbf432c8caee7c9d2ef323fdde519a065804c9c8da6ca1615e04f", "firstIndex": 6710886238},
{"blockNumber": 14277029, "blockHash": "0x5d0ed08b65db01e382c339dcb2715b2927fb472b0c1f4256841c386d785c3f3a", "firstIndex": 6777995247},
{"blockNumber": 14330955, "blockHash": "0x3e5014d82c416549acfe2e26b13ff5ddf29ad74943ef5b94a65eca0a4d72d82e", "firstIndex": 6845102969},
{"blockNumber": 14384139, "blockHash": "0x720e1fd687292019c3fa1812c39c73d20fde783c0196f43378a51cb4e0b46d65", "firstIndex": 6912212078},
{"blockNumber": 14437534, "blockHash": "0xe4342ab6f363066d9c2514a870241ceb63df232d33659fdb3928faf433360bb4", "firstIndex": 6979321493},
{"blockNumber": 14489073, "blockHash": "0xeb2f9923d816e773460eccfa42eb4ff2bbfc353b756ba274e98dc85c64ab1536", "firstIndex": 7046428879},
{"blockNumber": 14541559, "blockHash": "0x55cc33721fa9022964f47c30f4146fee7cc80f5b4158d7ecea0ff661cbba3487", "firstIndex": 7113538857},
{"blockNumber": 14594432, "blockHash": "0x676441bdc1ec430a9fbc05e56a0437a475606a253f9fdffc86f5f6286c159b5d", "firstIndex": 7180647956},
{"blockNumber": 14644942, "blockHash": "0x03232a1d3c9b2f1f0c2a4b780c83bce66238d9abdeed71c2a8af9e97a53011b0", "firstIndex": 7247754981},
{"blockNumber": 14695793, "blockHash": "0x6017f6d1d79188692e1a1a18de5f93b83b97905c8ff5bed5cef4b2e9f4d4d41b", "firstIndex": 7314863646},
{"blockNumber": 14746186, "blockHash": "0x332c8ac0b2960fb1627407d45443201a0f6de7421b52af077b32fa6e018e5401", "firstIndex": 7381974499},
{"blockNumber": 14798403, "blockHash": "0x4507ae1d9f27a3eada874dcf04112a5d4ee7155701afd3fb857fe58201a2dede", "firstIndex": 7449082752},
{"blockNumber": 14848093, "blockHash": "0xff98c820c66c3beaaa3b195158c94f099148573a7f032295ba7156c13f78eaa8", "firstIndex": 7516191978},
{"blockNumber": 14897508, "blockHash": "0x49c31d3433b197585f733d7c382ae16d9d78bf59c333a9b76362cc6c5fed270c", "firstIndex": 7583300663},
{"blockNumber": 14950638, "blockHash": "0xf1a1609206788977841927271808fe1c42d059026385d025bf4cea0c0a05966b", "firstIndex": 7650408992},
{"blockNumber": 15003958, "blockHash": "0xf9e6b41108d907672f29ba1df97ed83aca2ba933335e6b5ccfb5d4001807b33f", "firstIndex": 7717519105},
{"blockNumber": 15056783, "blockHash": "0x126a5a5c22e878a4c42bea890d48cfe00edf7fbc27d805c88a5269540bc94a42", "firstIndex": 7784628173},
{"blockNumber": 15108163, "blockHash": "0x5f8494ccacefce9fef8a95a52247f2670ed30bb5a3b3deb85c645e72c115d506", "firstIndex": 7851735261},
{"blockNumber": 15159385, "blockHash": "0x0fed6c5aafb53e9cdb0d8620c3eef9b619fb3ae9559296618f634eddae33f0c6", "firstIndex": 7918844299},
{"blockNumber": 15210872, "blockHash": "0x37bdb94b57044b67fbd9b9a7cdc728fcfd2066cf1904cab478fd42d41ec6e1f0", "firstIndex": 7985954119},
{"blockNumber": 15264226, "blockHash": "0xc173ad8a791e2293efd8e0b2c9f088fac49233aabb1e72db6a045aa9904f4728", "firstIndex": 8053061840},
{"blockNumber": 15315693, "blockHash": "0x985cf97bf28f0becb2b16abfe629a5f520596e45fc23f4936bc2f6298b8145f9", "firstIndex": 8120172048},
{"blockNumber": 15364606, "blockHash": "0x593362ef8a82140f3dd3ff13d0ce50287dcb0b8b3d31ffcc7813c6be16f7438d", "firstIndex": 8187281275},
{"blockNumber": 15412727, "blockHash": "0x5a6ff10acc48e9e8bcf963b042012ea094628b9488b8bf569783b3297c67eedd", "firstIndex": 8254388575},
{"blockNumber": 15462642, "blockHash": "0x137f15c5d112b0fdc6e4c84393032030646122c3b0d86286143a1ab8bfacbac4", "firstIndex": 8321497214},
{"blockNumber": 15509781, "blockHash": "0xe58a8ca223b7c03755d046a6212bc33fa9814f73efe52804d8eea7ae4b4bd7f9", "firstIndex": 8388606496},
{"blockNumber": 15558625, "blockHash": "0x4127d527b58c7685a1237d228508ea77d40f33dbeffb5cc30e47e83a4b6152a5", "firstIndex": 8455714910},
{"blockNumber": 15604075, "blockHash": "0x7d1c1bccfe5c23a1edd81246e6e55277f271cffef30bde22c9d1202f764dd96f", "firstIndex": 8522823324},
{"blockNumber": 15651742, "blockHash": "0x732e5203a504fd97510325c1fb876476ca132831e78182367cd4b4efae1232c3", "firstIndex": 8589934155},
{"blockNumber": 15700810, "blockHash": "0xe031decb5e42a69dafcc814441227085508cf51c9c77f35d06487db19bd05a6a", "firstIndex": 8657041374},
{"blockNumber": 15762855, "blockHash": "0xbdff9b1e193c44f35bf38945d2523c74ef59c4f352efc93fad02ef34eef1c5f0", "firstIndex": 8724150924},
{"blockNumber": 15814304, "blockHash": "0x524a226d9f48e37db00e252c7f6a1350d1829d38fe83c756021cc11c56d3b2dd", "firstIndex": 8791259373},
{"blockNumber": 15865514, "blockHash": "0x4e4ad46f3ae7fb2a2e7cb94aa804929cb7353c79c78cc2cbcee710da5f0ec4b5", "firstIndex": 8858369571},
{"blockNumber": 15920411, "blockHash": "0xa39e4cbd8e84b427e44b8d06b5b1149652f3af5fa88633b802665b8677772bc5", "firstIndex": 8925477782},
{"blockNumber": 15974193, "blockHash": "0x4a3f82935e6fcda6ce7636001989b65b79422d362e60833e01185a7960b2fa55", "firstIndex": 8992587369},
{"blockNumber": 16032739, "blockHash": "0x7f50bb76dfd01125e0a4a7595106e43329eb8bd02e819be2ade4248a403c714a", "firstIndex": 9059696611},
{"blockNumber": 16090896, "blockHash": "0xdc4db130973ef793d2223844e35122a2477e642d86c69616fb3604d2a79805c4", "firstIndex": 9126804425},
{"blockNumber": 16150783, "blockHash": "0xb46294fe1cbcbbdff7309ebf0bf481e78e2589d386b95fa4c781c437e02e0aec", "firstIndex": 9193912139},
{"blockNumber": 16207278, "blockHash": "0x8a3af0387572b56989bb6c98d8860e07b5cdefb2ebe7b1dc58be07c24a53e056", "firstIndex": 9261022215},
{"blockNumber": 16262389, "blockHash": "0xec5a03d88cfc1bf8c278ddce7ace5032c3f7ed5032b4575df54779ec1abca131", "firstIndex": 9328130680},
{"blockNumber": 16319525, "blockHash": "0x74ae702375e242205fe4986448b0ee494d4a38ccc813981489c4b0488e58a949", "firstIndex": 9395240779},
{"blockNumber": 16373441, "blockHash": "0x5f404555cf45c547ef0fc044f2701bc7a28ce1e432de06e0264dbfd1c17447d0", "firstIndex": 9462347671},
{"blockNumber": 16423343, "blockHash": "0xe4af890b6c4dd3b10af2e6c08f14563bd2a3121b22a3cc6a0db29d438c1dcc59", "firstIndex": 9529457704},
{"blockNumber": 16474687, "blockHash": "0x019a8b374cf150fcda1c8af2a3e7ba63603ae81744d6343897c4b17ee58411d5", "firstIndex": 9596566169},
{"blockNumber": 16525504, "blockHash": "0xad394ce7e84e1c904cc5dd6dabd51b1a575c0b2fb006f10f272875b7f921904e", "firstIndex": 9663675287},
{"blockNumber": 16574051, "blockHash": "0x098e2dca2eda0d700aa3127bdfb679ce16e3ca297a305978c6ecfff044269511", "firstIndex": 9730784563},
{"blockNumber": 16622457, "blockHash": "0x8627f1237731c1127116898724f1b6420b8e7bcd8e18a0ea568f192bbd2cd55d", "firstIndex": 9797893241},
{"blockNumber": 16672410, "blockHash": "0x3e32b46faa670b191b89bea84a68022d5ea8b90008b388a948ff27f871d17292", "firstIndex": 9865002964},
{"blockNumber": 16719964, "blockHash": "0x1668c3ea6cb256f306e93851cc2c00612f85df5cf7b5527ffce14c89088d7fdf", "firstIndex": 9932111708},
{"blockNumber": 16769014, "blockHash": "0xed9a6f88e47b5bfe4d5bc92e1c1bfeb252f716f0990ffa1f907655c0f5e3a365", "firstIndex": 9999220312},
{"blockNumber": 16818134, "blockHash": "0x93504ba617500698a1b46f3110cd26827b8efe468190b2a91d3595784e34a1b6", "firstIndex": 10066329167},
{"blockNumber": 16869369, "blockHash": "0x94ca4b395a5c36c4e9d07cbb08271f0680455af4e4659906b647e16abebd7d4e", "firstIndex": 10133438415},
{"blockNumber": 16921428, "blockHash": "0x66650775bff4cb95de56099c10b06f3dfe302a731b391f406d1aa551121308ba", "firstIndex": 10200547148},
{"blockNumber": 16974677, "blockHash": "0x58d5fc4cd45047dac10d55a036e0a4448faacc1041b29c4c7fb41a6e9394c3ff", "firstIndex": 10267655623},
{"blockNumber": 17031050, "blockHash": "0x9d15b831979e95fc4149ecae46478cac1e54470b277d57d7102d19a72f28eeba", "firstIndex": 10334764441},
{"blockNumber": 17086163, "blockHash": "0x978ff48863fcdb50b274ea4220e7635c13c41bae91912db9e24205fb917fde85", "firstIndex": 10401873176},
{"blockNumber": 17141086, "blockHash": "0x4084d442499893361731f17534944d260cccd8e07d41c52d7160910555770b90", "firstIndex": 10468982462},
{"blockNumber": 17190735, "blockHash": "0x42ed86fb42def23870df6ec23204f557669ec7cd8de59c9f3ef9ab90704e4b11", "firstIndex": 10536090726},
{"blockNumber": 17237026, "blockHash": "0x9a49d943f82a7335f8049df4d953080f25920e3b098f6a2d872520166b225961", "firstIndex": 10603199938},
{"blockNumber": 17287007, "blockHash": "0xac3d189676d24eadc62adcf97d8730a001a7766fe3ac27b18e745ca9727f6c46", "firstIndex": 10670308186},
{"blockNumber": 17338498, "blockHash": "0xe50e251aa0a3209fa8a0030b237cf8357b08eeb80492a432c8aa5969a820ee42", "firstIndex": 10737417623},
{"blockNumber": 17389178, "blockHash": "0x1e18cedcfa6c81bc845c9136fc92f9f27ca8fff8847289bb139f374f0f6ba733", "firstIndex": 10804526769},
{"blockNumber": 17442038, "blockHash": "0x07f47ebbee99938fb96beb1ffadf8266f71d0d73f393172f0e24facd40d45c97", "firstIndex": 10871635940},
{"blockNumber": 17497589, "blockHash": "0x247a5e852e36e46f7897ad1875e93d810ff4434bb835b469afae3a3d3d4a9c9a", "firstIndex": 10938744251},
{"blockNumber": 17554111, "blockHash": "0x420e420a1b599e4dc3bd115652b4d6f5eedc391086ef064f97a9cbffc02b62f9", "firstIndex": 11005853686},
{"blockNumber": 17608282, "blockHash": "0xb2ba644d3593d3c832f932f14ffec532e26e9e1cdcd8fbe7477f46e238c6bffd", "firstIndex": 11072962303},
{"blockNumber": 17664091, "blockHash": "0x6d0bfff8145652a617bfe03ab87e3af09219cdb822c9c214b42b16f20742c1eb", "firstIndex": 11140070260},
{"blockNumber": 17714917, "blockHash": "0x61509544b5174d3ff5d48816ff54f2929c8a400ef21454116056baa136b607b6", "firstIndex": 11207179013},
{"blockNumber": 17763850, "blockHash": "0x8da83012621f42670601406c902ce6e48e77c6285dcc6983e108e69b3a8d5d2d", "firstIndex": 11274288732},
{"blockNumber": 17814186, "blockHash": "0xc37bda200856589c220dc206e31186d6c12761fd193f5bc7123cf331fbbed7f5", "firstIndex": 11341397343},
{"blockNumber": 17864461, "blockHash": "0x99af43893dc9f54c55abbdbd7b000e27248145371e9b00db116dfb961b36749c", "firstIndex": 11408506122},
{"blockNumber": 17913212, "blockHash": "0x9b642236d63d756fcd3404713bc6abe97804b46a9e665ae5f1d9aa48e693fe78", "firstIndex": 11475614347},
{"blockNumber": 17961497, "blockHash": "0xaec9b1e851cf51646673ec7698e1d5964d44eb04729ad11385f6ae84d497070a", "firstIndex": 11542723713},
{"blockNumber": 18010859, "blockHash": "0xb15fb7fc308516cad5f1dadfb7dd92fd3fe612308f67154cf2a241990aa15bc4", "firstIndex": 11609833204},
{"blockNumber": 18061052, "blockHash": "0xd8ea94b8b8a7380d21dc433abb69973f3de2eeb339f4aaffdb79f85a5d86929d", "firstIndex": 11676938565},
{"blockNumber": 18111466, "blockHash": "0x5b5cc6d182bffaf06692a7efda8cad5d4742fdbce5c1146b3be3c5be10ea046a", "firstIndex": 11744048539},
{"blockNumber": 18166001, "blockHash": "0x901d8401c795542c75069cd917781fa587ad416da0a5298f144fae2f14305705", "firstIndex": 11811159949},
{"blockNumber": 18218505, "blockHash": "0x5894285023f34ec6fd501cd90b605e118b9688ca7c01082289bf69a6987142bc", "firstIndex": 11878268395},
{"blockNumber": 18271037, "blockHash": "0x3f0fce315fd993d966b6c599ebd360462466e67046d512dc430ac07f75243bae", "firstIndex": 11945377050},
{"blockNumber": 18322778, "blockHash": "0xfa6536b3b06e7b579e53a8d01368182808c950586e39e12485be3145c3acbf43", "firstIndex": 12012486036},
{"blockNumber": 18372240, "blockHash": "0x5531859d5b8a4093020e2fb971808d0eeaddd7d61dc58d70f34923081f97336f", "firstIndex": 12079595358},
{"blockNumber": 18421630, "blockHash": "0xfeadff676fe8f04d0362b77ea2d7c3ea59b05d68cc6b2cc95cfe751558b1bfa7", "firstIndex": 12146703872},
{"blockNumber": 18471472, "blockHash": "0x352b0a17e4160d334b876eb180d686fd67c7ddc40a78219b0bc4b59f5ae5912d", "firstIndex": 12213813174},
{"blockNumber": 18522088, "blockHash": "0xf7d408309246089531b47c787049c7ffd2820c1b87b4804033a458bc193a3432", "firstIndex": 12280921181},
{"blockNumber": 18572725, "blockHash": "0x44126d4a5b43594b09d26e0b0a5dc859c074865cb9b11ab78c80f28406fd0240", "firstIndex": 12348029615},
{"blockNumber": 18623223, "blockHash": "0xd3aec1f98cea30e870d0f530194e9a72e225eb45eefe435e38da5967eb2fa470", "firstIndex": 12415139522},
{"blockNumber": 18675228, "blockHash": "0x24deaef2508038bddf8e9020f95128ae34eb1786f786c3f98df0bdf95f3e97cc", "firstIndex": 12482248123},
{"blockNumber": 18725495, "blockHash": "0x2e5a7edd7337b20e862741e116680a472089426dbc2b0eb5f1c984f8d6da935b", "firstIndex": 12549357121},
{"blockNumber": 18778174, "blockHash": "0xe734a1084fa5eeff427bba8e8d3623181a30dd9bad128fc22de9b15dffd0e549", "firstIndex": 12616466131},
{"blockNumber": 18834807, "blockHash": "0x0924a55ef4f9c2605261433c5b236d47645484efa44b34dc9f15b7c2e6b47a74", "firstIndex": 12683575000},
{"blockNumber": 18883054, "blockHash": "0xef638ec491a1cf685b22bf4072e41710440d6133a53e31250762825247d3eb64", "firstIndex": 12750683712},
{"blockNumber": 18933405, "blockHash": "0x6e9eefc2e121d13e6c04544ea370fe8543afeb36fef3a8e2ea68afb079738c8c", "firstIndex": 12817792510},
{"blockNumber": 18988033, "blockHash": "0x4c98c0dbb6a7217609c4b277fe7ff9e2c32c8d000ee628a3f37b2b296e1ca421", "firstIndex": 12884901389},
{"blockNumber": 19041096, "blockHash": "0x61a219a23d111cd07861ff47b89d4b2840c0ab5421d5d55a6f1fe43da3f33b98", "firstIndex": 12952010147},
{"blockNumber": 19088914, "blockHash": "0xefb26ba58447e50e9b820e0b0d16c08da45793a824af12a2e1f38b6f46b7e36a", "firstIndex": 13019118891},
{"blockNumber": 19140413, "blockHash": "0x91dbb13623fc4d5a4e462dfa0a03c92bf5301948dd4b2c3e5fddd2bc88856d47", "firstIndex": 13086228137},
{"blockNumber": 19191876, "blockHash": "0xdd7f97af7ad206b2636656ed4722fe1c47c6ba9fa5531066785acbe4f8d4c4ad", "firstIndex": 13153336551},
{"blockNumber": 19237651, "blockHash": "0xf1e875f314314abe3654b667c6443c772dd67c525c52877b06515ea8fc5a391b", "firstIndex": 13220444812},
{"blockNumber": 19291032, "blockHash": "0x9eda269ac47957b619d2a8c408b5efc8cfb168a46bfde351f43325fa337fce47", "firstIndex": 13287553967},
{"blockNumber": 19344225, "blockHash": "0x62859b78d45f589f679fc6a3bf266a2e0aa906fd61d787755ec51cfadc05eaf6", "firstIndex": 13354663338},
{"blockNumber": 19394709, "blockHash": "0x7cda3d193bcfe8ddbc89b85e98c814dbbe31a54ccab52ab7738ecd003061f3c8", "firstIndex": 13421772549},
{"blockNumber": 19442850, "blockHash": "0xd95f3f8bab5b91c3e1f848c9455936850bf92cf6a5086b817f6f81031bc0f81e", "firstIndex": 13488880704},
{"blockNumber": 19488177, "blockHash": "0x3a1a9623ee332d6decada9359cf84eeafb81fffa997d6cf4f3efcc92e98e9310", "firstIndex": 13555989570},
{"blockNumber": 19534365, "blockHash": "0x5c7a8ee7cbba4894ea412cc335895a9a7d3369d35779bcdc97a6c24b6a81a7ba", "firstIndex": 13623097446},
{"blockNumber": 19579409, "blockHash": "0x4280f5f76cb0f9947f9da1cb9de6c4b6e65b37b3a3c174f5fb29d360cf389ffb", "firstIndex": 13690206790},
{"blockNumber": 19621406, "blockHash": "0x0a98ca72e97fd295c7c73263b17185d74dbe427f41c616a5d5454faa5a1c63a0", "firstIndex": 13757316653},
{"blockNumber": 19664868, "blockHash": "0xc8c53d89b7a849861f3daaf9503e6397a0340cecb085850c3636bee6ea2b7189", "firstIndex": 13824424608},
{"blockNumber": 19709011, "blockHash": "0xf65babc88ef6a9eb2ffce9a6844adf895995f65a2facd1956f5df17ca68dbf94", "firstIndex": 13891534141},
{"blockNumber": 19755170, "blockHash": "0xf903882449eeb86d69af33b6797099275e82d8787334a02a42a2168dbd739b95", "firstIndex": 13958642627},
{"blockNumber": 19803666, "blockHash": "0xa8a975f570db082a3771790bb1fe405667dde4674ff49e31092687fa92727e36", "firstIndex": 14025752433},
{"blockNumber": 19847730, "blockHash": "0x89d479a4b4d9faa7af3e3f61d13a7a9c80a11c4adf7cd018859eb0ddba30bdd5", "firstIndex": 14092860598},
{"blockNumber": 19894138, "blockHash": "0x740278e6dc79cfd2b030780868a1b8e1520dbae0e8d3ee0a3ef43c84c83c6e0a", "firstIndex": 14159952129},
{"blockNumber": 19938334, "blockHash": "0x05ae7ce9813235c19baa57658bc5c8e160130328a1fd2cc58d2baf2d9a4ae097", "firstIndex": 14227078393},
{"blockNumber": 19985138, "blockHash": "0x511c8876a93a77fa473ae0d29e25d8163162c09b7212f506be0ea73840ea3971", "firstIndex": 14294187397},
{"blockNumber": 20028221, "blockHash": "0x64379baedf429cfa65df076142a7d9909d7e369d6aa6b8e161bb686944d169ff", "firstIndex": 14361296446},
{"blockNumber": 20071555, "blockHash": "0x4f43d8b1091d2bd0c6b080ce51bce08aae6be605e00889be3cdd17cb80614ff8", "firstIndex": 14428405409},
{"blockNumber": 20113611, "blockHash": "0xebbf87409ef6732a53513414926b0aa998a7269f9e9717a3acbe96a33c2de937", "firstIndex": 14495514255},
{"blockNumber": 20156628, "blockHash": "0x70218108c0669f42c4ac1afff80947cf4a0a2fd8e9300b31d45bbe330a5159c9", "firstIndex": 14562622257},
{"blockNumber": 20199886, "blockHash": "0x1d6aaa4b041eb0cc9caed34347bb943e50a139caf8998d781ff4f06d4aa727dc", "firstIndex": 14629732017},
{"blockNumber": 20244148, "blockHash": "0x37fed57b6945a815ec56c84c6261e90cf9f3883d9cbffc62b70d9722c1f0f363", "firstIndex": 14696839730},
{"blockNumber": 20288249, "blockHash": "0x1d9fe664431e093c4ef6139ad35fea767a2e66825e605cfea9e91d1738665619", "firstIndex": 14763948803},
{"blockNumber": 20333358, "blockHash": "0x1ac7c1362791507057b7e87d7118b4f61c2919c8ebdb498ecf7d64a59ba3d2e2", "firstIndex": 14831058814},
{"blockNumber": 20376855, "blockHash": "0xf6a19eac8a402f591079a4aa7ff57349c350ec96d683a031e4d85c05bc0541b2", "firstIndex": 14898167076},
{"blockNumber": 20421474, "blockHash": "0xe8767aa7516b632dc8c0fed3a366273ef14bd7b8c2212998b7105fb80b4c0d9b", "firstIndex": 14965276285},
{"blockNumber": 20466974, "blockHash": "0xafa2d2e6c9594762c163c339069cb15aa9fd27d301268892e12f99a3f2912ade", "firstIndex": 15032384674},
{"blockNumber": 20512183, "blockHash": "0xc1c51f9372d781d7ac0a1a7aa052503bbde3e1706bf65f225b0f6a6eed93d055", "firstIndex": 15099493832},
{"blockNumber": 20557251, "blockHash": "0xa905087c15e2b12b19cd23f20089b471cd9815dcff73842d2043e13a276fcc55", "firstIndex": 15166601921},
{"blockNumber": 20595654, "blockHash": "0x9e7772dd946f65e7569f34f6896d8e597ac8ff88700b871de21ff578c0c995e6", "firstIndex": 15233711474},
{"blockNumber": 20638340, "blockHash": "0xc08b46af83db6f0e5d1acad7020bacd59d0f18df72651d32c9b60edd0351a886", "firstIndex": 15300820584},
{"blockNumber": 20683359, "blockHash": "0x7ec93a1f1d211c24d93ce7e344634efef680a6388374cf3831d250ecd63711f9", "firstIndex": 15367929168},
{"blockNumber": 20728401, "blockHash": "0x4ad4124c2595abcf4477cac4eb70c0c9372bbd48f2066c7ac5b8e9f94817ab48", "firstIndex": 15435038303},
{"blockNumber": 20771272, "blockHash": "0x68fd6b12fdb4add8b2957105a21b571c7dcb114c6c2ca02f5fa40730a108f864", "firstIndex": 15502146509},
{"blockNumber": 20814862, "blockHash": "0x4e7f640bf3c135b2442ca4c744023021af163e8d45e8d776fb7051c49d79fcff", "firstIndex": 15569256173},
{"blockNumber": 20857627, "blockHash": "0x9b023bffed12530b5a68db263bb621a147e0cf02b30ec39a82b82a1fdeb7ca36", "firstIndex": 15636364955},
{"blockNumber": 20896662, "blockHash": "0x8c3adf694b8520ed1edd38282b2f58cf383426176628b5be6330a56882e2a07e", "firstIndex": 15703473445},
{"blockNumber": 20939156, "blockHash": "0x0649e2559f767da945a674c9d6c700628109e18d0757ad33496a228a827413a4", "firstIndex": 15770581979},
{"blockNumber": 20981054, "blockHash": "0xebf0ecca671b833f77df70106cf1171d4dd3fa0f062607e4845569073997f1cd", "firstIndex": 15837691319},
{"blockNumber": 21022980, "blockHash": "0x07479520c08a727c42e3616aca7fc4435773f4ef277d84369190263159606d71", "firstIndex": 15904799391},
{"blockNumber": 21068139, "blockHash": "0x669cfd03b293a61815588cb9cef928a512d328c1fe9191fe6b281573f429f8b8", "firstIndex": 15971908604},
{"blockNumber": 21112368, "blockHash": "0x98957aedaf8b0483be208e8d16a3e6d73ff217e732f3649650671c92c8fb57a5", "firstIndex": 16039007815},
{"blockNumber": 21155756, "blockHash": "0xc301e40988fbe19b1254ab3abe2f78fe0cd35259898be02476179a041926f777", "firstIndex": 16106127130},
{"blockNumber": 21200717, "blockHash": "0x8d35b199ee65c5ccf4ca0fc22b50282850cd88b72580679f1568eea67d3431e8", "firstIndex": 16173235593},
{"blockNumber": 21244396, "blockHash": "0xe7ec084eb8a7125fe26c4ef64fdbb3e0a727b2d882fb1e4fdad1d7fa3dc55fd2", "firstIndex": 16240343803},
{"blockNumber": 21290031, "blockHash": "0x8bb31b883d30875b7ed8a01c35a6f51e603b8d974c4e74af20fb9c723e5ac5df", "firstIndex": 16307452772},
{"blockNumber": 21335893, "blockHash": "0xd0976b7c946a7556e607e47a02d2cfcccd259508e2828b9c4faa5c7105a597a8", "firstIndex": 16374562657},
{"blockNumber": 21378611, "blockHash": "0x72d42953f013aeb7c84bb22c9e07828b92f73885b20c208af612874df8fe85ed", "firstIndex": 16441671192},
{"blockNumber": 21421343, "blockHash": "0x663b3cddcf3f7cfa59ed29b4894e43fa784c07576cfec897ff1a3838235ecb50", "firstIndex": 16508779766},
{"blockNumber": 21466598, "blockHash": "0xe04ebd60289dbcc09a27e053e7b240a59e928e2a76a85a6fa4044ee387c46d18", "firstIndex": 16575888836},
{"blockNumber": 21511717, "blockHash": "0x5ea9863a0b504f381010ff383d60a56390ba9cef162d9d5a077b1884d2adac98", "firstIndex": 16642998234},
{"blockNumber": 21550050, "blockHash": "0x2a7b3e635c80521887ca3ebda04541090ec720dd817cd06fcba61e7ae0f78a4f", "firstIndex": 16710106733},
{"blockNumber": 21592440, "blockHash": "0xc7095268d0d53cb5a069c2ea7107cc42172bb1b817f04154938e6b52bdbb369c", "firstIndex": 16777215261},
{"blockNumber": 21636007, "blockHash": "0x55f463b932053ebfc02728a12c2cdfb2cb4700a115cde686c9ab22bb4c336ecb", "firstIndex": 16844324844},
{"blockNumber": 21680799, "blockHash": "0x4521d473aa7f5150f920c66fe639b20f30cb373b32e425fcb7dcce9803b506be", "firstIndex": 16911431255},
{"blockNumber": 21725330, "blockHash": "0x793876007ce3594df603628766b139a02273ceee43d5b3ca2d0ca2f13a5fc5d9", "firstIndex": 16978541626},
{"blockNumber": 21771310, "blockHash": "0xd0db23bb28d6e39917348b1879dd560c73354ea6d7d0bbee0b7a8fcbcede1091", "firstIndex": 17045650902},
{"blockNumber": 21811348, "blockHash": "0x8165948c08db34aeb8cc2a3e331a00af2d09fbff9c466019fb2f2892ca3f7b71", "firstIndex": 17112760024},
{"blockNumber": 21851615, "blockHash": "0x3083e8f4fbb4d726ef19b1a5bce20e639a0418eac384f9967f24c7697a7e2dfc", "firstIndex": 17179868870},
{"blockNumber": 21894041, "blockHash": "0x9b0776a020374e4985f6a7e1199596bedd387ff085f65224d724ab11955c56fe", "firstIndex": 17246977342},
{"blockNumber": 21932218, "blockHash": "0xe9c50eddc21759d3ab790e2babba56f4b8d38eb739f1b561bc337a1ba6e15393", "firstIndex": 17314085541},
{"blockNumber": 21959188, "blockHash": "0x6b9c482cc4822af2ad62a359b923062556ab6a046d1a39a8243b764848690601", "firstIndex": 17381193886},
{"blockNumber": 21994911, "blockHash": "0x0d99ef9dd42dd1c62fc5249eb4f618e7672ab93fa0ba7545c77360371cf972e5", "firstIndex": 17448302795},
{"blockNumber": 22026007, "blockHash": "0xf7cdc7f6694f2c2ed31fa8a607f65cfa59d0dd7d7424ab5c017f959ae2982c71", "firstIndex": 17515413036},
{"blockNumber": 22059890, "blockHash": "0x82b768a0dddefda2eefd3a33596ea2be075312f1dd4b01f6b0d377faca2af98b", "firstIndex": 17582521768},
{"blockNumber": 22090784, "blockHash": "0xf97c2eaf9a550360ac24000c0ff17ffa388a2bdd6f73f2f36718e332edfa107a", "firstIndex": 17649630983},
{"blockNumber": 22121157, "blockHash": "0xa790025235db782e899f23d8b09663ec2d74ec149e4125d62989f98829b08e2d", "firstIndex": 17716724973},
{"blockNumber": 22148056, "blockHash": "0xbe25ac4f1bdd89a7db5782bf1157e6c4378d80220d67a029397853ef16cd1a4c", "firstIndex": 17783838366},
{"blockNumber": 22168652, "blockHash": "0x6ae43618c915e636794e2cc2d75dde9992766881c7405fe6479c045ed4bee57e", "firstIndex": 17850956277},
{"blockNumber": 22190975, "blockHash": "0x9437121647899a4b7b84d67fbea7cc6ff967481c2eab4328ccd86e2cefe19420", "firstIndex": 17918066140},
{"blockNumber": 22234357, "blockHash": "0x036030830134f9224160d5a0b62da35ec7813dc8855d554bd22e9d38545243ed", "firstIndex": 17985175075},
{"blockNumber": 22276736, "blockHash": "0x5ceb96d98aa1b4c1c2f2fa253ae9cdb1b04e0420c11bf795400e8762c0a1635c", "firstIndex": 18052284344},
{"blockNumber": 22321282, "blockHash": "0x8a601ebf6a757020c6d43a978f0bd2c150c4acc1ffdd50c7ee88afc78b0c11f8", "firstIndex": 18119392242},
{"blockNumber": 22349231, "blockHash": "0xb751c026a92ba5be95ad7ea4e2729a175b0d0e11a4c108f47cab232b4715d1a2", "firstIndex": 18186501218},
{"blockNumber": 22377469, "blockHash": "0xa47916860a22f7e26761ec2d7f717410791bd3ed0237b2f6266750214c7bbf08", "firstIndex": 18253610249},
{"blockNumber": 22422685, "blockHash": "0x8beaee39603af55fad222730f556c840c41cd76a5eef0bad367ac94d3b86c7aa", "firstIndex": 18320716377},
{"blockNumber": 22462378, "blockHash": "0x6dba9c5d2949f5a6a072267b590e8b15e6fb157a0fc22719387f1fd6bfcd8d5d", "firstIndex": 18387828426},
{"blockNumber": 22500185, "blockHash": "0x2484c380df0a8f7edfdf8d917570d23fab8499aea80c35b6cf4e5fe1e34106e9", "firstIndex": 18454936227},
{"blockNumber": 22539624, "blockHash": "0xd418071906803d25afc3842a6a6468ad3b5fea27107b314ce4e2ccf08b478acf", "firstIndex": 18522044531},
{"blockNumber": 22577021, "blockHash": "0xff222982693f3ff60d2097822171f80a6ddd979080aeb7e995bfb1b973497c84", "firstIndex": 18589154438},
{"blockNumber": 22614525, "blockHash": "0x9868da1fea2ffca3f67e35570f02eb5707b27f6967ea4a109eb4ddbf24566efd", "firstIndex": 18656264174},
{"blockNumber": 22652848, "blockHash": "0x060a911da11ab0f1dda307f5196e622d23901d198925749e70ab58a439477c5a", "firstIndex": 18723372617},
{"blockNumber": 22692432, "blockHash": "0x6a937f2c283aba8c778c1f5ef340b225fd820f8a7dfa6f24f5fe541994f32f2d", "firstIndex": 18790480232},
{"blockNumber": 22731200, "blockHash": "0x00d57a9e7a2dad252436fe9f0382c6a8860d301a9f9ffe6d7ac64c82b95300f8", "firstIndex": 18857590076},
{"blockNumber": 22769000, "blockHash": "0xa48db20307c19c373ef2d31d85088ea14b8df0450491c31982504c87b04edbc0", "firstIndex": 18924699130},
{"blockNumber": 22808126, "blockHash": "0x1419c64ff003edca0586f1c8ec3063da5c54c57ff826cfb34bc866cc18949653", "firstIndex": 18991807807},
{"blockNumber": 22845231, "blockHash": "0x691f87217e61c5d7ae9ad53a44d30e1ab6b1cc3f2b689b9fbf7c38fbacacfe3e", "firstIndex": 19058917062},
{"blockNumber": 22884189, "blockHash": "0x7f102d44c0ea7803f5b0e1a98a6abf0e8383eb99fb114d6f7b4591753ce8bba3", "firstIndex": 19126024122},
{"blockNumber": 22920923, "blockHash": "0x04fe6179495016fc3fe56d8ef5311c360a5761a898262173849c3494fdd73d92", "firstIndex": 19193134595},
{"blockNumber": 22958100, "blockHash": "0xe38e0ff7b0c4065ca42ea577bc32f2566ca46f2ddeedcc4bc1f8fb00e7f26329", "firstIndex": 19260242424},
{"blockNumber": 22988600, "blockHash": "0x04ca74758b22e0ea54b8c992022ff21c16a2af9c45144c3b0f80de921a7eee82", "firstIndex": 19327351273},
{"blockNumber": 23018392, "blockHash": "0x61cc979b00bc97b48356f986a5b9ec997d674bc904c2a2e4b0f17de08e50b3bb", "firstIndex": 19394459627},
{"blockNumber": 23048524, "blockHash": "0x489de15d95739ede4ab15e8b5151d80d4dc85ae10e7be800b1a4723094a678df", "firstIndex": 19461570073}
]

View file

@ -1,99 +1,99 @@
[
{"blockNumber": 3246675, "blockId": "0x36bf7de9e1f151963088ca3efa206b6e78411d699d2f64f3bf86895294275e0b", "firstIndex": 67108765},
{"blockNumber": 3575560, "blockId": "0x4652e3bee440f946aeaa3358c9a62b9bc4d41f663737ab00fded00c2662bfa29", "firstIndex": 134217644},
{"blockNumber": 3694262, "blockId": "0x433573f97e563ca04fa53e0d0eb019d0ffe9dd032a05ee5b33ce7705aedfd34d", "firstIndex": 201316990},
{"blockNumber": 3725628, "blockId": "0x1e25ebd76b9e2a2007bfdd7c82433cce7dae8beb190d249c82bf83d0cd177735", "firstIndex": 268426577},
{"blockNumber": 3795378, "blockId": "0xca999083e01d5947212fb878d529abecdd18a6012997b0d5326afcf6908f17e5", "firstIndex": 335543438},
{"blockNumber": 3856681, "blockId": "0x48148cd35de3de6c626f76ce5d97cfc6acd88772af69c0aef3d640c29ba82096", "firstIndex": 402641717},
{"blockNumber": 3869367, "blockId": "0x08bafccf9ece7b72e4e069fc3c7493fbf25732881758917df2e8f9a51fc2c75f", "firstIndex": 469761225},
{"blockNumber": 3938346, "blockId": "0xc638eb890c0e65ce1169ef0bb6b74dede2f4f6144ff8a5739c4894ff41a22fa2", "firstIndex": 536865235},
{"blockNumber": 3984892, "blockId": "0x6a7782f0765b0f0c406b3cb107fd8bb14c6906293d9bdc5cc00e081bfd1ae59f", "firstIndex": 603972697},
{"blockNumber": 4002660, "blockId": "0xda473de053602bc42fcbb074a32c908303f7f6db2ed8c38192665dce80276702", "firstIndex": 671087183},
{"blockNumber": 4113149, "blockId": "0xd1782f677262226194725028858da83e672748e409fe1c21bdd77c07be0662c8", "firstIndex": 738197365},
{"blockNumber": 4260733, "blockId": "0xc356600819e49d311963312157c481fd9be43a36ad040a0512fd17559c78d394", "firstIndex": 805305957},
{"blockNumber": 4391073, "blockId": "0x06f903dc765b4b0ca1a40847eb74d30b0b51904a70c46f2e836dc5b3ea3dd8d3", "firstIndex": 872415067},
{"blockNumber": 4515567, "blockId": "0xe99d3849755a1b4ec9a65d003260c111d4886e29ad6243fc750c72dbc79dbe4c", "firstIndex": 939523746},
{"blockNumber": 4634815, "blockId": "0x9f281e749b192ef58242ff9fccd496a9075c30bd28c61ed4ce6487fc2d167e5c", "firstIndex": 1006629595},
{"blockNumber": 4718286, "blockId": "0x23d2a40ec3ae059a45006ba651a70f308b6b282829b9cfdf2534e4311e34050f", "firstIndex": 1073731435},
{"blockNumber": 4753427, "blockId": "0x6df7295f9ccb45a95e6cc72a8ee56bda992513b3bf4baa3d62219871a0b5a912", "firstIndex": 1140843105},
{"blockNumber": 4786511, "blockId": "0x705c79435411c1af81385a1eca34343427eceb3f6af72e7f665552d6b0eceb6c", "firstIndex": 1207957185},
{"blockNumber": 4811696, "blockId": "0x69a6e60d1b958b469feafb1dca97f38f97620a90c21ed92f90080f6443ee6c78", "firstIndex": 1275061401},
{"blockNumber": 4841770, "blockId": "0x0a0e9d2d3d177601a478428297b5b9a42124166fefe42f2221c7a612c31e7ca1", "firstIndex": 1342174442},
{"blockNumber": 4914785, "blockId": "0x5ca0e12230aeacbce9064dff15dafb9f24e20b4a737f7d04df8549008b829fa3", "firstIndex": 1409284680},
{"blockNumber": 4992516, "blockId": "0xb9cb1700d8b65615fc3a019b2e9b2ece3b4f1e0e2fcfc01ff40bcf53e7291b10", "firstIndex": 1476387066},
{"blockNumber": 5088617, "blockId": "0x33122f693a264fb2ab626232fac5e2714010fd65bdceb772109511f7c9497333", "firstIndex": 1543503849},
{"blockNumber": 5155024, "blockId": "0x187716822dd6ab9c21138ae5a8e763197a5e7ec48bf0e756c5e6fa2c9f2f24bf", "firstIndex": 1610604247},
{"blockNumber": 5204373, "blockId": "0x753b6495979ca90a1b915edad6af33c58bbabe68b45f95c7645113ec1d35d0e6", "firstIndex": 1677721200},
{"blockNumber": 5269952, "blockId": "0xcbb0409ed824631120d6951b55537143d9a1c82ca6dd6255d2eb31c57844685c", "firstIndex": 1744829248},
{"blockNumber": 5337623, "blockId": "0x0b460658511c4ae5205c038430789f9b31f3c08ba4acc06d273b7a13a149cf8b", "firstIndex": 1811939255},
{"blockNumber": 5399044, "blockId": "0x9d07e37321e2b019f4837cc4f95f103da87ecdcb77e131e9d42d8643b0446524", "firstIndex": 1879041131},
{"blockNumber": 5422692, "blockId": "0x9d9e27614635989788e32f15cd7a0abf7470dccf3336b77547baeb3423b629ca", "firstIndex": 1946154968},
{"blockNumber": 5454259, "blockId": "0x01e694443d471d3411fe0b52720fd4a189e687b14cc8ede116f5e93f0ad96bb6", "firstIndex": 2013265148},
{"blockNumber": 5498863, "blockId": "0xc4cb906c96d9f80b830b8add25318c38b3ff857c1bd577cd9cae0a324a068ffc", "firstIndex": 2080373677},
{"blockNumber": 5554788, "blockId": "0x9b934b2d66723559d82e0f4cd49a2497234fe892248cb2f46c13f6a9585cfddd", "firstIndex": 2147475372},
{"blockNumber": 5594711, "blockId": "0x840c0b7889b8c882cfa0eceb516dd73043b51c0e5a0c1be849a0e2a5dda7e842", "firstIndex": 2214592267},
{"blockNumber": 5645179, "blockId": "0x188873a43506fee33349bc33df4a55059401e98dc7f0a5d488a0bc5c87dfd431", "firstIndex": 2281695869},
{"blockNumber": 5687634, "blockId": "0x7ea8d7a2afccf795a2a02eb00746d89db57f13f8936cf7ab0f0a7db1b1ceb341", "firstIndex": 2348806824},
{"blockNumber": 5727808, "blockId": "0x3f3eaf36c8e0271403737676907eac15312927c1d60ce814337654b7d7f2b2bd", "firstIndex": 2415915517},
{"blockNumber": 5784480, "blockId": "0x10ec5adf01449c29550f5117f09ecafc49fc614d7da5886cf01619950827f850", "firstIndex": 2483023750},
{"blockNumber": 5843906, "blockId": "0x9e6e14b3fba395a9ae7b3f1295f33a12d0f8b9f10e1aaab1649a90a56a323676", "firstIndex": 2550136398},
{"blockNumber": 5906280, "blockId": "0x2188869a164e9c19232f6702a5233dc27a2304839159dd7230cc93ebdfc6c048", "firstIndex": 2617245012},
{"blockNumber": 5977929, "blockId": "0xb247181ed597d9473c2c6e5762ae45a05f328d8df215fdac83d19ae4c028109a", "firstIndex": 2684347033},
{"blockNumber": 6051479, "blockId": "0x0e8634c95dac31b7c835b44d0a14290e9212b31b9c3f621997ad1cca2d8ab0ba", "firstIndex": 2751463372},
{"blockNumber": 6118422, "blockId": "0xa642c4457d4b63d68a5502bdba2355616e34495bf3ab2d8518a45ded54a16be4", "firstIndex": 2818563217},
{"blockNumber": 6174274, "blockId": "0xbb9808bc56b5f14636e654bdd6d50d35b31345255926341ef3ddb54840095bcc", "firstIndex": 2885680274},
{"blockNumber": 6276207, "blockId": "0x7ae027a18889e1b60a46ea3e5f2759035f3d9041de68eca0b154068ec533db59", "firstIndex": 2952789656},
{"blockNumber": 6368344, "blockId": "0x8b4401f89589d9e99259e6c05c18b1780e96b65027caf9d83edf80390d317f66", "firstIndex": 3019898378},
{"blockNumber": 6470718, "blockId": "0x0e2f8de093392c38750581c41e93c2fb1c0f6676b65c9af52792f3c56e894457", "firstIndex": 3087007589},
{"blockNumber": 6553237, "blockId": "0xa6c4f3a1fe3b4e116c367fcbb1ae98e0f25570fdb2f1eadee2681deb39462c1e", "firstIndex": 3154116356},
{"blockNumber": 6663750, "blockId": "0x9049748ddf655c7cc2ccbe2d36010b3a67ae011d114d32b1c812a7a53102f27f", "firstIndex": 3221225375},
{"blockNumber": 6766894, "blockId": "0xc98188fb313b1beb7fc4f415b4a74c363903b3b5e0172437d79446bc011238b5", "firstIndex": 3288334172},
{"blockNumber": 6886576, "blockId": "0x4ae8237a94198c82c266f01e472fdcd3ae150c84ecf714205a9be672f1e705b8", "firstIndex": 3355443040},
{"blockNumber": 6978746, "blockId": "0xb2b69ce29c5c9c46c5cbc6e96260c34313d8b0b4a739cf4091c98f37093aa732", "firstIndex": 3422552017},
{"blockNumber": 7098871, "blockId": "0x9ab2b681feeb38287a1b689fa64a50efd3407b6331fd6896187cad98a29ce107", "firstIndex": 3489660879},
{"blockNumber": 7203023, "blockId": "0xdedfff5985c1c93d476120d2afbaa61967bae4edabb9711c7794a1b1cd453aa1", "firstIndex": 3556769365},
{"blockNumber": 7256709, "blockId": "0xc078041cffc1802b6342fd2ba5a92ff8076ac0f5b72b4a77ba75cd486fe1b3c9", "firstIndex": 3623876563},
{"blockNumber": 7307744, "blockId": "0xdf8dcdd4af9213a79587eb2f4d91f4111da74f99207ee05e4ae50bcb4f8d5435", "firstIndex": 3690986225},
{"blockNumber": 7369268, "blockId": "0x62315823621a38a1f138e193b85259988fcc15f4a74d0cf19a412920bcdcad98", "firstIndex": 3758096030},
{"blockNumber": 7445109, "blockId": "0x8b668ac1274a8dcc9526eb115c2f67bd56f6657b5072c7542da2252daca10fef", "firstIndex": 3825204921},
{"blockNumber": 7511512, "blockId": "0x056ad7411652aa64849c67705a86e42221cb88f7fb6a7012ab7d3d5b9a30eb76", "firstIndex": 3892313524},
{"blockNumber": 7557259, "blockId": "0xef660b97661f073b7c7319b69451e16d26ecaed54f9237e209999d137da2bfd4", "firstIndex": 3959415096},
{"blockNumber": 7606260, "blockId": "0x9f5f06ad381166261780864bf17ad4d3cdd475f87ba51fa7f9ba621bf0ac0acb", "firstIndex": 4026521703},
{"blockNumber": 7654244, "blockId": "0xefe5b537435001203741bf445ca26466baf7ad3f63c2bba94f0f658aed8c244b", "firstIndex": 4093627258},
{"blockNumber": 7700433, "blockId": "0x89af6e8c5aa934a9139944b3de7a15232069479dba387e0fdd043ad36156c8bf", "firstIndex": 4160749200},
{"blockNumber": 7730079, "blockId": "0x08e0d511a93e2a9c004b3456d77d687ae86247417ada1cdd1f85332a62dfed1d", "firstIndex": 4227846795},
{"blockNumber": 7777515, "blockId": "0xeba8faed2e12bb9ed5e7fad19dd82662f15f6f4f512a0d232eedf1e676712428", "firstIndex": 4294966082},
{"blockNumber": 7828860, "blockId": "0x62c054bd24be351dc4777460ee922a75fdcea5c9830455cbac6fa29552719c85", "firstIndex": 4362076087},
{"blockNumber": 7869890, "blockId": "0x5664bcfa6151f798781e22bea4f9a2c796d097402350cb131e4e3c78e13e461a", "firstIndex": 4429183267},
{"blockNumber": 7911722, "blockId": "0x9a85e48e3135c97c51fc1786f2af0596c802e021b6c53cfca65a129cafcd23ed", "firstIndex": 4496287265},
{"blockNumber": 7960147, "blockId": "0xc9359cc76d7090e1c8a031108f0ab7a8935d971efd4325fe53612a1d99562f6f", "firstIndex": 4563402388},
{"blockNumber": 8030418, "blockId": "0x21867e68cd8327aed2da2601399d60f7f9e41dca4a4f2f9be982e5a2b9304a88", "firstIndex": 4630511616},
{"blockNumber": 8087701, "blockId": "0x0fa8c8d7549cc9a8d308262706fe248efe759f8b63511efb1e7f3926e9af2dcb", "firstIndex": 4697614758},
{"blockNumber": 8149130, "blockId": "0x655ea638fd9e35cc25f4332f260d7bf98f4f6fa9a72e1bff861209f18659e94c", "firstIndex": 4764727744},
{"blockNumber": 8208672, "blockId": "0xb5847a670dc3b6181f9e2e40e4218548048366d237a0d12e938b9879bc8cf800", "firstIndex": 4831837882},
{"blockNumber": 8271345, "blockId": "0x96797214946f29093883b877ccb0f2a9f771a9a3db3794a642b5dcb781c4d194", "firstIndex": 4898942160},
{"blockNumber": 8302858, "blockId": "0x6a5977b3382ca69a9e0412333f97b911c1f69f857d8f31dd0fc930980e24f2fc", "firstIndex": 4966054626},
{"blockNumber": 8333618, "blockId": "0x2547294aa23b67c42adbdddfcf424b17a95c4ff0f352a6a2442c529cfb0c892a", "firstIndex": 5033163605},
{"blockNumber": 8360582, "blockId": "0xf34f5dceb0ef22e0f782b56c12790472acc675997b9c45075bd4e18a9dacd03c", "firstIndex": 5100273631},
{"blockNumber": 8387230, "blockId": "0x0fbea42e87620b5beeb76b67febc173847c54333d7dce9fa2f8f2a3fa9c8c22a", "firstIndex": 5167381673},
{"blockNumber": 8414795, "blockId": "0x6c9c000cf5e35da3a7e9e1cd56147c8ce9b43a76d6de945675efd9dc03b628c9", "firstIndex": 5234477010},
{"blockNumber": 8444749, "blockId": "0xba85f8c9abaddc34e2113eb49385667ba4b008168ae701f46aa7a7ce78c633a1", "firstIndex": 5301598562},
{"blockNumber": 8474551, "blockId": "0x720866a40242f087dd25b6f0dd79224884f435b114a39e60c5669f5c942c78c1", "firstIndex": 5368707262},
{"blockNumber": 8501310, "blockId": "0x2b6da233532c701202fb5ac67e005f7d3eb71f88a9fac10c25d24dd11ada05e5", "firstIndex": 5435803858},
{"blockNumber": 8526970, "blockId": "0x005f9bbad0a10234129d09894d7fcf04bf1398d326510eedb4195808c282802d", "firstIndex": 5502926509},
{"blockNumber": 8550412, "blockId": "0x37c9f3efc9f33cf62f590087c8c9ac70011883f75e648647a6fd0fec00ca627c", "firstIndex": 5570034950},
{"blockNumber": 8573540, "blockId": "0x81cfb46a07be7c70bb8a0f76b03a4cd502f92032bea68ad7ba10e26351673000", "firstIndex": 5637137662},
{"blockNumber": 8590416, "blockId": "0x5c223d58ef22d7b0dd8c498e8498da4787b5dc706681c2bc83849441f5d0922d", "firstIndex": 5704252906},
{"blockNumber": 8616793, "blockId": "0x9043ce02742fb5ec43a696602867b7ce6003a95b36cd28a37eeb9785a46ad49f", "firstIndex": 5771357264},
{"blockNumber": 8647290, "blockId": "0xd90115193764b0a33f3f2a719381b3ddbce2532607c72fb287a864eb391eeada", "firstIndex": 5838466144},
{"blockNumber": 8673192, "blockId": "0x9bc92d340cccaf4c8c03372efc24eb92c5159106729de8d2e9e064f5568d082b", "firstIndex": 5905577457},
{"blockNumber": 8700694, "blockId": "0xb3d656a173b962bc6825198e94a4974289db06a8998060bd0f5ee2044a7a7deb", "firstIndex": 5972679345},
{"blockNumber": 8724533, "blockId": "0x253ffc6d77b88fe18736e4c313e9930341c444bc87b2ee22b26cfe8d9d0b178d", "firstIndex": 6039795829},
{"blockNumber": 8743948, "blockId": "0x04eb66d0261705d31e629193148d0685058d7759ba5f95d2d38e412dbadb8256", "firstIndex": 6106901747},
{"blockNumber": 8758378, "blockId": "0x64adf54e662d11db716610157da672c3d8b45f001dbce40a269871b86a84d026", "firstIndex": 6174011544},
{"blockNumber": 8777722, "blockId": "0x0a7f9a956024b404c915e70b42221aa027b2dd715b0697f099dccefae0b9af97", "firstIndex": 6241124215},
{"blockNumber": 8800154, "blockId": "0x411f90dc18f2bca31fa63615c2866c907bbac1fae8c06782cabfaf788efba665", "firstIndex": 6308233216},
{"blockNumber": 8829725, "blockId": "0x5686f3a5eec1b070d0113c588f8f4a560d57ad96b8045cedb5c08bbadaa0273e", "firstIndex": 6375340033},
{"blockNumber": 8858036, "blockId": "0x4f9b5d9fac9c6f6e2224f613cda12e8ab95d636774ce87489dce8a9f805ee2e5", "firstIndex": 6442450330},
{"blockNumber": 8884811, "blockId": "0x9cf74f978872683802c065e72b5a5326fdad95f19733c34d927b575cd85fd0bd", "firstIndex": 6509559380}
{"blockNumber": 3246675, "blockHash": "0x36bf7de9e1f151963088ca3efa206b6e78411d699d2f64f3bf86895294275e0b", "firstIndex": 67108765},
{"blockNumber": 3575560, "blockHash": "0x4652e3bee440f946aeaa3358c9a62b9bc4d41f663737ab00fded00c2662bfa29", "firstIndex": 134217644},
{"blockNumber": 3694262, "blockHash": "0x433573f97e563ca04fa53e0d0eb019d0ffe9dd032a05ee5b33ce7705aedfd34d", "firstIndex": 201316990},
{"blockNumber": 3725628, "blockHash": "0x1e25ebd76b9e2a2007bfdd7c82433cce7dae8beb190d249c82bf83d0cd177735", "firstIndex": 268426577},
{"blockNumber": 3795378, "blockHash": "0xca999083e01d5947212fb878d529abecdd18a6012997b0d5326afcf6908f17e5", "firstIndex": 335543438},
{"blockNumber": 3856681, "blockHash": "0x48148cd35de3de6c626f76ce5d97cfc6acd88772af69c0aef3d640c29ba82096", "firstIndex": 402641717},
{"blockNumber": 3869367, "blockHash": "0x08bafccf9ece7b72e4e069fc3c7493fbf25732881758917df2e8f9a51fc2c75f", "firstIndex": 469761225},
{"blockNumber": 3938346, "blockHash": "0xc638eb890c0e65ce1169ef0bb6b74dede2f4f6144ff8a5739c4894ff41a22fa2", "firstIndex": 536865235},
{"blockNumber": 3984892, "blockHash": "0x6a7782f0765b0f0c406b3cb107fd8bb14c6906293d9bdc5cc00e081bfd1ae59f", "firstIndex": 603972697},
{"blockNumber": 4002660, "blockHash": "0xda473de053602bc42fcbb074a32c908303f7f6db2ed8c38192665dce80276702", "firstIndex": 671087183},
{"blockNumber": 4113149, "blockHash": "0xd1782f677262226194725028858da83e672748e409fe1c21bdd77c07be0662c8", "firstIndex": 738197365},
{"blockNumber": 4260733, "blockHash": "0xc356600819e49d311963312157c481fd9be43a36ad040a0512fd17559c78d394", "firstIndex": 805305957},
{"blockNumber": 4391073, "blockHash": "0x06f903dc765b4b0ca1a40847eb74d30b0b51904a70c46f2e836dc5b3ea3dd8d3", "firstIndex": 872415067},
{"blockNumber": 4515567, "blockHash": "0xe99d3849755a1b4ec9a65d003260c111d4886e29ad6243fc750c72dbc79dbe4c", "firstIndex": 939523746},
{"blockNumber": 4634815, "blockHash": "0x9f281e749b192ef58242ff9fccd496a9075c30bd28c61ed4ce6487fc2d167e5c", "firstIndex": 1006629595},
{"blockNumber": 4718286, "blockHash": "0x23d2a40ec3ae059a45006ba651a70f308b6b282829b9cfdf2534e4311e34050f", "firstIndex": 1073731435},
{"blockNumber": 4753427, "blockHash": "0x6df7295f9ccb45a95e6cc72a8ee56bda992513b3bf4baa3d62219871a0b5a912", "firstIndex": 1140843105},
{"blockNumber": 4786511, "blockHash": "0x705c79435411c1af81385a1eca34343427eceb3f6af72e7f665552d6b0eceb6c", "firstIndex": 1207957185},
{"blockNumber": 4811696, "blockHash": "0x69a6e60d1b958b469feafb1dca97f38f97620a90c21ed92f90080f6443ee6c78", "firstIndex": 1275061401},
{"blockNumber": 4841770, "blockHash": "0x0a0e9d2d3d177601a478428297b5b9a42124166fefe42f2221c7a612c31e7ca1", "firstIndex": 1342174442},
{"blockNumber": 4914785, "blockHash": "0x5ca0e12230aeacbce9064dff15dafb9f24e20b4a737f7d04df8549008b829fa3", "firstIndex": 1409284680},
{"blockNumber": 4992516, "blockHash": "0xb9cb1700d8b65615fc3a019b2e9b2ece3b4f1e0e2fcfc01ff40bcf53e7291b10", "firstIndex": 1476387066},
{"blockNumber": 5088617, "blockHash": "0x33122f693a264fb2ab626232fac5e2714010fd65bdceb772109511f7c9497333", "firstIndex": 1543503849},
{"blockNumber": 5155024, "blockHash": "0x187716822dd6ab9c21138ae5a8e763197a5e7ec48bf0e756c5e6fa2c9f2f24bf", "firstIndex": 1610604247},
{"blockNumber": 5204373, "blockHash": "0x753b6495979ca90a1b915edad6af33c58bbabe68b45f95c7645113ec1d35d0e6", "firstIndex": 1677721200},
{"blockNumber": 5269952, "blockHash": "0xcbb0409ed824631120d6951b55537143d9a1c82ca6dd6255d2eb31c57844685c", "firstIndex": 1744829248},
{"blockNumber": 5337623, "blockHash": "0x0b460658511c4ae5205c038430789f9b31f3c08ba4acc06d273b7a13a149cf8b", "firstIndex": 1811939255},
{"blockNumber": 5399044, "blockHash": "0x9d07e37321e2b019f4837cc4f95f103da87ecdcb77e131e9d42d8643b0446524", "firstIndex": 1879041131},
{"blockNumber": 5422692, "blockHash": "0x9d9e27614635989788e32f15cd7a0abf7470dccf3336b77547baeb3423b629ca", "firstIndex": 1946154968},
{"blockNumber": 5454259, "blockHash": "0x01e694443d471d3411fe0b52720fd4a189e687b14cc8ede116f5e93f0ad96bb6", "firstIndex": 2013265148},
{"blockNumber": 5498863, "blockHash": "0xc4cb906c96d9f80b830b8add25318c38b3ff857c1bd577cd9cae0a324a068ffc", "firstIndex": 2080373677},
{"blockNumber": 5554788, "blockHash": "0x9b934b2d66723559d82e0f4cd49a2497234fe892248cb2f46c13f6a9585cfddd", "firstIndex": 2147475372},
{"blockNumber": 5594711, "blockHash": "0x840c0b7889b8c882cfa0eceb516dd73043b51c0e5a0c1be849a0e2a5dda7e842", "firstIndex": 2214592267},
{"blockNumber": 5645179, "blockHash": "0x188873a43506fee33349bc33df4a55059401e98dc7f0a5d488a0bc5c87dfd431", "firstIndex": 2281695869},
{"blockNumber": 5687634, "blockHash": "0x7ea8d7a2afccf795a2a02eb00746d89db57f13f8936cf7ab0f0a7db1b1ceb341", "firstIndex": 2348806824},
{"blockNumber": 5727808, "blockHash": "0x3f3eaf36c8e0271403737676907eac15312927c1d60ce814337654b7d7f2b2bd", "firstIndex": 2415915517},
{"blockNumber": 5784480, "blockHash": "0x10ec5adf01449c29550f5117f09ecafc49fc614d7da5886cf01619950827f850", "firstIndex": 2483023750},
{"blockNumber": 5843906, "blockHash": "0x9e6e14b3fba395a9ae7b3f1295f33a12d0f8b9f10e1aaab1649a90a56a323676", "firstIndex": 2550136398},
{"blockNumber": 5906280, "blockHash": "0x2188869a164e9c19232f6702a5233dc27a2304839159dd7230cc93ebdfc6c048", "firstIndex": 2617245012},
{"blockNumber": 5977929, "blockHash": "0xb247181ed597d9473c2c6e5762ae45a05f328d8df215fdac83d19ae4c028109a", "firstIndex": 2684347033},
{"blockNumber": 6051479, "blockHash": "0x0e8634c95dac31b7c835b44d0a14290e9212b31b9c3f621997ad1cca2d8ab0ba", "firstIndex": 2751463372},
{"blockNumber": 6118422, "blockHash": "0xa642c4457d4b63d68a5502bdba2355616e34495bf3ab2d8518a45ded54a16be4", "firstIndex": 2818563217},
{"blockNumber": 6174274, "blockHash": "0xbb9808bc56b5f14636e654bdd6d50d35b31345255926341ef3ddb54840095bcc", "firstIndex": 2885680274},
{"blockNumber": 6276207, "blockHash": "0x7ae027a18889e1b60a46ea3e5f2759035f3d9041de68eca0b154068ec533db59", "firstIndex": 2952789656},
{"blockNumber": 6368344, "blockHash": "0x8b4401f89589d9e99259e6c05c18b1780e96b65027caf9d83edf80390d317f66", "firstIndex": 3019898378},
{"blockNumber": 6470718, "blockHash": "0x0e2f8de093392c38750581c41e93c2fb1c0f6676b65c9af52792f3c56e894457", "firstIndex": 3087007589},
{"blockNumber": 6553237, "blockHash": "0xa6c4f3a1fe3b4e116c367fcbb1ae98e0f25570fdb2f1eadee2681deb39462c1e", "firstIndex": 3154116356},
{"blockNumber": 6663750, "blockHash": "0x9049748ddf655c7cc2ccbe2d36010b3a67ae011d114d32b1c812a7a53102f27f", "firstIndex": 3221225375},
{"blockNumber": 6766894, "blockHash": "0xc98188fb313b1beb7fc4f415b4a74c363903b3b5e0172437d79446bc011238b5", "firstIndex": 3288334172},
{"blockNumber": 6886576, "blockHash": "0x4ae8237a94198c82c266f01e472fdcd3ae150c84ecf714205a9be672f1e705b8", "firstIndex": 3355443040},
{"blockNumber": 6978746, "blockHash": "0xb2b69ce29c5c9c46c5cbc6e96260c34313d8b0b4a739cf4091c98f37093aa732", "firstIndex": 3422552017},
{"blockNumber": 7098871, "blockHash": "0x9ab2b681feeb38287a1b689fa64a50efd3407b6331fd6896187cad98a29ce107", "firstIndex": 3489660879},
{"blockNumber": 7203023, "blockHash": "0xdedfff5985c1c93d476120d2afbaa61967bae4edabb9711c7794a1b1cd453aa1", "firstIndex": 3556769365},
{"blockNumber": 7256709, "blockHash": "0xc078041cffc1802b6342fd2ba5a92ff8076ac0f5b72b4a77ba75cd486fe1b3c9", "firstIndex": 3623876563},
{"blockNumber": 7307744, "blockHash": "0xdf8dcdd4af9213a79587eb2f4d91f4111da74f99207ee05e4ae50bcb4f8d5435", "firstIndex": 3690986225},
{"blockNumber": 7369268, "blockHash": "0x62315823621a38a1f138e193b85259988fcc15f4a74d0cf19a412920bcdcad98", "firstIndex": 3758096030},
{"blockNumber": 7445109, "blockHash": "0x8b668ac1274a8dcc9526eb115c2f67bd56f6657b5072c7542da2252daca10fef", "firstIndex": 3825204921},
{"blockNumber": 7511512, "blockHash": "0x056ad7411652aa64849c67705a86e42221cb88f7fb6a7012ab7d3d5b9a30eb76", "firstIndex": 3892313524},
{"blockNumber": 7557259, "blockHash": "0xef660b97661f073b7c7319b69451e16d26ecaed54f9237e209999d137da2bfd4", "firstIndex": 3959415096},
{"blockNumber": 7606260, "blockHash": "0x9f5f06ad381166261780864bf17ad4d3cdd475f87ba51fa7f9ba621bf0ac0acb", "firstIndex": 4026521703},
{"blockNumber": 7654244, "blockHash": "0xefe5b537435001203741bf445ca26466baf7ad3f63c2bba94f0f658aed8c244b", "firstIndex": 4093627258},
{"blockNumber": 7700433, "blockHash": "0x89af6e8c5aa934a9139944b3de7a15232069479dba387e0fdd043ad36156c8bf", "firstIndex": 4160749200},
{"blockNumber": 7730079, "blockHash": "0x08e0d511a93e2a9c004b3456d77d687ae86247417ada1cdd1f85332a62dfed1d", "firstIndex": 4227846795},
{"blockNumber": 7777515, "blockHash": "0xeba8faed2e12bb9ed5e7fad19dd82662f15f6f4f512a0d232eedf1e676712428", "firstIndex": 4294966082},
{"blockNumber": 7828860, "blockHash": "0x62c054bd24be351dc4777460ee922a75fdcea5c9830455cbac6fa29552719c85", "firstIndex": 4362076087},
{"blockNumber": 7869890, "blockHash": "0x5664bcfa6151f798781e22bea4f9a2c796d097402350cb131e4e3c78e13e461a", "firstIndex": 4429183267},
{"blockNumber": 7911722, "blockHash": "0x9a85e48e3135c97c51fc1786f2af0596c802e021b6c53cfca65a129cafcd23ed", "firstIndex": 4496287265},
{"blockNumber": 7960147, "blockHash": "0xc9359cc76d7090e1c8a031108f0ab7a8935d971efd4325fe53612a1d99562f6f", "firstIndex": 4563402388},
{"blockNumber": 8030418, "blockHash": "0x21867e68cd8327aed2da2601399d60f7f9e41dca4a4f2f9be982e5a2b9304a88", "firstIndex": 4630511616},
{"blockNumber": 8087701, "blockHash": "0x0fa8c8d7549cc9a8d308262706fe248efe759f8b63511efb1e7f3926e9af2dcb", "firstIndex": 4697614758},
{"blockNumber": 8149130, "blockHash": "0x655ea638fd9e35cc25f4332f260d7bf98f4f6fa9a72e1bff861209f18659e94c", "firstIndex": 4764727744},
{"blockNumber": 8208672, "blockHash": "0xb5847a670dc3b6181f9e2e40e4218548048366d237a0d12e938b9879bc8cf800", "firstIndex": 4831837882},
{"blockNumber": 8271345, "blockHash": "0x96797214946f29093883b877ccb0f2a9f771a9a3db3794a642b5dcb781c4d194", "firstIndex": 4898942160},
{"blockNumber": 8302858, "blockHash": "0x6a5977b3382ca69a9e0412333f97b911c1f69f857d8f31dd0fc930980e24f2fc", "firstIndex": 4966054626},
{"blockNumber": 8333618, "blockHash": "0x2547294aa23b67c42adbdddfcf424b17a95c4ff0f352a6a2442c529cfb0c892a", "firstIndex": 5033163605},
{"blockNumber": 8360582, "blockHash": "0xf34f5dceb0ef22e0f782b56c12790472acc675997b9c45075bd4e18a9dacd03c", "firstIndex": 5100273631},
{"blockNumber": 8387230, "blockHash": "0x0fbea42e87620b5beeb76b67febc173847c54333d7dce9fa2f8f2a3fa9c8c22a", "firstIndex": 5167381673},
{"blockNumber": 8414795, "blockHash": "0x6c9c000cf5e35da3a7e9e1cd56147c8ce9b43a76d6de945675efd9dc03b628c9", "firstIndex": 5234477010},
{"blockNumber": 8444749, "blockHash": "0xba85f8c9abaddc34e2113eb49385667ba4b008168ae701f46aa7a7ce78c633a1", "firstIndex": 5301598562},
{"blockNumber": 8474551, "blockHash": "0x720866a40242f087dd25b6f0dd79224884f435b114a39e60c5669f5c942c78c1", "firstIndex": 5368707262},
{"blockNumber": 8501310, "blockHash": "0x2b6da233532c701202fb5ac67e005f7d3eb71f88a9fac10c25d24dd11ada05e5", "firstIndex": 5435803858},
{"blockNumber": 8526970, "blockHash": "0x005f9bbad0a10234129d09894d7fcf04bf1398d326510eedb4195808c282802d", "firstIndex": 5502926509},
{"blockNumber": 8550412, "blockHash": "0x37c9f3efc9f33cf62f590087c8c9ac70011883f75e648647a6fd0fec00ca627c", "firstIndex": 5570034950},
{"blockNumber": 8573540, "blockHash": "0x81cfb46a07be7c70bb8a0f76b03a4cd502f92032bea68ad7ba10e26351673000", "firstIndex": 5637137662},
{"blockNumber": 8590416, "blockHash": "0x5c223d58ef22d7b0dd8c498e8498da4787b5dc706681c2bc83849441f5d0922d", "firstIndex": 5704252906},
{"blockNumber": 8616793, "blockHash": "0x9043ce02742fb5ec43a696602867b7ce6003a95b36cd28a37eeb9785a46ad49f", "firstIndex": 5771357264},
{"blockNumber": 8647290, "blockHash": "0xd90115193764b0a33f3f2a719381b3ddbce2532607c72fb287a864eb391eeada", "firstIndex": 5838466144},
{"blockNumber": 8673192, "blockHash": "0x9bc92d340cccaf4c8c03372efc24eb92c5159106729de8d2e9e064f5568d082b", "firstIndex": 5905577457},
{"blockNumber": 8700694, "blockHash": "0xb3d656a173b962bc6825198e94a4974289db06a8998060bd0f5ee2044a7a7deb", "firstIndex": 5972679345},
{"blockNumber": 8724533, "blockHash": "0x253ffc6d77b88fe18736e4c313e9930341c444bc87b2ee22b26cfe8d9d0b178d", "firstIndex": 6039795829},
{"blockNumber": 8743948, "blockHash": "0x04eb66d0261705d31e629193148d0685058d7759ba5f95d2d38e412dbadb8256", "firstIndex": 6106901747},
{"blockNumber": 8758378, "blockHash": "0x64adf54e662d11db716610157da672c3d8b45f001dbce40a269871b86a84d026", "firstIndex": 6174011544},
{"blockNumber": 8777722, "blockHash": "0x0a7f9a956024b404c915e70b42221aa027b2dd715b0697f099dccefae0b9af97", "firstIndex": 6241124215},
{"blockNumber": 8800154, "blockHash": "0x411f90dc18f2bca31fa63615c2866c907bbac1fae8c06782cabfaf788efba665", "firstIndex": 6308233216},
{"blockNumber": 8829725, "blockHash": "0x5686f3a5eec1b070d0113c588f8f4a560d57ad96b8045cedb5c08bbadaa0273e", "firstIndex": 6375340033},
{"blockNumber": 8858036, "blockHash": "0x4f9b5d9fac9c6f6e2224f613cda12e8ab95d636774ce87489dce8a9f805ee2e5", "firstIndex": 6442450330},
{"blockNumber": 8884811, "blockHash": "0x9cf74f978872683802c065e72b5a5326fdad95f19733c34d927b575cd85fd0bd", "firstIndex": 6509559380}
]

View file

@ -1,889 +0,0 @@
// Copyright 2024 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package filtermaps
import (
"errors"
"fmt"
"os"
"slices"
"sync"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/lru"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics"
)
var (
mapCountGauge = metrics.NewRegisteredGauge("filtermaps/maps/count", nil) // actual number of rendered maps
mapLogValueMeter = metrics.NewRegisteredMeter("filtermaps/maps/logvalues", nil) // number of log values processed
mapBlockMeter = metrics.NewRegisteredMeter("filtermaps/maps/blocks", nil) // number of block delimiters processed
mapRenderTimer = metrics.NewRegisteredTimer("filtermaps/maps/rendertime", nil) // time elapsed while rendering a single map
mapWriteTimer = metrics.NewRegisteredTimer("filtermaps/maps/writetime", nil) // time elapsed while writing a batch of finished maps to db
matchRequestTimer = metrics.NewRegisteredTimer("filtermaps/match/requesttime", nil) // processing time a matching request in a single epoch
matchEpochTimer = metrics.NewRegisteredTimer("filtermaps/match/epochtime", nil) // total processing time a matching request
matchBaseRowAccessMeter = metrics.NewRegisteredMeter("filtermaps/match/baserowaccess", nil) // number of accessed rows on layer 0
matchBaseRowSizeMeter = metrics.NewRegisteredMeter("filtermaps/match/baserowsize", nil) // size of accessed rows on layer 0
matchExtRowAccessMeter = metrics.NewRegisteredMeter("filtermaps/match/extrowaccess", nil) // number of accessed rows on higher layers
matchExtRowSizeMeter = metrics.NewRegisteredMeter("filtermaps/match/extrowsize", nil) // size of accessed rows on higher layers
matchLogLookup = metrics.NewRegisteredMeter("filtermaps/match/loglookup", nil) // number of log lookups based on potential matches
matchAllMeter = metrics.NewRegisteredMeter("filtermaps/match/matchall", nil) // number of requests returned with ErrMatchAll
)
const (
databaseVersion = 2 // reindexed if database version does not match
cachedLastBlocks = 1000 // last block of map pointers
cachedLvPointers = 1000 // first log value pointer of block pointers
cachedFilterMaps = 3 // complete filter maps (cached by map renderer)
cachedRenderSnapshots = 8 // saved map renderer data at block boundaries
)
// FilterMaps is the in-memory representation of the log index structure that is
// responsible for building and updating the index according to the canonical
// chain.
//
// Note that FilterMaps implements the same data structure as proposed in EIP-7745
// without the tree hashing and consensus changes:
// https://eips.ethereum.org/EIPS/eip-7745
type FilterMaps struct {
// If disabled is set, log indexing is fully disabled.
// This is configured by the --history.logs.disable Geth flag.
// We chose to implement disabling this way because it requires less special
// case logic in eth/filters.
disabled bool
disabledCh chan struct{} // closed by indexer if disabled
closeCh chan struct{}
closeWg sync.WaitGroup
history uint64
hashScheme bool // use hashdb-safe delete range method
exportFileName string
Params
db ethdb.KeyValueStore
// fields written by the indexer and read by matcher backend. Indexer can
// read them without a lock and write them under indexLock write lock.
// Matcher backend can read them under indexLock read lock.
indexLock sync.RWMutex
indexedRange filterMapsRange
indexedView *ChainView // always consistent with the log index
hasTempRange bool
// cleanedEpochsBefore indicates that all unindexed data before this point
// has been cleaned.
//
// This field is only accessed and modified within tryUnindexTail, so no
// explicit locking is required.
cleanedEpochsBefore uint32
// also accessed by indexer and matcher backend but no locking needed.
filterMapCache *lru.Cache[uint32, filterMap]
lastBlockCache *lru.Cache[uint32, lastBlockOfMap]
lvPointerCache *lru.Cache[uint64, uint64]
// the matchers set and the fields of FilterMapsMatcherBackend instances are
// read and written both by exported functions and the indexer.
// Note that if both indexLock and matchersLock needs to be locked then
// indexLock should be locked first.
matchersLock sync.Mutex
matchers map[*FilterMapsMatcherBackend]struct{}
// fields only accessed by the indexer (no mutex required).
renderSnapshots *lru.Cache[uint64, *renderedMap]
startedHeadIndex, startedTailIndex, startedTailUnindex bool
startedHeadIndexAt, startedTailIndexAt, startedTailUnindexAt time.Time
loggedHeadIndex, loggedTailIndex bool
lastLogHeadIndex, lastLogTailIndex time.Time
ptrHeadIndex, ptrTailIndex, ptrTailUnindexBlock uint64
ptrTailUnindexMap uint32
targetView *ChainView
matcherSyncRequests []*FilterMapsMatcherBackend
historyCutoff uint64
finalBlock, lastFinal uint64
lastFinalEpoch uint32
stop bool
targetCh chan targetUpdate
blockProcessingCh chan bool
blockProcessing bool
matcherSyncCh chan *FilterMapsMatcherBackend
waitIdleCh chan chan bool
tailRenderer *mapRenderer
// test hooks
testDisableSnapshots, testSnapshotUsed bool
testProcessEventsHook func()
}
// filterMap is a full or partial in-memory representation of a filter map where
// rows are allowed to have a nil value meaning the row is not stored in the
// structure. Note that therefore a known empty row should be represented with
// a zero-length slice.
// It can be used as a memory cache or an overlay while preparing a batch of
// changes to the structure. In either case a nil value should be interpreted
// as transparent (uncached/unchanged).
type filterMap []FilterRow
// fastCopy returns a copy of the given filter map. Note that the row slices are
// copied but their contents are not. This permits appending to the rows further
// (which happens during map rendering) without affecting the validity of
// copies made for snapshots during rendering.
// Appending to the rows of both the original map and the fast copy, or two fast
// copies of the same map would result in data corruption, therefore a fast copy
// should always be used in a read only way.
func (fm filterMap) fastCopy() filterMap {
return slices.Clone(fm)
}
// fullCopy returns a copy of the given filter map, also making a copy of each
// individual filter row, ensuring that a modification to either one will never
// affect the other.
func (fm filterMap) fullCopy() filterMap {
c := make(filterMap, len(fm))
for i, row := range fm {
c[i] = slices.Clone(row)
}
return c
}
// FilterRow encodes a single row of a filter map as a list of column indices.
// Note that the values are always stored in the same order as they were added
// and if the same column index is added twice, it is also stored twice.
// Order of column indices and potential duplications do not matter when searching
// for a value but leaving the original order makes reverting to a previous state
// simpler.
type FilterRow []uint32
// Equal returns true if the given filter rows are equivalent.
func (a FilterRow) Equal(b FilterRow) bool {
return slices.Equal(a, b)
}
// filterMapsRange describes the rendered range of filter maps and the range
// of fully rendered blocks.
type filterMapsRange struct {
initialized bool
headIndexed bool
headDelimiter uint64 // zero if headIndexed is false
// if initialized then all maps are rendered in the maps range
maps common.Range[uint32]
// if tailPartialEpoch > 0 then maps between firstRenderedMap-mapsPerEpoch and
// firstRenderedMap-mapsPerEpoch+tailPartialEpoch-1 are rendered
tailPartialEpoch uint32
// if initialized then all log values in the blocks range are fully
// rendered
// blockLvPointers are available in the blocks range
blocks common.Range[uint64]
}
// hasIndexedBlocks returns true if the range has at least one fully indexed block.
func (fmr *filterMapsRange) hasIndexedBlocks() bool {
return fmr.initialized && !fmr.blocks.IsEmpty() && !fmr.maps.IsEmpty()
}
// lastBlockOfMap is used for caching the (number, id) pairs belonging to the
// last block of each map.
type lastBlockOfMap struct {
number uint64
id common.Hash
}
// Config contains the configuration options for NewFilterMaps.
type Config struct {
History uint64 // number of historical blocks to index
Disabled bool // disables indexing completely
// This option enables the checkpoint JSON file generator.
// If set, the given file will be updated with checkpoint information.
ExportFileName string
// expect trie nodes of hash based state scheme in the filtermaps key range;
// use safe iterator based implementation of DeleteRange that skips them
HashScheme bool
}
// NewFilterMaps creates a new FilterMaps and starts the indexer.
func NewFilterMaps(db ethdb.KeyValueStore, initView *ChainView, historyCutoff, finalBlock uint64, params Params, config Config) (*FilterMaps, error) {
rs, initialized, err := rawdb.ReadFilterMapsRange(db)
if err != nil || (initialized && rs.Version != databaseVersion) {
rs, initialized = rawdb.FilterMapsRange{}, false
log.Warn("Invalid log index database version; resetting log index")
}
if err := params.sanitize(); err != nil {
return nil, err
}
f := &FilterMaps{
db: db,
closeCh: make(chan struct{}),
waitIdleCh: make(chan chan bool),
targetCh: make(chan targetUpdate, 1),
blockProcessingCh: make(chan bool, 1),
history: config.History,
disabled: config.Disabled,
hashScheme: config.HashScheme,
disabledCh: make(chan struct{}),
exportFileName: config.ExportFileName,
Params: params,
targetView: initView,
indexedView: initView,
indexedRange: filterMapsRange{
initialized: initialized,
headIndexed: rs.HeadIndexed,
headDelimiter: rs.HeadDelimiter,
blocks: common.NewRange(rs.BlocksFirst, rs.BlocksAfterLast-rs.BlocksFirst),
maps: common.NewRange(rs.MapsFirst, rs.MapsAfterLast-rs.MapsFirst),
tailPartialEpoch: rs.TailPartialEpoch,
},
// deleting last unindexed epoch might have been interrupted by shutdown
cleanedEpochsBefore: max(rs.MapsFirst>>params.logMapsPerEpoch, 1) - 1,
historyCutoff: historyCutoff,
finalBlock: finalBlock,
matcherSyncCh: make(chan *FilterMapsMatcherBackend),
matchers: make(map[*FilterMapsMatcherBackend]struct{}),
filterMapCache: lru.NewCache[uint32, filterMap](cachedFilterMaps),
lastBlockCache: lru.NewCache[uint32, lastBlockOfMap](cachedLastBlocks),
lvPointerCache: lru.NewCache[uint64, uint64](cachedLvPointers),
renderSnapshots: lru.NewCache[uint64, *renderedMap](cachedRenderSnapshots),
}
f.checkRevertRange() // revert maps that are inconsistent with the current chain view
if f.indexedRange.hasIndexedBlocks() {
log.Info("Initialized log indexer",
"firstblock", f.indexedRange.blocks.First(), "lastblock", f.indexedRange.blocks.Last(),
"firstmap", f.indexedRange.maps.First(), "lastmap", f.indexedRange.maps.Last(),
"headindexed", f.indexedRange.headIndexed)
}
return f, nil
}
// Start starts the indexer.
func (f *FilterMaps) Start() {
if !f.testDisableSnapshots && f.indexedRange.hasIndexedBlocks() && f.indexedRange.headIndexed {
// previous target head rendered; load last map as snapshot
if err := f.loadHeadSnapshot(); err != nil {
log.Error("Could not load head filter map snapshot", "error", err)
}
}
f.closeWg.Add(2)
go f.removeBloomBits()
go f.indexerLoop()
}
// Stop ensures that the indexer is fully stopped before returning.
func (f *FilterMaps) Stop() {
close(f.closeCh)
f.closeWg.Wait()
}
// checkRevertRange checks whether the existing index is consistent with the
// current indexed view and reverts inconsistent maps if necessary.
func (f *FilterMaps) checkRevertRange() {
if f.indexedRange.maps.Count() == 0 {
return
}
lastMap := f.indexedRange.maps.Last()
lastBlockNumber, lastBlockId, err := f.getLastBlockOfMap(lastMap)
if err != nil {
log.Error("Error initializing log index database; resetting log index", "error", err)
f.reset()
return
}
for lastBlockNumber > f.indexedView.HeadNumber() || f.indexedView.BlockId(lastBlockNumber) != lastBlockId {
// revert last map
if f.indexedRange.maps.Count() == 1 {
f.reset() // reset database if no rendered maps remained
return
}
lastMap--
newRange := f.indexedRange
newRange.maps.SetLast(lastMap)
lastBlockNumber, lastBlockId, err = f.getLastBlockOfMap(lastMap)
if err != nil {
log.Error("Error initializing log index database; resetting log index", "error", err)
f.reset()
return
}
newRange.blocks.SetAfterLast(lastBlockNumber) // lastBlockNumber is probably partially indexed
newRange.headIndexed = false
newRange.headDelimiter = 0
// only shorten range and leave map data; next head render will overwrite it
f.setRange(f.db, f.indexedView, newRange, false)
}
}
// reset un-initializes the FilterMaps structure and removes all related data from
// the database.
// Note that in case of leveldb database the fallback implementation of DeleteRange
// might take a long time to finish and deleting the entire database may be
// interrupted by a shutdown. Deleting the filterMapsRange entry first does
// guarantee though that the next init() will not return successfully until the
// entire database has been cleaned.
func (f *FilterMaps) reset() {
f.indexLock.Lock()
f.indexedRange = filterMapsRange{}
f.indexedView = nil
f.filterMapCache.Purge()
f.renderSnapshots.Purge()
f.lastBlockCache.Purge()
f.lvPointerCache.Purge()
f.indexLock.Unlock()
// deleting the range first ensures that resetDb will be called again at next
// startup and any leftover data will be removed even if it cannot finish now.
rawdb.DeleteFilterMapsRange(f.db)
f.safeDeleteWithLogs(rawdb.DeleteFilterMapsDb, "Resetting log index database", f.isShuttingDown)
}
// isShuttingDown returns true if FilterMaps is shutting down.
func (f *FilterMaps) isShuttingDown() bool {
select {
case <-f.closeCh:
return true
default:
return false
}
}
// init initializes an empty log index according to the current targetView.
func (f *FilterMaps) init() error {
// ensure that there is no remaining data in the filter maps key range
if err := f.safeDeleteWithLogs(rawdb.DeleteFilterMapsDb, "Resetting log index database", f.isShuttingDown); err != nil {
return err
}
f.indexLock.Lock()
defer f.indexLock.Unlock()
var bestIdx, bestLen int
for idx, checkpointList := range checkpoints {
// binary search for the last matching epoch head
min, max := 0, len(checkpointList)
for min < max {
mid := (min + max + 1) / 2
cp := checkpointList[mid-1]
if cp.BlockNumber <= f.targetView.HeadNumber() && f.targetView.BlockId(cp.BlockNumber) == cp.BlockId {
min = mid
} else {
max = mid - 1
}
}
if max > bestLen {
bestIdx, bestLen = idx, max
}
}
var initBlockNumber uint64
if bestLen > 0 {
initBlockNumber = checkpoints[bestIdx][bestLen-1].BlockNumber
}
if initBlockNumber < f.historyCutoff {
return errors.New("cannot start indexing before history cutoff point")
}
batch := f.db.NewBatch()
for epoch := range bestLen {
cp := checkpoints[bestIdx][epoch]
f.storeLastBlockOfMap(batch, f.lastEpochMap(uint32(epoch)), cp.BlockNumber, cp.BlockId)
f.storeBlockLvPointer(batch, cp.BlockNumber, cp.FirstIndex)
}
fmr := filterMapsRange{
initialized: true,
}
if bestLen > 0 {
cp := checkpoints[bestIdx][bestLen-1]
fmr.blocks = common.NewRange(cp.BlockNumber+1, 0)
fmr.maps = common.NewRange(f.firstEpochMap(uint32(bestLen)), 0)
}
f.setRange(batch, f.targetView, fmr, false)
return batch.Write()
}
// removeBloomBits removes old bloom bits data from the database.
func (f *FilterMaps) removeBloomBits() {
f.safeDeleteWithLogs(rawdb.DeleteBloomBitsDb, "Removing old bloom bits database", f.isShuttingDown)
f.closeWg.Done()
}
// safeDeleteWithLogs is a wrapper for a function that performs a safe range
// delete operation using rawdb.SafeDeleteRange. It emits log messages if the
// process takes long enough to call the stop callback.
func (f *FilterMaps) safeDeleteWithLogs(deleteFn func(db ethdb.KeyValueStore, hashScheme bool, stopCb func(bool) bool) error, action string, stopCb func() bool) error {
var (
start = time.Now()
logPrinted bool
lastLogPrinted = start
)
switch err := deleteFn(f.db, f.hashScheme, func(deleted bool) bool {
if deleted && !logPrinted || time.Since(lastLogPrinted) > time.Second*10 {
log.Info(action+" in progress...", "elapsed", common.PrettyDuration(time.Since(start)))
logPrinted, lastLogPrinted = true, time.Now()
}
return stopCb()
}); {
case err == nil:
if logPrinted {
log.Info(action+" finished", "elapsed", common.PrettyDuration(time.Since(start)))
}
return nil
case errors.Is(err, rawdb.ErrDeleteRangeInterrupted):
log.Warn(action+" interrupted", "elapsed", common.PrettyDuration(time.Since(start)))
return err
default:
log.Error(action+" failed", "error", err)
return err
}
}
// setRange updates the indexed chain view and covered range and also adds the
// changes to the given batch.
//
// Note that this function assumes that the index write lock is being held.
func (f *FilterMaps) setRange(batch ethdb.KeyValueWriter, newView *ChainView, newRange filterMapsRange, isTempRange bool) {
f.indexedView = newView
f.indexedRange = newRange
f.hasTempRange = isTempRange
f.updateMatchersValidRange()
if newRange.initialized {
rs := rawdb.FilterMapsRange{
Version: databaseVersion,
HeadIndexed: newRange.headIndexed,
HeadDelimiter: newRange.headDelimiter,
BlocksFirst: newRange.blocks.First(),
BlocksAfterLast: newRange.blocks.AfterLast(),
MapsFirst: newRange.maps.First(),
MapsAfterLast: newRange.maps.AfterLast(),
TailPartialEpoch: newRange.tailPartialEpoch,
}
rawdb.WriteFilterMapsRange(batch, rs)
if !isTempRange {
mapCountGauge.Update(int64(newRange.maps.Count() + newRange.tailPartialEpoch))
}
} else {
rawdb.DeleteFilterMapsRange(batch)
mapCountGauge.Update(0)
}
}
// getLogByLvIndex returns the log at the given log value index. If the index does
// not point to the first log value entry of a log then no log and no error are
// returned as this can happen when the log value index was a false positive.
// Note that this function assumes that the log index structure is consistent
// with the canonical chain at the point where the given log value index points.
// If this is not the case then an invalid result or an error may be returned.
//
// Note that this function assumes that the indexer read lock is being held when
// called from outside the indexerLoop goroutine.
func (f *FilterMaps) getLogByLvIndex(lvIndex uint64) (*types.Log, error) {
mapIndex := uint32(lvIndex >> f.logValuesPerMap)
if !f.indexedRange.maps.Includes(mapIndex) {
return nil, nil
}
// find possible block range based on map to block pointers
lastBlockNumber, _, err := f.getLastBlockOfMap(mapIndex)
if err != nil {
return nil, fmt.Errorf("failed to retrieve last block of map %d containing searched log value index %d: %v", mapIndex, lvIndex, err)
}
var firstBlockNumber uint64
if mapIndex > 0 {
firstBlockNumber, _, err = f.getLastBlockOfMap(mapIndex - 1)
if err != nil {
return nil, fmt.Errorf("failed to retrieve last block of map %d before searched log value index %d: %v", mapIndex, lvIndex, err)
}
}
if firstBlockNumber < f.indexedRange.blocks.First() {
firstBlockNumber = f.indexedRange.blocks.First()
}
// find block with binary search based on block to log value index pointers
for firstBlockNumber < lastBlockNumber {
midBlockNumber := (firstBlockNumber + lastBlockNumber + 1) / 2
midLvPointer, err := f.getBlockLvPointer(midBlockNumber)
if err != nil {
return nil, fmt.Errorf("failed to retrieve log value pointer of block %d while binary searching log value index %d: %v", midBlockNumber, lvIndex, err)
}
if lvIndex < midLvPointer {
lastBlockNumber = midBlockNumber - 1
} else {
firstBlockNumber = midBlockNumber
}
}
// get block receipts
receipts := f.indexedView.Receipts(firstBlockNumber)
if receipts == nil {
return nil, fmt.Errorf("failed to retrieve receipts for block %d containing searched log value index %d: %v", firstBlockNumber, lvIndex, err)
}
lvPointer, err := f.getBlockLvPointer(firstBlockNumber)
if err != nil {
return nil, fmt.Errorf("failed to retrieve log value pointer of block %d containing searched log value index %d: %v", firstBlockNumber, lvIndex, err)
}
// iterate through receipts to find the exact log starting at lvIndex
for _, receipt := range receipts {
for _, log := range receipt.Logs {
l := uint64(len(log.Topics) + 1)
r := f.valuesPerMap - lvPointer%f.valuesPerMap
if l > r {
lvPointer += r // skip to map boundary
}
if lvPointer > lvIndex {
// lvIndex does not point to the first log value (address value)
// generated by a log as true matches should always do, so it
// is considered a false positive (no log and no error returned).
return nil, nil
}
if lvPointer == lvIndex {
return log, nil // potential match
}
lvPointer += l
}
}
return nil, nil
}
// getFilterMap fetches an entire filter map from the database.
func (f *FilterMaps) getFilterMap(mapIndex uint32) (filterMap, error) {
if fm, ok := f.filterMapCache.Get(mapIndex); ok {
return fm, nil
}
fm := make(filterMap, f.mapHeight)
for rowIndex := range fm {
rows, err := f.getFilterMapRows([]uint32{mapIndex}, uint32(rowIndex), false)
if err != nil {
return nil, fmt.Errorf("failed to load filter map %d from database: %v", mapIndex, err)
}
fm[rowIndex] = rows[0]
}
f.filterMapCache.Add(mapIndex, fm)
return fm, nil
}
// getFilterMapRows fetches a set of filter map rows at the corresponding map
// indices and a shared row index. If baseLayerOnly is true then only the first
// baseRowLength entries are returned.
func (f *FilterMaps) getFilterMapRows(mapIndices []uint32, rowIndex uint32, baseLayerOnly bool) ([]FilterRow, error) {
rows := make([]FilterRow, len(mapIndices))
var ptr int
for len(mapIndices) > ptr {
var (
groupIndex = f.mapGroupIndex(mapIndices[ptr])
groupLength = 1
)
for ptr+groupLength < len(mapIndices) && f.mapGroupIndex(mapIndices[ptr+groupLength]) == groupIndex {
groupLength++
}
if err := f.getFilterMapRowsOfGroup(rows[ptr:ptr+groupLength], mapIndices[ptr:ptr+groupLength], rowIndex, baseLayerOnly); err != nil {
return nil, err
}
ptr += groupLength
}
return rows, nil
}
// getFilterMapRowsOfGroup fetches a set of filter map rows at map indices
// belonging to the same base row group.
func (f *FilterMaps) getFilterMapRowsOfGroup(target []FilterRow, mapIndices []uint32, rowIndex uint32, baseLayerOnly bool) error {
var (
groupIndex = f.mapGroupIndex(mapIndices[0])
mapRowIndex = f.mapRowIndex(groupIndex, rowIndex)
)
baseRows, err := rawdb.ReadFilterMapBaseRows(f.db, mapRowIndex, f.baseRowGroupSize, f.logMapWidth)
if err != nil {
return fmt.Errorf("failed to retrieve base row group %d of row %d: %v", groupIndex, rowIndex, err)
}
for i, mapIndex := range mapIndices {
if f.mapGroupIndex(mapIndex) != groupIndex {
return fmt.Errorf("maps are not in the same base row group, index: %d, group: %d", mapIndex, groupIndex)
}
row := baseRows[f.mapGroupOffset(mapIndex)]
if !baseLayerOnly {
extRow, err := rawdb.ReadFilterMapExtRow(f.db, f.mapRowIndex(mapIndex, rowIndex), f.logMapWidth)
if err != nil {
return fmt.Errorf("failed to retrieve filter map %d extended row %d: %v", mapIndex, rowIndex, err)
}
row = append(row, extRow...)
}
target[i] = row
}
return nil
}
// storeFilterMapRows stores a set of filter map rows at the corresponding map
// indices and a shared row index.
func (f *FilterMaps) storeFilterMapRows(batch ethdb.Batch, mapIndices []uint32, rowIndex uint32, rows []FilterRow) error {
for len(mapIndices) > 0 {
var (
pos = 1
groupIndex = f.mapGroupIndex(mapIndices[0])
)
for pos < len(mapIndices) && f.mapGroupIndex(mapIndices[pos]) == groupIndex {
pos++
}
if err := f.storeFilterMapRowsOfGroup(batch, mapIndices[:pos], rowIndex, rows[:pos]); err != nil {
return err
}
mapIndices, rows = mapIndices[pos:], rows[pos:]
}
return nil
}
// storeFilterMapRowsOfGroup stores a set of filter map rows at map indices
// belonging to the same base row group.
func (f *FilterMaps) storeFilterMapRowsOfGroup(batch ethdb.Batch, mapIndices []uint32, rowIndex uint32, rows []FilterRow) error {
var (
baseRows [][]uint32
groupIndex = f.mapGroupIndex(mapIndices[0])
mapRowIndex = f.mapRowIndex(groupIndex, rowIndex)
)
if uint32(len(mapIndices)) != f.baseRowGroupSize { // skip base rows read if all rows are replaced
var err error
baseRows, err = rawdb.ReadFilterMapBaseRows(f.db, mapRowIndex, f.baseRowGroupSize, f.logMapWidth)
if err != nil {
return fmt.Errorf("failed to retrieve filter map %d base rows %d for modification: %v", groupIndex, rowIndex, err)
}
} else {
baseRows = make([][]uint32, f.baseRowGroupSize)
}
for i, mapIndex := range mapIndices {
if f.mapGroupIndex(mapIndex) != groupIndex {
return fmt.Errorf("maps are not in the same base row group, index: %d, group: %d", mapIndex, groupIndex)
}
baseRow := []uint32(rows[i])
var extRow FilterRow
if uint32(len(rows[i])) > f.baseRowLength {
extRow = baseRow[f.baseRowLength:]
baseRow = baseRow[:f.baseRowLength]
}
baseRows[f.mapGroupOffset(mapIndex)] = baseRow
rawdb.WriteFilterMapExtRow(batch, f.mapRowIndex(mapIndex, rowIndex), extRow, f.logMapWidth)
}
rawdb.WriteFilterMapBaseRows(batch, mapRowIndex, baseRows, f.logMapWidth)
return nil
}
// mapRowIndex calculates the unified storage index where the given row of the
// given map is stored. Note that this indexing scheme is the same as the one
// proposed in EIP-7745 for tree-hashing the filter map structure and for the
// same data proximity reasons it is also suitable for database representation.
// See also:
// https://eips.ethereum.org/EIPS/eip-7745#hash-tree-structure
func (f *FilterMaps) mapRowIndex(mapIndex, rowIndex uint32) uint64 {
epochIndex, mapSubIndex := mapIndex>>f.logMapsPerEpoch, mapIndex&(f.mapsPerEpoch-1)
return (uint64(epochIndex)<<f.logMapHeight+uint64(rowIndex))<<f.logMapsPerEpoch + uint64(mapSubIndex)
}
// getBlockLvPointer returns the starting log value index where the log values
// generated by the given block are located.
//
// Note that this function assumes that the indexer read lock is being held when
// called from outside the indexerLoop goroutine.
func (f *FilterMaps) getBlockLvPointer(blockNumber uint64) (uint64, error) {
if lvPointer, ok := f.lvPointerCache.Get(blockNumber); ok {
return lvPointer, nil
}
lvPointer, err := rawdb.ReadBlockLvPointer(f.db, blockNumber)
if err != nil {
return 0, fmt.Errorf("failed to retrieve log value pointer of block %d: %v", blockNumber, err)
}
f.lvPointerCache.Add(blockNumber, lvPointer)
return lvPointer, nil
}
// storeBlockLvPointer stores the starting log value index where the log values
// generated by the given block are located.
func (f *FilterMaps) storeBlockLvPointer(batch ethdb.Batch, blockNumber, lvPointer uint64) {
f.lvPointerCache.Add(blockNumber, lvPointer)
rawdb.WriteBlockLvPointer(batch, blockNumber, lvPointer)
}
// deleteBlockLvPointer deletes the starting log value index where the log values
// generated by the given block are located.
func (f *FilterMaps) deleteBlockLvPointer(batch ethdb.Batch, blockNumber uint64) {
f.lvPointerCache.Remove(blockNumber)
rawdb.DeleteBlockLvPointer(batch, blockNumber)
}
// getLastBlockOfMap returns the number and id of the block that generated the
// last log value entry of the given map.
func (f *FilterMaps) getLastBlockOfMap(mapIndex uint32) (uint64, common.Hash, error) {
if lastBlock, ok := f.lastBlockCache.Get(mapIndex); ok {
return lastBlock.number, lastBlock.id, nil
}
number, id, err := rawdb.ReadFilterMapLastBlock(f.db, mapIndex)
if err != nil {
return 0, common.Hash{}, fmt.Errorf("failed to retrieve last block of map %d: %v", mapIndex, err)
}
f.lastBlockCache.Add(mapIndex, lastBlockOfMap{number: number, id: id})
return number, id, nil
}
// storeLastBlockOfMap stores the number of the block that generated the last
// log value entry of the given map.
func (f *FilterMaps) storeLastBlockOfMap(batch ethdb.Batch, mapIndex uint32, number uint64, id common.Hash) {
f.lastBlockCache.Add(mapIndex, lastBlockOfMap{number: number, id: id})
rawdb.WriteFilterMapLastBlock(batch, mapIndex, number, id)
}
// deleteLastBlockOfMap deletes the number of the block that generated the last
// log value entry of the given map.
func (f *FilterMaps) deleteLastBlockOfMap(batch ethdb.Batch, mapIndex uint32) {
f.lastBlockCache.Remove(mapIndex)
rawdb.DeleteFilterMapLastBlock(batch, mapIndex)
}
// deleteTailEpoch deletes index data from the specified epoch. The last block
// pointer for the last map of the epoch and the corresponding block log value
// pointer are retained as these are always assumed to be available for each
// epoch as boundary markers.
// The function returns true if all index data related to the epoch (except for
// the boundary markers) has been fully removed.
func (f *FilterMaps) deleteTailEpoch(epoch uint32) (bool, error) {
f.indexLock.Lock()
defer f.indexLock.Unlock()
// determine epoch boundaries
lastBlock, _, err := f.getLastBlockOfMap(f.lastEpochMap(epoch))
if err != nil {
return false, fmt.Errorf("failed to retrieve last block of deleted epoch %d: %v", epoch, err)
}
var firstBlock uint64
firstMap := f.firstEpochMap(epoch)
if epoch > 0 {
firstBlock, _, err = f.getLastBlockOfMap(firstMap - 1)
if err != nil {
return false, fmt.Errorf("failed to retrieve last block before deleted epoch %d: %v", epoch, err)
}
firstBlock++
}
// update rendered range if necessary
var (
fmr = f.indexedRange
firstEpoch = f.mapEpoch(f.indexedRange.maps.First())
afterLastEpoch = f.mapEpoch(f.indexedRange.maps.AfterLast() + f.mapsPerEpoch - 1)
)
if f.indexedRange.tailPartialEpoch != 0 && firstEpoch > 0 {
firstEpoch--
}
switch {
case epoch < firstEpoch:
// cleanup of already unindexed epoch; range not affected
case epoch == firstEpoch && epoch+1 < afterLastEpoch:
// first fully or partially rendered epoch and there is at least one
// rendered map in the next epoch; remove from indexed range
fmr.tailPartialEpoch = 0
fmr.maps.SetFirst(f.firstEpochMap(epoch + 1))
fmr.blocks.SetFirst(lastBlock + 1)
f.setRange(f.db, f.indexedView, fmr, false)
default:
// cannot be cleaned or unindexed; return with error
return false, errors.New("invalid tail epoch number")
}
// remove index data
deleteFn := func(db ethdb.KeyValueStore, hashScheme bool, stopCb func(bool) bool) error {
first := f.mapRowIndex(firstMap, 0)
count := f.mapRowIndex(firstMap+f.mapsPerEpoch, 0) - first
if err := rawdb.DeleteFilterMapRows(f.db, common.NewRange(first, count), hashScheme, stopCb); err != nil {
return err
}
for mapIndex := firstMap; mapIndex < firstMap+f.mapsPerEpoch; mapIndex++ {
f.filterMapCache.Remove(mapIndex)
}
delMapRange := common.NewRange(firstMap, f.mapsPerEpoch-1) // keep last entry
if err := rawdb.DeleteFilterMapLastBlocks(f.db, delMapRange, hashScheme, stopCb); err != nil {
return err
}
for mapIndex := firstMap; mapIndex < firstMap+f.mapsPerEpoch-1; mapIndex++ {
f.lastBlockCache.Remove(mapIndex)
}
delBlockRange := common.NewRange(firstBlock, lastBlock-firstBlock) // keep last entry
if err := rawdb.DeleteBlockLvPointers(f.db, delBlockRange, hashScheme, stopCb); err != nil {
return err
}
for blockNumber := firstBlock; blockNumber < lastBlock; blockNumber++ {
f.lvPointerCache.Remove(blockNumber)
}
return nil
}
action := fmt.Sprintf("Deleting tail epoch #%d", epoch)
stopFn := func() bool {
f.processEvents()
return f.stop || !f.targetHeadIndexed()
}
if err := f.safeDeleteWithLogs(deleteFn, action, stopFn); err == nil {
// everything removed; mark as cleaned and report success
if f.cleanedEpochsBefore == epoch {
f.cleanedEpochsBefore = epoch + 1
}
return true, nil
} else {
// more data left in epoch range; mark as dirty and report unfinished
if f.cleanedEpochsBefore > epoch {
f.cleanedEpochsBefore = epoch
}
if errors.Is(err, rawdb.ErrDeleteRangeInterrupted) {
return false, nil
}
return false, err
}
}
// exportCheckpoints exports epoch checkpoints in the format used by checkpoints.go.
//
// Note: acquiring the indexLock read lock is unnecessary here, as this function
// is always called within the indexLoop.
func (f *FilterMaps) exportCheckpoints() {
finalLvPtr, err := f.getBlockLvPointer(f.finalBlock + 1)
if err != nil {
log.Error("Error fetching log value pointer of finalized block", "block", f.finalBlock, "error", err)
return
}
epochCount := uint32(finalLvPtr >> (f.logValuesPerMap + f.logMapsPerEpoch))
if epochCount == f.lastFinalEpoch {
return
}
w, err := os.Create(f.exportFileName)
if err != nil {
log.Error("Error creating checkpoint export file", "name", f.exportFileName, "error", err)
return
}
defer w.Close()
log.Info("Exporting log index checkpoints", "epochs", epochCount, "file", f.exportFileName)
w.WriteString("[\n")
comma := ","
for epoch := uint32(0); epoch < epochCount; epoch++ {
lastBlock, lastBlockId, err := f.getLastBlockOfMap(f.lastEpochMap(epoch))
if err != nil {
log.Error("Error fetching last block of epoch", "epoch", epoch, "error", err)
return
}
lvPtr, err := f.getBlockLvPointer(lastBlock)
if err != nil {
log.Error("Error fetching log value pointer of last block", "block", lastBlock, "error", err)
return
}
if epoch == epochCount-1 {
comma = ""
}
w.WriteString(fmt.Sprintf("{\"blockNumber\": %d, \"blockId\": \"0x%064x\", \"firstIndex\": %d}%s\n", lastBlock, lastBlockId, lvPtr, comma))
}
w.WriteString("]\n")
f.lastFinalEpoch = epochCount
}

View file

@ -0,0 +1,274 @@
// Copyright 2025 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package filtermaps
import (
"errors"
"sync/atomic"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)
var (
ErrInvalidView = errors.New("index view already invalidated")
)
type IndexView struct {
// if invalid <= 0 then the view is considered invalid and will return error
// on any read operation (storage maps before firstMemoryMap might have changed
// since its creation)
refCount, invalid int32
storage *mapStorage
tailEpoch uint32 //TODO apply to read functions
blockRange common.Range[uint64] //TODO apply to read functions
headBlockHash common.Hash
headLvPointer uint64 // points after head block delimiter
firstMemoryMap uint32
firstMemoryBlock uint64
finishedMaps []*finishedMap
headMapIndex uint32
headMap *memoryMap
}
func (iv *IndexView) Release() {
iv.addRefCount(-1)
}
func (iv *IndexView) addRefCount(add int32) bool {
return atomic.AddInt32(&iv.refCount, add) <= 0
}
func (iv *IndexView) checkReleased() bool {
return atomic.LoadInt32(&iv.refCount) <= 0
}
func (iv *IndexView) invalidate() {
atomic.StoreInt32(&iv.invalid, 1)
}
func (iv *IndexView) checkInvalid() bool {
return atomic.LoadInt32(&iv.invalid) != 0
}
func (iv *IndexView) GetParams() *Params {
return iv.storage.params
}
func (iv *IndexView) BlockRange() common.Range[uint64] {
return iv.blockRange
}
// getBlockLvPointer returns the starting log value index where the log values
// generated by the given block are located.
// Note that the function returns a valid result (the next free log value index)
// for headNumber + 1.
func (iv *IndexView) GetBlockLvPointer(blockNumber uint64) (uint64, error) {
if iv.checkInvalid() {
return 0, ErrInvalidView
}
if blockNumber < iv.firstMemoryBlock {
lvPtr, err := iv.storage.getBlockLvPointer(blockNumber)
if iv.checkInvalid() {
return 0, ErrInvalidView
}
return lvPtr, err
}
if blockNumber == iv.blockRange.AfterLast() {
return iv.headLvPointer, nil
}
if blockNumber > iv.blockRange.AfterLast() {
return 0, ErrOutOfRange
}
for _, fm := range iv.finishedMaps {
if blockNumber >= fm.firstBlock() && blockNumber <= fm.lastBlock.number {
return fm.blockPtrs[blockNumber-fm.firstBlock()], nil
}
}
if blockNumber >= iv.headMap.firstBlock() && blockNumber <= iv.headMap.lastBlock.number {
return iv.headMap.blockPtrs[blockNumber-iv.headMap.firstBlock()], nil
}
panic("IndexView.GetBlockLvPointer: gap in blockLvPtrs")
}
// GetLastBlockOfMap returns the number and hash of the block that generated the
// last log value entry of the given map.
func (iv *IndexView) GetLastBlockOfMap(mapIndex uint32) (uint64, common.Hash, error) {
if iv.checkInvalid() {
return 0, common.Hash{}, ErrInvalidView
}
if mapIndex < iv.firstMemoryMap {
lastNumber, lastHash, err := iv.storage.getLastBlockOfMap(mapIndex)
if iv.checkInvalid() {
return 0, common.Hash{}, ErrInvalidView
}
return lastNumber, lastHash, err
}
if mapIndex > iv.headMapIndex {
return 0, common.Hash{}, ErrOutOfRange
}
if mapIndex == iv.headMapIndex {
return iv.headMap.lastBlock.number, iv.headMap.lastBlock.hash, nil
}
fm := iv.finishedMaps[mapIndex-iv.firstMemoryMap]
return fm.lastBlock.number, fm.lastBlock.hash, nil
}
// GetFilterMapRows returns a batch of filter maps rows from the same row index,
// each truncated to the length limit of the specified mapping layer.
// The function assumes that the map indices are in strictly ascending order.
func (iv *IndexView) GetFilterMapRows(mapIndices []uint32, rowIndex, layerIndex uint32) (rows []FilterRow, err error) {
if iv.checkInvalid() {
return nil, ErrInvalidView
}
dbIndices := len(mapIndices)
for dbIndices > 0 && mapIndices[dbIndices-1] >= iv.firstMemoryMap {
dbIndices--
}
if dbIndices > 0 {
rows, err = iv.storage.getFilterMapRows(mapIndices[:dbIndices], rowIndex, layerIndex)
if iv.checkInvalid() {
return nil, ErrInvalidView
}
if err != nil {
return nil, err
}
}
for i := dbIndices; i < len(mapIndices); i++ {
mapIndex := mapIndices[i]
var row FilterRow
if mapIndex == iv.headMapIndex {
row = iv.headMap.getRow(rowIndex, iv.storage.params.getMaxRowLength(layerIndex+1))
}
if mapIndex < iv.headMapIndex {
row = iv.finishedMaps[mapIndex-iv.firstMemoryMap].getRow(rowIndex, iv.storage.params.getMaxRowLength(layerIndex+1))
}
rows = append(rows, row)
}
return rows, nil
}
type renderState struct {
params *Params
renderRange common.Range[uint32]
lvPointer uint64
mapIndex uint32
currentMap *memoryMap
finishedMaps []*finishedMap
nextBlock uint64
partialBlock bool
partialBlockHash common.Hash
}
func (rs *renderState) checkNextHash(hash common.Hash) bool {
if rs.partialBlock && rs.partialBlockHash != hash {
return false
}
rs.partialBlock = false
return true
}
func (rs *renderState) addReceipts(receipts types.Receipts) {
if rs.partialBlock {
panic("checkNextHash has to be called before adding partially rendered block")
}
if rs.currentMap != nil {
rs.currentMap.blockPtrs = append(rs.currentMap.blockPtrs, rs.lvPointer)
rs.currentMap.lastBlock = lastBlockOfMap{number: rs.nextBlock} // hash will be set by addHeader
}
for _, receipt := range receipts {
//TODO add tx delimiter
for _, log := range receipt.Logs {
mapRemaining := rs.params.valuesPerMap - rs.lvPointer%rs.params.valuesPerMap
if mapRemaining <= uint64(len(log.Topics)) {
rs.advance(mapRemaining)
}
if rs.currentMap != nil {
rs.addValue(addressValue(log.Address))
for _, topic := range log.Topics {
rs.addValue(topicValue(topic))
}
} else {
rs.advance(uint64(len(log.Topics) + 1))
}
if rs.finished() {
return
}
}
}
}
func (rs *renderState) addHeader(header *types.Header) (uint32, []*finishedMap) {
if rs.partialBlock {
panic("checkNextHash has to be called before adding partially rendered block")
}
if rs.nextBlock != header.Number.Uint64() {
panic("wrong block number")
}
if !rs.finished() {
rs.advance(1) //TODO blockValue
}
rs.nextBlock++
lastBlock := lastBlockOfMap{number: header.Number.Uint64(), hash: header.Hash()}
if rs.currentMap != nil {
rs.currentMap.lastBlock = lastBlock
}
for _, fm := range rs.finishedMaps {
fm.lastBlock = lastBlock
}
firstMemoryMap, finishedMaps := rs.mapIndex-uint32(len(rs.finishedMaps)), rs.finishedMaps
rs.finishedMaps = nil
return firstMemoryMap, finishedMaps
}
// assumes currentMap != nil
func (rs *renderState) addValue(logValue common.Hash) {
if rs.renderRange.Includes(rs.mapIndex) {
for layerIndex := uint32(0); ; layerIndex++ {
rowIndex := rs.params.rowIndex(rs.mapIndex, layerIndex, logValue)
if rs.currentMap.rowLength(rowIndex) < rs.params.getMaxRowLength(layerIndex) {
rs.currentMap.addToRow(rowIndex, rs.params.columnIndex(rs.lvPointer, &logValue))
break
}
}
}
rs.advance(1)
}
func (rs *renderState) advance(count uint64) {
rs.lvPointer += count
if uint32(rs.lvPointer>>rs.params.logValuesPerMap) > rs.mapIndex {
if rs.currentMap != nil {
rs.finishedMaps = append(rs.finishedMaps, rs.currentMap.finished())
}
rs.mapIndex++
if rs.renderRange.Includes(rs.mapIndex) {
rs.currentMap = rs.params.newMemoryMap()
} else {
rs.currentMap = nil
}
}
}
func (rs *renderState) finished() bool {
return rs.mapIndex >= rs.renderRange.AfterLast()
}

File diff suppressed because it is too large Load diff

View file

@ -1,637 +0,0 @@
// Copyright 2024 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package filtermaps
import (
"context"
crand "crypto/rand"
"crypto/sha256"
"encoding/binary"
"math/big"
"math/rand"
"sync"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus/ethash"
"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/ethdb"
"github.com/ethereum/go-ethereum/params"
"github.com/ethereum/go-ethereum/rlp"
)
var testParams = Params{
logMapHeight: 2,
logMapWidth: 24,
logMapsPerEpoch: 4,
logValuesPerMap: 4,
baseRowGroupSize: 4,
baseRowLengthRatio: 2,
logLayerDiff: 2,
}
func TestIndexerRandomRange(t *testing.T) {
ts := newTestSetup(t)
defer ts.close()
forks := make([][]common.Hash, 10)
ts.chain.addBlocks(1000, 5, 2, 4, false)
for i := range forks {
if i != 0 {
forkBlock := rand.Intn(1000)
ts.chain.setHead(forkBlock)
ts.chain.addBlocks(1000-forkBlock, 5, 2, 4, false)
}
forks[i] = ts.chain.getCanonicalChain()
}
expspos := func(block uint64) uint64 { // expected position of block start
if block == 0 {
return 0
}
logCount := (block - 1) * 5 * 2
mapIndex := logCount / 3
spos := mapIndex*16 + (logCount%3)*5
if mapIndex == 0 || logCount%3 != 0 {
spos++
}
return spos
}
expdpos := func(block uint64) uint64 { // expected position of delimiter
if block == 0 {
return 0
}
logCount := block * 5 * 2
mapIndex := (logCount - 1) / 3
return mapIndex*16 + (logCount-mapIndex*3)*5
}
ts.setHistory(0, false)
var (
history int
noHistory bool
fork, head = len(forks) - 1, 1000
checkSnapshot bool
)
ts.fm.WaitIdle()
for i := 0; i < 200; i++ {
switch rand.Intn(3) {
case 0:
// change history settings
switch rand.Intn(10) {
case 0:
history, noHistory = 0, false
case 1:
history, noHistory = 0, true
default:
history, noHistory = rand.Intn(1000)+1, false
}
ts.testDisableSnapshots = rand.Intn(2) == 0
ts.setHistory(uint64(history), noHistory)
case 1:
// change head to random position of random fork
fork, head = rand.Intn(len(forks)), rand.Intn(1001)
ts.chain.setCanonicalChain(forks[fork][:head+1])
case 2:
checkSnapshot = false
if head < 1000 {
checkSnapshot = !noHistory && head != 0 // no snapshot generated for block 0
// add blocks after the current head
head += rand.Intn(1000-head) + 1
ts.fm.testSnapshotUsed = false
ts.chain.setCanonicalChain(forks[fork][:head+1])
}
}
ts.fm.WaitIdle()
if checkSnapshot {
if ts.fm.testSnapshotUsed == ts.fm.testDisableSnapshots {
ts.t.Fatalf("Invalid snapshot used state after head extension (used: %v, disabled: %v)", ts.fm.testSnapshotUsed, ts.fm.testDisableSnapshots)
}
checkSnapshot = false
}
if noHistory {
if ts.fm.indexedRange.initialized {
t.Fatalf("filterMapsRange initialized while indexing is disabled")
}
continue
}
if !ts.fm.indexedRange.initialized {
t.Fatalf("filterMapsRange not initialized while indexing is enabled")
}
var tailBlock uint64
if history > 0 && history <= head {
tailBlock = uint64(head + 1 - history)
}
var tailEpoch uint32
if tailBlock > 0 {
tailLvPtr := expspos(tailBlock) - 1
tailEpoch = uint32(tailLvPtr >> (testParams.logValuesPerMap + testParams.logMapsPerEpoch))
}
tailLvPtr := uint64(tailEpoch) << (testParams.logValuesPerMap + testParams.logMapsPerEpoch) // first available lv ptr
var expTailBlock uint64
if tailEpoch > 0 {
for expspos(expTailBlock) <= tailLvPtr {
expTailBlock++
}
}
if ts.fm.indexedRange.blocks.Last() != uint64(head) {
ts.t.Fatalf("Invalid index head (expected #%d, got #%d)", head, ts.fm.indexedRange.blocks.Last())
}
expHeadDelimiter := expdpos(uint64(head))
if ts.fm.indexedRange.headDelimiter != expHeadDelimiter {
ts.t.Fatalf("Invalid index head delimiter pointer (expected %d, got %d)", expHeadDelimiter, ts.fm.indexedRange.headDelimiter)
}
if ts.fm.indexedRange.blocks.First() != expTailBlock {
ts.t.Fatalf("Invalid index tail block (expected #%d, got #%d)", expTailBlock, ts.fm.indexedRange.blocks.First())
}
}
}
func TestIndexerMatcherView(t *testing.T) {
testIndexerMatcherView(t, false)
}
func TestIndexerMatcherViewWithConcurrentRead(t *testing.T) {
testIndexerMatcherView(t, true)
}
func testIndexerMatcherView(t *testing.T, concurrentRead bool) {
ts := newTestSetup(t)
defer ts.close()
forks := make([][]common.Hash, 20)
hashes := make([]common.Hash, 20)
ts.chain.addBlocks(100, 5, 2, 4, true)
ts.setHistory(0, false)
for i := range forks {
if i != 0 {
ts.chain.setHead(100 - i)
ts.chain.addBlocks(i, 5, 2, 4, true)
}
ts.fm.WaitIdle()
forks[i] = ts.chain.getCanonicalChain()
hashes[i] = ts.matcherViewHash()
}
fork := len(forks) - 1
for i := 0; i < 5000; i++ {
oldFork := fork
fork = rand.Intn(len(forks))
stopCh := make(chan chan struct{})
if concurrentRead {
go func() {
for {
ts.matcherViewHash()
select {
case ch := <-stopCh:
close(ch)
return
default:
}
}
}()
}
ts.chain.setCanonicalChain(forks[fork])
ts.fm.WaitIdle()
if concurrentRead {
ch := make(chan struct{})
stopCh <- ch
<-ch
}
hash := ts.matcherViewHash()
if hash != hashes[fork] {
t.Fatalf("Matcher view hash mismatch when switching from for %d to %d", oldFork, fork)
}
}
}
func TestLogsByIndex(t *testing.T) {
ts := newTestSetup(t)
defer func() {
ts.fm.testProcessEventsHook = nil
ts.close()
}()
ts.chain.addBlocks(1000, 10, 3, 4, true)
ts.setHistory(0, false)
ts.fm.WaitIdle()
firstLog := make([]uint64, 1001) // first valid log position per block
lastLog := make([]uint64, 1001) // last valid log position per block
for i := uint64(0); i <= ts.fm.indexedRange.headDelimiter; i++ {
log, err := ts.fm.getLogByLvIndex(i)
if err != nil {
t.Fatalf("Error getting log by index %d: %v", i, err)
}
if log != nil {
if firstLog[log.BlockNumber] == 0 {
firstLog[log.BlockNumber] = i
}
lastLog[log.BlockNumber] = i
}
}
var failed bool
ts.fm.testProcessEventsHook = func() {
if ts.fm.indexedRange.blocks.IsEmpty() {
return
}
if lvi := firstLog[ts.fm.indexedRange.blocks.First()]; lvi != 0 {
log, err := ts.fm.getLogByLvIndex(lvi)
if log == nil || err != nil {
t.Errorf("Error getting first log of indexed block range: %v", err)
failed = true
}
}
if lvi := lastLog[ts.fm.indexedRange.blocks.Last()]; lvi != 0 {
log, err := ts.fm.getLogByLvIndex(lvi)
if log == nil || err != nil {
t.Errorf("Error getting last log of indexed block range: %v", err)
failed = true
}
}
}
chain := ts.chain.getCanonicalChain()
for i := 0; i < 1000 && !failed; i++ {
head := rand.Intn(len(chain))
ts.chain.setCanonicalChain(chain[:head+1])
ts.fm.WaitIdle()
}
}
func TestIndexerCompareDb(t *testing.T) {
ts := newTestSetup(t)
defer ts.close()
ts.chain.addBlocks(500, 10, 3, 4, true)
ts.setHistory(0, false)
ts.fm.WaitIdle()
// revert points are stored after block 500
ts.chain.addBlocks(500, 10, 3, 4, true)
ts.fm.WaitIdle()
chain1 := ts.chain.getCanonicalChain()
ts.storeDbHash("chain 1 [0, 1000]")
ts.chain.setHead(600)
ts.fm.WaitIdle()
ts.storeDbHash("chain 1/2 [0, 600]")
ts.chain.addBlocks(600, 10, 3, 4, true)
ts.fm.WaitIdle()
chain2 := ts.chain.getCanonicalChain()
ts.storeDbHash("chain 2 [0, 1200]")
ts.chain.setHead(600)
ts.fm.WaitIdle()
ts.checkDbHash("chain 1/2 [0, 600]")
ts.setHistory(800, false)
ts.chain.setCanonicalChain(chain1)
ts.fm.WaitIdle()
ts.storeDbHash("chain 1 [201, 1000]")
ts.setHistory(0, false)
ts.fm.WaitIdle()
ts.checkDbHash("chain 1 [0, 1000]")
ts.setHistory(800, false)
ts.chain.setCanonicalChain(chain2)
ts.fm.WaitIdle()
ts.storeDbHash("chain 2 [401, 1200]")
ts.setHistory(0, true)
ts.fm.WaitIdle()
ts.storeDbHash("no index")
ts.chain.setCanonicalChain(chain2[:501])
ts.setHistory(0, false)
ts.fm.WaitIdle()
ts.chain.setCanonicalChain(chain2)
ts.fm.WaitIdle()
ts.checkDbHash("chain 2 [0, 1200]")
ts.chain.setCanonicalChain(chain1)
ts.fm.WaitIdle()
ts.setHistory(800, false)
ts.fm.WaitIdle()
ts.checkDbHash("chain 1 [201, 1000]")
ts.chain.setCanonicalChain(chain2)
ts.fm.WaitIdle()
ts.checkDbHash("chain 2 [401, 1200]")
ts.setHistory(0, true)
ts.fm.WaitIdle()
ts.checkDbHash("no index")
}
type testSetup struct {
t *testing.T
fm *FilterMaps
db ethdb.Database
chain *testChain
params Params
dbHashes map[string]common.Hash
testDisableSnapshots bool
}
func newTestSetup(t *testing.T) *testSetup {
params := testParams
params.deriveFields()
ts := &testSetup{
t: t,
db: rawdb.NewMemoryDatabase(),
params: params,
dbHashes: make(map[string]common.Hash),
}
ts.chain = ts.newTestChain()
return ts
}
func (ts *testSetup) setHistory(history uint64, noHistory bool) {
if ts.fm != nil {
ts.fm.Stop()
}
head := ts.chain.CurrentBlock()
view := NewChainView(ts.chain, head.Number.Uint64(), head.Hash())
config := Config{
History: history,
Disabled: noHistory,
}
ts.fm, _ = NewFilterMaps(ts.db, view, 0, 0, ts.params, config)
ts.fm.testDisableSnapshots = ts.testDisableSnapshots
ts.fm.Start()
}
func (ts *testSetup) storeDbHash(id string) {
dbHash := ts.fmDbHash()
for otherId, otherHash := range ts.dbHashes {
if otherHash == dbHash {
ts.t.Fatalf("Unexpected equal database hashes `%s` and `%s`", id, otherId)
}
}
ts.dbHashes[id] = dbHash
}
func (ts *testSetup) checkDbHash(id string) {
if ts.fmDbHash() != ts.dbHashes[id] {
ts.t.Fatalf("Database `%s` hash mismatch", id)
}
}
func (ts *testSetup) fmDbHash() common.Hash {
hasher := sha256.New()
it := ts.db.NewIterator(nil, nil)
for it.Next() {
hasher.Write(it.Key())
hasher.Write(it.Value())
}
it.Release()
var result common.Hash
hasher.Sum(result[:0])
return result
}
func (ts *testSetup) matcherViewHash() common.Hash {
mb := ts.fm.NewMatcherBackend()
defer mb.Close()
ctx := context.Background()
params := mb.GetParams()
hasher := sha256.New()
var headPtr uint64
for b := uint64(0); ; b++ {
lvptr, err := mb.GetBlockLvPointer(ctx, b)
if err != nil || (b > 0 && lvptr == headPtr) {
break
}
var enc [8]byte
binary.LittleEndian.PutUint64(enc[:], lvptr)
hasher.Write(enc[:])
headPtr = lvptr
}
headMap := uint32(headPtr >> params.logValuesPerMap)
var enc [12]byte
for r := uint32(0); r < params.mapHeight; r++ {
binary.LittleEndian.PutUint32(enc[:4], r)
for m := uint32(0); m <= headMap; m++ {
binary.LittleEndian.PutUint32(enc[4:8], m)
rows, _ := mb.GetFilterMapRows(ctx, []uint32{m}, r, false)
for _, row := range rows {
for _, v := range row {
binary.LittleEndian.PutUint32(enc[8:], v)
hasher.Write(enc[:])
}
}
}
}
var hash common.Hash
hasher.Sum(hash[:0])
for i := 0; i < 50; i++ {
hasher.Reset()
hasher.Write(hash[:])
lvptr := binary.LittleEndian.Uint64(hash[:8]) % headPtr
if log, _ := mb.GetLogByLvIndex(ctx, lvptr); log != nil {
enc, err := rlp.EncodeToBytes(log)
if err != nil {
panic(err)
}
hasher.Write(enc)
}
hasher.Sum(hash[:0])
}
return hash
}
func (ts *testSetup) close() {
if ts.fm != nil {
ts.fm.Stop()
}
ts.db.Close()
ts.chain.db.Close()
}
type testChain struct {
ts *testSetup
db ethdb.Database
lock sync.RWMutex
canonical []common.Hash
blocks map[common.Hash]*types.Block
receipts map[common.Hash]types.Receipts
}
func (ts *testSetup) newTestChain() *testChain {
return &testChain{
ts: ts,
blocks: make(map[common.Hash]*types.Block),
receipts: make(map[common.Hash]types.Receipts),
}
}
func (tc *testChain) CurrentBlock() *types.Header {
tc.lock.RLock()
defer tc.lock.RUnlock()
if len(tc.canonical) == 0 {
return nil
}
return tc.blocks[tc.canonical[len(tc.canonical)-1]].Header()
}
func (tc *testChain) GetHeader(hash common.Hash, number uint64) *types.Header {
tc.lock.RLock()
defer tc.lock.RUnlock()
if block := tc.blocks[hash]; block != nil {
return block.Header()
}
return nil
}
func (tc *testChain) GetCanonicalHash(number uint64) common.Hash {
tc.lock.RLock()
defer tc.lock.RUnlock()
if uint64(len(tc.canonical)) <= number {
return common.Hash{}
}
return tc.canonical[number]
}
func (tc *testChain) GetReceiptsByHash(hash common.Hash) types.Receipts {
tc.lock.RLock()
defer tc.lock.RUnlock()
return tc.receipts[hash]
}
func (tc *testChain) GetRawReceipts(hash common.Hash, number uint64) types.Receipts {
tc.lock.RLock()
defer tc.lock.RUnlock()
return tc.receipts[hash]
}
func (tc *testChain) addBlocks(count, maxTxPerBlock, maxLogsPerReceipt, maxTopicsPerLog int, random bool) {
tc.lock.Lock()
blockGen := func(i int, gen *core.BlockGen) {
var txCount int
if random {
txCount = rand.Intn(maxTxPerBlock + 1)
} else {
txCount = maxTxPerBlock
}
for k := txCount; k > 0; k-- {
receipt := types.NewReceipt(nil, false, 0)
var logCount int
if random {
logCount = rand.Intn(maxLogsPerReceipt + 1)
} else {
logCount = maxLogsPerReceipt
}
receipt.Logs = make([]*types.Log, logCount)
for i := range receipt.Logs {
log := &types.Log{}
receipt.Logs[i] = log
crand.Read(log.Address[:])
var topicCount int
if random {
topicCount = rand.Intn(maxTopicsPerLog + 1)
} else {
topicCount = maxTopicsPerLog
}
log.Topics = make([]common.Hash, topicCount)
for j := range log.Topics {
crand.Read(log.Topics[j][:])
}
}
gen.AddUncheckedReceipt(receipt)
gen.AddUncheckedTx(types.NewTransaction(999, common.HexToAddress("0x999"), big.NewInt(999), 999, gen.BaseFee(), nil))
}
}
var (
blocks []*types.Block
receipts []types.Receipts
engine = ethash.NewFaker()
)
if len(tc.canonical) == 0 {
gspec := &core.Genesis{
Alloc: types.GenesisAlloc{},
BaseFee: big.NewInt(params.InitialBaseFee),
Config: params.TestChainConfig,
}
tc.db, blocks, receipts = core.GenerateChainWithGenesis(gspec, engine, count, blockGen)
gblock := gspec.ToBlock()
ghash := gblock.Hash()
tc.canonical = []common.Hash{ghash}
tc.blocks[ghash] = gblock
tc.receipts[ghash] = types.Receipts{}
} else {
blocks, receipts = core.GenerateChain(params.TestChainConfig, tc.blocks[tc.canonical[len(tc.canonical)-1]], engine, tc.db, count, blockGen)
}
for i, block := range blocks {
num, hash := int(block.NumberU64()), block.Hash()
if len(tc.canonical) != num {
panic("canonical chain length mismatch")
}
tc.canonical = append(tc.canonical, hash)
tc.blocks[hash] = block
if receipts[i] != nil {
tc.receipts[hash] = receipts[i]
} else {
tc.receipts[hash] = types.Receipts{}
}
}
tc.lock.Unlock()
tc.setTargetHead()
}
func (tc *testChain) setHead(headNum int) {
tc.lock.Lock()
tc.canonical = tc.canonical[:headNum+1]
tc.lock.Unlock()
tc.setTargetHead()
}
func (tc *testChain) setTargetHead() {
head := tc.CurrentBlock()
if tc.ts.fm != nil {
if !tc.ts.fm.disabled {
//tc.ts.fm.targetViewCh <- NewChainView(tc, head.Number.Uint64(), head.Hash())
tc.ts.fm.SetTarget(NewChainView(tc, head.Number.Uint64(), head.Hash()), 0, 0)
}
}
}
func (tc *testChain) getCanonicalChain() []common.Hash {
tc.lock.RLock()
defer tc.lock.RUnlock()
cc := make([]common.Hash, len(tc.canonical))
copy(cc, tc.canonical)
return cc
}
// restore an earlier state of the chain
func (tc *testChain) setCanonicalChain(cc []common.Hash) {
tc.lock.Lock()
tc.canonical = make([]common.Hash, len(cc))
copy(tc.canonical, cc)
tc.lock.Unlock()
tc.setTargetHead()
}

View file

@ -0,0 +1,575 @@
// Copyright 2025 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package filtermaps
import (
"errors"
"fmt"
"math"
"sort"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/lru"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
)
const (
databaseVersion = 4 // reindexed if database version does not match
cachedLastBlocks = 1000 // last block of map pointers
cachedLvPointers = 1000 // first log value pointer of block pointers
maxWritesPerBatch = 1000000
)
// mapDatabase implements the database storage layer for filter maps and the
// belonging pointers.
type mapDatabase struct {
params *Params
db ethdb.KeyValueStore
hashScheme bool
lastBlockCache *lru.Cache[uint32, lastBlockOfMap]
lvPointerCache *lru.Cache[uint64, uint64]
testReadRows bool
}
// lastBlockOfMap is used for storing and caching the (number, hash) pairs
// belonging to the last block of each map.
type lastBlockOfMap struct {
number uint64
hash common.Hash
}
// newMapDatabase created a new mapDatabase layer.
func newMapDatabase(params *Params, db ethdb.KeyValueStore, hashScheme bool) *mapDatabase {
return &mapDatabase{
params: params,
db: db,
hashScheme: hashScheme,
lastBlockCache: lru.NewCache[uint32, lastBlockOfMap](cachedLastBlocks),
lvPointerCache: lru.NewCache[uint64, uint64](cachedLvPointers),
}
}
// loadMapRange loads the valid and dirty map ranges and known epoch boundaries
// from the database.
func (m *mapDatabase) loadMapRange() (valid, dirty rangeSet[uint32], knownEpochs uint32, found bool) {
fmr, ok, err := rawdb.ReadFilterMapsRange(m.db)
if !ok || err != nil || fmr.Version != databaseVersion {
return
}
return decodeRangeSet32(fmr.ValidMaps), decodeRangeSet32(fmr.DirtyMaps), fmr.KnownEpochs, true
}
// storeMapRange stores the valid and dirty map ranges and known epoch boundaries
// into the database.
func (m *mapDatabase) storeMapRange(valid, dirty rangeSet[uint32], knownEpochs uint32) {
rawdb.WriteFilterMapsRange(m.db, rawdb.FilterMapsRange{
Version: databaseVersion,
ValidMaps: encodeRangeSet32(valid),
DirtyMaps: encodeRangeSet32(dirty),
KnownEpochs: knownEpochs,
})
}
// deleteMapRange removes the map range entry from the database, signaling a
// revert to the non-initialized state.
func (m *mapDatabase) deleteMapRange() {
rawdb.DeleteFilterMapsRange(m.db)
}
func decodeRangeSet32(enc []uint32) rangeSet[uint32] {
rs := make(rangeSet[uint32], len(enc)/2)
for i := range rs {
rs[i] = common.NewRange(enc[i*2], enc[i*2+1])
}
return rs
}
func encodeRangeSet32(rs rangeSet[uint32]) []uint32 {
enc := make([]uint32, len(rs)*2)
for i, r := range rs {
enc[i*2] = r.First()
enc[i*2+1] = r.Count()
}
return enc
}
// getBlockLvPointer returns the starting log value index where the log values
// generated by the given block are located.
func (m *mapDatabase) getBlockLvPointer(blockNumber uint64) (uint64, error) {
if lvPointer, ok := m.lvPointerCache.Get(blockNumber); ok {
return lvPointer, nil
}
lvPointer, err := rawdb.ReadBlockLvPointer(m.db, blockNumber)
if err != nil {
return 0, fmt.Errorf("failed to retrieve log value pointer of block %d: %v", blockNumber, err)
}
m.lvPointerCache.Add(blockNumber, lvPointer)
return lvPointer, nil
}
// storeBlockLvPointer stores the starting log value index where the log values
// generated by the given block are located.
func (m *mapDatabase) storeBlockLvPointer(db ethdb.KeyValueWriter, blockNumber, lvPointer uint64) {
m.lvPointerCache.Add(blockNumber, lvPointer)
rawdb.WriteBlockLvPointer(db, blockNumber, lvPointer)
}
// getLastBlockOfMap returns the number and hash of the block that generated the
// last log value entry of the given map.
func (m *mapDatabase) getLastBlockOfMap(mapIndex uint32) (uint64, common.Hash, error) {
if lastBlock, ok := m.lastBlockCache.Get(mapIndex); ok {
return lastBlock.number, lastBlock.hash, nil
}
number, hash, err := rawdb.ReadFilterMapLastBlock(m.db, mapIndex)
if err != nil {
return 0, common.Hash{}, fmt.Errorf("failed to retrieve last block of map %d: %v", mapIndex, err)
}
m.lastBlockCache.Add(mapIndex, lastBlockOfMap{number: number, hash: hash})
return number, hash, nil
}
// storeLastBlockOfMap stores the number of the block that generated the last
// log value entry of the given map.
func (m *mapDatabase) storeLastBlockOfMap(db ethdb.KeyValueWriter, mapIndex uint32, number uint64, hash common.Hash) {
m.lastBlockCache.Add(mapIndex, lastBlockOfMap{number: number, hash: hash})
rawdb.WriteFilterMapLastBlock(db, mapIndex, number, hash)
}
// deleteEpochRows deletes the map rows from an entire epoch.
func (m *mapDatabase) deleteEpochRows(epoch uint32, stopCallback func() bool) (bool, error) {
deleteFn := func(db ethdb.KeyValueStore, hashScheme bool, stopCb func(bool) bool) error {
first := m.params.mapRowIndex(m.params.firstEpochMap(epoch), 0)
afterLast := m.params.mapRowIndex(m.params.firstEpochMap(epoch+1), 0)
return rawdb.DeleteFilterMapRows(db, common.NewRange(first, afterLast-first), hashScheme, stopCb)
}
action := fmt.Sprintf("Deleting epoch #%d", epoch)
switch err := m.safeDeleteWithLogs(deleteFn, action, stopCallback); err {
case nil:
return true, nil
case rawdb.ErrDeleteRangeInterrupted:
return false, nil
default:
return false, err
}
}
// reset deletes the entire log index database. Note that deleteMapRange should
// be used first in order to prevent trying to use a partially deleted database
// later.
func (m *mapDatabase) reset(stopCallback func() bool) (bool, error) {
rawdb.DeleteFilterMapsRange(m.db)
if err := m.safeDeleteWithLogs(rawdb.DeleteFilterMapsDb, "Resetting log index database", stopCallback); err != rawdb.ErrDeleteRangeInterrupted {
return err == nil, err
}
return false, nil
}
// deletePointers removes the last block of map and block log value pointers
// belonging to the specified map range.
// Note that in order to determine the relevant block range, the last block
// pointer of the map before the specified map range has to be present unless
// the range starts at map 0. The last block pointer of the last map in the range
// also has to be present unless the range ends at MaxUint32-1.
func (m *mapDatabase) deletePointers(deleteMaps common.Range[uint32], stopCallback func() bool) error {
var firstBlock, afterLastBlock uint64
if deleteMaps.First() > 0 {
lb, _, err := m.getLastBlockOfMap(deleteMaps.First() - 1)
if err != nil {
return err
}
firstBlock = lb + 1
}
if deleteMaps.AfterLast() < math.MaxUint32 {
lb, _, err := m.getLastBlockOfMap(deleteMaps.Last())
if err != nil {
return err
}
afterLastBlock = lb
} else {
afterLastBlock = math.MaxUint64
}
if afterLastBlock > firstBlock {
m.lvPointerCache.Purge()
blockRange := common.NewRange[uint64](firstBlock, afterLastBlock-firstBlock) // keep last pointer
if err := rawdb.DeleteBlockLvPointers(m.db, blockRange, m.hashScheme, func(bool) bool { return stopCallback() }); err != nil {
return err
}
}
if deleteMaps.Count() > 1 {
m.lastBlockCache.Purge()
mapRange := common.NewRange[uint32](deleteMaps.First(), deleteMaps.Count()-1) // keep last pointer
if err := rawdb.DeleteFilterMapLastBlocks(m.db, mapRange, m.hashScheme, func(bool) bool { return stopCallback() }); err != nil {
return err
}
}
return nil
}
// writePointers writes the pointers belonging to the give maps to the database.
func (m *mapDatabase) writePointers(writeRange common.Range[uint32], maps []*finishedMap, stopCallback func() bool) (bool, error) {
batch := m.db.NewBatch()
var (
batchCnt uint32
writeErr error
)
checkStopOrCommit := func() bool {
batchCnt++
if batchCnt >= maxWritesPerBatch {
if writeErr = batch.Write(); writeErr != nil {
return true
}
if stopCallback() {
return true
}
batch = m.db.NewBatch()
batchCnt = 0
}
return false
}
for mapIndex := range writeRange.Iter() {
fm := maps[mapIndex-writeRange.First()]
m.storeLastBlockOfMap(batch, mapIndex, fm.lastBlock.number, fm.lastBlock.hash)
if checkStopOrCommit() {
return false, writeErr
}
for i, lvPtr := range fm.blockPtrs {
m.storeBlockLvPointer(batch, fm.firstBlock()+uint64(i), lvPtr)
if checkStopOrCommit() {
return false, writeErr
}
}
}
if err := batch.Write(); err != nil {
return false, err
}
return true, nil
}
// getFilterMapRows returns a batch of filter maps rows from the same row index,
// each truncated to the length limit of the specified mapping layer.
// The function assumes that the map indices are in strictly ascending order.
// Note that the function modifies the mapIndices slice.
func (m *mapDatabase) getFilterMapRows(mapIndices []uint32, rowIndex, layerIndex uint32) ([]FilterRow, error) {
rows := make([]FilterRow, len(mapIndices))
readRows := make([]*FilterRow, len(mapIndices))
for i := range rows {
readRows[i] = &rows[i]
}
for dbLayer := range min(layerIndex+1, uint32(len(m.params.rowGroupSize))) {
if err := m.getFilterMapRowsOfDbLayer(mapIndices, rowIndex, dbLayer, readRows); err != nil {
return nil, err
}
j := 0
maxRowLength := m.params.maxRowLength[dbLayer]
for i, row := range readRows {
if uint32(len(*row)) == maxRowLength {
if j != i {
mapIndices[j] = mapIndices[i]
readRows[j] = readRows[i]
}
j++
}
}
if j == 0 {
break
}
mapIndices = mapIndices[:j]
readRows = readRows[:j]
}
return rows, nil
}
// getFilterMapRowsOfDbLayer loads the part of the given filter rows belonging to
// the specified database layer and appends it to a slice of already existing
// rows. If dbLayer > 0 then it is assumed that the data from all previous database
// layers have been appended to the filter rows.
func (m *mapDatabase) getFilterMapRowsOfDbLayer(mapIndices []uint32, rowIndex, dbLayer uint32, appendTo []*FilterRow) error {
var ptr int
for ptr < len(mapIndices) {
var (
groupIndex = m.params.mapGroupIndex(mapIndices[ptr], dbLayer)
groupLength = 1
)
for ptr+groupLength < len(mapIndices) && m.params.mapGroupIndex(mapIndices[ptr+groupLength], dbLayer) == groupIndex {
groupLength++
}
if err := m.getFilterMapRowsOfDbLayerGroup(mapIndices[ptr:ptr+groupLength], rowIndex, dbLayer, appendTo[ptr:ptr+groupLength]); err != nil {
return err
}
ptr += groupLength
}
return nil
}
// getFilterMapRowsOfDbLayerGroup loads the part of the given filter rows belonging to
// the specified database layer and appends it to a slice of already existing
// rows. The map indices should be located in the same row group according to the
// group size at the given database layer.
func (m *mapDatabase) getFilterMapRowsOfDbLayerGroup(mapIndices []uint32, rowIndex, dbLayer uint32, appendTo []*FilterRow) error {
var (
groupSize = m.params.rowGroupSize[dbLayer]
groupIndex = m.params.mapGroupIndex(mapIndices[0], dbLayer)
mapRowIndex = m.params.mapRowIndex(groupIndex, rowIndex)
)
if groupSize == 1 {
row, err := rawdb.ReadFilterMapSingleRow(m.db, mapRowIndex, dbLayer, m.params.logMapWidth)
if err != nil {
return fmt.Errorf("failed to retrieve row %d of row %d layer %d: %v", groupIndex, rowIndex, dbLayer, err)
}
*appendTo[0] = append(*appendTo[0], FilterRow(row)...)
return nil
}
rows, err := rawdb.ReadFilterMapRowGroup(m.db, mapRowIndex, dbLayer, groupSize, m.params.logMapWidth)
if err != nil {
return fmt.Errorf("failed to retrieve row group %d of row %d layer %d: %v", groupIndex, rowIndex, dbLayer, err)
}
for i, mapIndex := range mapIndices {
if m.params.mapGroupIndex(mapIndex, dbLayer) != groupIndex {
return fmt.Errorf("maps are not in the same base row group, index: %d, group: %d", mapIndex, groupIndex)
}
*appendTo[i] = append(*appendTo[i], FilterRow(rows[m.params.mapGroupOffset(mapIndex, dbLayer)])...)
}
return nil
}
// getFilterMap returns the filter map at the specified index.
func (m *mapDatabase) getFilterMap(mapIndex uint32) (*finishedMap, error) {
fm := new(finishedMap)
lastBlock, lbHash, err := m.getLastBlockOfMap(mapIndex)
if err != nil {
return nil, fmt.Errorf("failed to retrieve last block of map %d: %v", mapIndex, err)
}
fm.lastBlock = lastBlockOfMap{number: lastBlock, hash: lbHash}
var firstBlock uint64
if mapIndex > 0 {
plb, _, err := m.getLastBlockOfMap(mapIndex - 1)
if err != nil {
return nil, fmt.Errorf("failed to retrieve last block of map %d: %v", mapIndex-1, err)
}
firstBlock = plb + 1
}
fm.blockPtrs = make([]uint64, lastBlock+1-firstBlock)
for i := range fm.blockPtrs {
blockPtr, err := m.getBlockLvPointer(firstBlock + uint64(i))
if err != nil {
return nil, fmt.Errorf("failed to retrieve block pointer %d: %v", firstBlock+uint64(i), err)
}
fm.blockPtrs[i] = blockPtr
}
for rowIndex := range m.params.mapHeight {
var mi [1]uint32
mi[0] = mapIndex
row, err := m.getFilterMapRows(mi[:], rowIndex, uint32(len(m.params.rowGroupSize)-1))
if err != nil {
return nil, fmt.Errorf("failed to retrieve row %d of map %d: %v", rowIndex, mapIndex, err)
}
fm.rowData = append(fm.rowData, row[0]...)
fm.rowPtrs = append(fm.rowPtrs, uint16(len(fm.rowData)))
}
return fm, nil
}
// writeMapRows updates a section of the filter map range, writing a continuous
// range of new map data and/or removing dirty data. writeRange and deleteRange
// can and should overlap if dirty data is being overwritten with new map data.
// writeRange alone should only be specified where the database is known to be
// empty.
// keepEmptyRange is optional, it signals that a certain range is known to be
// empty in the database and should also stay so. It should not overlap with the
// other two ranges. If these three ranges together cover an entire row group
// then the entire group can be updated without reading its previous value,
// thereby speeding up the write process significantly.
// Note that writeMapRows only updates the filter map rows and does not touch
// the belonging pointers.
func (m *mapDatabase) writeMapRows(writeRange, deleteRange, keepEmptyRange common.Range[uint32], maps []*finishedMap, stopCallback func() bool) (bool, error) {
if !writeRange.Intersection(keepEmptyRange).IsEmpty() || !deleteRange.Intersection(keepEmptyRange).IsEmpty() {
panic("invalid writeMapRows map ranges")
}
writePattern := m.makeWritePattern(writeRange, deleteRange, keepEmptyRange)
batch := m.db.NewBatch()
rowsPerBatch := uint32(max(maxWritesPerBatch/len(writePattern), 1))
var (
batchCnt uint32
writeErr error
)
checkStopOrCommit := func() bool {
batchCnt++
if batchCnt >= rowsPerBatch {
if writeErr = batch.Write(); writeErr != nil {
return true
}
if stopCallback() {
return true
}
batch = m.db.NewBatch()
batchCnt = 0
}
return false
}
for rowIndex := range m.params.mapHeight {
if err := m.writeRowUpdates(batch, writePattern, writeRange, maps, rowIndex); err != nil {
return false, err
}
if checkStopOrCommit() {
return false, writeErr
}
}
if err := batch.Write(); err != nil {
return false, err
}
return true, nil
}
type writePatterItem struct {
mapIndex, dbLayer uint32
writeRange, deleteRange, keepEmptyRange common.Range[uint32]
}
// makeWritePattern pre-generates the order of operations that need to be repeated
// for every row.
func (m *mapDatabase) makeWritePattern(writeRange, deleteRange, keepEmptyRange common.Range[uint32]) (writePattern []writePatterItem) {
for dbLayer, groupSize := range m.params.rowGroupSize {
updateRange := deleteRange.Union(writeRange)
firstGroup, lastGroup := updateRange.First()/groupSize, updateRange.Last()/groupSize
for i := firstGroup; i <= lastGroup; i++ {
groupRange := common.NewRange[uint32](i*groupSize, groupSize)
writePattern = append(writePattern, writePatterItem{
mapIndex: i * groupSize,
dbLayer: uint32(dbLayer),
writeRange: writeRange.Intersection(groupRange),
deleteRange: deleteRange.Intersection(groupRange),
keepEmptyRange: keepEmptyRange.Intersection(groupRange),
})
}
}
sort.Slice(writePattern, func(i, j int) bool {
return writePattern[i].mapIndex < writePattern[j].mapIndex ||
(writePattern[i].mapIndex == writePattern[j].mapIndex && writePattern[i].dbLayer < writePattern[j].dbLayer)
})
return
}
// writeRowUpdates performs the filter row update operation on a single row.
func (m *mapDatabase) writeRowUpdates(batch ethdb.Batch, writePattern []writePatterItem, writeRange common.Range[uint32], maps []*finishedMap, rowIndex uint32) error {
for _, w := range writePattern {
if groupSize := m.params.rowGroupSize[w.dbLayer]; groupSize == 1 {
var row FilterRow
if writeRange.Includes(w.mapIndex) {
row = maps[w.mapIndex-writeRange.First()].getRow(rowIndex, m.params.maxRowLength[w.dbLayer])
}
var from uint32
if w.dbLayer > 0 {
from = m.params.maxRowLength[w.dbLayer-1]
}
if uint32(len(row)) > from {
row = row[from:]
} else {
row = nil
}
if len(row) > 0 || !w.deleteRange.IsEmpty() {
rawdb.WriteFilterMapSingleRow(batch, m.params.mapRowIndex(w.mapIndex, rowIndex), w.dbLayer, row, m.params.logMapWidth)
}
} else {
rows := make([][]uint32, groupSize)
writeGroup := !w.deleteRange.IsEmpty()
if w.writeRange.Count()+w.deleteRange.Count()-w.writeRange.Intersection(w.deleteRange).Count()+w.keepEmptyRange.Count() < groupSize {
oldRows, err := rawdb.ReadFilterMapRowGroup(m.db, m.params.mapRowIndex(w.mapIndex, rowIndex), w.dbLayer, groupSize, m.params.logMapWidth)
m.testReadRows = true
if err != nil {
return err
}
for i := range groupSize {
if mapIndex := w.mapIndex + i; !w.writeRange.Includes(mapIndex) && !w.deleteRange.Includes(mapIndex) && !w.keepEmptyRange.Includes(mapIndex) {
rows[i] = oldRows[i]
}
}
}
var from uint32
if w.dbLayer > 0 {
from = m.params.maxRowLength[w.dbLayer-1]
}
to := m.params.maxRowLength[w.dbLayer]
for i := range groupSize {
if writeRange.Includes(w.mapIndex + i) {
if row := maps[w.mapIndex+i-writeRange.First()].getRow(rowIndex, to); uint32(len(row)) > from {
rows[i] = row[from:]
writeGroup = true
}
}
}
if writeGroup {
rawdb.WriteFilterMapRowGroup(batch, m.params.mapRowIndex(w.mapIndex, rowIndex), w.dbLayer, rows, m.params.logMapWidth)
}
}
}
return nil
}
// safeDeleteWithLogs is a wrapper for a function that performs a safe range
// delete operation using rawdb.SafeDeleteRange. It emits log messages if the
// process takes long enough to call the stop callback.
func (m *mapDatabase) safeDeleteWithLogs(deleteFn func(db ethdb.KeyValueStore, hashScheme bool, stopCb func(bool) bool) error, action string, stopCb func() bool) error {
var (
start = time.Now()
logPrinted bool
lastLogPrinted = start
)
switch err := deleteFn(m.db, m.hashScheme, func(deleted bool) bool {
if deleted && !logPrinted || time.Since(lastLogPrinted) > time.Second*10 {
log.Info(action+" in progress...", "elapsed", common.PrettyDuration(time.Since(start)))
logPrinted, lastLogPrinted = true, time.Now()
}
return stopCb()
}); {
case err == nil:
if logPrinted {
log.Info(action+" finished", "elapsed", common.PrettyDuration(time.Since(start)))
}
return nil
case errors.Is(err, rawdb.ErrDeleteRangeInterrupted):
log.Warn(action+" interrupted", "elapsed", common.PrettyDuration(time.Since(start)))
return err
default:
log.Error(action+" failed", "error", err)
return err
}
}
// removeBloomBits removes old bloom bits data from the database.
func (m *mapDatabase) removeBloomBits(stopCb func() bool) {
m.safeDeleteWithLogs(rawdb.DeleteBloomBitsDb, "Removing old bloom bits database", stopCb)
}
// storeEpochCheckpoint writes a single checkpoint (known epoch boundary) to the
// database.
func (m *mapDatabase) storeEpochCheckpoint(epoch uint32, cp epochCheckpoint) {
m.storeLastBlockOfMap(m.db, m.params.lastEpochMap(epoch), cp.BlockNumber, cp.BlockHash)
m.storeBlockLvPointer(m.db, cp.BlockNumber, cp.FirstIndex)
}
// storeCheckpointList writes a list of checkpoints to the database.
func (m *mapDatabase) storeCheckpointList(firstEpoch uint32, cpList checkpointList) {
for i, cp := range cpList {
m.storeEpochCheckpoint(firstEpoch+uint32(i), cp)
}
}

View file

@ -0,0 +1,79 @@
// Copyright 2025 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package filtermaps
import (
"math"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethdb/memorydb"
)
var testParams = Params{
logMapHeight: 8,
logMapWidth: 24,
logMapsPerEpoch: 6,
logValuesPerMap: 8,
logMappingFrequency: []uint{6, 4, 2, 0},
maxRowLength: []uint32{4, 16, 64, 256},
rowGroupSize: []uint32{16, 4, 1, 1},
}
func testStop() bool { return false }
func TestMapDatabase(t *testing.T) {
params := testParams
params.sanitize()
db := memorydb.New()
mapDb := newMapDatabase(&params, db, false)
reader := mapReader{
getFilterMapRows: mapDb.getFilterMapRows,
getFilterMap: mapDb.getFilterMap,
getBlockLvPointer: mapDb.getBlockLvPointer,
getLastBlockOfMap: mapDb.getLastBlockOfMap,
}
// initialize database with checkpoints
maps := generateTestMaps(&params, nil, 0x200)
cpList := generateTestCheckpoints(&params, maps)
mapDb.storeCheckpointList(0, cpList)
// add new maps to the head
maps = generateTestMaps(&params, maps, 0x50)
mapDb.writeMapRows(common.NewRange[uint32](0x200, 0x50), common.Range[uint32]{}, common.NewRange[uint32](0x250, math.MaxUint32-0x250), maps[0x200:], testStop)
mapDb.writePointers(common.NewRange[uint32](0x200, 0x50), maps[0x200:], testStop)
testMapReader(t, "mapDatabase test #1", &params, reader, cpList, maps[0x200:])
// backfill previous epoch with a single write
mapDb.writeMapRows(common.NewRange[uint32](0x1c0, 0x40), common.Range[uint32]{}, common.Range[uint32]{}, maps[0x1c0:0x200], testStop)
mapDb.writePointers(common.NewRange[uint32](0x1c0, 0x40), maps[0x1c0:0x200], testStop)
testMapReader(t, "mapDatabase test #2", &params, reader, cpList[:7], maps[0x1c0:])
// backfill previous epoch in two steps
mapDb.writeMapRows(common.NewRange[uint32](0x180, 0x10), common.Range[uint32]{}, common.NewRange[uint32](0x190, 0x30), maps[0x180:0x190], testStop)
mapDb.writePointers(common.NewRange[uint32](0x180, 0x10), maps[0x180:0x190], testStop)
mapDb.writeMapRows(common.NewRange[uint32](0x190, 0x30), common.Range[uint32]{}, common.Range[uint32]{}, maps[0x190:0x1c0], testStop)
mapDb.writePointers(common.NewRange[uint32](0x190, 0x30), maps[0x190:0x1c0], testStop)
testMapReader(t, "mapDatabase test #3", &params, reader, cpList[:6], maps[0x180:])
// add new maps while reorging some existing ones
maps = generateTestMaps(&params, maps[:0x230], 0x30)
mapDb.writeMapRows(common.NewRange[uint32](0x230, 0x30), common.NewRange[uint32](0x230, 0x30), common.NewRange[uint32](0x260, math.MaxUint32-0x260), maps[0x230:], testStop)
mapDb.deletePointers(common.NewRange[uint32](0x230, math.MaxUint32-0x230), testStop)
mapDb.writePointers(common.NewRange[uint32](0x230, 0x30), maps[0x230:], testStop)
testMapReader(t, "mapDatabase test #4", &params, reader, cpList[:6], maps[0x180:])
// unindex tail epoch
mapDb.writeMapRows(common.Range[uint32]{}, common.NewRange[uint32](0x180, 0x40), common.Range[uint32]{}, nil, testStop)
mapDb.deletePointers(common.NewRange[uint32](0x180, 0x40), testStop)
testMapReader(t, "mapDatabase test #5", &params, reader, cpList[:7], maps[0x1c0:])
}

View file

@ -1,815 +0,0 @@
// Copyright 2024 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package filtermaps
import (
"errors"
"fmt"
"math"
"slices"
"sort"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/lru"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
)
const (
maxMapsPerBatch = 32 // maximum number of maps rendered in memory
valuesPerCallback = 1024 // log values processed per event process callback
cachedRowMappings = 10000 // log value to row mappings cached during rendering
// Number of rows written to db in a single batch.
// The map renderer splits up writes like this to ensure that regular
// block processing latency is not affected by large batch writes.
rowsPerBatch = 1024
)
var (
errChainUpdate = errors.New("rendered section of chain updated")
)
// mapRenderer represents a process that renders filter maps in a specified
// range according to the actual targetView.
type mapRenderer struct {
f *FilterMaps
renderBefore uint32
currentMap *renderedMap
finishedMaps map[uint32]*renderedMap
finished common.Range[uint32]
iterator *logIterator
}
// renderedMap represents a single filter map that is being rendered in memory.
type renderedMap struct {
filterMap filterMap
mapIndex uint32
lastBlock uint64
lastBlockId common.Hash
blockLvPtrs []uint64 // start pointers of blocks starting in this map; last one is lastBlock
finished bool // iterator finished; all values rendered
headDelimiter uint64 // if finished then points to the future block delimiter of the head block
}
// firstBlock returns the first block number that starts in the given map.
func (r *renderedMap) firstBlock() uint64 {
return r.lastBlock + 1 - uint64(len(r.blockLvPtrs))
}
// renderMapsBefore creates a mapRenderer that renders the log index until the
// specified map index boundary, starting from the latest available starting
// point that is consistent with the current targetView.
// The renderer ensures that filterMapsRange, indexedView and the actual map
// data are always consistent with each other. If renderBefore is greater than
// the latest existing rendered map then indexedView is updated to targetView,
// otherwise it is checked that the rendered range is consistent with both
// views.
func (f *FilterMaps) renderMapsBefore(renderBefore uint32) (*mapRenderer, error) {
nextMap, startBlock, startLvPtr, err := f.lastCanonicalMapBoundaryBefore(renderBefore)
if err != nil {
return nil, err
}
if snapshot := f.lastCanonicalSnapshotOfMap(nextMap); snapshot != nil {
return f.renderMapsFromSnapshot(snapshot)
}
if nextMap >= renderBefore {
return nil, nil
}
return f.renderMapsFromMapBoundary(nextMap, renderBefore, startBlock, startLvPtr)
}
// renderMapsFromSnapshot creates a mapRenderer that starts rendering from a
// snapshot made at a block boundary.
func (f *FilterMaps) renderMapsFromSnapshot(cp *renderedMap) (*mapRenderer, error) {
f.testSnapshotUsed = true
iter, err := f.newLogIteratorFromBlockDelimiter(cp.lastBlock, cp.headDelimiter)
if err != nil {
return nil, fmt.Errorf("failed to create log iterator from block delimiter %d: %v", cp.lastBlock, err)
}
return &mapRenderer{
f: f,
currentMap: &renderedMap{
filterMap: cp.filterMap.fullCopy(),
mapIndex: cp.mapIndex,
lastBlock: cp.lastBlock,
blockLvPtrs: slices.Clone(cp.blockLvPtrs),
},
finishedMaps: make(map[uint32]*renderedMap),
finished: common.NewRange(cp.mapIndex, 0),
renderBefore: math.MaxUint32,
iterator: iter,
}, nil
}
// renderMapsFromMapBoundary creates a mapRenderer that starts rendering at a
// map boundary.
func (f *FilterMaps) renderMapsFromMapBoundary(firstMap, renderBefore uint32, startBlock, startLvPtr uint64) (*mapRenderer, error) {
iter, err := f.newLogIteratorFromMapBoundary(firstMap, startBlock, startLvPtr)
if err != nil {
return nil, fmt.Errorf("failed to create log iterator from map boundary %d: %v", firstMap, err)
}
return &mapRenderer{
f: f,
currentMap: &renderedMap{
filterMap: f.emptyFilterMap(),
mapIndex: firstMap,
lastBlock: iter.blockNumber,
},
finishedMaps: make(map[uint32]*renderedMap),
finished: common.NewRange(firstMap, 0),
renderBefore: renderBefore,
iterator: iter,
}, nil
}
// lastCanonicalSnapshotOfMap returns the latest cached snapshot of the given map
// that is also consistent with the current targetView.
func (f *FilterMaps) lastCanonicalSnapshotOfMap(mapIndex uint32) *renderedMap {
var best *renderedMap
for _, blockNumber := range f.renderSnapshots.Keys() {
if cp, _ := f.renderSnapshots.Get(blockNumber); cp != nil && blockNumber < f.indexedRange.blocks.AfterLast() &&
blockNumber <= f.indexedView.HeadNumber() && f.indexedView.BlockId(blockNumber) == cp.lastBlockId &&
blockNumber <= f.targetView.HeadNumber() && f.targetView.BlockId(blockNumber) == cp.lastBlockId &&
cp.mapIndex == mapIndex && (best == nil || blockNumber > best.lastBlock) {
best = cp
}
}
return best
}
// lastCanonicalMapBoundaryBefore returns the latest map boundary before the
// specified map index that matches the current targetView. This can either
// be a checkpoint (hardcoded or left from a previously unindexed tail epoch)
// or the boundary of a currently rendered map.
// Along with the next map index where the rendering can be started, the number
// and starting log value pointer of the last block is also returned.
func (f *FilterMaps) lastCanonicalMapBoundaryBefore(renderBefore uint32) (nextMap uint32, startBlock, startLvPtr uint64, err error) {
if !f.indexedRange.initialized {
return 0, 0, 0, nil
}
mapIndex := renderBefore
for {
var ok bool
if mapIndex, ok = f.lastMapBoundaryBefore(mapIndex); !ok {
return 0, 0, 0, nil
}
lastBlock, lastBlockId, err := f.getLastBlockOfMap(mapIndex)
if err != nil {
return 0, 0, 0, fmt.Errorf("failed to retrieve last block of reverse iterated map %d: %v", mapIndex, err)
}
if (f.indexedRange.headIndexed && mapIndex >= f.indexedRange.maps.Last()) ||
lastBlock >= f.targetView.HeadNumber() || lastBlockId != f.targetView.BlockId(lastBlock) {
continue // map is not full or inconsistent with targetView; roll back
}
lvPtr, err := f.getBlockLvPointer(lastBlock)
if err != nil {
return 0, 0, 0, fmt.Errorf("failed to retrieve log value pointer of last canonical boundary block %d: %v", lastBlock, err)
}
return mapIndex + 1, lastBlock, lvPtr, nil
}
}
// lastMapBoundaryBefore returns the latest map boundary before the specified
// map index.
func (f *FilterMaps) lastMapBoundaryBefore(renderBefore uint32) (uint32, bool) {
if !f.indexedRange.initialized || f.indexedRange.maps.AfterLast() == 0 || renderBefore == 0 {
return 0, false
}
afterLastFullMap := f.indexedRange.maps.AfterLast()
if afterLastFullMap > 0 && f.indexedRange.headIndexed {
afterLastFullMap-- // last map is not full
}
firstRendered := min(renderBefore-1, afterLastFullMap)
if firstRendered == 0 {
return 0, false
}
if firstRendered >= f.indexedRange.maps.First() {
return firstRendered - 1, true
}
if firstRendered+f.mapsPerEpoch > f.indexedRange.maps.First() {
firstRendered = min(firstRendered, f.indexedRange.maps.First()-f.mapsPerEpoch+f.indexedRange.tailPartialEpoch)
} else {
firstRendered = (firstRendered >> f.logMapsPerEpoch) << f.logMapsPerEpoch
}
if firstRendered == 0 {
return 0, false
}
return firstRendered - 1, true
}
// emptyFilterMap returns an empty filter map.
func (f *FilterMaps) emptyFilterMap() filterMap {
return make(filterMap, f.mapHeight)
}
// loadHeadSnapshot loads the last rendered map from the database and creates
// a snapshot.
func (f *FilterMaps) loadHeadSnapshot() error {
fm, err := f.getFilterMap(f.indexedRange.maps.Last())
if err != nil {
return fmt.Errorf("failed to load head snapshot map %d: %v", f.indexedRange.maps.Last(), err)
}
lastBlock, _, err := f.getLastBlockOfMap(f.indexedRange.maps.Last())
if err != nil {
return fmt.Errorf("failed to retrieve last block of head snapshot map %d: %v", f.indexedRange.maps.Last(), err)
}
var firstBlock uint64
if f.indexedRange.maps.AfterLast() > 1 {
prevLastBlock, _, err := f.getLastBlockOfMap(f.indexedRange.maps.Last() - 1)
if err != nil {
return fmt.Errorf("failed to retrieve last block of map %d before head snapshot: %v", f.indexedRange.maps.Last()-1, err)
}
firstBlock = prevLastBlock + 1
}
lvPtrs := make([]uint64, lastBlock+1-firstBlock)
for i := range lvPtrs {
lvPtrs[i], err = f.getBlockLvPointer(firstBlock + uint64(i))
if err != nil {
return fmt.Errorf("failed to retrieve log value pointer of head snapshot block %d: %v", firstBlock+uint64(i), err)
}
}
f.renderSnapshots.Add(f.indexedRange.blocks.Last(), &renderedMap{
filterMap: fm.fullCopy(),
mapIndex: f.indexedRange.maps.Last(),
lastBlock: f.indexedRange.blocks.Last(),
lastBlockId: f.indexedView.BlockId(f.indexedRange.blocks.Last()),
blockLvPtrs: lvPtrs,
finished: true,
headDelimiter: f.indexedRange.headDelimiter,
})
return nil
}
// makeSnapshot creates a snapshot of the current state of the rendered map.
func (r *mapRenderer) makeSnapshot() {
if r.iterator.blockNumber != r.currentMap.lastBlock || r.iterator.chainView != r.f.targetView {
panic("iterator state inconsistent with current rendered map")
}
r.f.renderSnapshots.Add(r.currentMap.lastBlock, &renderedMap{
filterMap: r.currentMap.filterMap.fastCopy(),
mapIndex: r.currentMap.mapIndex,
lastBlock: r.currentMap.lastBlock,
lastBlockId: r.iterator.chainView.BlockId(r.currentMap.lastBlock),
blockLvPtrs: r.currentMap.blockLvPtrs,
finished: true,
headDelimiter: r.iterator.lvIndex,
})
}
// run does the actual map rendering. It periodically calls the stopCb callback
// and if it returns true the process is interrupted an can be resumed later
// by calling run again. The writeCb callback is called after new maps have
// been written to disk and the index range has been updated accordingly.
func (r *mapRenderer) run(stopCb func() bool, writeCb func()) (bool, error) {
for {
if done, err := r.renderCurrentMap(stopCb); !done {
return done, err // stopped or failed
}
// map finished
r.finishedMaps[r.currentMap.mapIndex] = r.currentMap
r.finished.SetLast(r.finished.AfterLast())
if len(r.finishedMaps) >= maxMapsPerBatch || r.f.mapGroupOffset(r.finished.AfterLast()) == 0 {
if err := r.writeFinishedMaps(stopCb); err != nil {
return false, err
}
writeCb()
}
if r.finished.AfterLast() == r.renderBefore || r.iterator.finished {
if err := r.writeFinishedMaps(stopCb); err != nil {
return false, err
}
writeCb()
return true, nil
}
r.currentMap = &renderedMap{
filterMap: r.f.emptyFilterMap(),
mapIndex: r.finished.AfterLast(),
}
}
}
// renderCurrentMap renders a single map.
func (r *mapRenderer) renderCurrentMap(stopCb func() bool) (bool, error) {
var (
totalTime time.Duration
logValuesProcessed, blocksProcessed int64
)
start := time.Now()
if !r.iterator.updateChainView(r.f.targetView) {
return false, errChainUpdate
}
var waitCnt int
if r.iterator.lvIndex == 0 {
r.currentMap.blockLvPtrs = []uint64{0}
}
type lvPos struct{ rowIndex, layerIndex uint32 }
rowMappingCache := lru.NewCache[common.Hash, lvPos](cachedRowMappings)
defer rowMappingCache.Purge()
for r.iterator.lvIndex < uint64(r.currentMap.mapIndex+1)<<r.f.logValuesPerMap && !r.iterator.finished {
waitCnt++
if waitCnt >= valuesPerCallback {
totalTime += time.Since(start)
if stopCb() {
return false, nil
}
start = time.Now()
if !r.iterator.updateChainView(r.f.targetView) {
return false, errChainUpdate
}
waitCnt = 0
}
if logValue := r.iterator.getValueHash(); logValue != (common.Hash{}) {
lvp, cached := rowMappingCache.Get(logValue)
if !cached {
lvp = lvPos{rowIndex: r.f.rowIndex(r.currentMap.mapIndex, 0, logValue)}
}
for uint32(len(r.currentMap.filterMap[lvp.rowIndex])) >= r.f.maxRowLength(lvp.layerIndex) {
lvp.layerIndex++
lvp.rowIndex = r.f.rowIndex(r.currentMap.mapIndex, lvp.layerIndex, logValue)
cached = false
}
r.currentMap.filterMap[lvp.rowIndex] = append(r.currentMap.filterMap[lvp.rowIndex], r.f.columnIndex(r.iterator.lvIndex, &logValue))
if !cached {
rowMappingCache.Add(logValue, lvp)
}
}
if err := r.iterator.next(); err != nil {
return false, fmt.Errorf("failed to advance log iterator at %d while rendering map %d: %v", r.iterator.lvIndex, r.currentMap.mapIndex, err)
}
if !r.iterator.skipToBoundary {
logValuesProcessed++
r.currentMap.lastBlock = r.iterator.blockNumber
if r.iterator.blockStart {
blocksProcessed++
r.currentMap.blockLvPtrs = append(r.currentMap.blockLvPtrs, r.iterator.lvIndex)
}
if !r.f.testDisableSnapshots && r.renderBefore >= r.f.indexedRange.maps.AfterLast() &&
(r.iterator.delimiter || r.iterator.finished) {
r.makeSnapshot()
}
}
}
if r.iterator.finished {
r.currentMap.finished = true
r.currentMap.headDelimiter = r.iterator.lvIndex
}
r.currentMap.lastBlockId = r.f.targetView.BlockId(r.currentMap.lastBlock)
totalTime += time.Since(start)
mapRenderTimer.Update(totalTime)
mapLogValueMeter.Mark(logValuesProcessed)
mapBlockMeter.Mark(blocksProcessed)
return true, nil
}
// writeFinishedMaps writes rendered maps to the database and updates
// filterMapsRange and indexedView accordingly.
func (r *mapRenderer) writeFinishedMaps(pauseCb func() bool) error {
var totalTime time.Duration
start := time.Now()
if len(r.finishedMaps) == 0 {
return nil
}
r.f.indexLock.Lock()
defer r.f.indexLock.Unlock()
oldRange := r.f.indexedRange
tempRange, err := r.getTempRange()
if err != nil {
return fmt.Errorf("failed to get temporary rendered range: %v", err)
}
newRange, err := r.getUpdatedRange()
if err != nil {
return fmt.Errorf("failed to get updated rendered range: %v", err)
}
renderedView := r.f.targetView // pauseCb callback might still change targetView while writing finished maps
batch := r.f.db.NewBatch()
var writeCnt int
checkWriteCnt := func() {
writeCnt++
if writeCnt == rowsPerBatch {
writeCnt = 0
if err := batch.Write(); err != nil {
log.Crit("Error writing log index update batch", "error", err)
}
// do not exit while in partially written state but do allow processing
// events and pausing while block processing is in progress
r.f.indexLock.Unlock()
totalTime += time.Since(start)
pauseCb()
start = time.Now()
r.f.indexLock.Lock()
batch = r.f.db.NewBatch()
}
}
if tempRange != r.f.indexedRange {
r.f.setRange(batch, r.f.indexedView, tempRange, true)
}
// add or update filter rows
for rowIndex := uint32(0); rowIndex < r.f.mapHeight; rowIndex++ {
var (
mapIndices []uint32
rows []FilterRow
)
for mapIndex := range r.finished.Iter() {
row := r.finishedMaps[mapIndex].filterMap[rowIndex]
if fm, _ := r.f.filterMapCache.Get(mapIndex); fm != nil && row.Equal(fm[rowIndex]) {
continue
}
mapIndices = append(mapIndices, mapIndex)
rows = append(rows, row)
}
if newRange.maps.AfterLast() == r.finished.AfterLast() { // head updated; remove future entries
for mapIndex := r.finished.AfterLast(); mapIndex < oldRange.maps.AfterLast(); mapIndex++ {
if fm, _ := r.f.filterMapCache.Get(mapIndex); fm != nil && len(fm[rowIndex]) == 0 {
continue
}
mapIndices = append(mapIndices, mapIndex)
rows = append(rows, nil)
}
}
if err := r.f.storeFilterMapRows(batch, mapIndices, rowIndex, rows); err != nil {
return fmt.Errorf("failed to store filter maps %v row %d: %v", mapIndices, rowIndex, err)
}
checkWriteCnt()
}
// update filter map cache
if newRange.maps.AfterLast() == r.finished.AfterLast() {
// head updated; cache new head maps and remove future entries
for mapIndex := range r.finished.Iter() {
r.f.filterMapCache.Add(mapIndex, r.finishedMaps[mapIndex].filterMap)
}
for mapIndex := r.finished.AfterLast(); mapIndex < oldRange.maps.AfterLast(); mapIndex++ {
r.f.filterMapCache.Remove(mapIndex)
}
} else {
// head not updated; do not cache maps during tail rendering because we
// need head maps to be available in the cache
for mapIndex := range r.finished.Iter() {
r.f.filterMapCache.Remove(mapIndex)
}
}
var blockNumber uint64
if r.finished.First() > 0 {
// in order to always ensure continuous block pointers, initialize
// blockNumber based on the last block of the previous map, then verify
// against the first block associated with each rendered map
lastBlock, _, err := r.f.getLastBlockOfMap(r.finished.First() - 1)
if err != nil {
return fmt.Errorf("failed to get last block of previous map %d: %v", r.finished.First()-1, err)
}
blockNumber = lastBlock + 1
}
// add or update block pointers
for mapIndex := range r.finished.Iter() {
renderedMap := r.finishedMaps[mapIndex]
if blockNumber != renderedMap.firstBlock() {
return fmt.Errorf("non-continuous block numbers in rendered map %d (next expected: %d first rendered: %d)", mapIndex, blockNumber, renderedMap.firstBlock())
}
r.f.storeLastBlockOfMap(batch, mapIndex, renderedMap.lastBlock, renderedMap.lastBlockId)
checkWriteCnt()
for _, lvPtr := range renderedMap.blockLvPtrs {
r.f.storeBlockLvPointer(batch, blockNumber, lvPtr)
checkWriteCnt()
blockNumber++
}
}
if newRange.maps.AfterLast() == r.finished.AfterLast() { // head updated; remove future entries
for mapIndex := r.finished.AfterLast(); mapIndex < oldRange.maps.AfterLast(); mapIndex++ {
r.f.deleteLastBlockOfMap(batch, mapIndex)
checkWriteCnt()
}
for ; blockNumber < oldRange.blocks.AfterLast(); blockNumber++ {
r.f.deleteBlockLvPointer(batch, blockNumber)
checkWriteCnt()
}
}
r.finishedMaps = make(map[uint32]*renderedMap)
r.finished.SetFirst(r.finished.AfterLast())
r.f.setRange(batch, renderedView, newRange, false)
if err := batch.Write(); err != nil {
log.Crit("Error writing log index update batch", "error", err)
}
totalTime += time.Since(start)
mapWriteTimer.Update(totalTime)
return nil
}
// getTempRange returns a temporary filterMapsRange that is committed to the
// database while the newly rendered maps are partially written. Writing all
// processed maps in a single database batch would be a serious hit on db
// performance so instead safety is ensured by first reverting the valid map
// range to the unchanged region until all new map data is committed.
func (r *mapRenderer) getTempRange() (filterMapsRange, error) {
tempRange := r.f.indexedRange
if err := tempRange.addRenderedRange(r.finished.First(), r.finished.First(), r.renderBefore, r.f.mapsPerEpoch); err != nil {
return filterMapsRange{}, fmt.Errorf("failed to update temporary rendered range: %v", err)
}
if tempRange.maps.First() != r.f.indexedRange.maps.First() {
// first rendered map changed; update first indexed block
if tempRange.maps.First() > 0 {
firstBlock, _, err := r.f.getLastBlockOfMap(tempRange.maps.First() - 1)
if err != nil {
return filterMapsRange{}, fmt.Errorf("failed to retrieve last block of map %d before temporary range: %v", tempRange.maps.First()-1, err)
}
tempRange.blocks.SetFirst(firstBlock + 1) // firstBlock is probably partially rendered
} else {
tempRange.blocks.SetFirst(0)
}
}
if tempRange.maps.AfterLast() != r.f.indexedRange.maps.AfterLast() {
// last rendered map changed; update last indexed block
if !tempRange.maps.IsEmpty() {
lastBlock, _, err := r.f.getLastBlockOfMap(tempRange.maps.Last())
if err != nil {
return filterMapsRange{}, fmt.Errorf("failed to retrieve last block of map %d at the end of temporary range: %v", tempRange.maps.Last(), err)
}
tempRange.blocks.SetAfterLast(lastBlock) // lastBlock is probably partially rendered
} else {
tempRange.blocks.SetAfterLast(0)
}
tempRange.headIndexed = false
tempRange.headDelimiter = 0
}
return tempRange, nil
}
// getUpdatedRange returns the updated filterMapsRange after writing the newly
// rendered maps.
func (r *mapRenderer) getUpdatedRange() (filterMapsRange, error) {
// update filterMapsRange
newRange := r.f.indexedRange
if err := newRange.addRenderedRange(r.finished.First(), r.finished.AfterLast(), r.renderBefore, r.f.mapsPerEpoch); err != nil {
return filterMapsRange{}, fmt.Errorf("failed to update rendered range: %v", err)
}
if newRange.maps.First() != r.f.indexedRange.maps.First() {
// first rendered map changed; update first indexed block
if newRange.maps.First() > 0 {
firstBlock, _, err := r.f.getLastBlockOfMap(newRange.maps.First() - 1)
if err != nil {
return filterMapsRange{}, fmt.Errorf("failed to retrieve last block of map %d before rendered range: %v", newRange.maps.First()-1, err)
}
newRange.blocks.SetFirst(firstBlock + 1) // firstBlock is probably partially rendered
} else {
newRange.blocks.SetFirst(0)
}
}
if newRange.maps.AfterLast() == r.finished.AfterLast() {
// last rendered map changed; update last indexed block and head pointers
lm := r.finishedMaps[r.finished.Last()]
newRange.headIndexed = lm.finished
if lm.finished {
newRange.blocks.SetLast(r.f.targetView.HeadNumber())
if lm.lastBlock != r.f.targetView.HeadNumber() {
panic("map rendering finished but last block != head block")
}
newRange.headDelimiter = lm.headDelimiter
} else {
newRange.blocks.SetAfterLast(lm.lastBlock) // lastBlock is probably partially rendered
newRange.headDelimiter = 0
}
} else {
// last rendered map not replaced; ensure that target chain view matches
// indexed chain view on the rendered section
if lastBlock := r.finishedMaps[r.finished.Last()].lastBlock; !matchViews(r.f.indexedView, r.f.targetView, lastBlock) {
return filterMapsRange{}, errChainUpdate
}
}
return newRange, nil
}
// addRenderedRange adds the range [firstRendered, afterLastRendered) and
// removes [afterLastRendered, afterLastRemoved) from the set of rendered maps.
func (fmr *filterMapsRange) addRenderedRange(firstRendered, afterLastRendered, afterLastRemoved, mapsPerEpoch uint32) error {
if !fmr.initialized {
return errors.New("log index not initialized")
}
// Here we create a slice of endpoints for the rendered sections. There are two endpoints
// for each section: the index of the first map, and the index after the last map in the
// section. We then iterate the endpoints -- adding d values -- to determine whether the
// sections are contiguous or whether they have a gap.
type endpoint struct {
m uint32
d int
}
endpoints := []endpoint{{fmr.maps.First(), 1}, {fmr.maps.AfterLast(), -1}, {firstRendered, 1}, {afterLastRendered, -101}, {afterLastRemoved, 100}}
if fmr.tailPartialEpoch > 0 {
endpoints = append(endpoints, []endpoint{{fmr.maps.First() - mapsPerEpoch, 1}, {fmr.maps.First() - mapsPerEpoch + fmr.tailPartialEpoch, -1}}...)
}
sort.Slice(endpoints, func(i, j int) bool { return endpoints[i].m < endpoints[j].m })
var (
sum int
merged []uint32
last bool
)
for i, e := range endpoints {
sum += e.d
if i < len(endpoints)-1 && endpoints[i+1].m == e.m {
continue
}
if (sum > 0) != last {
merged = append(merged, e.m)
last = !last
}
}
switch len(merged) {
case 0:
// Initialized database, but no finished maps yet.
fmr.tailPartialEpoch = 0
fmr.maps = common.NewRange(firstRendered, 0)
case 2:
// One rendered section (no partial tail epoch).
fmr.tailPartialEpoch = 0
fmr.maps = common.NewRange(merged[0], merged[1]-merged[0])
case 4:
// Two rendered sections (with a gap).
// First section (merged[0]-merged[1]) is for the partial tail epoch,
// and it has to start exactly one epoch before the main section.
if merged[2] != merged[0]+mapsPerEpoch {
return fmt.Errorf("invalid tail partial epoch: %v", merged)
}
fmr.tailPartialEpoch = merged[1] - merged[0]
fmr.maps = common.NewRange(merged[2], merged[3]-merged[2])
default:
return fmt.Errorf("invalid number of rendered sections: %v", merged)
}
return nil
}
// logIterator iterates on the linear log value index range.
type logIterator struct {
params *Params
chainView *ChainView
blockNumber uint64
receipts types.Receipts
blockStart, delimiter, skipToBoundary, finished bool
txIndex, logIndex, topicIndex int
lvIndex uint64
}
var errUnindexedRange = errors.New("unindexed range")
// newLogIteratorFromBlockDelimiter creates a logIterator starting at the
// given block's first log value entry (the block delimiter), according to the
// current targetView.
func (f *FilterMaps) newLogIteratorFromBlockDelimiter(blockNumber, lvIndex uint64) (*logIterator, error) {
if blockNumber > f.targetView.HeadNumber() {
return nil, fmt.Errorf("iterator entry point %d after target chain head block %d", blockNumber, f.targetView.HeadNumber())
}
if !f.indexedRange.blocks.Includes(blockNumber) {
return nil, errUnindexedRange
}
finished := blockNumber == f.targetView.HeadNumber()
l := &logIterator{
chainView: f.targetView,
params: &f.Params,
blockNumber: blockNumber,
finished: finished,
delimiter: !finished,
lvIndex: lvIndex,
}
l.enforceValidState()
return l, nil
}
// newLogIteratorFromMapBoundary creates a logIterator starting at the given
// map boundary, according to the current targetView.
func (f *FilterMaps) newLogIteratorFromMapBoundary(mapIndex uint32, startBlock, startLvPtr uint64) (*logIterator, error) {
if startBlock > f.targetView.HeadNumber() {
return nil, fmt.Errorf("iterator entry point %d after target chain head block %d", startBlock, f.targetView.HeadNumber())
}
// get block receipts
receipts := f.targetView.RawReceipts(startBlock)
if receipts == nil {
return nil, fmt.Errorf("receipts not found for start block %d", startBlock)
}
// initialize iterator at block start
l := &logIterator{
chainView: f.targetView,
params: &f.Params,
blockNumber: startBlock,
receipts: receipts,
blockStart: true,
lvIndex: startLvPtr,
}
l.enforceValidState()
targetIndex := uint64(mapIndex) << f.logValuesPerMap
if l.lvIndex > targetIndex {
return nil, fmt.Errorf("log value pointer %d of last block of map is after map boundary %d", l.lvIndex, targetIndex)
}
// iterate to map boundary
for l.lvIndex < targetIndex {
if l.finished {
return nil, fmt.Errorf("iterator already finished at %d before map boundary target %d", l.lvIndex, targetIndex)
}
if err := l.next(); err != nil {
return nil, fmt.Errorf("failed to advance log iterator at %d before map boundary target %d: %v", l.lvIndex, targetIndex, err)
}
}
return l, nil
}
// updateChainView updates the iterator's chain view if it still matches the
// previous view at the current position. Returns true if successful.
func (l *logIterator) updateChainView(cv *ChainView) bool {
if !matchViews(cv, l.chainView, l.blockNumber) {
return false
}
l.chainView = cv
return true
}
// getValueHash returns the log value hash at the current position.
func (l *logIterator) getValueHash() common.Hash {
if l.delimiter || l.finished || l.skipToBoundary {
return common.Hash{}
}
log := l.receipts[l.txIndex].Logs[l.logIndex]
if l.topicIndex == 0 {
return addressValue(log.Address)
}
return topicValue(log.Topics[l.topicIndex-1])
}
// next moves the iterator to the next log value index.
func (l *logIterator) next() error {
if l.skipToBoundary {
l.lvIndex++
if l.lvIndex%l.params.valuesPerMap == 0 {
l.skipToBoundary = false
}
return nil
}
if l.finished {
return nil
}
if l.delimiter {
l.delimiter = false
l.blockNumber++
l.receipts = l.chainView.RawReceipts(l.blockNumber)
if l.receipts == nil {
return fmt.Errorf("receipts not found for block %d", l.blockNumber)
}
l.txIndex, l.logIndex, l.topicIndex, l.blockStart = 0, 0, 0, true
} else {
l.topicIndex++
l.blockStart = false
}
l.lvIndex++
l.enforceValidState()
return nil
}
// enforceValidState updates the internal transaction, log and topic index pointers
// to the next existing log value of the given block if necessary.
// Note that enforceValidState does not advance the log value index pointer.
func (l *logIterator) enforceValidState() {
if l.delimiter || l.finished || l.skipToBoundary {
return
}
for ; l.txIndex < len(l.receipts); l.txIndex++ {
receipt := l.receipts[l.txIndex]
for ; l.logIndex < len(receipt.Logs); l.logIndex++ {
log := receipt.Logs[l.logIndex]
if l.topicIndex == 0 && uint64(len(log.Topics)+1) > l.params.valuesPerMap-l.lvIndex%l.params.valuesPerMap {
// next log would be split by map boundary; skip to boundary
l.skipToBoundary = true
return
}
if l.topicIndex <= len(log.Topics) {
return
}
l.topicIndex = 0
}
l.logIndex = 0
}
if l.blockNumber == l.chainView.HeadNumber() {
l.finished = true
} else {
l.delimiter = true
}
}

View file

@ -0,0 +1,711 @@
// Copyright 2025 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package filtermaps
import (
"errors"
"fmt"
"math"
"sync"
"sync/atomic"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
)
var ErrOutOfRange = errors.New("pointer out of indexed range")
// mapStorage implements a filter map storage layer over mapDatabase that ensures
// efficient database usage while also providing a low latency interface to the
// indexer. It uses a memory layer over mapDatabase allowing consistently quick
// addMap and deleteMaps calls while doing the actual database updates in the
// background asynchronously.
type mapStorage struct {
params *Params
mapDb *mapDatabase
triggerCh, closeCh chan struct{}
closeWg sync.WaitGroup
mtForceWrite, mtBusy uint32
lock sync.RWMutex
initialized bool
knownEpochs uint32 // epochs initialized with last map block pointer and corresponding reverse block lv pointer
knownEpochBlocks uint64
valid, dirty rangeSet[uint32] // valid and dirty maps in database
writeInProgress, deleteInProgress rangeSet[uint32] // write cycle in progress
overlay rangeSet[uint32] // memory maps
overlayCount uint32
overlayMaps map[uint32]*finishedMap
validBlocks, overlayBlocks rangeSet[uint64]
epochTrigger rangeSet[uint32]
suspended uint32
testHookCh chan bool
}
// newMapStorage creates a new mapStorage layer over the given mapDatabase.
func newMapStorage(params *Params, mapDb *mapDatabase, testHookCh chan bool) *mapStorage {
m := &mapStorage{
params: params,
mapDb: mapDb,
triggerCh: make(chan struct{}, 1),
closeCh: make(chan struct{}),
overlayMaps: make(map[uint32]*finishedMap),
testHookCh: testHookCh,
mtForceWrite: params.rowGroupSize[0] * 9 / 8,
mtBusy: params.rowGroupSize[0] * 17 / 8,
}
if valid, dirty, knownEpochs, ok := m.mapDb.loadMapRange(); ok {
m.valid, m.dirty, m.knownEpochs, m.initialized = valid, dirty, knownEpochs, true
if knownEpochs > 0 {
if lastBlock, _, err := m.mapDb.getLastBlockOfMap(m.params.lastEpochMap(knownEpochs - 1)); err == nil {
m.knownEpochBlocks = lastBlock + 1
} else {
m.resetWithError(fmt.Sprintf("could not initialize known epoch range: %v", err))
}
}
if err := m.updateValidBlocks(); err != nil {
m.resetWithError(fmt.Sprintf("could not initialize valid block range: %v", err))
}
}
m.closeWg.Add(1)
go m.eventLoop()
return m
}
// stop stops mapStorage.
func (m *mapStorage) stop() {
close(m.closeCh)
m.closeWg.Wait()
}
// isReady returns false if there are too many memory overlay maps at the moment.
// In this case the caller should suspend the indexing process until some maps
// are written to the database.
func (m *mapStorage) isReady() bool {
m.lock.Lock()
defer m.lock.Unlock()
if m.overlayCount < m.mtBusy {
m.trigger()
return true
}
return false
}
// tailEpoch returns the first epoch of the continuous rendered range. If it is
// larger than zero then the tail renderer can start indexing the previous epoch
// if necessary. After checkpoint initialization it points right after the last
// known epoch boundary (in this case the tail epoch can be empty).
func (m *mapStorage) tailEpoch() uint32 {
m.lock.Lock()
defer m.lock.Unlock()
mapRange := m.valid.union(m.overlay)
if len(mapRange) > 0 && m.params.mapEpoch(mapRange[len(mapRange)-1].AfterLast()) >= m.knownEpochs {
return min(m.knownEpochs, m.params.mapEpoch(mapRange[len(mapRange)-1].First()+m.params.mapsPerEpoch-1))
}
return m.knownEpochs
}
// tailNumberOfEpoch returns the number of the first block that starts in the
// given epoch.
func (m *mapStorage) tailNumberOfEpoch(epoch uint32) (uint64, error) {
if epoch == 0 {
return 0, nil
}
number, _, err := m.getLastBlockOfMap(m.params.lastEpochMap(epoch - 1))
if err != nil {
return 0, err
}
return number + 1, nil
}
// lastBoundaryBefore returns the most recent map index that is less than
// or equal to the given mapIndex parameter and is either located after a stored
// map or after a known epoch boundary.
// The returned map index position may or may not contain a stored map but if it
// is empty then it is always suitable to start a rendering process.
func (m *mapStorage) lastBoundaryBefore(mapIndex uint32) uint32 {
m.lock.Lock()
defer m.lock.Unlock()
if mapIndex == 0 {
return 0
}
lastBoundary := m.params.firstEpochMap(min(m.params.mapEpoch(mapIndex), m.knownEpochs))
if m, ok := m.valid.closestLte(mapIndex - 1); ok {
lastBoundary = max(lastBoundary, m+1)
}
if m, ok := m.overlay.closestLte(mapIndex - 1); ok {
lastBoundary = max(lastBoundary, m+1)
}
return lastBoundary
}
// matchKnownEpochs returns true if the given list of checkpoints matches the
// checkpoints already stored in the database (always true with an empty database).
func (m *mapStorage) matchKnownEpochs(cpList checkpointList) bool {
m.lock.Lock()
defer m.lock.Unlock()
if m.knownEpochs == 0 || len(cpList) == 0 {
return true
}
epoch := min(m.knownEpochs, uint32(len(cpList))) - 1
number, hash, err := m.getLastBlockOfMap(m.params.lastEpochMap(epoch))
if err != nil {
m.resetWithError(fmt.Sprintf("could not read last known epoch boundary: %v", err))
return true
}
return number == cpList[epoch].BlockNumber && hash == cpList[epoch].BlockHash
}
// addKnownEpochs adds the known epoch boundaries based on the given checkpoints.
func (m *mapStorage) addKnownEpochs(cpList checkpointList) error {
m.lock.Lock()
defer m.lock.Unlock()
if uint32(len(cpList)) <= m.knownEpochs {
return errors.New("checkpoint init list has no new epochs")
}
if m.knownEpochs > 0 {
lastNumber, lastHash, err := m.mapDb.getLastBlockOfMap(m.params.lastEpochMap(m.knownEpochs - 1))
if err != nil {
return err
}
lvPointer, err := m.mapDb.getBlockLvPointer(lastNumber)
if err != nil {
return err
}
if cp := cpList[m.knownEpochs-1]; cp.BlockNumber != lastNumber || cp.BlockHash != lastHash || cp.FirstIndex != lvPointer {
return errors.New("checkpoint init list does not match known epochs")
}
}
m.mapDb.storeCheckpointList(m.knownEpochs, cpList[m.knownEpochs:])
m.knownEpochs = uint32(len(cpList))
m.knownEpochBlocks = cpList[len(cpList)-1].BlockNumber + 1
m.mapDb.storeMapRange(m.valid, m.dirty, m.knownEpochs)
return nil
}
// addMap adds a new map to the storage. If forceCommit is true then a write is
// always triggered. If it is false then a write is only triggered when a row
// group boundary is reached or if the total number of memory maps reaches a limit.
// addMap always returns right after adding the new map to the memory layer.
func (m *mapStorage) addMap(mapIndex uint32, fm *finishedMap, forceCommit bool) {
m.lock.Lock()
defer m.lock.Unlock()
if fm == nil {
panic("trying to add nil map")
}
if m.valid.includes(mapIndex) || m.overlay.includes(mapIndex) {
panic("addMap to non-empty map index")
}
epoch := m.params.mapEpoch(mapIndex)
if (epoch > m.knownEpochs || mapIndex != m.params.firstEpochMap(epoch)) &&
!m.valid.includes(mapIndex-1) && !m.overlay.includes(mapIndex-1) {
panic("addMap to map index with no known boundary")
}
mapRs := singleRangeSet[uint32](common.NewRange[uint32](mapIndex, 1))
m.overlay = m.overlay.union(mapRs)
m.writeInProgress = m.writeInProgress.exclude(mapRs)
m.overlayMaps[mapIndex] = fm
m.updateOverlayBlocks()
if forceCommit || (mapIndex+1)%m.params.rowGroupSize[0] == 0 {
m.epochTrigger = m.epochTrigger.union(singleRangeSet[uint32](common.NewRange[uint32](epoch, 1)))
m.trigger()
}
}
// deleteMaps deletes the given map range. Note that similarly to addMap, it only
// performs memory operations before returning. The deleted map range is marked
// dirty, then later the database update goroutine will actually delete or
// overwrite the dirty maps.
func (m *mapStorage) deleteMaps(maps common.Range[uint32]) {
m.lock.Lock()
defer m.lock.Unlock()
dr := singleRangeSet[uint32](maps)
for i := range dr.intersection(m.overlay).iter() {
delete(m.overlayMaps, i)
}
m.writeInProgress = m.writeInProgress.exclude(dr)
knownEpochs := m.knownEpochs
if maps.Includes(m.params.firstEpochMap(knownEpochs)) {
knownEpochs = m.params.mapEpoch(maps.First())
}
if err := m.updateRange(m.valid.exclude(dr), m.dirty.union(dr.intersection(m.valid)), m.overlay.exclude(dr), knownEpochs); err != nil {
m.resetWithError(fmt.Sprintf("could not revert valid block range: %v", err))
}
m.trigger()
}
// suspendOrResume suspends or resumes the background database update operations.
func (m *mapStorage) suspendOrResume(suspend bool) {
var suspendU32 uint32
if suspend {
suspendU32 = 1
}
old := atomic.SwapUint32(&m.suspended, suspendU32)
if suspendU32 < old {
m.trigger()
}
}
// eventLoop is the main event loop of the database update operations.
func (m *mapStorage) eventLoop() {
defer m.closeWg.Done()
var stopped bool
selectEvent := func(blocking bool) {
if m.testHookCh != nil {
select {
case <-m.closeCh:
stopped = true
return
case m.testHookCh <- blocking:
}
}
if blocking {
select {
case <-m.closeCh:
stopped = true
return
case <-m.triggerCh:
}
} else {
select {
case <-m.closeCh:
stopped = true
return
case <-m.triggerCh:
default:
}
}
if m.testHookCh != nil {
select {
case <-m.closeCh:
stopped = true
return
case <-m.testHookCh:
}
}
}
stopCallback := func() bool {
selectEvent(atomic.LoadUint32(&m.suspended) == 1)
return stopped
}
for !stopped {
if !m.initialized {
if done, _ := m.mapDb.reset(stopCallback); !done {
return // node stopped before cleaning old database
}
m.lock.Lock()
m.initialized = true
m.lock.Unlock()
}
more, err := m.doWriteCycle(stopCallback)
if err != nil {
m.resetWithError(fmt.Sprintf("write cycle failed: %v", err))
continue
}
selectEvent(!more && !stopped) // wait for next event if no changes done
}
}
// trigger ensures that the database update cycle will restart if it was in a
// waiting state.
func (m *mapStorage) trigger() {
select {
case m.triggerCh <- struct{}{}:
default:
}
}
// getBlockLvPointer returns the starting log value index where the log values
// generated by the given block are located.
func (m *mapStorage) getBlockLvPointer(blockNumber uint64) (uint64, error) {
m.lock.RLock()
defer m.lock.RUnlock()
if m.overlayBlocks.includes(blockNumber) {
for _, fm := range m.overlayMaps {
if fm.blocks().Includes(blockNumber) {
return fm.blockPtrs[blockNumber-fm.firstBlock()], nil
}
}
return 0, errors.New("memory overlay block pointer not found")
}
if blockNumber < m.knownEpochBlocks || m.validBlocks.includes(blockNumber) {
return m.mapDb.getBlockLvPointer(blockNumber)
}
return 0, ErrOutOfRange
}
// getLastBlockOfMap returns the number and hash of the block that generated the
// last log value entry of the given map.
func (m *mapStorage) getLastBlockOfMap(mapIndex uint32) (uint64, common.Hash, error) {
m.lock.RLock()
defer m.lock.RUnlock()
if m.overlay.includes(mapIndex) {
fm := m.overlayMaps[mapIndex]
if fm == nil {
return 0, common.Hash{}, errors.New("memory overlay map not found")
}
return fm.lastBlock.number, fm.lastBlock.hash, nil
}
if mapIndex < m.params.firstEpochMap(m.knownEpochs) || m.valid.includes(mapIndex) || m.valid.includes(mapIndex+1) {
return m.mapDb.getLastBlockOfMap(mapIndex)
}
return 0, common.Hash{}, ErrOutOfRange
}
// getFilterMapRows returns a batch of filter maps rows from the same row index,
// each truncated to the length limit of the specified mapping layer.
// The function assumes that the map indices are in strictly ascending order.
func (m *mapStorage) getFilterMapRows(mapIndices []uint32, rowIndex, layerIndex uint32) ([]FilterRow, error) {
m.lock.RLock()
defer m.lock.RUnlock()
rows := make([]FilterRow, len(mapIndices))
dbMaps := make([]uint32, 0, len(mapIndices))
for i, mapIndex := range mapIndices {
if m.overlay.includes(mapIndex) {
fm := m.overlayMaps[mapIndex]
if fm == nil {
return nil, errors.New("memory overlay map not found")
}
rows[i] = fm.getRow(rowIndex, m.params.getMaxRowLength(layerIndex))
} else {
dbMaps = append(dbMaps, mapIndex)
}
}
dbRows, err := m.mapDb.getFilterMapRows(dbMaps, rowIndex, layerIndex)
if err != nil {
return nil, err
}
var j int
for i, row := range rows {
if row == nil { // zero length row is represented as zero length slice
rows[i] = dbRows[j]
j++
}
}
if j != len(dbMaps) {
panic("rows length mismatch")
}
return rows, nil
}
// getFilterMap returns the filter map at the specified index.
func (m *mapStorage) getFilterMap(mapIndex uint32) (*finishedMap, error) {
m.lock.RLock()
defer m.lock.RUnlock()
if m.overlay.includes(mapIndex) {
fm := m.overlayMaps[mapIndex]
if fm == nil {
return nil, errors.New("memory overlay map not found")
}
return fm, nil
}
if m.valid.includes(mapIndex) {
return m.mapDb.getFilterMap(mapIndex)
}
return nil, nil
}
// extendDeletedPointerRange takes a map range where pointers need to be deleted
// and extends it so that on both ends there is a neighboring stored map, a
// known epoch boundary or one end of the map index range. This is required
// in order to determine the block range covered by the deleted map range.
func (m *mapStorage) extendDeletedPointerRange(deleteRange common.Range[uint32]) common.Range[uint32] {
epoch := m.params.mapEpoch(deleteRange.First())
if m.params.mapEpoch(deleteRange.Last()) != epoch {
panic("deleted map range crosses epoch boundary")
}
first := m.params.firstEpochMap(min(m.knownEpochs, epoch))
if deleteRange.First() > 0 {
if c, ok := m.valid.closestLte(deleteRange.First() - 1); ok {
first = max(first, c+1)
}
}
afterLast := uint32(math.MaxUint32)
if epoch < m.knownEpochs {
afterLast = m.params.firstEpochMap(epoch + 1)
}
if fa, ok := m.valid.closestGte(deleteRange.AfterLast()); ok {
afterLast = min(afterLast, fa)
}
return common.NewRange[uint32](first, afterLast-first)
}
// selectEpochTriggeredWrite selects the next epoch to update if one of the epochs
// in the canSelect list has been triggered by addMap.
func (m *mapStorage) selectEpochTriggeredWrite(canSelect []uint32) (uint32, bool) {
for _, epoch := range canSelect {
if !m.epochTrigger.includes(epoch) {
continue
}
m.epochTrigger = m.epochTrigger.exclude(singleRangeSet[uint32](common.NewRange[uint32](epoch, 1)))
epochRange := common.NewRange[uint32](m.params.firstEpochMap(epoch), m.params.mapsPerEpoch)
if len(m.overlay.intersection(singleRangeSet[uint32](epochRange))) > 0 {
return epoch, true
}
}
return 0, false
}
// selectEpochForcedWrite selects the next epoch to update if the total number
// of memory maps has reached a threshold.
func (m *mapStorage) selectEpochForcedWrite(canSelect []uint32) (uint32, bool) {
if m.overlayCount < m.mtForceWrite {
return 0, false
}
var best, count uint32
for _, epoch := range canSelect {
epochRange := common.NewRange[uint32](m.params.firstEpochMap(epoch), m.params.mapsPerEpoch)
if c := m.overlay.intersection(singleRangeSet[uint32](epochRange)).count(); c > count {
best, count = epoch, c
}
}
if count == 0 {
return 0, false
}
return best, true
}
// mapToEpochRange returns the set of epochs that are either fully or partially
// covered by the specified mapRange.
func (m *mapStorage) mapToEpochRange(mapRange rangeSet[uint32]) rangeSet[uint32] {
vb := make(rangeBoundaries[uint32], 0, len(mapRange)*2)
for _, r := range mapRange {
first := m.params.mapEpoch(r.First())
last := m.params.mapEpoch(r.Last())
vb.add(common.NewRange[uint32](first, last+1-first), 1)
}
return vb.makeSet(1)
}
// selectEpochDeleteOnly selects an epoch to update where there are no memory
// maps to write, only dirty maps to clean up.
func (m *mapStorage) selectEpochDeleteOnly() (uint32, bool) {
epochs := m.mapToEpochRange(m.dirty).exclude(m.mapToEpochRange(m.overlay))
if len(epochs) == 0 {
return 0, false
}
return epochs[0].First(), true
}
// doWriteCycle selects an epoch where there are memory overlay maps and/or
// dirty maps in the underlying database, cleans up dirty data and writes the
// new maps to the database.
// If an epoch has been successfully updated then the function returns (true, nil).
// In this case a new write cycle can be attempted immediately until there is
// nothing more left to do.
// If there was nothing to do or if stopCallback aborted the operation then the
// function returns (false, nil). In case of an error it returns (false, err).
// Note that if the operation is aborted or failed then the partially updated
// maps are left as marked dirty and the database remains consistent.
func (m *mapStorage) doWriteCycle(stopCallback func() bool) (bool, error) {
m.lock.Lock()
defer m.lock.Unlock()
// always operate on a single epoch
var canSelect []uint32
if len(m.valid) == 0 {
canSelect = []uint32{m.knownEpochs}
} else {
lastValid := m.valid[len(m.valid)-1]
tailEpoch := m.params.mapEpoch(lastValid.First())
headEpoch := m.params.mapEpoch(lastValid.AfterLast())
if tailEpoch > 0 {
canSelect = []uint32{headEpoch, tailEpoch - 1}
} else {
canSelect = []uint32{headEpoch}
}
}
epoch, ok := m.selectEpochTriggeredWrite(canSelect)
if !ok {
epoch, ok = m.selectEpochForcedWrite(canSelect)
if !ok {
epoch, ok = m.selectEpochDeleteOnly()
if !ok {
return false, nil
}
}
}
epochRange := singleRangeSet[uint32](common.NewRange[uint32](m.params.firstEpochMap(epoch), m.params.mapsPerEpoch))
writeMaps := epochRange.intersection(m.overlay).singleRange()
validInEpoch := epochRange.intersection(m.valid).singleRange()
dirtyInEpoch := epochRange.intersection(m.dirty).singleRange()
keepEmptyFrom := max(m.params.firstEpochMap(epoch), writeMaps.AfterLast(), validInEpoch.AfterLast(), dirtyInEpoch.AfterLast())
keepEmptyInEpoch := common.NewRange[uint32](keepEmptyFrom, m.params.firstEpochMap(epoch+1)-keepEmptyFrom)
// delete old pointers
if !dirtyInEpoch.IsEmpty() {
m.mapDb.deletePointers(m.extendDeletedPointerRange(dirtyInEpoch), stopCallback)
}
if writeMaps.IsEmpty() && validInEpoch.IsEmpty() {
// delete map rows of entire epoch if nothing to write or keep
m.lock.Unlock()
done, err := m.mapDb.deleteEpochRows(epoch, stopCallback)
m.lock.Lock()
if done {
if err := m.updateRange(m.valid, m.dirty.exclude(epochRange), m.overlay, m.knownEpochs); err != nil {
return false, err
}
}
return done, err
}
maps := make([]*finishedMap, writeMaps.Count())
for i := range writeMaps.Iter() {
maps[i-writeMaps.First()] = m.overlayMaps[i]
}
// temporarily mark newly written maps as dirty (replaced/deleted maps are already dirty)
writeInProgress := singleRangeSet[uint32](writeMaps)
deleteInProgress := singleRangeSet[uint32](dirtyInEpoch).exclude(writeInProgress)
if err := m.updateRange(m.valid, m.dirty.union(writeInProgress), m.overlay, m.knownEpochs); err != nil {
return false, err
}
m.writeInProgress = writeInProgress
m.deleteInProgress = deleteInProgress
m.lock.Unlock()
// write/overwrite map rows and delete dirty map data, write new pointers
done, err := m.mapDb.writeMapRows(writeMaps, dirtyInEpoch, keepEmptyInEpoch, maps, stopCallback)
if done {
done, err = m.mapDb.writePointers(writeMaps, maps, stopCallback)
}
m.lock.Lock()
writeInProgress = m.writeInProgress
m.writeInProgress, m.deleteInProgress = nil, nil
if !done {
return false, err
}
for mapIndex := range writeInProgress.iter() {
delete(m.overlayMaps, mapIndex)
}
knownEpochs := m.knownEpochs
if len(writeInProgress) > 0 {
knownEpochs = max(knownEpochs, m.params.mapEpoch(writeInProgress[len(writeInProgress)-1].Last()))
}
if err := m.updateRange(m.valid.union(writeInProgress), m.dirty.exclude(writeInProgress.union(deleteInProgress)), m.overlay.exclude(writeInProgress), knownEpochs); err != nil {
return false, err
}
return true, nil
}
// updateRange updates the stored valid, dirty and memory overlay map ranges and
// the number of known epoch boundaries. It also stores these ranges in the
// database and determines the corresponding block ranges.
func (m *mapStorage) updateRange(valid, dirty, overlay rangeSet[uint32], knownEpochs uint32) error {
if !valid.equal(m.valid) {
m.valid = valid
if err := m.updateValidBlocks(); err != nil {
return err
}
}
if knownEpochs != m.knownEpochs {
m.knownEpochs = knownEpochs
if err := m.updateKnownEpochBlocks(); err != nil {
return err
}
}
m.dirty = dirty
if !overlay.equal(m.overlay) {
m.overlay = overlay
m.updateOverlayBlocks()
}
m.mapDb.storeMapRange(valid, dirty, m.knownEpochs)
return nil
}
// resetWithError resets an invalid log index database after an unrecoverable error.
func (m *mapStorage) resetWithError(errStr string) {
log.Error("Resetting invalid log index database", "error", errStr)
m.uninitialize()
}
// uninitialize resets the map storage and returns to its non-initialized state.
func (m *mapStorage) uninitialize() {
m.valid, m.dirty, m.overlay, m.knownEpochs, m.initialized = nil, nil, nil, 0, false
m.mapDb.deleteMapRange()
m.trigger()
}
// updateKnownEpochBlocks determines the block number belonging to the last known
// epoch boundary.
func (m *mapStorage) updateKnownEpochBlocks() error {
if m.knownEpochs > 0 {
if lastBlock, _, err := m.mapDb.getLastBlockOfMap(m.params.lastEpochMap(m.knownEpochs - 1)); err == nil {
m.knownEpochBlocks = lastBlock + 1
} else {
return err
}
} else {
m.knownEpochBlocks = 0
}
return nil
}
// updateValidBlocks determines the the set of blocks where the starting log value
// pointer points into the valid map set.
func (m *mapStorage) updateValidBlocks() error {
if len(m.valid) > 2 || (len(m.valid) == 2 && m.valid[0].Count() >= m.params.mapsPerEpoch) {
panic("invalid mapStorage.valid")
return errors.New("invalid mapStorage.valid range set")
}
vb := make(rangeBoundaries[uint64], 0, len(m.valid)*2)
for _, vr := range m.valid {
var first uint64
if vr.First() > 0 {
lb, _, err := m.mapDb.getLastBlockOfMap(vr.First() - 1)
if err != nil {
return err
}
first = lb + 1
}
last, _, err := m.mapDb.getLastBlockOfMap(vr.Last())
if err != nil {
return err
}
vb.add(common.NewRange[uint64](first, last+1-first), 1)
}
m.validBlocks = vb.makeSet(1)
return nil
}
// updateOverlayBlocks determines the the set of blocks where the starting log
// value pointer points into the memory overlay map set.
func (m *mapStorage) updateOverlayBlocks() {
ob := make(rangeBoundaries[uint64], 0, len(m.overlay)*2)
for _, or := range m.overlay {
first := m.overlayMaps[or.First()].firstBlock()
last := m.overlayMaps[or.Last()].lastBlock.number
ob.add(common.NewRange[uint64](first, last+1-first), 1)
}
m.overlayBlocks = ob.makeSet(1)
m.overlayCount = m.overlay.count()
}

View file

@ -0,0 +1,150 @@
// Copyright 2025 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package filtermaps
import (
"math"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethdb/memorydb"
)
func TestMapStorage(t *testing.T) {
params := testParams
params.sanitize()
db := memorydb.New()
mapDb := newMapDatabase(&params, db, false)
ms := newMapStorage(&params, mapDb, make(chan bool))
<-ms.testHookCh
defer ms.stop()
reader := mapReader{
getFilterMapRows: ms.getFilterMapRows,
getFilterMap: ms.getFilterMap,
getBlockLvPointer: ms.getBlockLvPointer,
getLastBlockOfMap: ms.getLastBlockOfMap,
}
waitCycle := func() bool {
ms.testHookCh <- true
return !<-ms.testHookCh
}
expKnownEpochs := func(testCase int, exp uint32) {
if ms.knownEpochs != exp {
t.Fatalf("Invalid known epochs number in test case #%d (expected %d, got %d)", testCase, exp, ms.knownEpochs)
}
}
expTailEpoch := func(testCase int, exp uint32) {
if ms.tailEpoch() != exp {
t.Fatalf("Invalid tail epoch in test case #%d (expected %d, got %d)", testCase, exp, ms.tailEpoch())
}
}
expReadRows := func(testCase int, exp bool) {
if mapDb.testReadRows != exp {
t.Fatalf("Invalid read rows flag in test case #%d (expected %v, got %v)", testCase, exp, mapDb.testReadRows)
}
mapDb.testReadRows = false
}
// initialize database with checkpoints
maps := generateTestMaps(&params, nil, 0x200)
cpList := generateTestCheckpoints(&params, maps)
ms.addKnownEpochs(cpList)
expTailEpoch(0, 8)
expKnownEpochs(0, 8)
// add new maps to the head
maps = generateTestMaps(&params, maps, 0x50)
for m := uint32(0x200); m < 0x250; m++ {
ms.addMap(m, maps[m], true)
}
for waitCycle() {
testMapReader(t, "mapStorage test #1", &params, reader, cpList, maps[0x200:])
}
expReadRows(1, false)
expTailEpoch(1, 8)
expKnownEpochs(1, 9)
// backfill previous epoch with a single write
for m := uint32(0x1c0); m < 0x200; m++ {
ms.addMap(m, maps[m], false)
}
for waitCycle() {
testMapReader(t, "mapDatabase test #2", &params, reader, cpList[:7], maps[0x1c0:])
}
expReadRows(2, false)
expTailEpoch(2, 7)
expKnownEpochs(2, 9)
// backfill previous epoch in two steps
for m := uint32(0x180); m < 0x192; m++ {
ms.addMap(m, maps[m], true)
}
for waitCycle() {
}
expReadRows(3, false)
expTailEpoch(3, 7)
for m := uint32(0x192); m < 0x1c0; m++ {
ms.addMap(m, maps[m], false)
}
for waitCycle() {
testMapReader(t, "mapDatabase test #3", &params, reader, cpList[:6], maps[0x180:])
}
expReadRows(3, true)
expTailEpoch(3, 6)
expKnownEpochs(3, 9)
// add new maps while reorging some existing ones
maps = generateTestMaps(&params, maps[:0x234], 0x30)
ms.deleteMaps(common.NewRange[uint32](0x234, math.MaxUint32-0x234))
expKnownEpochs(4, 8)
for m := uint32(0x234); m < 0x264; m++ {
ms.addMap(m, maps[m], true)
}
for waitCycle() {
testMapReader(t, "mapDatabase test #4", &params, reader, cpList[:6], maps[0x180:])
}
expReadRows(4, true)
expTailEpoch(4, 6)
expKnownEpochs(4, 9)
// unindex tail epoch
ms.deleteMaps(common.NewRange[uint32](0x180, 0x40))
expTailEpoch(5, 7)
for waitCycle() {
testMapReader(t, "mapDatabase test #5", &params, reader, cpList[:7], maps[0x1c0:])
}
expReadRows(5, false)
expTailEpoch(5, 7)
expKnownEpochs(5, 9)
// remove head maps
maps = maps[:0x253]
ms.deleteMaps(common.NewRange[uint32](0x253, math.MaxUint32-0x253))
for waitCycle() {
testMapReader(t, "mapDatabase test #6", &params, reader, cpList[:7], maps[0x1c0:])
}
expReadRows(6, true)
expTailEpoch(6, 7)
expKnownEpochs(6, 9)
// add maps until epoch boundary and check known epochs increase
maps = generateTestMaps(&params, maps, 0x30)
for m := uint32(0x253); m < 0x283; m++ {
ms.addMap(m, maps[m], true)
}
expTailEpoch(7, 7)
expKnownEpochs(7, 9)
for waitCycle() {
testMapReader(t, "mapDatabase test #7", &params, reader, cpList[:7], maps[0x1c0:])
}
expReadRows(7, true)
expTailEpoch(7, 7)
expKnownEpochs(7, 10)
}

View file

@ -28,6 +28,18 @@ import (
"github.com/ethereum/go-ethereum/common/mclock"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/metrics"
)
var (
matchRequestTimer = metrics.NewRegisteredTimer("filtermaps/match/requesttime", nil) // processing time a matching request in a single epoch
matchEpochTimer = metrics.NewRegisteredTimer("filtermaps/match/epochtime", nil) // total processing time a matching request
matchBaseRowAccessMeter = metrics.NewRegisteredMeter("filtermaps/match/baserowaccess", nil) // number of accessed rows on layer 0
matchBaseRowSizeMeter = metrics.NewRegisteredMeter("filtermaps/match/baserowsize", nil) // size of accessed rows on layer 0
matchExtRowAccessMeter = metrics.NewRegisteredMeter("filtermaps/match/extrowaccess", nil) // number of accessed rows on higher layers
matchExtRowSizeMeter = metrics.NewRegisteredMeter("filtermaps/match/extrowsize", nil) // size of accessed rows on higher layers
matchLogLookup = metrics.NewRegisteredMeter("filtermaps/match/loglookup", nil) // number of log lookups based on potential matches
matchAllMeter = metrics.NewRegisteredMeter("filtermaps/match/matchall", nil) // number of requests returned with ErrMatchAll
)
const doRuntimeStats = false
@ -37,32 +49,32 @@ const doRuntimeStats = false
// would actually be slower than reverting to legacy filter.
var ErrMatchAll = errors.New("match all patterns not supported")
// MatcherBackend defines the functions required for searching in the log index
// data structure. It is currently implemented by FilterMapsMatcherBackend but
// once EIP-7745 is implemented and active, these functions can also be trustlessly
// served by a remote prover.
type MatcherBackend interface {
GetParams() *Params
GetBlockLvPointer(ctx context.Context, blockNumber uint64) (uint64, error)
GetFilterMapRows(ctx context.Context, mapIndices []uint32, rowIndex uint32, baseLayerOnly bool) ([]FilterRow, error)
GetLogByLvIndex(ctx context.Context, lvIndex uint64) (*types.Log, error)
SyncLogIndex(ctx context.Context) (SyncRange, error)
Close()
type LogPosition struct {
BlockNumber, startPtr, logPtr, valuesPerMap uint64
}
// SyncRange is returned by MatcherBackend.SyncLogIndex. It contains the latest
// chain head, the indexed range that is currently consistent with the chain
// and the valid range that has not been changed and has been consistent with
// all states of the chain since the previous SyncLogIndex or the creation of
// the matcher backend.
type SyncRange struct {
IndexedView *ChainView
// block range where the index has not changed since the last matcher sync
// and therefore the set of matches found in this region is guaranteed to
// be valid and complete.
ValidBlocks common.Range[uint64]
// block range indexed according to the given chain head.
IndexedBlocks common.Range[uint64]
func (l *LogPosition) GetLog(receipts types.Receipts) (*types.Log, error) {
ptr := l.startPtr
for _, receipt := range receipts {
for _, log := range receipt.Logs {
step := uint64(len(log.Topics) + 1)
subPtr := ptr % l.valuesPerMap
if subPtr+step > l.valuesPerMap {
ptr += l.valuesPerMap - subPtr
}
if ptr == l.logPtr {
return log, nil
}
ptr += step
if ptr > l.logPtr {
return nil, nil // log position is in block range but there is no log there (false positive)
}
}
}
if ptr == l.logPtr {
return nil, nil // points to the block delimiter (false positive)
}
return nil, fmt.Errorf("invalid log position: block #%d start pointer %d log pointer %d", l.BlockNumber, l.startPtr, l.logPtr)
}
// GetPotentialMatches returns a list of logs that are potential matches for the
@ -70,14 +82,14 @@ type SyncRange struct {
// missing or changed during the search process then the resulting logs belonging
// to that block range might be missing or incorrect.
// Also note that the returned list may contain false positives.
func GetPotentialMatches(ctx context.Context, backend MatcherBackend, firstBlock, lastBlock uint64, addresses []common.Address, topics [][]common.Hash) ([]*types.Log, error) {
func GetPotentialMatches(ctx context.Context, backend *IndexView, firstBlock, lastBlock uint64, addresses []common.Address, topics [][]common.Hash) ([]LogPosition, error) {
params := backend.GetParams()
// find the log value index range to search
firstIndex, err := backend.GetBlockLvPointer(ctx, firstBlock)
firstIndex, err := backend.GetBlockLvPointer(firstBlock)
if err != nil {
return nil, fmt.Errorf("failed to retrieve log value pointer for first block %d: %v", firstBlock, err)
}
lastIndex, err := backend.GetBlockLvPointer(ctx, lastBlock+1)
lastIndex, err := backend.GetBlockLvPointer(lastBlock + 1)
if err != nil {
return nil, fmt.Errorf("failed to retrieve log value pointer after last block %d: %v", lastBlock, err)
}
@ -112,10 +124,11 @@ func GetPotentialMatches(ctx context.Context, backend MatcherBackend, firstBlock
matcher := newMatchSequence(params, matchers)
m := &matcherEnv{
ctx: ctx,
backend: backend,
params: params,
matcher: matcher,
firstBlock: firstBlock,
lastBlock: lastBlock,
firstIndex: firstIndex,
lastIndex: lastIndex,
firstMap: uint32(firstIndex >> params.logValuesPerMap),
@ -123,7 +136,7 @@ func GetPotentialMatches(ctx context.Context, backend MatcherBackend, firstBlock
}
start := time.Now()
res, err := m.process()
res, err := m.process(ctx)
matchRequestTimer.Update(time.Since(start))
if doRuntimeStats {
@ -142,18 +155,18 @@ func GetPotentialMatches(ctx context.Context, backend MatcherBackend, firstBlock
type matcherEnv struct {
getLogStats runtimeStats // 64 bit aligned
ctx context.Context
backend MatcherBackend
backend *IndexView
params *Params
matcher matcher
firstBlock, lastBlock uint64
firstIndex, lastIndex uint64
firstMap, lastMap uint32
}
func (m *matcherEnv) process() ([]*types.Log, error) {
func (m *matcherEnv) process(ctx context.Context) ([]LogPosition, error) {
type task struct {
epochIndex uint32
logs []*types.Log
logs []LogPosition
err error
done chan struct{}
}
@ -182,7 +195,7 @@ func (m *matcherEnv) process() ([]*types.Log, error) {
}
firstEpoch, lastEpoch := m.firstMap>>m.params.logMapsPerEpoch, m.lastMap>>m.params.logMapsPerEpoch
var logs []*types.Log
var logs []LogPosition
// startEpoch is the next task to send whenever a worker can accept it.
// waitEpoch is the next task we are waiting for to finish in order to append
// results in the correct order.
@ -214,15 +227,17 @@ func (m *matcherEnv) process() ([]*types.Log, error) {
tasks[waitEpoch] = &task{epochIndex: waitEpoch, done: make(chan struct{})}
}
}
/*case <-ctx.Done(): //TODO
return nil, ctx.Err()*/
}
}
return logs, nil
}
// processEpoch returns the potentially matching logs from the given epoch.
func (m *matcherEnv) processEpoch(epochIndex uint32) ([]*types.Log, error) {
func (m *matcherEnv) processEpoch(epochIndex uint32) ([]LogPosition, error) {
start := time.Now()
var logs []*types.Log
var logs []LogPosition
// create a list of map indices to process
fm, lm := epochIndex<<m.params.logMapsPerEpoch, (epochIndex+1)<<m.params.logMapsPerEpoch-1
if fm < m.firstMap {
@ -260,22 +275,56 @@ func (m *matcherEnv) processEpoch(epochIndex uint32) ([]*types.Log, error) {
return logs, nil
}
func (m *matcherEnv) getLogPosition(lvIndex uint64) (LogPosition, error) {
// find possible block range based on map to block pointers
mapIndex := uint32(lvIndex >> m.params.logValuesPerMap)
lastBlockNumber, _, err := m.backend.GetLastBlockOfMap(mapIndex)
if err != nil {
return LogPosition{}, fmt.Errorf("failed to retrieve last block of map %d containing searched log value index %d: %v", mapIndex, lvIndex, err)
}
var firstBlockNumber uint64
if mapIndex > 0 {
firstBlockNumber, _, err = m.backend.GetLastBlockOfMap(mapIndex - 1)
if err != nil {
return LogPosition{}, fmt.Errorf("failed to retrieve last block of map %d before searched log value index %d: %v", mapIndex, lvIndex, err)
}
}
firstBlockNumber = max(firstBlockNumber, m.firstBlock)
lastBlockNumber = min(lastBlockNumber, m.lastBlock)
// find block with binary search based on block to log value index pointers
for firstBlockNumber < lastBlockNumber {
midBlockNumber := (firstBlockNumber + lastBlockNumber + 1) / 2
midLvPointer, err := m.backend.GetBlockLvPointer(midBlockNumber)
if err != nil {
return LogPosition{}, fmt.Errorf("failed to retrieve log value pointer of block %d while binary searching log value index %d: %v", midBlockNumber, lvIndex, err)
}
if lvIndex < midLvPointer {
lastBlockNumber = midBlockNumber - 1
} else {
firstBlockNumber = midBlockNumber
}
}
blockLvPointer, err := m.backend.GetBlockLvPointer(firstBlockNumber)
if err != nil {
return LogPosition{}, fmt.Errorf("failed to retrieve log value pointer of block %d containing searched log value index %d: %v", firstBlockNumber, lvIndex, err)
}
return LogPosition{BlockNumber: firstBlockNumber, startPtr: blockLvPointer, logPtr: lvIndex, valuesPerMap: m.params.valuesPerMap}, nil
}
// getLogsFromMatches returns the list of potentially matching logs located at
// the given list of matching log indices. Matches outside the firstIndex to
// lastIndex range are not returned.
func (m *matcherEnv) getLogsFromMatches(matches potentialMatches) ([]*types.Log, error) {
var logs []*types.Log
func (m *matcherEnv) getLogsFromMatches(matches potentialMatches) ([]LogPosition, error) {
var logs []LogPosition
for _, match := range matches {
if match < m.firstIndex || match > m.lastIndex {
continue
}
log, err := m.backend.GetLogByLvIndex(m.ctx, match)
log, err := m.getLogPosition(match)
if err != nil {
return logs, fmt.Errorf("failed to retrieve log at index %d: %v", match, err)
}
if log != nil {
logs = append(logs, log)
}
logs = append(logs, log)
matchLogLookup.Mark(1)
}
return logs, nil
@ -288,7 +337,7 @@ func (m *matcherEnv) getAllMatches(mapIndices []uint32) ([]potentialMatches, err
instance := m.matcher.newInstance(mapIndices)
resultsMap := make(map[uint32]potentialMatches)
for layerIndex := uint32(0); len(resultsMap) < len(mapIndices); layerIndex++ {
results, err := instance.getMatchesForLayer(m.ctx, layerIndex)
results, err := instance.getMatchesForLayer(layerIndex)
if err != nil {
return nil, err
}
@ -319,7 +368,7 @@ type matcher interface {
// a result has been returned has no effect. Exactly one matcherResult is
// returned per requested map index unless dropped.
type matcherInstance interface {
getMatchesForLayer(ctx context.Context, layerIndex uint32) ([]matcherResult, error)
getMatchesForLayer(layerIndex uint32) ([]matcherResult, error)
dropIndices(mapIndices []uint32)
}
@ -332,7 +381,7 @@ type matcherResult struct {
// singleMatcher implements matcher by returning matches for a single log value hash.
type singleMatcher struct {
backend MatcherBackend
backend *IndexView
value common.Hash
stats runtimeStats
}
@ -360,7 +409,7 @@ func (m *singleMatcher) newInstance(mapIndices []uint32) matcherInstance {
}
// getMatchesForLayer implements matcherInstance.
func (m *singleMatcherInstance) getMatchesForLayer(ctx context.Context, layerIndex uint32) (results []matcherResult, err error) {
func (m *singleMatcherInstance) getMatchesForLayer(layerIndex uint32) (results []matcherResult, err error) {
var st int
m.stats.setState(&st, stOther)
params := m.backend.GetParams()
@ -378,7 +427,7 @@ func (m *singleMatcherInstance) getMatchesForLayer(ctx context.Context, layerInd
} else {
m.stats.setState(&st, stFetchMore)
}
groupRows, err := m.backend.GetFilterMapRows(ctx, m.mapIndices[ptr:ptr+groupLength], rowIndex, layerIndex == 0)
groupRows, err := m.backend.GetFilterMapRows(m.mapIndices[ptr:ptr+groupLength], rowIndex, layerIndex)
if err != nil {
m.stats.setState(&st, stNone)
return nil, fmt.Errorf("failed to retrieve filter map %d row %d: %v", m.mapIndices[ptr], rowIndex, err)
@ -400,7 +449,7 @@ func (m *singleMatcherInstance) getMatchesForLayer(ctx context.Context, layerInd
}
m.stats.addAmount(st, int64(len(filterRow)))
filterRows = append(filterRows, filterRow)
if uint32(len(filterRow)) < params.maxRowLength(layerIndex) {
if uint32(len(filterRow)) < params.getMaxRowLength(layerIndex) {
m.stats.setState(&st, stProcess)
matches := params.potentialMatches(filterRows, mapIndex, m.value)
m.stats.addAmount(st, int64(len(matches)))
@ -416,6 +465,12 @@ func (m *singleMatcherInstance) getMatchesForLayer(ctx context.Context, layerInd
}
ptr += groupLength
}
if len(m.mapIndices) > 0 {
var r int
for _, res := range results {
r += len(res.matches)
}
}
m.cleanMapIndices()
m.stats.setState(&st, stNone)
return results, nil
@ -491,7 +546,7 @@ func (m matchAny) newInstance(mapIndices []uint32) matcherInstance {
}
// getMatchesForLayer implements matcherInstance.
func (m *matchAnyInstance) getMatchesForLayer(ctx context.Context, layerIndex uint32) (mergedResults []matcherResult, err error) {
func (m *matchAnyInstance) getMatchesForLayer(layerIndex uint32) (mergedResults []matcherResult, err error) {
if len(m.matchAny) == 0 {
// return "wild card" results (potentialMatches(nil) is interpreted as a
// potential match at every log value index of the map).
@ -504,7 +559,7 @@ func (m *matchAnyInstance) getMatchesForLayer(ctx context.Context, layerIndex ui
return mergedResults, nil
}
for i, childInstance := range m.childInstances {
results, err := childInstance.getMatchesForLayer(ctx, layerIndex)
results, err := childInstance.getMatchesForLayer(layerIndex)
if err != nil {
return nil, fmt.Errorf("failed to evaluate child matcher on layer %d: %v", layerIndex, err)
}
@ -698,19 +753,19 @@ type matchSequenceInstance struct {
}
// getMatchesForLayer implements matcherInstance.
func (m *matchSequenceInstance) getMatchesForLayer(ctx context.Context, layerIndex uint32) (matchedResults []matcherResult, err error) {
func (m *matchSequenceInstance) getMatchesForLayer(layerIndex uint32) (matchedResults []matcherResult, err error) {
// decide whether to evaluate base or next matcher first
baseFirst := m.baseFirst()
if baseFirst {
if err := m.evalBase(ctx, layerIndex); err != nil {
if err := m.evalBase(layerIndex); err != nil {
return nil, err
}
}
if err := m.evalNext(ctx, layerIndex); err != nil {
if err := m.evalNext(layerIndex); err != nil {
return nil, err
}
if !baseFirst {
if err := m.evalBase(ctx, layerIndex); err != nil {
if err := m.evalBase(layerIndex); err != nil {
return nil, err
}
}
@ -753,8 +808,8 @@ func (m *matchSequenceInstance) dropIndices(dropIndices []uint32) {
// evalBase evaluates the base child matcher and drops map indices from the
// next matcher if possible.
func (m *matchSequenceInstance) evalBase(ctx context.Context, layerIndex uint32) error {
results, err := m.baseInstance.getMatchesForLayer(ctx, layerIndex)
func (m *matchSequenceInstance) evalBase(layerIndex uint32) error {
results, err := m.baseInstance.getMatchesForLayer(layerIndex)
if err != nil {
return fmt.Errorf("failed to evaluate base matcher on layer %d: %v", layerIndex, err)
}
@ -781,8 +836,8 @@ func (m *matchSequenceInstance) evalBase(ctx context.Context, layerIndex uint32)
// evalNext evaluates the next child matcher and drops map indices from the
// base matcher if possible.
func (m *matchSequenceInstance) evalNext(ctx context.Context, layerIndex uint32) error {
results, err := m.nextInstance.getMatchesForLayer(ctx, layerIndex)
func (m *matchSequenceInstance) evalNext(layerIndex uint32) error {
results, err := m.nextInstance.getMatchesForLayer(layerIndex)
if err != nil {
return fmt.Errorf("failed to evaluate next matcher on layer %d: %v", layerIndex, err)
}

View file

@ -1,201 +0,0 @@
// Copyright 2024 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package filtermaps
import (
"context"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
)
// FilterMapsMatcherBackend implements MatcherBackend.
type FilterMapsMatcherBackend struct {
f *FilterMaps
// these fields should be accessed under f.matchersLock mutex.
validBlocks common.Range[uint64]
syncCh chan SyncRange
}
// NewMatcherBackend returns a FilterMapsMatcherBackend after registering it in
// the active matcher set.
// Note that Close should always be called when the matcher is no longer used.
func (f *FilterMaps) NewMatcherBackend() *FilterMapsMatcherBackend {
f.indexLock.RLock()
f.matchersLock.Lock()
defer func() {
f.matchersLock.Unlock()
f.indexLock.RUnlock()
}()
fm := &FilterMapsMatcherBackend{f: f}
if f.indexedRange.initialized {
fm.validBlocks = f.indexedRange.blocks
}
f.matchers[fm] = struct{}{}
return fm
}
// GetParams returns the filtermaps parameters.
// GetParams implements MatcherBackend.
func (fm *FilterMapsMatcherBackend) GetParams() *Params {
return &fm.f.Params
}
// Close removes the matcher from the set of active matchers and ensures that
// any SyncLogIndex calls are cancelled.
// Close implements MatcherBackend.
func (fm *FilterMapsMatcherBackend) Close() {
fm.f.matchersLock.Lock()
defer fm.f.matchersLock.Unlock()
delete(fm.f.matchers, fm)
}
// GetFilterMapRows returns the given row of the given map. If the row is empty
// then a non-nil zero length row is returned. If baseLayerOnly is true then
// only the first baseRowLength entries of the row are guaranteed to be
// returned.
// Note that the returned slices should not be modified, they should be copied
// on write.
// GetFilterMapRows implements MatcherBackend.
func (fm *FilterMapsMatcherBackend) GetFilterMapRows(ctx context.Context, mapIndices []uint32, rowIndex uint32, baseLayerOnly bool) ([]FilterRow, error) {
return fm.f.getFilterMapRows(mapIndices, rowIndex, baseLayerOnly)
}
// GetBlockLvPointer returns the starting log value index where the log values
// generated by the given block are located. If blockNumber is beyond the last
// indexed block then the pointer will point right after this block, ensuring
// that the matcher does not fail and can return a set of results where the
// valid range is correct.
// GetBlockLvPointer implements MatcherBackend.
func (fm *FilterMapsMatcherBackend) GetBlockLvPointer(ctx context.Context, blockNumber uint64) (uint64, error) {
fm.f.indexLock.RLock()
defer fm.f.indexLock.RUnlock()
if blockNumber >= fm.f.indexedRange.blocks.AfterLast() {
if fm.f.indexedRange.headIndexed {
// return index after head block
return fm.f.indexedRange.headDelimiter + 1, nil
}
if fm.f.indexedRange.blocks.Count() > 0 {
// return index at the beginning of the last, partially indexed
// block (after the last fully indexed one)
blockNumber = fm.f.indexedRange.blocks.Last()
}
}
return fm.f.getBlockLvPointer(blockNumber)
}
// GetLogByLvIndex returns the log at the given log value index.
// Note that this function assumes that the log index structure is consistent
// with the canonical chain at the point where the given log value index points.
// If this is not the case then an invalid result may be returned or certain
// logs might not be returned at all.
// No error is returned though because of an inconsistency between the chain and
// the log index. It is the caller's responsibility to verify this consistency
// using SyncLogIndex and re-process certain blocks if necessary.
// GetLogByLvIndex implements MatcherBackend.
func (fm *FilterMapsMatcherBackend) GetLogByLvIndex(ctx context.Context, lvIndex uint64) (*types.Log, error) {
fm.f.indexLock.RLock()
defer fm.f.indexLock.RUnlock()
return fm.f.getLogByLvIndex(lvIndex)
}
// synced signals to the matcher that has triggered a synchronisation that it
// has been finished and the log index is consistent with the chain head passed
// as a parameter.
//
// Note that if the log index head was far behind the chain head then it might not
// be synced up to the given head in a single step. Still, the latest chain head
// should be passed as a parameter and the existing log index should be consistent
// with that chain.
//
// Note: acquiring the indexLock read lock is unnecessary here, as this function
// is always called within the indexLoop.
func (fm *FilterMapsMatcherBackend) synced() {
fm.f.matchersLock.Lock()
defer fm.f.matchersLock.Unlock()
indexedBlocks := fm.f.indexedRange.blocks
if !fm.f.indexedRange.headIndexed && !indexedBlocks.IsEmpty() {
indexedBlocks.SetAfterLast(indexedBlocks.Last()) // remove partially indexed last block
}
fm.syncCh <- SyncRange{
IndexedView: fm.f.indexedView,
ValidBlocks: fm.validBlocks,
IndexedBlocks: indexedBlocks,
}
fm.validBlocks = indexedBlocks
fm.syncCh = nil
}
// SyncLogIndex ensures that the log index is consistent with the current state
// of the chain and is synced up to the current head. It blocks until this state
// is achieved or the context is cancelled.
// If successful, it returns a SyncRange that contains the latest chain head,
// the indexed range that is currently consistent with the chain and the valid
// range that has not been changed and has been consistent with all states of the
// chain since the previous SyncLogIndex or the creation of the matcher backend.
func (fm *FilterMapsMatcherBackend) SyncLogIndex(ctx context.Context) (SyncRange, error) {
syncCh := make(chan SyncRange, 1)
fm.f.matchersLock.Lock()
fm.syncCh = syncCh
fm.f.matchersLock.Unlock()
select {
case fm.f.matcherSyncCh <- fm:
case <-ctx.Done():
return SyncRange{}, ctx.Err()
case <-fm.f.disabledCh:
// Note: acquiring the indexLock read lock is unnecessary here,
// as the indexer has already been terminated.
return SyncRange{IndexedView: fm.f.indexedView}, nil
}
select {
case vr := <-syncCh:
return vr, nil
case <-ctx.Done():
return SyncRange{}, ctx.Err()
case <-fm.f.disabledCh:
// Note: acquiring the indexLock read lock is unnecessary here,
// as the indexer has already been terminated.
return SyncRange{IndexedView: fm.f.indexedView}, nil
}
}
// updateMatchersValidRange iterates through active matchers and limits their
// valid range with the current indexed range. This function should be called
// whenever a part of the log index has been removed, before adding new blocks
// to it.
//
// Note: acquiring the indexLock read lock is unnecessary here, as this function
// is always called within the indexLoop.
func (f *FilterMaps) updateMatchersValidRange() {
f.matchersLock.Lock()
defer f.matchersLock.Unlock()
for fm := range f.matchers {
if !f.indexedRange.initialized {
fm.validBlocks = common.Range[uint64]{}
continue
}
fm.validBlocks = fm.validBlocks.Intersection(f.indexedRange.blocks)
}
}

View file

@ -16,6 +16,7 @@
package filtermaps
/*
import (
"context"
crand "crypto/rand"
@ -85,3 +86,4 @@ func TestMatcher(t *testing.T) {
}
}
}
*/

View file

@ -21,66 +21,59 @@ import (
"encoding/binary"
"fmt"
"hash/fnv"
"iter"
"math"
"slices"
"sort"
"github.com/ethereum/go-ethereum/common"
)
// FilterRow encodes a single row of a filter map as a list of column indices.
// Note that the values are always stored in the same order as they were added
// and if the same column index is added twice, it is also stored twice.
// Order of column indices and potential duplications do not matter when searching
// for a value but leaving the original order makes reverting to a previous state
// simpler.
type FilterRow []uint32
// Params defines the basic parameters of the log index structure.
type Params struct {
logMapHeight uint // The number of bits required to represent the map height
logMapWidth uint // The number of bits required to represent the map width
logMapsPerEpoch uint // The number of bits required to represent the number of maps per epoch
logValuesPerMap uint // The number of bits required to represent the number of log values per map
// baseRowLengthRatio represents the ratio of base row length
// to the average row length.
baseRowLengthRatio uint
// logLayerDiff defines the logarithmic growth factor (base 2) of
// the maximum row length per layer. It indicates how much the maximum
// row length increases as the layer depth increases.
//
// Specifically:
// - the row length in base layer (layer == 0) is baseRowLength
// - the row length in layer x is baseRowLength << (logLayerDiff * x)
logLayerDiff uint
logMapHeight uint // The number of bits required to represent the map height
logMapWidth uint // The number of bits required to represent the map width
logMapsPerEpoch uint // The number of bits required to represent the number of maps per epoch
logValuesPerMap uint // The number of bits required to represent the number of log values per map
logMappingFrequency []uint
maxRowLength []uint32
// These fields can be derived with the information above
mapHeight uint32 // The number of rows in the filter map
mapsPerEpoch uint32 // The number of maps in an epoch
valuesPerMap uint64 // The number of log values marked on each filter map
baseRowLength uint32 // maximum number of log values per row on layer 0
mapHeight uint32 // The number of rows in the filter map
mapsPerEpoch uint32 // The number of maps in an epoch
valuesPerMap uint64 // The number of log values marked on each filter map
// baseRowGroupSize defines the number of base row entries grouped together
// as a single database entry in the local database to optimize storage
// and retrieval efficiency.
//
// This value can be configured based on the specific implementation.
baseRowGroupSize uint32
rowGroupSize []uint32
}
// DefaultParams is the set of parameters used on mainnet.
var DefaultParams = Params{
logMapHeight: 16,
logMapWidth: 24,
logMapsPerEpoch: 10,
logValuesPerMap: 16,
baseRowGroupSize: 32,
baseRowLengthRatio: 8,
logLayerDiff: 4,
logMapHeight: 16,
logMapWidth: 24,
logMapsPerEpoch: 10,
logValuesPerMap: 16,
logMappingFrequency: []uint{10, 6, 2, 0},
maxRowLength: []uint32{8, 168, 2728, 10920},
rowGroupSize: []uint32{256, 16, 1, 1},
}
// RangeTestParams puts one log value per epoch, ensuring block exact tail unindexing for testing
var RangeTestParams = Params{
logMapHeight: 4,
logMapWidth: 24,
logMapsPerEpoch: 0,
logValuesPerMap: 0,
baseRowGroupSize: 32,
baseRowLengthRatio: 16, // baseRowLength >= 1
logLayerDiff: 4,
logMapHeight: 4,
logMapWidth: 24,
logMapsPerEpoch: 0,
logValuesPerMap: 0,
logMappingFrequency: []uint{10, 6, 2, 0},
maxRowLength: []uint32{8, 168, 2728, 10920},
rowGroupSize: []uint32{16, 4, 1, 1},
}
// deriveFields calculates the derived fields of the parameter set.
@ -88,7 +81,25 @@ func (p *Params) deriveFields() {
p.mapHeight = uint32(1) << p.logMapHeight
p.mapsPerEpoch = uint32(1) << p.logMapsPerEpoch
p.valuesPerMap = uint64(1) << p.logValuesPerMap
p.baseRowLength = uint32(p.valuesPerMap * uint64(p.baseRowLengthRatio) / uint64(p.mapHeight))
}
// mapRowIndex calculates the unified storage index where the given row of the
// given map is stored. Note that this indexing scheme is the same as the one
// proposed in EIP-7745 for tree-hashing the filter map structure and for the
// same data proximity reasons it is also suitable for database representation.
// See also:
// https://eips.ethereum.org/EIPS/eip-7745#hash-tree-structure
func (p *Params) mapRowIndex(mapIndex, rowIndex uint32) uint64 {
epochIndex, mapSubIndex := mapIndex>>p.logMapsPerEpoch, mapIndex&(p.mapsPerEpoch-1)
return (uint64(epochIndex)<<p.logMapHeight+uint64(rowIndex))<<p.logMapsPerEpoch + uint64(mapSubIndex)
}
func (p *Params) getLogMappingFrequency(layerIndex uint32) uint {
return p.logMappingFrequency[min(layerIndex, uint32(len(p.logMappingFrequency)-1))]
}
func (p *Params) getMaxRowLength(layerIndex uint32) uint32 {
return p.maxRowLength[min(layerIndex, uint32(len(p.maxRowLength)-1))]
}
// addressValue returns the log value hash of a log emitting address.
@ -115,21 +126,37 @@ func (p *Params) sanitize() error {
if p.logMapWidth%8 != 0 {
return fmt.Errorf("invalid configuration: logMapWidth (%d) must be a multiple of 8", p.logMapWidth)
}
if p.baseRowGroupSize == 0 || (p.baseRowGroupSize&(p.baseRowGroupSize-1)) != 0 {
return fmt.Errorf("invalid configuration: baseRowGroupSize (%d) must be a power of 2", p.baseRowGroupSize)
if p.logMapWidth > 32 { // column index stored as uint32
return fmt.Errorf("invalid configuration: logMapWidth (%d) should not exceed 32", p.logMapWidth)
}
if p.logMapHeight > 16 { // row index stored as uint16 in finishedMap
return fmt.Errorf("invalid configuration: logMapHeight (%d) should not exceed 32", p.logMapHeight)
}
for _, maxRowLength := range p.maxRowLength {
if maxRowLength >= 0x10000 { // index wrap-around issue in finishedMap
return fmt.Errorf("invalid configuration: maxRowLength entry (%d) should not exceed 2**16", maxRowLength)
}
}
for _, groupSize := range p.rowGroupSize {
if groupSize == 0 || (groupSize&(groupSize-1)) != 0 {
return fmt.Errorf("invalid configuration: rowGroupSize entry (%d) must be a power of 2", groupSize)
}
}
if len(p.maxRowLength) != len(p.rowGroupSize) {
return fmt.Errorf("invalid configuration: length of maxRowLength (%d) and rowGroupSize entry (%d) should be equal", len(p.maxRowLength), len(p.rowGroupSize))
}
return nil
}
// mapGroupIndex returns the start index of the base row group that contains the
// given map index. Assumes baseRowGroupSize is a power of 2.
func (p *Params) mapGroupIndex(index uint32) uint32 {
return index & ^(p.baseRowGroupSize - 1)
func (p *Params) mapGroupIndex(index, dbLayer uint32) uint32 {
return index & ^(p.rowGroupSize[dbLayer] - 1)
}
// mapGroupOffset returns the offset of the given map index within its base row group.
func (p *Params) mapGroupOffset(index uint32) uint32 {
return index & (p.baseRowGroupSize - 1)
func (p *Params) mapGroupOffset(index, dbLayer uint32) uint32 {
return index & (p.rowGroupSize[dbLayer] - 1)
}
// mapEpoch returns the epoch number that the given map index belongs to.
@ -181,38 +208,12 @@ func (p *Params) columnIndex(lvIndex uint64, logValue *common.Hash) uint32 {
return uint32(lvIndex%p.valuesPerMap)<<hashBits + (uint32(hash>>(64-hashBits)) ^ uint32(hash)>>(32-hashBits))
}
// maxRowLength returns the maximum length filter rows are populated up to when
// using the given mapping layer.
//
// A log value can be marked on the map according to a given mapping layer if
// the row mapping on that layer points to a row that has not yet reached the
// maxRowLength belonging to that layer.
//
// This means that a row that is considered full on a given layer may still be
// extended further on a higher order layer.
//
// Each value is marked on the lowest order layer possible, assuming that marks
// are added in ascending log value index order. When searching for a log value
// one should consider all layers and process corresponding rows up until the
// first one where the row mapped to the given layer is not full.
func (p *Params) maxRowLength(layerIndex uint32) uint32 {
logLayerDiff := uint(layerIndex) * p.logLayerDiff
if logLayerDiff > p.logMapsPerEpoch {
logLayerDiff = p.logMapsPerEpoch
}
return p.baseRowLength << logLayerDiff
}
// maskedMapIndex returns the index used for row mapping calculation on the
// given layer. On layer zero the mapping changes once per epoch, then the
// frequency of re-mapping increases with every new layer until it reaches
// the frequency where it is different for every mapIndex.
func (p *Params) maskedMapIndex(mapIndex, layerIndex uint32) uint32 {
logLayerDiff := uint(layerIndex) * p.logLayerDiff
if logLayerDiff > p.logMapsPerEpoch {
logLayerDiff = p.logMapsPerEpoch
}
return mapIndex & (uint32(math.MaxUint32) << (p.logMapsPerEpoch - logLayerDiff))
return mapIndex & (uint32(math.MaxUint32) << p.getLogMappingFrequency(layerIndex))
}
// potentialMatches returns the list of log value indices potentially matching
@ -229,7 +230,7 @@ func (p *Params) potentialMatches(rows []FilterRow, mapIndex uint32, logValue co
results := make(potentialMatches, 0, 8)
mapFirst := uint64(mapIndex) << p.logValuesPerMap
for i, row := range rows {
rowLen, maxLen := len(row), int(p.maxRowLength(uint32(i)))
rowLen, maxLen := len(row), int(p.getMaxRowLength(uint32(i)))
if rowLen > maxLen {
rowLen = maxLen // any additional entries are generated by another log value on a higher mapping layer
}
@ -264,3 +265,177 @@ func (p *Params) potentialMatches(rows []FilterRow, mapIndex uint32, logValue co
// indices in the filter map range are potential matches. If there are no
// potential matches in the given map's range then an empty slice should be used.
type potentialMatches []uint64
func (p potentialMatches) Len() int { return len(p) }
func (p potentialMatches) Less(i, j int) bool { return p[i] < p[j] }
func (p potentialMatches) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
type rangeSet[T uint32 | uint64] []common.Range[T]
func (a rangeSet[T]) includes(v T) bool {
for _, r := range a {
if r.Includes(v) {
return true
}
}
return false
}
func (a rangeSet[T]) closestLte(v T) (last T, found bool) {
for _, r := range a {
if r.First() > v {
return
}
if r.AfterLast() > v {
return v, true
}
last, found = r.Last(), true
}
return
}
func (a rangeSet[T]) closestGte(v T) (last T, found bool) {
for _, r := range a {
if r.First() > v {
return r.First(), true
}
if r.AfterLast() > v {
return v, true
}
}
return
}
type rangeBoundary[T uint32 | uint64] struct {
v T
d int
}
type rangeBoundaries[T uint32 | uint64] []rangeBoundary[T]
func (rb *rangeBoundaries[T]) add(r common.Range[T], d int) {
*rb = append((*rb), rangeBoundary[T]{v: r.First(), d: d}, rangeBoundary[T]{v: r.AfterLast(), d: -d})
}
func (rb rangeBoundaries[T]) makeSet(threshold int) rangeSet[T] {
res := make(rangeSet[T], 0, len(rb)/2)
sort.Slice(rb, func(i, j int) bool {
return rb[i].v < rb[j].v
})
var (
sum int
lastCmp bool
start T
)
for i, r := range rb {
sum += r.d
cmp := sum >= threshold
if cmp != lastCmp && (i == len(rb)-1 || rb[i+1].v != r.v) {
if cmp {
start = r.v
} else {
res = append(res, common.NewRange[T](start, r.v-start))
}
lastCmp = cmp
}
}
return res
}
func (a rangeSet[T]) intersection(b rangeSet[T]) rangeSet[T] {
if len(a) == 0 || len(b) == 0 {
return nil
}
rb := make(rangeBoundaries[T], 0, (len(a)+len(b))*2)
for _, r := range a {
rb.add(r, 1)
}
for _, r := range b {
rb.add(r, 1)
}
return rb.makeSet(2)
}
func (a rangeSet[T]) exclude(b rangeSet[T]) rangeSet[T] {
if len(a) == 0 {
return nil
}
if len(b) == 0 {
return a
}
rb := make(rangeBoundaries[T], 0, (len(a)+len(b))*2)
for _, r := range a {
rb.add(r, 1)
}
for _, r := range b {
rb.add(r, -1)
}
return rb.makeSet(1)
}
func (a rangeSet[T]) union(b rangeSet[T]) rangeSet[T] {
if len(a) == 0 {
return b
}
if len(b) == 0 {
return a
}
rb := make(rangeBoundaries[T], 0, (len(a)+len(b))*2)
for _, r := range a {
rb.add(r, 1)
}
for _, r := range b {
rb.add(r, 1)
}
return rb.makeSet(1)
}
// iter iterates all integers in the range set.
func (r rangeSet[T]) iter() iter.Seq[T] {
return func(yield func(T) bool) {
for _, rr := range r {
for i := range rr.Iter() {
if !yield(i) {
break
}
}
}
}
}
func (a rangeSet[T]) count() T {
var count T
for _, r := range a {
count += r.Count()
}
return count
}
func (a rangeSet[T]) singleRange() common.Range[T] {
if len(a) > 1 {
panic("singleRange called for non-continuous rangeSet")
}
if len(a) == 1 {
return a[0]
}
return common.NewRange[T](0, 0)
}
func singleRangeSet[T uint32 | uint64](r common.Range[T]) rangeSet[T] {
if r.IsEmpty() {
return nil
}
return rangeSet[T]{r}
}
func (a rangeSet[T]) equal(b rangeSet[T]) bool {
if len(a) != len(b) {
return false
}
for i, r := range a {
if b[i] != r {
return false
}
}
return true
}

View file

@ -87,7 +87,7 @@ func TestPotentialMatches(t *testing.T) {
// split up into a list of rows if longer than allowed
var rows []FilterRow
for layerIndex := uint32(0); row != nil; layerIndex++ {
maxLen := int(params.maxRowLength(layerIndex))
maxLen := int(params.getMaxRowLength(layerIndex))
if len(row) > maxLen {
rows = append(rows, row[:maxLen])
row = row[maxLen:]

View file

@ -0,0 +1,171 @@
// Copyright 2025 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package filtermaps
import (
"slices"
"github.com/ethereum/go-ethereum/common"
)
// memoryMap is an in-memory representation of a filter map that represents rows
// as linked lists and stores entries in a single slice.
// memoryMap allows adding new elements and is used for rendering maps. Completed
// maps can be transformed to an immutable finishedMap.
type memoryMap struct {
entries []mmEntry // size = valuesPerMap
rows []mmRow // size = mapHeight
nextEntry uint32
blockPtrs []uint64
lastBlock lastBlockOfMap
}
// mmEntry is a linked list entry. The field next points to the next entry in the
// entries slice. Zero singals the end of the list (entries[0] is always the first
// entry of a list).
type mmEntry struct {
value, next uint32
}
// mmRow stores the index of the first and last entry of a linked list in the
// entries slice. If length is zero then first and last are not initialized.
// Note that length could be determined by traversing the linked list but it is
// explicitly stored because map rendering needs quick access to row length.
type mmRow struct {
first, last, length uint32
}
// newMemoryMap creates an empty memoryMap.
func (p *Params) newMemoryMap() *memoryMap {
return &memoryMap{
entries: make([]mmEntry, p.valuesPerMap),
rows: make([]mmRow, p.mapHeight),
}
}
// clone creates a copy of the map.
func (m *memoryMap) clone() *memoryMap {
return &memoryMap{
//TODO no need to clone entries if one of the maps will never be changed further.
entries: slices.Clone(m.entries),
rows: slices.Clone(m.rows),
nextEntry: m.nextEntry,
blockPtrs: slices.Clone(m.blockPtrs),
lastBlock: m.lastBlock,
}
}
func (m *memoryMap) firstBlock() uint64 {
return m.lastBlock.number + 1 - uint64(len(m.blockPtrs))
}
func (m *memoryMap) blocks() common.Range[uint64] {
l := uint64(len(m.blockPtrs))
return common.NewRange[uint64](m.lastBlock.number+1-l, l)
}
// addToRow adds a new entry to the specified row.
func (m *memoryMap) addToRow(rowIndex, value uint32) {
row := &m.rows[rowIndex]
if row.length == 0 {
row.first = m.nextEntry
} else {
m.entries[row.last].next = m.nextEntry
}
row.last = m.nextEntry
row.length++
m.entries[m.nextEntry].value = value
m.nextEntry++
}
// rowLength returns the length of a row.
func (m *memoryMap) rowLength(rowIndex uint32) uint32 {
return m.rows[rowIndex].length
}
// getRow returns a row of the map, truncated if maxLen is smaller than the actual
// row length.
func (m *memoryMap) getRow(rowIndex, maxLen uint32) FilterRow {
row := m.rows[rowIndex]
length := min(row.length, maxLen)
res := make(FilterRow, length)
next := row.first
for i := range length {
entry := m.entries[next]
res[i], next = entry.value, entry.next
}
return res
}
// finishedMap is an immutable memory representation of a single filter map.
// It is more compact and allows more efficient row lookup than memoryMap.
// Note that it assumes params.mapHeight <= 2**16 which is checked in deriveFields.
type finishedMap struct {
rowPtrs []uint16 // points to rowData index after end of row; 2**16 can wrap around to 0
rowData []uint32
blockPtrs []uint64
lastBlock lastBlockOfMap
}
// finished creates a new finishedMap from a memoryMap.
func (m *memoryMap) finished() *finishedMap {
fm := &finishedMap{
rowPtrs: make([]uint16, len(m.rows)),
rowData: make([]uint32, m.nextEntry),
blockPtrs: slices.Clone(m.blockPtrs),
lastBlock: m.lastBlock,
}
var ptr uint16
for i, row := range m.rows {
next := row.first
for range row.length {
entry := m.entries[next]
fm.rowData[ptr] = entry.value
next = entry.next
ptr++
}
fm.rowPtrs[i] = ptr
}
return fm
}
func (fm *finishedMap) firstBlock() uint64 {
return fm.lastBlock.number + 1 - uint64(len(fm.blockPtrs))
}
func (fm *finishedMap) blocks() common.Range[uint64] {
l := uint64(len(fm.blockPtrs))
return common.NewRange[uint64](fm.lastBlock.number+1-l, l)
}
// getRow returns a row of the map, truncated if maxLen is smaller than the actual
// row length. If the row has zero length then it returns a zero length slice.
func (fm *finishedMap) getRow(rowIndex, maxLen uint32) FilterRow {
var start uint16
if rowIndex > 0 {
start = fm.rowPtrs[rowIndex-1]
}
// Note: uint16 subtraction ensures correct result if
// rowPtrs[rowIndex] has wrapped around to zero. For example if there are
// exactly 2**16 entries and last row has a length of 5 then
// rowPtr[0xFFFE] == 0xFFFB and rowPtr[0xFFFF] == 0x0000 and the uint16
// subtraction is 0x0000-0xFFFB == 0x0005.
// Also note that it is guaranteed by the filter map design that no single
// row can have a length of 0x10000.
length := fm.rowPtrs[rowIndex] - start
return FilterRow(fm.rowData[start : uint32(start)+min(maxLen, uint32(length))])
}

View file

@ -0,0 +1,226 @@
// Copyright 2025 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package filtermaps
import (
"math"
"math/rand"
"reflect"
"slices"
"testing"
"github.com/ethereum/go-ethereum/common"
)
const (
testFilterMapRowsCount = 1000
testFilterMapsCount = 100
testPointersCount = 1000
testMaxBlocksPerMap = 4
)
type mapReader struct {
getFilterMapRows func(mapIndices []uint32, rowIndex, layerIndex uint32) ([]FilterRow, error)
getFilterMap func(mapIndex uint32) (*finishedMap, error)
getBlockLvPointer func(blockNumber uint64) (uint64, error)
getLastBlockOfMap func(mapIndex uint32) (uint64, common.Hash, error)
}
func equalRows(a, b []FilterRow) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if !slices.Equal(a[i], b[i]) {
return false
}
}
return true
}
func testMapReader(t *testing.T, testCase string, params *Params, reader mapReader, knownEpochs checkpointList, maps []*finishedMap) {
mapRange := common.NewRange[uint32](uint32(len(knownEpochs))*params.mapsPerEpoch, uint32(len(maps)))
for range testFilterMapRowsCount {
rowIndex := uint32(rand.Intn(int(params.mapHeight)))
lastDbLayer := uint32(rand.Intn(len(params.maxRowLength)))
maxLen := params.maxRowLength[lastDbLayer]
mid := mapRange.First() + uint32(rand.Intn(int(mapRange.Count())))
count := uint32(rand.Intn(int(params.mapsPerEpoch)))
first := max(mid, count/2) - count/2
testRange := common.NewRange[uint32](first, count)
mapIndices := make([]uint32, 0, testRange.Count())
expResults := make([]FilterRow, 0, testRange.Count())
for mapIndex := range testRange.Iter() {
if rand.Intn(2) == 1 {
mapIndices = append(mapIndices, mapIndex)
if mapRange.Includes(mapIndex) {
expResults = append(expResults, maps[mapIndex-mapRange.First()].getRow(rowIndex, maxLen))
} else {
expResults = append(expResults, nil)
}
}
}
rows, err := reader.getFilterMapRows(mapIndices, rowIndex, lastDbLayer)
if err != nil {
t.Fatalf("Test case %s: error reading %d map indices in range %d <= i < %d: %v", testCase, len(mapIndices), testRange.First(), testRange.AfterLast(), err)
} else if !equalRows(rows, expResults) {
t.Fatalf("Test case %s: incorrect results when reading %d map indices in range %d <= i < %d", testCase, len(mapIndices), testRange.First(), testRange.AfterLast())
}
}
for range testFilterMapsCount {
mapIndex := mapRange.First() + uint32(rand.Intn(int(mapRange.Count())))
expResults := maps[mapIndex-mapRange.First()]
fm, err := reader.getFilterMap(mapIndex)
if err != nil {
t.Fatalf("Test case %s: error reading map %d: %v", testCase, mapIndex, err)
} else if !reflect.DeepEqual(fm, expResults) {
t.Fatalf("Test case %s: incorrect results when reading map %d", testCase, mapIndex)
}
}
testPointers := func(mapIndex uint32, expLastBlock lastBlockOfMap, blockNumber, expBlockPtr uint64) {
lastBlock, lbHash, err := reader.getLastBlockOfMap(mapIndex)
if err != nil {
t.Fatalf("Test case %s: error reading last block of map %d: %v", testCase, mapIndex, err)
} else if (lastBlockOfMap{number: lastBlock, hash: lbHash}) != expLastBlock {
t.Fatalf("Test case %s: incorrect results when reading last block of map %d (expected %v, got %v)", testCase, mapIndex, expLastBlock, lastBlockOfMap{number: lastBlock, hash: lbHash})
}
if blockNumber != math.MaxUint64 {
blockPtr, err := reader.getBlockLvPointer(blockNumber)
if err != nil {
t.Fatalf("Test case %s: error reading lv pointer of block %d: %v", testCase, blockNumber, err)
} else if blockPtr != expBlockPtr {
t.Fatalf("Test case %s: incorrect results when reading lv pointer of block %d (expected %v, got %v)", testCase, blockNumber, expBlockPtr, blockPtr)
}
}
}
testNoPointer := func(mapIndex uint32) {
if _, _, err := reader.getLastBlockOfMap(mapIndex); err == nil {
t.Fatalf("Test case %s: unexpected last block of map pointer at map %d", testCase, mapIndex)
}
}
testKnownEpoch := func(epoch uint32) {
testNoPointer(params.firstEpochMap(epoch))
testPointers(params.lastEpochMap(epoch), lastBlockOfMap{number: knownEpochs[epoch].BlockNumber, hash: knownEpochs[epoch].BlockHash}, knownEpochs[epoch].BlockNumber, knownEpochs[epoch].FirstIndex)
}
if len(knownEpochs) > 0 {
for range testPointersCount {
epoch := uint32(rand.Intn(len(knownEpochs)))
testKnownEpoch(epoch)
}
testKnownEpoch(uint32(len(knownEpochs) - 1))
}
testMapPointers := func(mapIndex uint32, blockPos int) { // 0: first 1: random 2: last
fm := maps[mapIndex-mapRange.First()]
if len(fm.blockPtrs) == 0 {
testPointers(mapIndex, fm.lastBlock, math.MaxUint64, 0)
} else {
var subIndex int
switch blockPos {
case 1:
subIndex = rand.Intn(len(fm.blockPtrs))
case 2:
subIndex = len(fm.blockPtrs) - 1
}
testPointers(mapIndex, fm.lastBlock, fm.firstBlock()+uint64(subIndex), fm.blockPtrs[subIndex])
}
}
if !mapRange.IsEmpty() {
testMapPointers(mapRange.First(), 0)
for range testPointersCount {
testMapPointers(mapRange.First()+uint32(rand.Intn(int(mapRange.Count()))), 1)
}
testMapPointers(mapRange.Last(), 2)
}
testNoPointer(mapRange.AfterLast())
testNoPointer(params.lastEpochMap(params.mapEpoch(mapRange.AfterLast())))
testNoPointer(params.firstEpochMap(params.mapEpoch(mapRange.AfterLast()) + 1))
}
func generateTestMaps(params *Params, maps []*finishedMap, amount uint32) []*finishedMap {
var lastBlock lastBlockOfMap
genesis := len(maps) == 0
if !genesis {
lastBlock = maps[len(maps)-1].lastBlock
}
for range amount {
blockCount := rand.Intn(testMaxBlocksPerMap + 1)
if genesis && blockCount == 0 {
blockCount = 1
}
fm := &finishedMap{
rowPtrs: make([]uint16, params.mapHeight),
rowData: make([]uint32, params.valuesPerMap),
blockPtrs: make([]uint64, blockCount),
}
addValues := params.valuesPerMap
for addValues > 0 {
addToRow := uint64(rand.Intn(int(min(addValues+1, params.valuesPerMap/2))))
addValues -= addToRow
var rowIndex uint32
for {
rowIndex = uint32(rand.Intn(int(params.mapHeight)))
if uint64(fm.rowPtrs[rowIndex])+addToRow <= params.valuesPerMap/2 {
break
}
}
fm.rowPtrs[rowIndex] += uint16(addToRow)
}
var ptr uint16
for i, r := range fm.rowPtrs {
ptr += r
fm.rowPtrs[i] = ptr
}
for i := range fm.rowData {
fm.rowData[i] = uint32(rand.Intn(1 << params.logMapWidth))
}
if blockCount > 0 {
startPtr := uint64(len(maps)) * params.valuesPerMap
for i := range fm.blockPtrs {
if genesis {
genesis = false
} else {
lastBlock.number++
fm.blockPtrs[i] = startPtr + uint64(i)*params.valuesPerMap/uint64(blockCount) + uint64(rand.Intn(int(params.valuesPerMap)/blockCount/2))
}
}
rand.Read(lastBlock.hash[:])
}
fm.lastBlock = lastBlock
maps = append(maps, fm)
}
return maps
}
func generateEpochCheckpoint(mapIndex uint32, maps []*finishedMap) epochCheckpoint {
for len(maps[mapIndex].blockPtrs) == 0 {
mapIndex--
}
return epochCheckpoint{
BlockNumber: maps[mapIndex].lastBlock.number,
BlockHash: maps[mapIndex].lastBlock.hash,
FirstIndex: maps[mapIndex].blockPtrs[len(maps[mapIndex].blockPtrs)-1],
}
}
func generateTestCheckpoints(params *Params, maps []*finishedMap) checkpointList {
cpList := make(checkpointList, uint32(len(maps))/params.mapsPerEpoch)
for i := range cpList {
cpList[i] = generateEpochCheckpoint(params.lastEpochMap(uint32(i)), maps)
}
return cpList
}

View file

@ -320,12 +320,12 @@ func ReadCanonicalRawReceipt(db ethdb.Reader, blockHash common.Hash, blockNumber
// same data proximity reasons it is also suitable for database representation.
// See also:
// https://eips.ethereum.org/EIPS/eip-7745#hash-tree-structure
func ReadFilterMapExtRow(db ethdb.KeyValueReader, mapRowIndex uint64, bitLength uint) ([]uint32, error) {
func ReadFilterMapSingleRow(db ethdb.KeyValueReader, mapRowIndex uint64, dbLayer uint32, bitLength uint) ([]uint32, error) {
byteLength := int(bitLength) / 8
if int(bitLength) != byteLength*8 {
panic("invalid bit length")
}
key := filterMapRowKey(mapRowIndex, false)
key := filterMapRowKey(mapRowIndex, dbLayer)
has, err := db.Has(key)
if err != nil {
return nil, err
@ -349,18 +349,19 @@ func ReadFilterMapExtRow(db ethdb.KeyValueReader, mapRowIndex uint64, bitLength
return row, nil
}
func ReadFilterMapBaseRows(db ethdb.KeyValueReader, mapRowIndex uint64, rowCount uint32, bitLength uint) ([][]uint32, error) {
func ReadFilterMapRowGroup(db ethdb.KeyValueReader, mapRowIndex uint64, dbLayer, rowCount uint32, bitLength uint) ([][]uint32, error) {
byteLength := int(bitLength) / 8
if int(bitLength) != byteLength*8 {
panic("invalid bit length")
}
key := filterMapRowKey(mapRowIndex, true)
key := filterMapRowKey(mapRowIndex, dbLayer)
has, err := db.Has(key)
if err != nil {
return nil, err
}
rows := make([][]uint32, rowCount)
if !has {
//fmt.Println(" no entry")
return rows, nil
}
encRows, err := db.Get(key)
@ -368,6 +369,7 @@ func ReadFilterMapBaseRows(db ethdb.KeyValueReader, mapRowIndex uint64, rowCount
return nil, err
}
encLen := len(encRows)
//fmt.Println(" encLen", encLen)
var (
entryCount, entriesInRow, rowIndex, headerLen, headerBits int
headerByte byte
@ -411,7 +413,7 @@ func ReadFilterMapBaseRows(db ethdb.KeyValueReader, mapRowIndex uint64, rowCount
// WriteFilterMapExtRow stores an extended filter map row at the given mapRowIndex
// or deletes any existing entry if the row is empty.
func WriteFilterMapExtRow(db ethdb.KeyValueWriter, mapRowIndex uint64, row []uint32, bitLength uint) {
func WriteFilterMapSingleRow(db ethdb.KeyValueWriter, mapRowIndex uint64, dbLayer uint32, row []uint32, bitLength uint) {
byteLength := int(bitLength) / 8
if int(bitLength) != byteLength*8 {
panic("invalid bit length")
@ -424,16 +426,24 @@ func WriteFilterMapExtRow(db ethdb.KeyValueWriter, mapRowIndex uint64, row []uin
binary.LittleEndian.PutUint32(b[:], c)
copy(encRow[i*byteLength:(i+1)*byteLength], b[:byteLength])
}
err = db.Put(filterMapRowKey(mapRowIndex, false), encRow)
err = db.Put(filterMapRowKey(mapRowIndex, dbLayer), encRow)
} else {
err = db.Delete(filterMapRowKey(mapRowIndex, false))
err = db.Delete(filterMapRowKey(mapRowIndex, dbLayer))
}
if err != nil {
log.Crit("Failed to store extended filter map row", "err", err)
}
}
func WriteFilterMapBaseRows(db ethdb.KeyValueWriter, mapRowIndex uint64, rows [][]uint32, bitLength uint) {
func WriteFilterMapRowGroup(db ethdb.KeyValueWriter, mapRowIndex uint64, dbLayer uint32, rows [][]uint32, bitLength uint) {
/*var totalsize int //TODO
for _, row := range rows {
totalsize += len(row)
}
if totalsize > 0 {
fmt.Println("WriteFilterMapRowGroup", mapRowIndex, dbLayer, totalsize)
}*/
byteLength := int(bitLength) / 8
if int(bitLength) != byteLength*8 {
panic("invalid bit length")
@ -476,9 +486,10 @@ func WriteFilterMapBaseRows(db ethdb.KeyValueWriter, mapRowIndex uint64, rows []
addHeaderBit(false)
zeroBits--
}
err = db.Put(filterMapRowKey(mapRowIndex, true), encRows)
//fmt.Println(" write encLen", len(encRows))
err = db.Put(filterMapRowKey(mapRowIndex, dbLayer), encRows)
} else {
err = db.Delete(filterMapRowKey(mapRowIndex, true))
err = db.Delete(filterMapRowKey(mapRowIndex, dbLayer))
}
if err != nil {
log.Crit("Failed to store base filter map rows", "err", err)
@ -486,7 +497,7 @@ func WriteFilterMapBaseRows(db ethdb.KeyValueWriter, mapRowIndex uint64, rows []
}
func DeleteFilterMapRows(db ethdb.KeyValueStore, mapRows common.Range[uint64], hashScheme bool, stopCallback func(bool) bool) error {
return SafeDeleteRange(db, filterMapRowKey(mapRows.First(), false), filterMapRowKey(mapRows.AfterLast(), false), hashScheme, stopCallback)
return SafeDeleteRange(db, filterMapRowKey(mapRows.First(), 0), filterMapRowKey(mapRows.AfterLast(), 0), hashScheme, stopCallback)
}
// ReadFilterMapLastBlock retrieves the number of the block that generated the
@ -565,12 +576,9 @@ func DeleteBlockLvPointers(db ethdb.KeyValueStore, blocks common.Range[uint64],
// FilterMapsRange is a storage representation of the block range covered by the
// filter maps structure and the corresponting log value index range.
type FilterMapsRange struct {
Version uint32
HeadIndexed bool
HeadDelimiter uint64
BlocksFirst, BlocksAfterLast uint64
MapsFirst, MapsAfterLast uint32
TailPartialEpoch uint32
Version uint32
ValidMaps, DirtyMaps []uint32
KnownEpochs uint32
}
// ReadFilterMapsRange retrieves the filter maps range data. Note that if the

View file

@ -152,7 +152,7 @@ var (
SyncCommitteeKey = []byte("committee-") // bigEndian64(syncPeriod) -> serialized committee
// new log index
filterMapsPrefix = "fm-"
filterMapsPrefix = "fm*" //TODO fm-
filterMapsRangeKey = []byte(filterMapsPrefix + "R")
filterMapRowPrefix = []byte(filterMapsPrefix + "r") // filterMapRowPrefix + mapRowIndex (uint64 big endian) -> filter row
filterMapLastBlockPrefix = []byte(filterMapsPrefix + "b") // filterMapLastBlockPrefix + mapIndex (uint32 big endian) -> block number (uint64 big endian)
@ -353,15 +353,12 @@ func IsStorageTrieNode(key []byte) bool {
}
// filterMapRowKey = filterMapRowPrefix + mapRowIndex (uint64 big endian)
func filterMapRowKey(mapRowIndex uint64, base bool) []byte {
extLen := 8
if base {
extLen = 9
}
func filterMapRowKey(mapRowIndex uint64, dbLayer uint32) []byte {
l := len(filterMapRowPrefix)
key := make([]byte, l+extLen)
key := make([]byte, l+9)
copy(key[:l], filterMapRowPrefix)
binary.BigEndian.PutUint64(key[l:l+8], mapRowIndex)
key[l+8] = byte(dbLayer)
return key
}

View file

@ -459,7 +459,7 @@ func (b *EthAPIBackend) RPCTxFeeCap() float64 {
return b.eth.config.RPCTxFeeCap
}
func (b *EthAPIBackend) CurrentView() *filtermaps.ChainView {
func (b *EthAPIBackend) CurrentChainView() *filtermaps.ChainView {
head := b.eth.blockchain.CurrentBlock()
if head == nil {
return nil
@ -467,8 +467,8 @@ func (b *EthAPIBackend) CurrentView() *filtermaps.ChainView {
return filtermaps.NewChainView(b.eth.blockchain, head.Number.Uint64(), head.Hash())
}
func (b *EthAPIBackend) NewMatcherBackend() filtermaps.MatcherBackend {
return b.eth.filterMaps.NewMatcherBackend()
func (b *EthAPIBackend) GetIndexView(headBlockHash common.Hash) *filtermaps.IndexView {
return b.eth.logIndexer.GetIndexView(headBlockHash)
}
func (b *EthAPIBackend) Engine() consensus.Engine {

View file

@ -108,8 +108,7 @@ type Ethereum struct {
engine consensus.Engine
accountManager *accounts.Manager
filterMaps *filtermaps.FilterMaps
closeFilterMaps chan chan struct{}
logIndexer *filtermaps.Indexer
APIBackend *EthAPIBackend
@ -285,18 +284,8 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) {
ExportFileName: config.LogExportCheckpoints,
HashScheme: scheme == rawdb.HashScheme,
}
chainView := eth.newChainView(eth.blockchain.CurrentBlock())
historyCutoff, _ := eth.blockchain.HistoryPruningCutoff()
var finalBlock uint64
if fb := eth.blockchain.CurrentFinalBlock(); fb != nil {
finalBlock = fb.Number.Uint64()
}
filterMaps, err := filtermaps.NewFilterMaps(chainDb, chainView, historyCutoff, finalBlock, filtermaps.DefaultParams, fmConfig)
if err != nil {
return nil, err
}
eth.filterMaps = filterMaps
eth.closeFilterMaps = make(chan chan struct{})
eth.logIndexer = filtermaps.NewIndexer(chainDb, filtermaps.DefaultParams, fmConfig)
eth.blockchain.RegisterIndexer(eth.logIndexer, "log index")
// TxPool
if config.TxPool.Journal != "" {
@ -452,71 +441,9 @@ func (s *Ethereum) Start() error {
// Start the connection manager
s.dropper.Start(s.p2pServer, func() bool { return !s.Synced() })
// start log indexer
s.filterMaps.Start()
go s.updateFilterMapsHeads()
return nil
}
func (s *Ethereum) newChainView(head *types.Header) *filtermaps.ChainView {
if head == nil {
return nil
}
return filtermaps.NewChainView(s.blockchain, head.Number.Uint64(), head.Hash())
}
func (s *Ethereum) updateFilterMapsHeads() {
headEventCh := make(chan core.ChainEvent, 10)
blockProcCh := make(chan bool, 10)
sub := s.blockchain.SubscribeChainEvent(headEventCh)
sub2 := s.blockchain.SubscribeBlockProcessingEvent(blockProcCh)
defer func() {
sub.Unsubscribe()
sub2.Unsubscribe()
for {
select {
case <-headEventCh:
case <-blockProcCh:
default:
return
}
}
}()
var head *types.Header
setHead := func(newHead *types.Header) {
if newHead == nil {
return
}
if head == nil || newHead.Hash() != head.Hash() {
head = newHead
chainView := s.newChainView(head)
historyCutoff, _ := s.blockchain.HistoryPruningCutoff()
var finalBlock uint64
if fb := s.blockchain.CurrentFinalBlock(); fb != nil {
finalBlock = fb.Number.Uint64()
}
s.filterMaps.SetTarget(chainView, historyCutoff, finalBlock)
}
}
setHead(s.blockchain.CurrentBlock())
for {
select {
case ev := <-headEventCh:
setHead(ev.Header)
case blockProc := <-blockProcCh:
s.filterMaps.SetBlockProcessing(blockProc)
case <-time.After(time.Second * 10):
setHead(s.blockchain.CurrentBlock())
case ch := <-s.closeFilterMaps:
close(ch)
return
}
}
}
func (s *Ethereum) setupDiscovery() error {
eth.StartENRUpdater(s.blockchain, s.p2pServer.LocalNode())
@ -575,10 +502,6 @@ func (s *Ethereum) Stop() error {
s.handler.Stop()
// Then stop everything else.
ch := make(chan struct{})
s.closeFilterMaps <- ch
<-ch
s.filterMaps.Stop()
s.txPool.Close()
s.blockchain.Stop()
s.engine.Close()

View file

@ -19,9 +19,11 @@ package filters
import (
"context"
"errors"
"fmt"
"math"
"math/big"
"slices"
"sync"
"time"
"github.com/ethereum/go-ethereum/common"
@ -43,7 +45,12 @@ type Filter struct {
block *common.Hash // Block hash if filtering a single block
begin, end int64 // Range interval if filtering multiple blocks
rangeLogsTestHook chan rangeLogsTestEvent
testFilterRanges []testFilterRange
}
type testFilterRange struct {
begin, end uint64
indexed bool
}
// NewRangeFilter creates a new filter which uses a bloom filter on blocks to
@ -146,272 +153,101 @@ func (f *Filter) Logs(ctx context.Context) ([]*types.Log, error) {
return f.rangeLogs(ctx, begin, end)
}
const (
rangeLogsTestDone = iota // zero range
rangeLogsTestSync // before sync; zero range
rangeLogsTestSynced // after sync; valid blocks range
rangeLogsTestIndexed // individual search range
rangeLogsTestUnindexed // individual search range
rangeLogsTestResults // results range after search iteration
rangeLogsTestReorg // results range trimmed by reorg
)
type rangeLogsTestEvent struct {
event int
blocks common.Range[uint64]
}
// searchSession represents a single search session.
type searchSession struct {
ctx context.Context
filter *Filter
mb filtermaps.MatcherBackend
syncRange filtermaps.SyncRange // latest synchronized state with the matcher
chainView *filtermaps.ChainView // can be more recent than the indexed view in syncRange
// block ranges always refer to the current chainView
firstBlock, lastBlock uint64 // specified search range; MaxUint64 means latest block
searchRange common.Range[uint64] // actual search range; end trimmed to latest head
matchRange common.Range[uint64] // range in which we have results (subset of searchRange)
matches []*types.Log // valid set of matches in matchRange
forceUnindexed bool // revert to unindexed search
}
// newSearchSession returns a new searchSession.
func newSearchSession(ctx context.Context, filter *Filter, mb filtermaps.MatcherBackend, firstBlock, lastBlock uint64) (*searchSession, error) {
s := &searchSession{
ctx: ctx,
filter: filter,
mb: mb,
firstBlock: firstBlock,
lastBlock: lastBlock,
}
// enforce a consistent state before starting the search in order to be able
// to determine valid range later
var err error
s.syncRange, err = s.mb.SyncLogIndex(s.ctx)
if err != nil {
return nil, err
}
if err := s.updateChainView(); err != nil {
return nil, err
}
return s, nil
}
// updateChainView updates to the latest view of the underlying chain and sets
// searchRange by replacing MaxUint64 (meaning latest block) with actual head
// number in the specified search range.
// If the session already had an existing chain view and set of matches then
// it also trims part of the match set that a chain reorg might have invalidated.
func (s *searchSession) updateChainView() error {
// update chain view based on current chain head (might be more recent than
// the indexed view of syncRange as the indexer updates it asynchronously
// with some delay
newChainView := s.filter.sys.backend.CurrentView()
if newChainView == nil {
return errors.New("head block not available")
}
head := newChainView.HeadNumber()
// update actual search range based on current head number
firstBlock, lastBlock := s.firstBlock, s.lastBlock
if firstBlock == math.MaxUint64 {
firstBlock = head
}
if lastBlock == math.MaxUint64 {
lastBlock = head
}
if firstBlock > lastBlock || lastBlock > head {
return errInvalidBlockRange
}
s.searchRange = common.NewRange(firstBlock, lastBlock+1-firstBlock)
// Trim existing match set in case a reorg may have invalidated some results
if !s.matchRange.IsEmpty() {
trimRange := newChainView.SharedRange(s.chainView).Intersection(s.searchRange)
s.matchRange, s.matches = s.trimMatches(trimRange, s.matchRange, s.matches)
}
s.chainView = newChainView
return nil
}
// trimMatches removes any entries from the specified set of matches that is
// outside the given range.
func (s *searchSession) trimMatches(trimRange, matchRange common.Range[uint64], matches []*types.Log) (common.Range[uint64], []*types.Log) {
newRange := matchRange.Intersection(trimRange)
if newRange == matchRange {
return matchRange, matches
}
if newRange.IsEmpty() {
return newRange, nil
}
for len(matches) > 0 && matches[0].BlockNumber < newRange.First() {
matches = matches[1:]
}
for len(matches) > 0 && matches[len(matches)-1].BlockNumber > newRange.Last() {
matches = matches[:len(matches)-1]
}
return newRange, matches
}
// searchInRange performs a single range search, either indexed or unindexed.
func (s *searchSession) searchInRange(r common.Range[uint64], indexed bool) (common.Range[uint64], []*types.Log, error) {
if indexed {
if s.filter.rangeLogsTestHook != nil {
s.filter.rangeLogsTestHook <- rangeLogsTestEvent{rangeLogsTestIndexed, r}
}
results, err := s.filter.indexedLogs(s.ctx, s.mb, r.First(), r.Last())
if err != nil && !errors.Is(err, filtermaps.ErrMatchAll) {
return common.Range[uint64]{}, nil, err
}
if err == nil {
// sync with filtermaps matcher
if s.filter.rangeLogsTestHook != nil {
s.filter.rangeLogsTestHook <- rangeLogsTestEvent{rangeLogsTestSync, common.Range[uint64]{}}
}
var syncErr error
if s.syncRange, syncErr = s.mb.SyncLogIndex(s.ctx); syncErr != nil {
return common.Range[uint64]{}, nil, syncErr
}
if s.filter.rangeLogsTestHook != nil {
s.filter.rangeLogsTestHook <- rangeLogsTestEvent{rangeLogsTestSynced, s.syncRange.ValidBlocks}
}
// discard everything that might be invalid
trimRange := s.syncRange.ValidBlocks.Intersection(s.chainView.SharedRange(s.syncRange.IndexedView))
matchRange, matches := s.trimMatches(trimRange, r, results)
return matchRange, matches, nil
}
// "match all" filters are not supported by filtermaps; fall back to
// unindexed search which is the most efficient in this case
s.forceUnindexed = true
// fall through to unindexed case
}
if s.filter.rangeLogsTestHook != nil {
s.filter.rangeLogsTestHook <- rangeLogsTestEvent{rangeLogsTestUnindexed, r}
}
matches, err := s.filter.unindexedLogs(s.ctx, s.chainView, r.First(), r.Last())
if err != nil {
return common.Range[uint64]{}, nil, err
}
return r, matches, nil
}
// doSearchIteration performs a search on a range missing from an incomplete set
// of results, adds the new section and removes invalidated entries.
func (s *searchSession) doSearchIteration() error {
switch {
case s.matchRange.IsEmpty():
// no results yet; try search in entire range
indexedSearchRange := s.searchRange.Intersection(s.syncRange.IndexedBlocks)
if s.forceUnindexed = indexedSearchRange.IsEmpty(); !s.forceUnindexed {
// indexed search on the intersection of indexed and searched range
matchRange, matches, err := s.searchInRange(indexedSearchRange, true)
if err != nil {
return err
}
s.matchRange = matchRange
s.matches = matches
return nil
} else {
// no intersection of indexed and searched range; unindexed search on
// the whole searched range
matchRange, matches, err := s.searchInRange(s.searchRange, false)
if err != nil {
return err
}
s.matchRange = matchRange
s.matches = matches
return nil
}
case !s.matchRange.IsEmpty() && s.matchRange.First() > s.searchRange.First():
// Results are available, but the tail section is missing. Perform an unindexed
// search for the missing tail, while still allowing indexed search for the head.
//
// The unindexed search is necessary because the tail portion of the indexes
// has been pruned.
tailRange := common.NewRange(s.searchRange.First(), s.matchRange.First()-s.searchRange.First())
_, tailMatches, err := s.searchInRange(tailRange, false)
if err != nil {
return err
}
s.matches = append(tailMatches, s.matches...)
s.matchRange = tailRange.Union(s.matchRange)
return nil
case !s.matchRange.IsEmpty() && s.matchRange.First() == s.searchRange.First() && s.searchRange.AfterLast() > s.matchRange.AfterLast():
// Results are available, but the head section is missing. Try to perform
// the indexed search for the missing head, or fallback to unindexed search
// if the tail portion of indexed range has been pruned.
headRange := common.NewRange(s.matchRange.AfterLast(), s.searchRange.AfterLast()-s.matchRange.AfterLast())
if !s.forceUnindexed {
indexedHeadRange := headRange.Intersection(s.syncRange.IndexedBlocks)
if !indexedHeadRange.IsEmpty() && indexedHeadRange.First() == headRange.First() {
headRange = indexedHeadRange
} else {
// The tail portion of the indexes has been pruned, falling back
// to unindexed search.
s.forceUnindexed = true
}
}
headMatchRange, headMatches, err := s.searchInRange(headRange, !s.forceUnindexed)
if err != nil {
return err
}
if headMatchRange.First() != s.matchRange.AfterLast() {
// improbable corner case, first part of new head range invalidated by tail unindexing
s.matches, s.matchRange = headMatches, headMatchRange
return nil
}
s.matches = append(s.matches, headMatches...)
s.matchRange = s.matchRange.Union(headMatchRange)
return nil
default:
panic("invalid search session state")
}
}
func (f *Filter) rangeLogs(ctx context.Context, firstBlock, lastBlock uint64) ([]*types.Log, error) {
if f.rangeLogsTestHook != nil {
defer func() {
f.rangeLogsTestHook <- rangeLogsTestEvent{rangeLogsTestDone, common.Range[uint64]{}}
close(f.rangeLogsTestHook)
}()
}
if firstBlock > lastBlock {
return nil, nil
}
mb := f.sys.backend.NewMatcherBackend()
defer mb.Close()
chainView := f.sys.backend.CurrentChainView()
session, err := newSearchSession(ctx, f, mb, firstBlock, lastBlock)
if firstBlock > chainView.HeadNumber() {
firstBlock = chainView.HeadNumber()
}
if lastBlock > chainView.HeadNumber() {
lastBlock = chainView.HeadNumber()
}
indexView := f.sys.backend.GetIndexView(chainView.BlockHash(chainView.HeadNumber()))
if indexView == nil {
return f.unindexedLogs(ctx, chainView, firstBlock, lastBlock)
}
searchRange := common.NewRange[uint64](firstBlock, lastBlock+1-firstBlock)
indexedRange := indexView.BlockRange().Intersection(searchRange)
if indexedRange.IsEmpty() {
indexView.Release()
return f.unindexedLogs(ctx, chainView, firstBlock, lastBlock)
}
var (
res1, res2, res3 []*types.Log
err error
)
res2, err = f.indexedLogs(ctx, chainView, indexView, indexedRange.First(), indexedRange.Last())
indexView.Release()
if err == filtermaps.ErrMatchAll {
return f.unindexedLogs(ctx, chainView, firstBlock, lastBlock)
}
if err != nil {
return nil, err
}
for session.searchRange != session.matchRange {
if err := session.doSearchIteration(); err != nil {
if searchRange.First() < indexedRange.First() {
res1, err = f.unindexedLogs(ctx, chainView, searchRange.First(), indexedRange.First()-1)
if err != nil {
return nil, err
}
if f.rangeLogsTestHook != nil {
f.rangeLogsTestHook <- rangeLogsTestEvent{rangeLogsTestResults, session.matchRange}
}
mr := session.matchRange
if err := session.updateChainView(); err != nil {
return nil, err
}
if f.rangeLogsTestHook != nil && session.matchRange != mr {
f.rangeLogsTestHook <- rangeLogsTestEvent{rangeLogsTestReorg, session.matchRange}
}
}
return session.matches, nil
if indexedRange.Last() < searchRange.Last() {
res3, err = f.unindexedLogs(ctx, chainView, indexedRange.AfterLast(), searchRange.Last())
if err != nil {
return nil, err
}
}
return append(append(res1, res2...), res3...), nil
}
func (f *Filter) indexedLogs(ctx context.Context, mb filtermaps.MatcherBackend, begin, end uint64) ([]*types.Log, error) {
func (f *Filter) indexedLogs(ctx context.Context, chainView *filtermaps.ChainView, indexView *filtermaps.IndexView, begin, end uint64) ([]*types.Log, error) {
if f.testFilterRanges != nil {
f.testFilterRanges = append(f.testFilterRanges, testFilterRange{begin: begin, end: end, indexed: true})
}
start := time.Now()
potentialMatches, err := filtermaps.GetPotentialMatches(ctx, mb, begin, end, f.addresses, f.topics)
matches := filterLogs(potentialMatches, nil, nil, f.addresses, f.topics)
potentialMatches, err := filtermaps.GetPotentialMatches(ctx, indexView, begin, end, f.addresses, f.topics)
potentialLogs := make([]*types.Log, len(potentialMatches))
indexCh := make(chan int)
errCh := make(chan error, 1)
var wg sync.WaitGroup
worker := func() {
defer wg.Done()
for i := range indexCh {
logPosition := potentialMatches[i]
receipts := chainView.Receipts(logPosition.BlockNumber)
if receipts == nil {
select {
case errCh <- fmt.Errorf("receipts for block #%d not found", logPosition.BlockNumber):
default:
}
return
}
log, err := logPosition.GetLog(receipts)
if err != nil {
select {
case errCh <- err:
default:
}
return
}
potentialLogs[i] = log
}
}
for range 4 { //TODO
wg.Add(1)
go worker()
}
for i := range potentialMatches {
indexCh <- i
}
close(indexCh)
wg.Wait()
matches := filterLogs(potentialLogs, nil, nil, f.addresses, f.topics)
log.Trace("Performed indexed log search", "begin", begin, "end", end, "true matches", len(matches), "false positives", len(potentialMatches)-len(matches), "elapsed", common.PrettyDuration(time.Since(start)))
return matches, err
}
@ -419,6 +255,9 @@ func (f *Filter) indexedLogs(ctx context.Context, mb filtermaps.MatcherBackend,
// unindexedLogs returns the logs matching the filter criteria based on raw block
// iteration and bloom matching.
func (f *Filter) unindexedLogs(ctx context.Context, chainView *filtermaps.ChainView, begin, end uint64) ([]*types.Log, error) {
if f.testFilterRanges != nil {
f.testFilterRanges = append(f.testFilterRanges, testFilterRange{begin: begin, end: end, indexed: false})
}
start := time.Now()
log.Debug("Performing unindexed log search", "begin", begin, "end", end)
var matches []*types.Log
@ -517,7 +356,7 @@ func filterLogs(logs []*types.Log, fromBlock, toBlock *big.Int, addresses []comm
}
var ret []*types.Log
for _, log := range logs {
if check(log) {
if log != nil && check(log) {
ret = append(ret, log)
}
}

View file

@ -72,8 +72,8 @@ type Backend interface {
SubscribeRemovedLogsEvent(ch chan<- core.RemovedLogsEvent) event.Subscription
SubscribeLogsEvent(ch chan<- []*types.Log) event.Subscription
CurrentView() *filtermaps.ChainView
NewMatcherBackend() filtermaps.MatcherBackend
CurrentChainView() *filtermaps.ChainView
GetIndexView(headBlockHash common.Hash) *filtermaps.IndexView
}
// FilterSystem holds resources shared by all filters.

View file

@ -42,7 +42,7 @@ import (
type testBackend struct {
db ethdb.Database
fm *filtermaps.FilterMaps
logIndexer *filtermaps.Indexer
txFeed event.Feed
logsFeed event.Feed
rmLogsFeed event.Feed
@ -160,31 +160,51 @@ func (b *testBackend) SubscribeChainEvent(ch chan<- core.ChainEvent) event.Subsc
return b.chainFeed.Subscribe(ch)
}
func (b *testBackend) CurrentView() *filtermaps.ChainView {
func (b *testBackend) CurrentChainView() *filtermaps.ChainView {
head := b.CurrentBlock()
return filtermaps.NewChainView(b, head.Number.Uint64(), head.Hash())
}
func (b *testBackend) NewMatcherBackend() filtermaps.MatcherBackend {
return b.fm.NewMatcherBackend()
func (b *testBackend) GetIndexView(headBlockHash common.Hash) *filtermaps.IndexView {
return b.logIndexer.GetIndexView(headBlockHash)
}
func (b *testBackend) startFilterMaps(history uint64, disabled bool, params filtermaps.Params) {
head := b.CurrentBlock()
chainView := filtermaps.NewChainView(b, head.Number.Uint64(), head.Hash())
config := filtermaps.Config{
History: history,
Disabled: disabled,
ExportFileName: "",
}
b.fm, _ = filtermaps.NewFilterMaps(b.db, chainView, 0, 0, params, config)
b.fm.Start()
b.fm.WaitIdle()
b.logIndexer = filtermaps.NewIndexer(b.db, params, config)
if !disabled {
b.indexHead(head.Number.Uint64())
}
}
func (b *testBackend) indexHead(head uint64) {
header, _ := b.HeaderByNumber(context.Background(), rpc.BlockNumber(head))
receipts, _ := b.GetReceipts(context.Background(), header.Hash())
b.logIndexer.AddBlockData([]*types.Header{header}, []types.Receipts{receipts})
b.backfillBlocks()
}
func (b *testBackend) backfillBlocks() {
ready, needBlocks := b.logIndexer.Status()
for !ready || !needBlocks.IsEmpty() {
for !ready {
time.Sleep(time.Millisecond)
ready, needBlocks = b.logIndexer.Status()
}
header, _ := b.HeaderByNumber(context.Background(), rpc.BlockNumber(needBlocks.First()))
receipts, _ := b.GetReceipts(context.Background(), header.Hash())
ready, needBlocks = b.logIndexer.AddBlockData([]*types.Header{header}, []types.Receipts{receipts})
}
}
func (b *testBackend) stopFilterMaps() {
b.fm.Stop()
b.fm = nil
b.logIndexer.Stop()
b.logIndexer = nil
}
func (b *testBackend) setPending(block *types.Block, receipts types.Receipts) {

View file

@ -20,6 +20,7 @@ import (
"context"
"encoding/json"
"math/big"
"slices"
"strings"
"testing"
"time"
@ -431,173 +432,45 @@ func TestRangeLogs(t *testing.T) {
t.Fatal(err)
}
chain, _ := core.GenerateChain(gspec.Config, gspec.ToBlock(), ethash.NewFaker(), db, 1000, func(i int, gen *core.BlockGen) {})
options := core.DefaultConfig().WithStateScheme(rawdb.HashScheme)
options.TxLookupLimit = 0 // index all txs
bc, err := core.NewBlockChain(db, gspec, ethash.NewFaker(), options)
if err != nil {
t.Fatal(err)
}
_, err = bc.InsertChain(chain[:600])
if err != nil {
t.Fatal(err)
}
backend.startFilterMaps(200, false, filtermaps.RangeTestParams)
defer backend.stopFilterMaps()
var (
testCase, event int
filter *Filter
addresses = []common.Address{{}}
)
expEvent := func(expEvent int, expFirst, expAfterLast uint64) {
exp := rangeLogsTestEvent{expEvent, common.NewRange[uint64](expFirst, expAfterLast-expFirst)}
event++
ev := <-filter.rangeLogsTestHook
if ev != exp {
t.Fatalf("Test case #%d: wrong test event #%d received (got %v, expected %v)", testCase, event, ev, exp)
rangeTest := func(testCase int, begin, end int64, expRanges []testFilterRange) {
filter := sys.NewRangeFilter(begin, end, []common.Address{{}}, nil)
filter.testFilterRanges = []testFilterRange{}
filter.Logs(context.Background())
if !slices.Equal(filter.testFilterRanges, expRanges) {
t.Fatalf("Test case #%d: wrong list of filtered ranges received (got %v, expected %v)", testCase, filter.testFilterRanges, expRanges)
}
}
newFilter := func(begin, end int64) {
testCase++
event = 0
filter = sys.NewRangeFilter(begin, end, addresses, nil)
filter.rangeLogsTestHook = make(chan rangeLogsTestEvent)
go func(filter *Filter) {
filter.Logs(context.Background())
// ensure that filter will not be blocked if we exit early
for range filter.rangeLogsTestHook {
}
}(filter)
}
bc.InsertChain(chain[:600])
backend.startFilterMaps(200, false, filtermaps.RangeTestParams)
defer backend.stopFilterMaps()
updateHead := func() {
head := bc.CurrentBlock()
backend.fm.SetTarget(filtermaps.NewChainView(backend, head.Number.Uint64(), head.Hash()), 0, 0)
backend.fm.WaitIdle()
}
rangeTest(1, 0, 300, []testFilterRange{{0, 300, false}})
rangeTest(2, 300, 500, []testFilterRange{{400, 500, true}, {300, 399, false}})
rangeTest(3, 400, 10000, []testFilterRange{{400, 600, true}})
bc.SetCanonical(chain[99])
backend.logIndexer.Revert(100)
rangeTest(4, 0, 10000, []testFilterRange{{0, 100, false}})
backend.backfillBlocks()
bc.InsertChain(chain[100:120])
backend.indexHead(120)
rangeTest(5, 0, 10000, []testFilterRange{{0, 120, true}})
bc.InsertChain(chain[120:800])
backend.indexHead(800)
rangeTest(6, 400, 800, []testFilterRange{{600, 800, true}, {400, 599, false}})
backend.stopFilterMaps()
// test case #1
newFilter(300, 500)
expEvent(rangeLogsTestIndexed, 401, 501)
expEvent(rangeLogsTestSync, 0, 0)
expEvent(rangeLogsTestSynced, 401, 601)
expEvent(rangeLogsTestResults, 401, 501)
expEvent(rangeLogsTestUnindexed, 300, 401)
if _, err := bc.InsertChain(chain[600:700]); err != nil {
t.Fatal(err)
}
updateHead()
expEvent(rangeLogsTestResults, 300, 501)
expEvent(rangeLogsTestDone, 0, 0)
backend.startFilterMaps(0, true, filtermaps.RangeTestParams)
rangeTest(7, 0, 10000, []testFilterRange{{0, 800, false}})
backend.stopFilterMaps()
// test case #2
newFilter(400, int64(rpc.LatestBlockNumber))
expEvent(rangeLogsTestIndexed, 501, 701)
if _, err := bc.InsertChain(chain[700:800]); err != nil {
t.Fatal(err)
}
updateHead()
expEvent(rangeLogsTestSync, 0, 0)
expEvent(rangeLogsTestSynced, 601, 699)
expEvent(rangeLogsTestResults, 601, 699)
expEvent(rangeLogsTestUnindexed, 400, 601)
expEvent(rangeLogsTestResults, 400, 699)
expEvent(rangeLogsTestIndexed, 699, 801)
if _, err := bc.SetCanonical(chain[749]); err != nil { // set head to block 750
t.Fatal(err)
}
updateHead()
expEvent(rangeLogsTestSync, 0, 0)
expEvent(rangeLogsTestSynced, 601, 749)
expEvent(rangeLogsTestResults, 400, 749)
expEvent(rangeLogsTestIndexed, 749, 751)
expEvent(rangeLogsTestSync, 0, 0)
expEvent(rangeLogsTestSynced, 551, 751)
expEvent(rangeLogsTestResults, 400, 751)
expEvent(rangeLogsTestDone, 0, 0)
// test case #3
newFilter(int64(rpc.LatestBlockNumber), int64(rpc.LatestBlockNumber))
expEvent(rangeLogsTestIndexed, 750, 751)
if _, err := bc.SetCanonical(chain[739]); err != nil {
t.Fatal(err)
}
updateHead()
expEvent(rangeLogsTestSync, 0, 0)
expEvent(rangeLogsTestSynced, 551, 739)
expEvent(rangeLogsTestResults, 0, 0)
expEvent(rangeLogsTestIndexed, 740, 741)
if _, err := bc.InsertChain(chain[740:750]); err != nil {
t.Fatal(err)
}
updateHead()
expEvent(rangeLogsTestSync, 0, 0)
expEvent(rangeLogsTestSynced, 551, 739)
expEvent(rangeLogsTestResults, 0, 0)
expEvent(rangeLogsTestIndexed, 750, 751)
expEvent(rangeLogsTestSync, 0, 0)
expEvent(rangeLogsTestSynced, 551, 751)
expEvent(rangeLogsTestResults, 750, 751)
expEvent(rangeLogsTestDone, 0, 0)
// test case #4
if _, err := bc.SetCanonical(chain[499]); err != nil {
t.Fatal(err)
}
updateHead()
newFilter(400, int64(rpc.LatestBlockNumber))
expEvent(rangeLogsTestIndexed, 400, 501)
if _, err := bc.InsertChain(chain[500:650]); err != nil {
t.Fatal(err)
}
updateHead()
expEvent(rangeLogsTestSync, 0, 0)
expEvent(rangeLogsTestSynced, 451, 499)
expEvent(rangeLogsTestResults, 451, 499)
expEvent(rangeLogsTestUnindexed, 400, 451)
expEvent(rangeLogsTestResults, 400, 499)
// indexed head extension seems possible
expEvent(rangeLogsTestIndexed, 499, 651)
// further head extension causes tail unindexing in searched range
if _, err := bc.InsertChain(chain[650:750]); err != nil {
t.Fatal(err)
}
updateHead()
expEvent(rangeLogsTestSync, 0, 0)
expEvent(rangeLogsTestSynced, 551, 649)
// tail trimmed to 551; cannot merge with existing results
expEvent(rangeLogsTestResults, 551, 649)
expEvent(rangeLogsTestUnindexed, 400, 551)
expEvent(rangeLogsTestResults, 400, 649)
expEvent(rangeLogsTestIndexed, 649, 751)
expEvent(rangeLogsTestSync, 0, 0)
expEvent(rangeLogsTestSynced, 551, 751)
expEvent(rangeLogsTestResults, 400, 751)
expEvent(rangeLogsTestDone, 0, 0)
// test case #5
newFilter(400, int64(rpc.LatestBlockNumber))
expEvent(rangeLogsTestIndexed, 551, 751)
expEvent(rangeLogsTestSync, 0, 0)
expEvent(rangeLogsTestSynced, 551, 751)
expEvent(rangeLogsTestResults, 551, 751)
expEvent(rangeLogsTestUnindexed, 400, 551)
if _, err := bc.InsertChain(chain[750:1000]); err != nil {
t.Fatal(err)
}
updateHead()
expEvent(rangeLogsTestResults, 400, 751)
// indexed tail already beyond results head; revert to unindexed head search
expEvent(rangeLogsTestUnindexed, 751, 1001)
if _, err := bc.SetCanonical(chain[899]); err != nil {
t.Fatal(err)
}
updateHead()
expEvent(rangeLogsTestResults, 400, 1001)
expEvent(rangeLogsTestReorg, 400, 901)
expEvent(rangeLogsTestDone, 0, 0)
backend.startFilterMaps(0, false, filtermaps.RangeTestParams)
rangeTest(8, 0, 10000, []testFilterRange{{0, 800, true}})
}

View file

@ -690,10 +690,10 @@ func (b testBackend) SubscribeRemovedLogsEvent(ch chan<- core.RemovedLogsEvent)
func (b testBackend) SubscribeLogsEvent(ch chan<- []*types.Log) event.Subscription {
panic("implement me")
}
func (b testBackend) CurrentView() *filtermaps.ChainView {
func (b testBackend) CurrentChainView() *filtermaps.ChainView {
panic("implement me")
}
func (b testBackend) NewMatcherBackend() filtermaps.MatcherBackend {
func (b testBackend) GetIndexView(headBlockHash common.Hash) *filtermaps.IndexView {
panic("implement me")
}

View file

@ -99,8 +99,8 @@ type Backend interface {
SubscribeRemovedLogsEvent(ch chan<- core.RemovedLogsEvent) event.Subscription
SubscribeLogsEvent(ch chan<- []*types.Log) event.Subscription
CurrentView() *filtermaps.ChainView
NewMatcherBackend() filtermaps.MatcherBackend
CurrentChainView() *filtermaps.ChainView
GetIndexView(headBlockHash common.Hash) *filtermaps.IndexView
}
func GetAPIs(apiBackend Backend) []rpc.API {

View file

@ -407,7 +407,7 @@ func (b *backendMock) SubscribeRemovedLogsEvent(ch chan<- core.RemovedLogsEvent)
func (b *backendMock) Engine() consensus.Engine { return nil }
func (b *backendMock) CurrentView() *filtermaps.ChainView { return nil }
func (b *backendMock) NewMatcherBackend() filtermaps.MatcherBackend { return nil }
func (b *backendMock) CurrentChainView() *filtermaps.ChainView { return nil }
func (b *backendMock) GetIndexView(headBlockHash common.Hash) *filtermaps.IndexView { return nil }
func (b *backendMock) HistoryPruningCutoff() uint64 { return 0 }