Merge branch 'ethereum:master' into master

This commit is contained in:
CertiK 2025-10-11 16:01:30 +08:00 committed by GitHub
commit 624503cdcf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
70 changed files with 19429 additions and 11365 deletions

View file

@ -10,7 +10,7 @@ on:
jobs: jobs:
lint: lint:
name: Lint name: Lint
runs-on: self-hosted-ghr runs-on: [self-hosted-ghr, size-s-x64]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
@ -37,7 +37,7 @@ jobs:
test: test:
name: Test name: Test
needs: lint needs: lint
runs-on: self-hosted-ghr runs-on: [self-hosted-ghr, size-l-x64]
strategy: strategy:
matrix: matrix:
go: go:
@ -55,4 +55,4 @@ jobs:
cache: false cache: false
- name: Run tests - name: Run tests
run: go run build/ci.go test run: go run build/ci.go test -p 8

View file

@ -2,7 +2,6 @@ clone_depth: 5
version: "{branch}.{build}" version: "{branch}.{build}"
image: image:
- Ubuntu
- Visual Studio 2019 - Visual Studio 2019
environment: environment:
@ -17,25 +16,6 @@ install:
- go version - go version
for: for:
# Linux has its own script without -arch and -cc.
# The linux builder also runs lint.
- matrix:
only:
- image: Ubuntu
build_script:
- go run build/ci.go lint
- go run build/ci.go check_generate
- go run build/ci.go check_baddeps
- go run build/ci.go install -dlgo
test_script:
- go run build/ci.go test -dlgo -short
# linux/386 is disabled.
- matrix:
exclude:
- image: Ubuntu
GETH_ARCH: 386
# Windows builds for amd64 + 386. # Windows builds for amd64 + 386.
- matrix: - matrix:
only: only:
@ -56,4 +36,4 @@ for:
- go run build/ci.go archive -arch %GETH_ARCH% -type zip -signer WINDOWS_SIGNING_KEY -upload gethstore/builds - go run build/ci.go archive -arch %GETH_ARCH% -type zip -signer WINDOWS_SIGNING_KEY -upload gethstore/builds
- go run build/ci.go nsis -arch %GETH_ARCH% -signer WINDOWS_SIGNING_KEY -upload gethstore/builds - go run build/ci.go nsis -arch %GETH_ARCH% -signer WINDOWS_SIGNING_KEY -upload gethstore/builds
test_script: test_script:
- go run build/ci.go test -dlgo -arch %GETH_ARCH% -cc %GETH_CC% -short - go run build/ci.go test -dlgo -arch %GETH_ARCH% -cc %GETH_CC% -short -skip-spectests

View file

@ -289,12 +289,18 @@ func doTest(cmdline []string) {
race = flag.Bool("race", false, "Execute the race detector") race = flag.Bool("race", false, "Execute the race detector")
short = flag.Bool("short", false, "Pass the 'short'-flag to go test") short = flag.Bool("short", false, "Pass the 'short'-flag to go test")
cachedir = flag.String("cachedir", "./build/cache", "directory for caching downloads") cachedir = flag.String("cachedir", "./build/cache", "directory for caching downloads")
skipspectests = flag.Bool("skip-spectests", false, "Skip downloading execution-spec-tests fixtures")
threads = flag.Int("p", 1, "Number of CPU threads to use for testing")
) )
flag.CommandLine.Parse(cmdline) flag.CommandLine.Parse(cmdline)
// Get test fixtures. // Load checksums file (needed for both spec tests and dlgo)
csdb := download.MustLoadChecksums("build/checksums.txt") csdb := download.MustLoadChecksums("build/checksums.txt")
// Get test fixtures.
if !*skipspectests {
downloadSpecTestFixtures(csdb, *cachedir) downloadSpecTestFixtures(csdb, *cachedir)
}
// Configure the toolchain. // Configure the toolchain.
tc := build.GoToolchain{GOARCH: *arch, CC: *cc} tc := build.GoToolchain{GOARCH: *arch, CC: *cc}
@ -315,7 +321,7 @@ func doTest(cmdline []string) {
// Test a single package at a time. CI builders are slow // Test a single package at a time. CI builders are slow
// and some tests run into timeouts under load. // and some tests run into timeouts under load.
gotest.Args = append(gotest.Args, "-p", "1") gotest.Args = append(gotest.Args, "-p", fmt.Sprintf("%d", *threads))
if *coverage { if *coverage {
gotest.Args = append(gotest.Args, "-covermode=atomic", "-cover") gotest.Args = append(gotest.Args, "-covermode=atomic", "-cover")
} }

View file

@ -1,9 +1,10 @@
#!/bin/sh #!/bin/sh
hivechain generate \ hivechain generate \
--pos \
--fork-interval 6 \ --fork-interval 6 \
--tx-interval 1 \ --tx-interval 1 \
--length 500 \ --length 600 \
--outdir testdata \ --outdir testdata \
--lastfork cancun \ --lastfork prague \
--outputs accounts,genesis,chain,headstate,txinfo,headblock,headfcu,newpayload,forkenv --outputs accounts,genesis,chain,headstate,txinfo,headblock,headfcu,newpayload,forkenv

View file

@ -86,3 +86,9 @@ func protoOffset(proto Proto) uint64 {
panic("unhandled protocol") panic("unhandled protocol")
} }
} }
// msgTypePtr is the constraint for protocol message types.
type msgTypePtr[U any] interface {
*U
Kind() byte
}

View file

@ -86,9 +86,9 @@ func (s *Suite) TestSnapGetAccountRange(t *utesting.T) {
root: root, root: root,
startingHash: zero, startingHash: zero,
limitHash: ffHash, limitHash: ffHash,
expAccounts: 86, expAccounts: 67,
expFirst: firstKey, expFirst: firstKey,
expLast: common.HexToHash("0x445cb5c1278fdce2f9cbdb681bdd76c52f8e50e41dbd9e220242a69ba99ac099"), expLast: common.HexToHash("0x622e662246601dd04f996289ce8b85e86db7bb15bb17f86487ec9d543ddb6f9a"),
desc: "In this test, we request the entire state range, but limit the response to 4000 bytes.", desc: "In this test, we request the entire state range, but limit the response to 4000 bytes.",
}, },
{ {
@ -96,9 +96,9 @@ func (s *Suite) TestSnapGetAccountRange(t *utesting.T) {
root: root, root: root,
startingHash: zero, startingHash: zero,
limitHash: ffHash, limitHash: ffHash,
expAccounts: 65, expAccounts: 49,
expFirst: firstKey, expFirst: firstKey,
expLast: common.HexToHash("0x2e6fe1362b3e388184fd7bf08e99e74170b26361624ffd1c5f646da7067b58b6"), expLast: common.HexToHash("0x445cb5c1278fdce2f9cbdb681bdd76c52f8e50e41dbd9e220242a69ba99ac099"),
desc: "In this test, we request the entire state range, but limit the response to 3000 bytes.", desc: "In this test, we request the entire state range, but limit the response to 3000 bytes.",
}, },
{ {
@ -106,9 +106,9 @@ func (s *Suite) TestSnapGetAccountRange(t *utesting.T) {
root: root, root: root,
startingHash: zero, startingHash: zero,
limitHash: ffHash, limitHash: ffHash,
expAccounts: 44, expAccounts: 34,
expFirst: firstKey, expFirst: firstKey,
expLast: common.HexToHash("0x1c3f74249a4892081ba0634a819aec9ed25f34c7653f5719b9098487e65ab595"), expLast: common.HexToHash("0x2ef46ebd2073cecde499c2e8df028ad79a26d57bfaa812c4c6f7eb4c9617b913"),
desc: "In this test, we request the entire state range, but limit the response to 2000 bytes.", desc: "In this test, we request the entire state range, but limit the response to 2000 bytes.",
}, },
{ {
@ -177,9 +177,9 @@ The server should return the first available account.`,
root: root, root: root,
startingHash: firstKey, startingHash: firstKey,
limitHash: ffHash, limitHash: ffHash,
expAccounts: 86, expAccounts: 67,
expFirst: firstKey, expFirst: firstKey,
expLast: common.HexToHash("0x445cb5c1278fdce2f9cbdb681bdd76c52f8e50e41dbd9e220242a69ba99ac099"), expLast: common.HexToHash("0x622e662246601dd04f996289ce8b85e86db7bb15bb17f86487ec9d543ddb6f9a"),
desc: `In this test, startingHash is exactly the first available account key. desc: `In this test, startingHash is exactly the first available account key.
The server should return the first available account of the state as the first item.`, The server should return the first available account of the state as the first item.`,
}, },
@ -188,9 +188,9 @@ The server should return the first available account of the state as the first i
root: root, root: root,
startingHash: hashAdd(firstKey, 1), startingHash: hashAdd(firstKey, 1),
limitHash: ffHash, limitHash: ffHash,
expAccounts: 86, expAccounts: 67,
expFirst: secondKey, expFirst: secondKey,
expLast: common.HexToHash("0x4615e5f5df5b25349a00ad313c6cd0436b6c08ee5826e33a018661997f85ebaa"), expLast: common.HexToHash("0x66192e4c757fba1cdc776e6737008f42d50370d3cd801db3624274283bf7cd63"),
desc: `In this test, startingHash is after the first available key. desc: `In this test, startingHash is after the first available key.
The server should return the second account of the state as the first item.`, The server should return the second account of the state as the first item.`,
}, },
@ -226,9 +226,9 @@ server to return no data because genesis is older than 127 blocks.`,
root: s.chain.RootAt(int(s.chain.Head().Number().Uint64()) - 127), root: s.chain.RootAt(int(s.chain.Head().Number().Uint64()) - 127),
startingHash: zero, startingHash: zero,
limitHash: ffHash, limitHash: ffHash,
expAccounts: 84, expAccounts: 66,
expFirst: firstKey, expFirst: firstKey,
expLast: common.HexToHash("0x580aa878e2f92d113a12c0a3ce3c21972b03dbe80786858d49a72097e2c491a3"), expLast: common.HexToHash("0x729953a43ed6c913df957172680a17e5735143ad767bda8f58ac84ec62fbec5e"),
desc: `This test requests data at a state root that is 127 blocks old. desc: `This test requests data at a state root that is 127 blocks old.
We expect the server to have this state available.`, We expect the server to have this state available.`,
}, },
@ -657,8 +657,8 @@ The server should reject the request.`,
// It's a bit unfortunate these are hard-coded, but the result depends on // It's a bit unfortunate these are hard-coded, but the result depends on
// a lot of aspects of the state trie and can't be guessed in a simple // a lot of aspects of the state trie and can't be guessed in a simple
// way. So you'll have to update this when the test chain is changed. // way. So you'll have to update this when the test chain is changed.
common.HexToHash("0x3e963a69401a70224cbfb8c0cc2249b019041a538675d71ccf80c9328d114e2e"), common.HexToHash("0x5bdc0d6057b35642a16d27223ea5454e5a17a400e28f7328971a5f2a87773b76"),
common.HexToHash("0xd0670d09cdfbf3c6320eb3e92c47c57baa6c226551a2d488c05581091e6b1689"), common.HexToHash("0x0a76c9812ca90ffed8ee4d191e683f93386b6e50cfe3679c0760d27510aa7fc5"),
empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty,
empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty,
empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty, empty,
@ -678,8 +678,8 @@ The server should reject the request.`,
// be updated when the test chain is changed. // be updated when the test chain is changed.
expHashes: []common.Hash{ expHashes: []common.Hash{
empty, empty,
common.HexToHash("0xd0670d09cdfbf3c6320eb3e92c47c57baa6c226551a2d488c05581091e6b1689"), common.HexToHash("0x0a76c9812ca90ffed8ee4d191e683f93386b6e50cfe3679c0760d27510aa7fc5"),
common.HexToHash("0x3e963a69401a70224cbfb8c0cc2249b019041a538675d71ccf80c9328d114e2e"), common.HexToHash("0x5bdc0d6057b35642a16d27223ea5454e5a17a400e28f7328971a5f2a87773b76"),
}, },
}, },

View file

@ -196,6 +196,7 @@ to check if the node disconnects after receiving multiple invalid requests.`)
func (s *Suite) TestSimultaneousRequests(t *utesting.T) { func (s *Suite) TestSimultaneousRequests(t *utesting.T) {
t.Log(`This test requests blocks headers from the node, performing two requests t.Log(`This test requests blocks headers from the node, performing two requests
concurrently, with different request IDs.`) concurrently, with different request IDs.`)
conn, err := s.dialAndPeer(nil) conn, err := s.dialAndPeer(nil)
if err != nil { if err != nil {
t.Fatalf("peering failed: %v", err) t.Fatalf("peering failed: %v", err)
@ -235,37 +236,36 @@ concurrently, with different request IDs.`)
} }
// Wait for responses. // Wait for responses.
headers1 := new(eth.BlockHeadersPacket) // Note they can arrive in either order.
if err := conn.ReadMsg(ethProto, eth.BlockHeadersMsg, &headers1); err != nil { resp, err := collectResponses(conn, 2, func(msg *eth.BlockHeadersPacket) uint64 {
t.Fatalf("error reading block headers msg: %v", err) if msg.RequestId != 111 && msg.RequestId != 222 {
t.Fatalf("response with unknown request ID: %v", msg.RequestId)
} }
if got, want := headers1.RequestId, req1.RequestId; got != want { return msg.RequestId
t.Fatalf("unexpected request id in response: got %d, want %d", got, want) })
} if err != nil {
headers2 := new(eth.BlockHeadersPacket) t.Fatal(err)
if err := conn.ReadMsg(ethProto, eth.BlockHeadersMsg, &headers2); err != nil {
t.Fatalf("error reading block headers msg: %v", err)
}
if got, want := headers2.RequestId, req2.RequestId; got != want {
t.Fatalf("unexpected request id in response: got %d, want %d", got, want)
} }
// Check received headers for accuracy. // Check if headers match.
resp1 := resp[111]
if expected, err := s.chain.GetHeaders(req1); err != nil { if expected, err := s.chain.GetHeaders(req1); err != nil {
t.Fatalf("failed to get expected headers for request 1: %v", err) t.Fatalf("failed to get expected headers for request 1: %v", err)
} else if !headersMatch(expected, headers1.BlockHeadersRequest) { } else if !headersMatch(expected, resp1.BlockHeadersRequest) {
t.Fatalf("header mismatch: \nexpected %v \ngot %v", expected, headers1) t.Fatalf("header mismatch for request ID %v: \nexpected %v \ngot %v", 111, expected, resp1)
} }
resp2 := resp[222]
if expected, err := s.chain.GetHeaders(req2); err != nil { if expected, err := s.chain.GetHeaders(req2); err != nil {
t.Fatalf("failed to get expected headers for request 2: %v", err) t.Fatalf("failed to get expected headers for request 2: %v", err)
} else if !headersMatch(expected, headers2.BlockHeadersRequest) { } else if !headersMatch(expected, resp2.BlockHeadersRequest) {
t.Fatalf("header mismatch: \nexpected %v \ngot %v", expected, headers2) t.Fatalf("header mismatch for request ID %v: \nexpected %v \ngot %v", 222, expected, resp2)
} }
} }
func (s *Suite) TestSameRequestID(t *utesting.T) { func (s *Suite) TestSameRequestID(t *utesting.T) {
t.Log(`This test requests block headers, performing two concurrent requests with the t.Log(`This test requests block headers, performing two concurrent requests with the
same request ID. The node should handle the request by responding to both requests.`) same request ID. The node should handle the request by responding to both requests.`)
conn, err := s.dialAndPeer(nil) conn, err := s.dialAndPeer(nil)
if err != nil { if err != nil {
t.Fatalf("peering failed: %v", err) t.Fatalf("peering failed: %v", err)
@ -289,7 +289,7 @@ same request ID. The node should handle the request by responding to both reques
Origin: eth.HashOrNumber{ Origin: eth.HashOrNumber{
Number: 33, Number: 33,
}, },
Amount: 2, Amount: 3,
}, },
} }
@ -301,35 +301,52 @@ same request ID. The node should handle the request by responding to both reques
t.Fatalf("failed to write to connection: %v", err) t.Fatalf("failed to write to connection: %v", err)
} }
// Wait for the responses. // Wait for the responses. They can arrive in either order, and we can't tell them
headers1 := new(eth.BlockHeadersPacket) // apart by their request ID, so use the number of headers instead.
if err := conn.ReadMsg(ethProto, eth.BlockHeadersMsg, &headers1); err != nil { resp, err := collectResponses(conn, 2, func(msg *eth.BlockHeadersPacket) uint64 {
t.Fatalf("error reading from connection: %v", err) id := uint64(len(msg.BlockHeadersRequest))
if id != 2 && id != 3 {
t.Fatalf("invalid number of headers in response: %d", id)
} }
if got, want := headers1.RequestId, request1.RequestId; got != want { return id
t.Fatalf("unexpected request id: got %d, want %d", got, want) })
} if err != nil {
headers2 := new(eth.BlockHeadersPacket) t.Fatal(err)
if err := conn.ReadMsg(ethProto, eth.BlockHeadersMsg, &headers2); err != nil {
t.Fatalf("error reading from connection: %v", err)
}
if got, want := headers2.RequestId, request2.RequestId; got != want {
t.Fatalf("unexpected request id: got %d, want %d", got, want)
} }
// Check if headers match. // Check if headers match.
resp1 := resp[2]
if expected, err := s.chain.GetHeaders(request1); err != nil { if expected, err := s.chain.GetHeaders(request1); err != nil {
t.Fatalf("failed to get expected block headers: %v", err) t.Fatalf("failed to get expected headers for request 1: %v", err)
} else if !headersMatch(expected, headers1.BlockHeadersRequest) { } else if !headersMatch(expected, resp1.BlockHeadersRequest) {
t.Fatalf("header mismatch: \nexpected %v \ngot %v", expected, headers1) t.Fatalf("headers mismatch: \nexpected %v \ngot %v", expected, resp1)
} }
resp2 := resp[3]
if expected, err := s.chain.GetHeaders(request2); err != nil { if expected, err := s.chain.GetHeaders(request2); err != nil {
t.Fatalf("failed to get expected block headers: %v", err) t.Fatalf("failed to get expected headers for request 2: %v", err)
} else if !headersMatch(expected, headers2.BlockHeadersRequest) { } else if !headersMatch(expected, resp2.BlockHeadersRequest) {
t.Fatalf("header mismatch: \nexpected %v \ngot %v", expected, headers2) t.Fatalf("headers mismatch: \nexpected %v \ngot %v", expected, resp2)
} }
} }
// collectResponses waits for n messages of type T on the given connection.
// The messsages are collected according to the 'identity' function.
func collectResponses[T any, P msgTypePtr[T]](conn *Conn, n int, identity func(P) uint64) (map[uint64]P, error) {
resp := make(map[uint64]P, n)
for range n {
r := new(T)
if err := conn.ReadMsg(ethProto, eth.BlockHeadersMsg, r); err != nil {
return resp, fmt.Errorf("read error: %v", err)
}
id := identity(r)
if resp[id] != nil {
return resp, fmt.Errorf("duplicate response %v", r)
}
resp[id] = r
}
return resp, nil
}
func (s *Suite) TestZeroRequestID(t *utesting.T) { func (s *Suite) TestZeroRequestID(t *utesting.T) {
t.Log(`This test sends a GetBlockHeaders message with a request-id of zero, t.Log(`This test sends a GetBlockHeaders message with a request-id of zero,
and expects a response.`) and expects a response.`)
@ -887,7 +904,7 @@ func (s *Suite) makeBlobTxs(count, blobs int, discriminator byte) (txs types.Tra
from, nonce := s.chain.GetSender(5) from, nonce := s.chain.GetSender(5)
for i := 0; i < count; i++ { for i := 0; i < count; i++ {
// Make blob data, max of 2 blobs per tx. // Make blob data, max of 2 blobs per tx.
blobdata := make([]byte, blobs%3) blobdata := make([]byte, min(blobs, 2))
for i := range blobdata { for i := range blobdata {
blobdata[i] = discriminator blobdata[i] = discriminator
blobs -= 1 blobs -= 1

Binary file not shown.

View file

@ -1,20 +1,27 @@
{ {
"HIVE_CANCUN_TIMESTAMP": "840", "HIVE_CANCUN_BLOB_BASE_FEE_UPDATE_FRACTION": "3338477",
"HIVE_CANCUN_BLOB_MAX": "6",
"HIVE_CANCUN_BLOB_TARGET": "3",
"HIVE_CANCUN_TIMESTAMP": "60",
"HIVE_CHAIN_ID": "3503995874084926", "HIVE_CHAIN_ID": "3503995874084926",
"HIVE_FORK_ARROW_GLACIER": "60", "HIVE_FORK_ARROW_GLACIER": "0",
"HIVE_FORK_BERLIN": "48", "HIVE_FORK_BERLIN": "0",
"HIVE_FORK_BYZANTIUM": "18", "HIVE_FORK_BYZANTIUM": "0",
"HIVE_FORK_CONSTANTINOPLE": "24", "HIVE_FORK_CONSTANTINOPLE": "0",
"HIVE_FORK_GRAY_GLACIER": "66", "HIVE_FORK_GRAY_GLACIER": "0",
"HIVE_FORK_HOMESTEAD": "0", "HIVE_FORK_HOMESTEAD": "0",
"HIVE_FORK_ISTANBUL": "36", "HIVE_FORK_ISTANBUL": "0",
"HIVE_FORK_LONDON": "54", "HIVE_FORK_LONDON": "0",
"HIVE_FORK_MUIR_GLACIER": "42", "HIVE_FORK_MUIR_GLACIER": "0",
"HIVE_FORK_PETERSBURG": "30", "HIVE_FORK_PETERSBURG": "0",
"HIVE_FORK_SPURIOUS": "12", "HIVE_FORK_SPURIOUS": "0",
"HIVE_FORK_TANGERINE": "6", "HIVE_FORK_TANGERINE": "0",
"HIVE_MERGE_BLOCK_ID": "72", "HIVE_MERGE_BLOCK_ID": "0",
"HIVE_NETWORK_ID": "3503995874084926", "HIVE_NETWORK_ID": "3503995874084926",
"HIVE_SHANGHAI_TIMESTAMP": "780", "HIVE_PRAGUE_BLOB_BASE_FEE_UPDATE_FRACTION": "5007716",
"HIVE_TERMINAL_TOTAL_DIFFICULTY": "9454784" "HIVE_PRAGUE_BLOB_MAX": "9",
"HIVE_PRAGUE_BLOB_TARGET": "6",
"HIVE_PRAGUE_TIMESTAMP": "120",
"HIVE_SHANGHAI_TIMESTAMP": "0",
"HIVE_TERMINAL_TOTAL_DIFFICULTY": "131072"
} }

View file

@ -2,28 +2,35 @@
"config": { "config": {
"chainId": 3503995874084926, "chainId": 3503995874084926,
"homesteadBlock": 0, "homesteadBlock": 0,
"eip150Block": 6, "eip150Block": 0,
"eip155Block": 12, "eip155Block": 0,
"eip158Block": 12, "eip158Block": 0,
"byzantiumBlock": 18, "byzantiumBlock": 0,
"constantinopleBlock": 24, "constantinopleBlock": 0,
"petersburgBlock": 30, "petersburgBlock": 0,
"istanbulBlock": 36, "istanbulBlock": 0,
"muirGlacierBlock": 42, "muirGlacierBlock": 0,
"berlinBlock": 48, "berlinBlock": 0,
"londonBlock": 54, "londonBlock": 0,
"arrowGlacierBlock": 60, "arrowGlacierBlock": 0,
"grayGlacierBlock": 66, "grayGlacierBlock": 0,
"mergeNetsplitBlock": 72, "mergeNetsplitBlock": 0,
"shanghaiTime": 780, "shanghaiTime": 0,
"cancunTime": 840, "cancunTime": 60,
"terminalTotalDifficulty": 9454784, "pragueTime": 120,
"terminalTotalDifficulty": 131072,
"depositContractAddress": "0x0000000000000000000000000000000000000000",
"ethash": {}, "ethash": {},
"blobSchedule": { "blobSchedule": {
"cancun": { "cancun": {
"target": 3, "target": 3,
"max": 6, "max": 6,
"baseFeeUpdateFraction": 3338477 "baseFeeUpdateFraction": 3338477
},
"prague": {
"target": 6,
"max": 9,
"baseFeeUpdateFraction": 5007716
} }
} }
}, },
@ -35,6 +42,18 @@
"mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"coinbase": "0x0000000000000000000000000000000000000000", "coinbase": "0x0000000000000000000000000000000000000000",
"alloc": { "alloc": {
"00000961ef480eb55e80d19ad83579a64c007002": {
"code": "0x3373fffffffffffffffffffffffffffffffffffffffe1460cb5760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff146101f457600182026001905f5b5f82111560685781019083028483029004916001019190604d565b909390049250505036603814608857366101f457346101f4575f5260205ff35b34106101f457600154600101600155600354806003026004013381556001015f35815560010160203590553360601b5f5260385f601437604c5fa0600101600355005b6003546002548082038060101160df575060105b5f5b8181146101835782810160030260040181604c02815460601b8152601401816001015481526020019060020154807fffffffffffffffffffffffffffffffff00000000000000000000000000000000168252906010019060401c908160381c81600701538160301c81600601538160281c81600501538160201c81600401538160181c81600301538160101c81600201538160081c81600101535360010160e1565b910180921461019557906002556101a0565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff14156101cd57505f5b6001546002828201116101e25750505f6101e8565b01600290035b5f555f600155604c025ff35b5f5ffd",
"balance": "0x1"
},
"0000bbddc7ce488642fb579f8b00f3a590007251": {
"code": "0x3373fffffffffffffffffffffffffffffffffffffffe1460d35760115f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff1461019a57600182026001905f5b5f82111560685781019083028483029004916001019190604d565b9093900492505050366060146088573661019a573461019a575f5260205ff35b341061019a57600154600101600155600354806004026004013381556001015f358155600101602035815560010160403590553360601b5f5260605f60143760745fa0600101600355005b6003546002548082038060021160e7575060025b5f5b8181146101295782810160040260040181607402815460601b815260140181600101548152602001816002015481526020019060030154905260010160e9565b910180921461013b5790600255610146565b90505f6002555f6003555b5f54807fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff141561017357505f5b6001546001828201116101885750505f61018e565b01600190035b5f555f6001556074025ff35b5f5ffd",
"balance": "0x1"
},
"0000f90827f1c53a10cb7a02335b175320002935": {
"code": "0x3373fffffffffffffffffffffffffffffffffffffffe14604657602036036042575f35600143038111604257611fff81430311604257611fff9006545f5260205ff35b5f5ffd5b5f35611fff60014303065500",
"balance": "0x1"
},
"000f3df6d732807ef1319fb7b8bb8522d0beac02": { "000f3df6d732807ef1319fb7b8bb8522d0beac02": {
"code": "0x3373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500", "code": "0x3373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500",
"balance": "0x2a" "balance": "0x2a"
@ -81,6 +100,10 @@
"7435ed30a8b4aeb0877cef0c6e8cffe834eb865f": { "7435ed30a8b4aeb0877cef0c6e8cffe834eb865f": {
"balance": "0xc097ce7bc90715b34b9f1000000000" "balance": "0xc097ce7bc90715b34b9f1000000000"
}, },
"7dcd17433742f4c0ca53122ab541d0ba67fc27df": {
"code": "0x3680600080376000206000548082558060010160005560005263656d697460206000a2",
"balance": "0x0"
},
"83c7e323d189f18725ac510004fdc2941f8c4a78": { "83c7e323d189f18725ac510004fdc2941f8c4a78": {
"balance": "0xc097ce7bc90715b34b9f1000000000" "balance": "0xc097ce7bc90715b34b9f1000000000"
}, },
@ -112,7 +135,7 @@
"number": "0x0", "number": "0x0",
"gasUsed": "0x0", "gasUsed": "0x0",
"parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"baseFeePerGas": null, "baseFeePerGas": "0x3b9aca00",
"excessBlobGas": null, "excessBlobGas": null,
"blobGasUsed": null "blobGasUsed": null
} }

View file

@ -1,16 +1,16 @@
{ {
"parentHash": "0x96a73007443980c5e0985dfbb45279aa496dadea16918ad42c65c0bf8122ec39", "parentHash": "0x65151b101682b54cd08ba226f640c14c86176865ff9bfc57e0147dadaeac34bb",
"sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
"miner": "0x0000000000000000000000000000000000000000", "miner": "0x0000000000000000000000000000000000000000",
"stateRoot": "0xea4c1f4d9fa8664c22574c5b2f948a78c4b1a753cebc1861e7fb5b1aa21c5a94", "stateRoot": "0xce423ebc60fc7764a43f09f1fe3ae61eef25e3eb8d09b1108f7e7eb77dfff5e6",
"transactionsRoot": "0xecda39025fc4c609ce778d75eed0aa53b65ce1e3d1373b34bad8578cc31e5b48", "transactionsRoot": "0x7ec1ae3989efa75d7bcc766e5e2443afa8a89a5fda42ebba90050e7e702980f7",
"receiptsRoot": "0x056b23fbba480696b65fe5a59b8f2148a1299103c4f57df839233af2cf4ca2d2", "receiptsRoot": "0xfe160832b1ca85f38c6674cb0aae3a24693bc49be56e2ecdf3698b71a794de86",
"logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"difficulty": "0x0", "difficulty": "0x0",
"number": "0x1f4", "number": "0x258",
"gasLimit": "0x47e7c40", "gasLimit": "0x23f3e20",
"gasUsed": "0x5208", "gasUsed": "0x19d36",
"timestamp": "0x1388", "timestamp": "0x1770",
"extraData": "0x", "extraData": "0x",
"mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"nonce": "0x0000000000000000", "nonce": "0x0000000000000000",
@ -18,6 +18,7 @@
"withdrawalsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", "withdrawalsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
"blobGasUsed": "0x0", "blobGasUsed": "0x0",
"excessBlobGas": "0x0", "excessBlobGas": "0x0",
"parentBeaconBlockRoot": "0xf653da50cdff4733f13f7a5e338290e883bdf04adf3f112709728063ea965d6c", "parentBeaconBlockRoot": "0xf5003fc8f92358e790a114bce93ce1d9c283c85e1787f8d7d56714d3489b49e6",
"hash": "0x36a166f0dcd160fc5e5c61c9a7c2d7f236d9175bf27f43aaa2150e291f092ef7" "requestsHash": "0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"hash": "0xce8d86ba17a2ec303155f0e264c58a4b8f94ce3436274cf1924f91acdb7502d0"
} }

View file

@ -1,12 +1,12 @@
{ {
"jsonrpc": "2.0", "jsonrpc": "2.0",
"id": "fcu500", "id": "fcu600",
"method": "engine_forkchoiceUpdatedV3", "method": "engine_forkchoiceUpdatedV3",
"params": [ "params": [
{ {
"headBlockHash": "0x36a166f0dcd160fc5e5c61c9a7c2d7f236d9175bf27f43aaa2150e291f092ef7", "headBlockHash": "0xce8d86ba17a2ec303155f0e264c58a4b8f94ce3436274cf1924f91acdb7502d0",
"safeBlockHash": "0x36a166f0dcd160fc5e5c61c9a7c2d7f236d9175bf27f43aaa2150e291f092ef7", "safeBlockHash": "0xce8d86ba17a2ec303155f0e264c58a4b8f94ce3436274cf1924f91acdb7502d0",
"finalizedBlockHash": "0x36a166f0dcd160fc5e5c61c9a7c2d7f236d9175bf27f43aaa2150e291f092ef7" "finalizedBlockHash": "0xce8d86ba17a2ec303155f0e264c58a4b8f94ce3436274cf1924f91acdb7502d0"
}, },
null null
] ]

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -9,7 +9,7 @@ require (
require ( require (
github.com/StackExchange/wmi v1.2.1 // indirect github.com/StackExchange/wmi v1.2.1 // indirect
github.com/VictoriaMetrics/fastcache v1.12.2 // indirect github.com/VictoriaMetrics/fastcache v1.13.0 // indirect
github.com/bits-and-blooms/bitset v1.20.0 // indirect github.com/bits-and-blooms/bitset v1.20.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/consensys/gnark-crypto v0.18.0 // indirect github.com/consensys/gnark-crypto v0.18.0 // indirect
@ -24,7 +24,7 @@ require (
github.com/ferranbt/fastssz v0.1.4 // indirect github.com/ferranbt/fastssz v0.1.4 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
github.com/gofrs/flock v0.12.1 // indirect github.com/gofrs/flock v0.12.1 // indirect
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect github.com/golang/snappy v1.0.0 // indirect
github.com/holiman/bloomfilter/v2 v2.0.3 // indirect github.com/holiman/bloomfilter/v2 v2.0.3 // indirect
github.com/holiman/uint256 v1.3.2 // indirect github.com/holiman/uint256 v1.3.2 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect github.com/klauspost/cpuid/v2 v2.0.9 // indirect

View file

@ -2,15 +2,14 @@ github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ=
github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo=
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0=
github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI= github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU=
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8= github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8=
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU= github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU=
github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I=
@ -31,7 +30,6 @@ github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE
github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI=
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg= github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg=
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM= github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM= github.com/deckarep/golang-set/v2 v2.6.0 h1:XfcQbWM1LlMB8BsJ8N9vW5ehnnPVIw0je80NsVHagjM=
@ -61,9 +59,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao=
github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA=
github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
@ -112,8 +109,6 @@ github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU=
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe h1:nbdqkIGOGfUAD54q1s2YBcBz/WcsxCO9HUQ4aGV5hUw= github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe h1:nbdqkIGOGfUAD54q1s2YBcBz/WcsxCO9HUQ4aGV5hUw=
@ -134,7 +129,6 @@ golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=

View file

@ -394,7 +394,7 @@ func NewBlockChain(db ethdb.Database, genesis *Genesis, engine consensus.Engine,
bc.statedb = state.NewDatabase(bc.triedb, nil) bc.statedb = state.NewDatabase(bc.triedb, nil)
bc.validator = NewBlockValidator(chainConfig, bc) bc.validator = NewBlockValidator(chainConfig, bc)
bc.prefetcher = newStatePrefetcher(chainConfig, bc.hc) bc.prefetcher = newStatePrefetcher(chainConfig, bc.hc)
bc.processor = NewStateProcessor(chainConfig, bc.hc) bc.processor = NewStateProcessor(bc.hc)
genesisHeader := bc.GetHeaderByNumber(0) genesisHeader := bc.GetHeaderByNumber(0)
if genesisHeader == nil { if genesisHeader == nil {
@ -1690,7 +1690,12 @@ func (bc *BlockChain) writeBlockAndSetHead(block *types.Block, receipts []*types
// Set new head. // Set new head.
bc.writeHeadBlock(block) bc.writeHeadBlock(block)
bc.chainFeed.Send(ChainEvent{Header: block.Header()}) bc.chainFeed.Send(ChainEvent{
Header: block.Header(),
Receipts: receipts,
Transactions: block.Transactions(),
})
if len(logs) > 0 { if len(logs) > 0 {
bc.logsFeed.Send(logs) bc.logsFeed.Send(logs)
} }
@ -2342,6 +2347,13 @@ func (bc *BlockChain) recoverAncestors(block *types.Block, makeWitness bool) (co
// collectLogs collects the logs that were generated or removed during the // collectLogs collects the logs that were generated or removed during the
// processing of a block. These logs are later announced as deleted or reborn. // processing of a block. These logs are later announced as deleted or reborn.
func (bc *BlockChain) collectLogs(b *types.Block, removed bool) []*types.Log { func (bc *BlockChain) collectLogs(b *types.Block, removed bool) []*types.Log {
_, logs := bc.collectReceiptsAndLogs(b, removed)
return logs
}
// collectReceiptsAndLogs retrieves receipts from the database and returns both receipts and logs.
// This avoids duplicate database reads when both are needed.
func (bc *BlockChain) collectReceiptsAndLogs(b *types.Block, removed bool) ([]*types.Receipt, []*types.Log) {
var blobGasPrice *big.Int var blobGasPrice *big.Int
if b.ExcessBlobGas() != nil { if b.ExcessBlobGas() != nil {
blobGasPrice = eip4844.CalcBlobFee(bc.chainConfig, b.Header()) blobGasPrice = eip4844.CalcBlobFee(bc.chainConfig, b.Header())
@ -2359,7 +2371,7 @@ func (bc *BlockChain) collectLogs(b *types.Block, removed bool) []*types.Log {
logs = append(logs, log) logs = append(logs, log)
} }
} }
return logs return receipts, logs
} }
// reorg takes two blocks, an old chain and a new chain and will reconstruct the // reorg takes two blocks, an old chain and a new chain and will reconstruct the
@ -2588,8 +2600,14 @@ func (bc *BlockChain) SetCanonical(head *types.Block) (common.Hash, error) {
bc.writeHeadBlock(head) bc.writeHeadBlock(head)
// Emit events // Emit events
logs := bc.collectLogs(head, false) receipts, logs := bc.collectReceiptsAndLogs(head, false)
bc.chainFeed.Send(ChainEvent{Header: head.Header()})
bc.chainFeed.Send(ChainEvent{
Header: head.Header(),
Receipts: receipts,
Transactions: head.Transactions(),
})
if len(logs) > 0 { if len(logs) > 0 {
bc.logsFeed.Send(logs) bc.logsFeed.Send(logs)
} }

View file

@ -28,6 +28,8 @@ type RemovedLogsEvent struct{ Logs []*types.Log }
type ChainEvent struct { type ChainEvent struct {
Header *types.Header Header *types.Header
Receipts []*types.Receipt
Transactions []*types.Transaction
} }
type ChainHeadEvent struct { type ChainHeadEvent struct {

View file

@ -25,21 +25,16 @@ import (
"github.com/ethereum/go-ethereum/core/tracing" "github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/params"
"github.com/holiman/uint256" "github.com/holiman/uint256"
) )
// ChainContext supports retrieving headers and consensus parameters from the // ChainContext supports retrieving headers and consensus parameters from the
// current blockchain to be used during transaction processing. // current blockchain to be used during transaction processing.
type ChainContext interface { type ChainContext interface {
consensus.ChainHeaderReader
// Engine retrieves the chain's consensus engine. // Engine retrieves the chain's consensus engine.
Engine() consensus.Engine Engine() consensus.Engine
// GetHeader returns the header corresponding to the hash/number argument pair.
GetHeader(common.Hash, uint64) *types.Header
// Config returns the chain's configuration.
Config() *params.ChainConfig
} }
// NewEVMBlockContext creates a new context for use in the EVM. // NewEVMBlockContext creates a new context for use in the EVM.

View file

@ -664,15 +664,6 @@ func DeleteReceipts(db ethdb.KeyValueWriter, hash common.Hash, number uint64) {
} }
} }
// storedReceiptRLP is the storage encoding of a receipt.
// Re-definition in core/types/receipt.go.
// TODO: Re-use the existing definition.
type storedReceiptRLP struct {
PostStateOrStatus []byte
CumulativeGasUsed uint64
Logs []*types.Log
}
// ReceiptLogs is a barebone version of ReceiptForStorage which only keeps // ReceiptLogs is a barebone version of ReceiptForStorage which only keeps
// the list of logs. When decoding a stored receipt into this object we // the list of logs. When decoding a stored receipt into this object we
// avoid creating the bloom filter. // avoid creating the bloom filter.
@ -682,11 +673,11 @@ type receiptLogs struct {
// DecodeRLP implements rlp.Decoder. // DecodeRLP implements rlp.Decoder.
func (r *receiptLogs) DecodeRLP(s *rlp.Stream) error { func (r *receiptLogs) DecodeRLP(s *rlp.Stream) error {
var stored storedReceiptRLP var rs types.ReceiptForStorage
if err := s.Decode(&stored); err != nil { if err := rs.DecodeRLP(s); err != nil {
return err return err
} }
r.Logs = stored.Logs r.Logs = rs.Logs
return nil return nil
} }

View file

@ -46,6 +46,27 @@ func DeleteStateHistoryIndexMetadata(db ethdb.KeyValueWriter) {
} }
} }
// ReadTrienodeHistoryIndexMetadata retrieves the metadata of trienode history index.
func ReadTrienodeHistoryIndexMetadata(db ethdb.KeyValueReader) []byte {
data, _ := db.Get(headTrienodeHistoryIndexKey)
return data
}
// WriteTrienodeHistoryIndexMetadata stores the metadata of trienode history index
// into database.
func WriteTrienodeHistoryIndexMetadata(db ethdb.KeyValueWriter, blob []byte) {
if err := db.Put(headTrienodeHistoryIndexKey, blob); err != nil {
log.Crit("Failed to store the metadata of trienode history index", "err", err)
}
}
// DeleteTrienodeHistoryIndexMetadata removes the metadata of trienode history index.
func DeleteTrienodeHistoryIndexMetadata(db ethdb.KeyValueWriter) {
if err := db.Delete(headTrienodeHistoryIndexKey); err != nil {
log.Crit("Failed to delete the metadata of trienode history index", "err", err)
}
}
// ReadAccountHistoryIndex retrieves the account history index with the provided // ReadAccountHistoryIndex retrieves the account history index with the provided
// account address. // account address.
func ReadAccountHistoryIndex(db ethdb.KeyValueReader, addressHash common.Hash) []byte { func ReadAccountHistoryIndex(db ethdb.KeyValueReader, addressHash common.Hash) []byte {
@ -95,6 +116,30 @@ func DeleteStorageHistoryIndex(db ethdb.KeyValueWriter, addressHash common.Hash,
} }
} }
// ReadTrienodeHistoryIndex retrieves the trienode history index with the provided
// account address and storage key hash.
func ReadTrienodeHistoryIndex(db ethdb.KeyValueReader, addressHash common.Hash, path []byte) []byte {
data, err := db.Get(trienodeHistoryIndexKey(addressHash, path))
if err != nil || len(data) == 0 {
return nil
}
return data
}
// WriteTrienodeHistoryIndex writes the provided trienode history index into database.
func WriteTrienodeHistoryIndex(db ethdb.KeyValueWriter, addressHash common.Hash, path []byte, data []byte) {
if err := db.Put(trienodeHistoryIndexKey(addressHash, path), data); err != nil {
log.Crit("Failed to store trienode history index", "err", err)
}
}
// DeleteTrienodeHistoryIndex deletes the specified trienode index from the database.
func DeleteTrienodeHistoryIndex(db ethdb.KeyValueWriter, addressHash common.Hash, path []byte) {
if err := db.Delete(trienodeHistoryIndexKey(addressHash, path)); err != nil {
log.Crit("Failed to delete trienode history index", "err", err)
}
}
// ReadAccountHistoryIndexBlock retrieves the index block with the provided // ReadAccountHistoryIndexBlock retrieves the index block with the provided
// account address along with the block id. // account address along with the block id.
func ReadAccountHistoryIndexBlock(db ethdb.KeyValueReader, addressHash common.Hash, blockID uint32) []byte { func ReadAccountHistoryIndexBlock(db ethdb.KeyValueReader, addressHash common.Hash, blockID uint32) []byte {
@ -143,6 +188,30 @@ func DeleteStorageHistoryIndexBlock(db ethdb.KeyValueWriter, addressHash common.
} }
} }
// ReadTrienodeHistoryIndexBlock retrieves the index block with the provided state
// identifier along with the block id.
func ReadTrienodeHistoryIndexBlock(db ethdb.KeyValueReader, addressHash common.Hash, path []byte, blockID uint32) []byte {
data, err := db.Get(trienodeHistoryIndexBlockKey(addressHash, path, blockID))
if err != nil || len(data) == 0 {
return nil
}
return data
}
// WriteTrienodeHistoryIndexBlock writes the provided index block into database.
func WriteTrienodeHistoryIndexBlock(db ethdb.KeyValueWriter, addressHash common.Hash, path []byte, id uint32, data []byte) {
if err := db.Put(trienodeHistoryIndexBlockKey(addressHash, path, id), data); err != nil {
log.Crit("Failed to store trienode index block", "err", err)
}
}
// DeleteTrienodeHistoryIndexBlock deletes the specified index block from the database.
func DeleteTrienodeHistoryIndexBlock(db ethdb.KeyValueWriter, addressHash common.Hash, path []byte, id uint32) {
if err := db.Delete(trienodeHistoryIndexBlockKey(addressHash, path, id)); err != nil {
log.Crit("Failed to delete trienode index block", "err", err)
}
}
// increaseKey increase the input key by one bit. Return nil if the entire // increaseKey increase the input key by one bit. Return nil if the entire
// addition operation overflows. // addition operation overflows.
func increaseKey(key []byte) []byte { func increaseKey(key []byte) []byte {
@ -155,14 +224,26 @@ func increaseKey(key []byte) []byte {
return nil return nil
} }
// DeleteStateHistoryIndex completely removes all history indexing data, including // DeleteStateHistoryIndexes completely removes all history indexing data, including
// indexes for accounts and storages. // indexes for accounts and storages.
// func DeleteStateHistoryIndexes(db ethdb.KeyValueRangeDeleter) {
// Note, this method assumes the storage space with prefix `StateHistoryIndexPrefix` DeleteHistoryByRange(db, StateHistoryAccountMetadataPrefix)
// is exclusively occupied by the history indexing data! DeleteHistoryByRange(db, StateHistoryStorageMetadataPrefix)
func DeleteStateHistoryIndex(db ethdb.KeyValueRangeDeleter) { DeleteHistoryByRange(db, StateHistoryAccountBlockPrefix)
start := StateHistoryIndexPrefix DeleteHistoryByRange(db, StateHistoryStorageBlockPrefix)
limit := increaseKey(bytes.Clone(StateHistoryIndexPrefix)) }
// DeleteTrienodeHistoryIndexes completely removes all trienode history indexing data.
func DeleteTrienodeHistoryIndexes(db ethdb.KeyValueRangeDeleter) {
DeleteHistoryByRange(db, TrienodeHistoryMetadataPrefix)
DeleteHistoryByRange(db, TrienodeHistoryBlockPrefix)
}
// DeleteHistoryByRange completely removes all database entries with the specific prefix.
// Note, this method assumes the space with the given prefix is exclusively occupied!
func DeleteHistoryByRange(db ethdb.KeyValueRangeDeleter, prefix []byte) {
start := prefix
limit := increaseKey(bytes.Clone(prefix))
// Try to remove the data in the range by a loop, as the leveldb // Try to remove the data in the range by a loop, as the leveldb
// doesn't support the native range deletion. // doesn't support the native range deletion.

View file

@ -170,9 +170,11 @@ func ReadStateHistoryMetaList(db ethdb.AncientReaderOp, start uint64, count uint
return db.AncientRange(stateHistoryMeta, start-1, count, 0) return db.AncientRange(stateHistoryMeta, start-1, count, 0)
} }
// ReadStateAccountIndex retrieves the state root corresponding to the specified // ReadStateAccountIndex retrieves the account index blob for the specified
// state history. Compute the position of state history in freezer by minus one // state history. The index contains fixed-size entries with offsets and lengths
// since the id of first state history starts from one(zero for initial state). // into the concatenated account data table. Compute the position of state
// history in freezer by minus one since the id of first state history starts
// from one (zero for initial state).
func ReadStateAccountIndex(db ethdb.AncientReaderOp, id uint64) []byte { func ReadStateAccountIndex(db ethdb.AncientReaderOp, id uint64) []byte {
blob, err := db.Ancient(stateHistoryAccountIndex, id-1) blob, err := db.Ancient(stateHistoryAccountIndex, id-1)
if err != nil { if err != nil {
@ -181,9 +183,11 @@ func ReadStateAccountIndex(db ethdb.AncientReaderOp, id uint64) []byte {
return blob return blob
} }
// ReadStateStorageIndex retrieves the state root corresponding to the specified // ReadStateStorageIndex retrieves the storage index blob for the specified
// state history. Compute the position of state history in freezer by minus one // state history. The index contains fixed-size entries that locate storage slot
// since the id of first state history starts from one(zero for initial state). // data in the concatenated storage data table. Compute the position of state
// history in freezer by minus one since the id of first state history starts
// from one (zero for initial state).
func ReadStateStorageIndex(db ethdb.AncientReaderOp, id uint64) []byte { func ReadStateStorageIndex(db ethdb.AncientReaderOp, id uint64) []byte {
blob, err := db.Ancient(stateHistoryStorageIndex, id-1) blob, err := db.Ancient(stateHistoryStorageIndex, id-1)
if err != nil { if err != nil {
@ -192,9 +196,10 @@ func ReadStateStorageIndex(db ethdb.AncientReaderOp, id uint64) []byte {
return blob return blob
} }
// ReadStateAccountHistory retrieves the state root corresponding to the specified // ReadStateAccountHistory retrieves the concatenated account data blob for the
// state history. Compute the position of state history in freezer by minus one // specified state history. Offsets and lengths are resolved via the account
// since the id of first state history starts from one(zero for initial state). // index. Compute the position of state history in freezer by minus one since
// the id of first state history starts from one (zero for initial state).
func ReadStateAccountHistory(db ethdb.AncientReaderOp, id uint64) []byte { func ReadStateAccountHistory(db ethdb.AncientReaderOp, id uint64) []byte {
blob, err := db.Ancient(stateHistoryAccountData, id-1) blob, err := db.Ancient(stateHistoryAccountData, id-1)
if err != nil { if err != nil {
@ -203,9 +208,11 @@ func ReadStateAccountHistory(db ethdb.AncientReaderOp, id uint64) []byte {
return blob return blob
} }
// ReadStateStorageHistory retrieves the state root corresponding to the specified // ReadStateStorageHistory retrieves the concatenated storage slot data blob for
// state history. Compute the position of state history in freezer by minus one // the specified state history. Locations are resolved via the account and
// since the id of first state history starts from one(zero for initial state). // storage indexes. Compute the position of state history in freezer by minus
// one since the id of first state history starts from one (zero for initial
// state).
func ReadStateStorageHistory(db ethdb.AncientReaderOp, id uint64) []byte { func ReadStateStorageHistory(db ethdb.AncientReaderOp, id uint64) []byte {
blob, err := db.Ancient(stateHistoryStorageData, id-1) blob, err := db.Ancient(stateHistoryStorageData, id-1)
if err != nil { if err != nil {
@ -292,3 +299,76 @@ func WriteStateHistory(db ethdb.AncientWriter, id uint64, meta []byte, accountIn
}) })
return err return err
} }
// ReadTrienodeHistory retrieves the trienode history corresponding to the specified id.
// Compute the position of trienode history in freezer by minus one since the id of first
// trienode history starts from one(zero for initial state).
func ReadTrienodeHistory(db ethdb.AncientReaderOp, id uint64) ([]byte, []byte, []byte, error) {
header, err := db.Ancient(trienodeHistoryHeaderTable, id-1)
if err != nil {
return nil, nil, nil, err
}
keySection, err := db.Ancient(trienodeHistoryKeySectionTable, id-1)
if err != nil {
return nil, nil, nil, err
}
valueSection, err := db.Ancient(trienodeHistoryValueSectionTable, id-1)
if err != nil {
return nil, nil, nil, err
}
return header, keySection, valueSection, nil
}
// ReadTrienodeHistoryHeader retrieves the header section of trienode history.
func ReadTrienodeHistoryHeader(db ethdb.AncientReaderOp, id uint64) ([]byte, error) {
return db.Ancient(trienodeHistoryHeaderTable, id-1)
}
// ReadTrienodeHistoryKeySection retrieves the key section of trienode history.
func ReadTrienodeHistoryKeySection(db ethdb.AncientReaderOp, id uint64) ([]byte, error) {
return db.Ancient(trienodeHistoryKeySectionTable, id-1)
}
// ReadTrienodeHistoryValueSection retrieves the value section of trienode history.
func ReadTrienodeHistoryValueSection(db ethdb.AncientReaderOp, id uint64) ([]byte, error) {
return db.Ancient(trienodeHistoryValueSectionTable, id-1)
}
// ReadTrienodeHistoryList retrieves the a list of trienode history corresponding
// to the specified range.
// Compute the position of trienode history in freezer by minus one since the id
// of first trienode history starts from one(zero for initial state).
func ReadTrienodeHistoryList(db ethdb.AncientReaderOp, start uint64, count uint64) ([][]byte, [][]byte, [][]byte, error) {
header, err := db.AncientRange(trienodeHistoryHeaderTable, start-1, count, 0)
if err != nil {
return nil, nil, nil, err
}
keySection, err := db.AncientRange(trienodeHistoryKeySectionTable, start-1, count, 0)
if err != nil {
return nil, nil, nil, err
}
valueSection, err := db.AncientRange(trienodeHistoryValueSectionTable, start-1, count, 0)
if err != nil {
return nil, nil, nil, err
}
if len(header) != len(keySection) || len(header) != len(valueSection) {
return nil, nil, nil, errors.New("trienode history is corrupted")
}
return header, keySection, valueSection, nil
}
// WriteTrienodeHistory writes the provided trienode history to database.
// Compute the position of trienode history in freezer by minus one since
// the id of first state history starts from one(zero for initial state).
func WriteTrienodeHistory(db ethdb.AncientWriter, id uint64, header []byte, keySection []byte, valueSection []byte) error {
_, err := db.ModifyAncients(func(op ethdb.AncientWriteOp) error {
if err := op.AppendRaw(trienodeHistoryHeaderTable, id-1, header); err != nil {
return err
}
if err := op.AppendRaw(trienodeHistoryKeySectionTable, id-1, keySection); err != nil {
return err
}
return op.AppendRaw(trienodeHistoryValueSectionTable, id-1, valueSection)
})
return err
}

View file

@ -75,15 +75,38 @@ var stateFreezerTableConfigs = map[string]freezerTableConfig{
stateHistoryStorageData: {noSnappy: false, prunable: true}, stateHistoryStorageData: {noSnappy: false, prunable: true},
} }
const (
trienodeHistoryHeaderTable = "trienode.header"
trienodeHistoryKeySectionTable = "trienode.key"
trienodeHistoryValueSectionTable = "trienode.value"
)
// trienodeFreezerTableConfigs configures the settings for tables in the trienode freezer.
var trienodeFreezerTableConfigs = map[string]freezerTableConfig{
trienodeHistoryHeaderTable: {noSnappy: false, prunable: true},
// Disable snappy compression to allow efficient partial read.
trienodeHistoryKeySectionTable: {noSnappy: true, prunable: true},
// Disable snappy compression to allow efficient partial read.
trienodeHistoryValueSectionTable: {noSnappy: true, prunable: true},
}
// The list of identifiers of ancient stores. // The list of identifiers of ancient stores.
var ( var (
ChainFreezerName = "chain" // the folder name of chain segment ancient store. ChainFreezerName = "chain" // the folder name of chain segment ancient store.
MerkleStateFreezerName = "state" // the folder name of state history ancient store. MerkleStateFreezerName = "state" // the folder name of state history ancient store.
VerkleStateFreezerName = "state_verkle" // the folder name of state history ancient store. VerkleStateFreezerName = "state_verkle" // the folder name of state history ancient store.
MerkleTrienodeFreezerName = "trienode" // the folder name of trienode history ancient store.
VerkleTrienodeFreezerName = "trienode_verkle" // the folder name of trienode history ancient store.
) )
// freezers the collections of all builtin freezers. // freezers the collections of all builtin freezers.
var freezers = []string{ChainFreezerName, MerkleStateFreezerName, VerkleStateFreezerName} var freezers = []string{
ChainFreezerName,
MerkleStateFreezerName, VerkleStateFreezerName,
MerkleTrienodeFreezerName, VerkleTrienodeFreezerName,
}
// NewStateFreezer initializes the ancient store for state history. // NewStateFreezer initializes the ancient store for state history.
// //
@ -103,3 +126,22 @@ func NewStateFreezer(ancientDir string, verkle bool, readOnly bool) (ethdb.Reset
} }
return newResettableFreezer(name, "eth/db/state", readOnly, stateHistoryTableSize, stateFreezerTableConfigs) return newResettableFreezer(name, "eth/db/state", readOnly, stateHistoryTableSize, stateFreezerTableConfigs)
} }
// NewTrienodeFreezer initializes the ancient store for trienode history.
//
// - if the empty directory is given, initializes the pure in-memory
// trienode freezer (e.g. dev mode).
// - if non-empty directory is given, initializes the regular file-based
// trienode freezer.
func NewTrienodeFreezer(ancientDir string, verkle bool, readOnly bool) (ethdb.ResettableAncientStore, error) {
if ancientDir == "" {
return NewMemoryFreezer(readOnly, trienodeFreezerTableConfigs), nil
}
var name string
if verkle {
name = filepath.Join(ancientDir, VerkleTrienodeFreezerName)
} else {
name = filepath.Join(ancientDir, MerkleTrienodeFreezerName)
}
return newResettableFreezer(name, "eth/db/trienode", readOnly, stateHistoryTableSize, trienodeFreezerTableConfigs)
}

View file

@ -80,6 +80,10 @@ var (
// been indexed. // been indexed.
headStateHistoryIndexKey = []byte("LastStateHistoryIndex") headStateHistoryIndexKey = []byte("LastStateHistoryIndex")
// headTrienodeHistoryIndexKey tracks the ID of the latest state history that has
// been indexed.
headTrienodeHistoryIndexKey = []byte("LastTrienodeHistoryIndex")
// txIndexTailKey tracks the oldest block whose transactions have been indexed. // txIndexTailKey tracks the oldest block whose transactions have been indexed.
txIndexTailKey = []byte("TransactionIndexTail") txIndexTailKey = []byte("TransactionIndexTail")
@ -125,8 +129,10 @@ var (
StateHistoryIndexPrefix = []byte("m") // The global prefix of state history index data StateHistoryIndexPrefix = []byte("m") // The global prefix of state history index data
StateHistoryAccountMetadataPrefix = []byte("ma") // StateHistoryAccountMetadataPrefix + account address hash => account metadata StateHistoryAccountMetadataPrefix = []byte("ma") // StateHistoryAccountMetadataPrefix + account address hash => account metadata
StateHistoryStorageMetadataPrefix = []byte("ms") // StateHistoryStorageMetadataPrefix + account address hash + storage slot hash => slot metadata StateHistoryStorageMetadataPrefix = []byte("ms") // StateHistoryStorageMetadataPrefix + account address hash + storage slot hash => slot metadata
TrienodeHistoryMetadataPrefix = []byte("mt") // TrienodeHistoryMetadataPrefix + account address hash + trienode path => trienode metadata
StateHistoryAccountBlockPrefix = []byte("mba") // StateHistoryAccountBlockPrefix + account address hash + blockID => account block StateHistoryAccountBlockPrefix = []byte("mba") // StateHistoryAccountBlockPrefix + account address hash + blockID => account block
StateHistoryStorageBlockPrefix = []byte("mbs") // StateHistoryStorageBlockPrefix + account address hash + storage slot hash + blockID => slot block StateHistoryStorageBlockPrefix = []byte("mbs") // StateHistoryStorageBlockPrefix + account address hash + storage slot hash + blockID => slot block
TrienodeHistoryBlockPrefix = []byte("mbt") // TrienodeHistoryBlockPrefix + account address hash + trienode path + blockID => trienode block
// VerklePrefix is the database prefix for Verkle trie data, which includes: // VerklePrefix is the database prefix for Verkle trie data, which includes:
// (a) Trie nodes // (a) Trie nodes
@ -395,27 +401,34 @@ func storageHistoryIndexKey(addressHash common.Hash, storageHash common.Hash) []
return out return out
} }
// trienodeHistoryIndexKey = TrienodeHistoryMetadataPrefix + addressHash + trienode path
func trienodeHistoryIndexKey(addressHash common.Hash, path []byte) []byte {
totalLen := len(TrienodeHistoryMetadataPrefix) + common.HashLength + len(path)
out := make([]byte, totalLen)
off := 0
off += copy(out[off:], TrienodeHistoryMetadataPrefix)
off += copy(out[off:], addressHash.Bytes())
copy(out[off:], path)
return out
}
// accountHistoryIndexBlockKey = StateHistoryAccountBlockPrefix + addressHash + blockID // accountHistoryIndexBlockKey = StateHistoryAccountBlockPrefix + addressHash + blockID
func accountHistoryIndexBlockKey(addressHash common.Hash, blockID uint32) []byte { func accountHistoryIndexBlockKey(addressHash common.Hash, blockID uint32) []byte {
var buf4 [4]byte
binary.BigEndian.PutUint32(buf4[:], blockID)
totalLen := len(StateHistoryAccountBlockPrefix) + common.HashLength + 4 totalLen := len(StateHistoryAccountBlockPrefix) + common.HashLength + 4
out := make([]byte, totalLen) out := make([]byte, totalLen)
off := 0 off := 0
off += copy(out[off:], StateHistoryAccountBlockPrefix) off += copy(out[off:], StateHistoryAccountBlockPrefix)
off += copy(out[off:], addressHash.Bytes()) off += copy(out[off:], addressHash.Bytes())
copy(out[off:], buf4[:]) binary.BigEndian.PutUint32(out[off:], blockID)
return out return out
} }
// storageHistoryIndexBlockKey = StateHistoryStorageBlockPrefix + addressHash + storageHash + blockID // storageHistoryIndexBlockKey = StateHistoryStorageBlockPrefix + addressHash + storageHash + blockID
func storageHistoryIndexBlockKey(addressHash common.Hash, storageHash common.Hash, blockID uint32) []byte { func storageHistoryIndexBlockKey(addressHash common.Hash, storageHash common.Hash, blockID uint32) []byte {
var buf4 [4]byte
binary.BigEndian.PutUint32(buf4[:], blockID)
totalLen := len(StateHistoryStorageBlockPrefix) + 2*common.HashLength + 4 totalLen := len(StateHistoryStorageBlockPrefix) + 2*common.HashLength + 4
out := make([]byte, totalLen) out := make([]byte, totalLen)
@ -423,7 +436,21 @@ func storageHistoryIndexBlockKey(addressHash common.Hash, storageHash common.Has
off += copy(out[off:], StateHistoryStorageBlockPrefix) off += copy(out[off:], StateHistoryStorageBlockPrefix)
off += copy(out[off:], addressHash.Bytes()) off += copy(out[off:], addressHash.Bytes())
off += copy(out[off:], storageHash.Bytes()) off += copy(out[off:], storageHash.Bytes())
copy(out[off:], buf4[:]) binary.BigEndian.PutUint32(out[off:], blockID)
return out
}
// trienodeHistoryIndexBlockKey = TrienodeHistoryBlockPrefix + addressHash + trienode path + blockID
func trienodeHistoryIndexBlockKey(addressHash common.Hash, path []byte, blockID uint32) []byte {
totalLen := len(TrienodeHistoryBlockPrefix) + common.HashLength + len(path) + 4
out := make([]byte, totalLen)
off := 0
off += copy(out[off:], TrienodeHistoryBlockPrefix)
off += copy(out[off:], addressHash.Bytes())
off += copy(out[off:], path)
binary.BigEndian.PutUint32(out[off:], blockID)
return out return out
} }

View file

@ -35,18 +35,21 @@ import (
// //
// StateProcessor implements Processor. // StateProcessor implements Processor.
type StateProcessor struct { type StateProcessor struct {
config *params.ChainConfig // Chain configuration options chain ChainContext // Chain context interface
chain *HeaderChain // Canonical header chain
} }
// NewStateProcessor initialises a new StateProcessor. // NewStateProcessor initialises a new StateProcessor.
func NewStateProcessor(config *params.ChainConfig, chain *HeaderChain) *StateProcessor { func NewStateProcessor(chain ChainContext) *StateProcessor {
return &StateProcessor{ return &StateProcessor{
config: config,
chain: chain, chain: chain,
} }
} }
// chainConfig returns the chain configuration.
func (p *StateProcessor) chainConfig() *params.ChainConfig {
return p.chain.Config()
}
// Process processes the state changes according to the Ethereum rules by running // Process processes the state changes according to the Ethereum rules by running
// the transaction messages using the statedb and applying any rewards to both // the transaction messages using the statedb and applying any rewards to both
// the processor (coinbase) and any included uncles. // the processor (coinbase) and any included uncles.
@ -56,6 +59,7 @@ func NewStateProcessor(config *params.ChainConfig, chain *HeaderChain) *StatePro
// transactions failed to execute due to insufficient gas it will return an error. // transactions failed to execute due to insufficient gas it will return an error.
func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (*ProcessResult, error) { func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (*ProcessResult, error) {
var ( var (
config = p.chainConfig()
receipts types.Receipts receipts types.Receipts
usedGas = new(uint64) usedGas = new(uint64)
header = block.Header() header = block.Header()
@ -66,12 +70,12 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg
) )
// Mutate the block and state according to any hard-fork specs // Mutate the block and state according to any hard-fork specs
if p.config.DAOForkSupport && p.config.DAOForkBlock != nil && p.config.DAOForkBlock.Cmp(block.Number()) == 0 { if config.DAOForkSupport && config.DAOForkBlock != nil && config.DAOForkBlock.Cmp(block.Number()) == 0 {
misc.ApplyDAOHardFork(statedb) misc.ApplyDAOHardFork(statedb)
} }
var ( var (
context vm.BlockContext context vm.BlockContext
signer = types.MakeSigner(p.config, header.Number, header.Time) signer = types.MakeSigner(config, header.Number, header.Time)
) )
// Apply pre-execution system calls. // Apply pre-execution system calls.
@ -80,12 +84,12 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg
tracingStateDB = state.NewHookedState(statedb, hooks) tracingStateDB = state.NewHookedState(statedb, hooks)
} }
context = NewEVMBlockContext(header, p.chain, nil) context = NewEVMBlockContext(header, p.chain, nil)
evm := vm.NewEVM(context, tracingStateDB, p.config, cfg) evm := vm.NewEVM(context, tracingStateDB, config, cfg)
if beaconRoot := block.BeaconRoot(); beaconRoot != nil { if beaconRoot := block.BeaconRoot(); beaconRoot != nil {
ProcessBeaconBlockRoot(*beaconRoot, evm) ProcessBeaconBlockRoot(*beaconRoot, evm)
} }
if p.config.IsPrague(block.Number(), block.Time()) || p.config.IsVerkle(block.Number(), block.Time()) { if config.IsPrague(block.Number(), block.Time()) || config.IsVerkle(block.Number(), block.Time()) {
ProcessParentBlockHash(block.ParentHash(), evm) ProcessParentBlockHash(block.ParentHash(), evm)
} }
@ -106,10 +110,10 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg
} }
// Read requests if Prague is enabled. // Read requests if Prague is enabled.
var requests [][]byte var requests [][]byte
if p.config.IsPrague(block.Number(), block.Time()) { if config.IsPrague(block.Number(), block.Time()) {
requests = [][]byte{} requests = [][]byte{}
// EIP-6110 // EIP-6110
if err := ParseDepositLogs(&requests, allLogs, p.config); err != nil { if err := ParseDepositLogs(&requests, allLogs, config); err != nil {
return nil, fmt.Errorf("failed to parse deposit logs: %w", err) return nil, fmt.Errorf("failed to parse deposit logs: %w", err)
} }
// EIP-7002 // EIP-7002
@ -123,7 +127,7 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg
} }
// Finalize the block, applying any consensus engine specific extras (e.g. block rewards) // Finalize the block, applying any consensus engine specific extras (e.g. block rewards)
p.chain.engine.Finalize(p.chain, header, tracingStateDB, block.Body()) p.chain.Engine().Finalize(p.chain, header, tracingStateDB, block.Body())
return &ProcessResult{ return &ProcessResult{
Receipts: receipts, Receipts: receipts,

View file

@ -62,7 +62,7 @@ func ExecuteStateless(config *params.ChainConfig, vmconfig vm.Config, block *typ
headerCache: lru.NewCache[common.Hash, *types.Header](256), headerCache: lru.NewCache[common.Hash, *types.Header](256),
engine: beacon.New(ethash.NewFaker()), engine: beacon.New(ethash.NewFaker()),
} }
processor := NewStateProcessor(config, chain) processor := NewStateProcessor(chain)
validator := NewBlockValidator(config, nil) // No chain, we only validate the state, not the block validator := NewBlockValidator(config, nil) // No chain, we only validate the state, not the block
// Run the stateless blocks processing and self-validate certain fields // Run the stateless blocks processing and self-validate certain fields

View file

@ -984,17 +984,24 @@ func (pool *LegacyPool) Add(txs []*types.Transaction, sync bool) []error {
// addTxsLocked attempts to queue a batch of transactions if they are valid. // addTxsLocked attempts to queue a batch of transactions if they are valid.
// The transaction pool lock must be held. // The transaction pool lock must be held.
// Returns the error for each tx, and the set of accounts that might became promotable.
func (pool *LegacyPool) addTxsLocked(txs []*types.Transaction) ([]error, *accountSet) { func (pool *LegacyPool) addTxsLocked(txs []*types.Transaction) ([]error, *accountSet) {
dirty := newAccountSet(pool.signer) var (
errs := make([]error, len(txs)) dirty = newAccountSet(pool.signer)
errs = make([]error, len(txs))
valid int64
)
for i, tx := range txs { for i, tx := range txs {
replaced, err := pool.add(tx) replaced, err := pool.add(tx)
errs[i] = err errs[i] = err
if err == nil && !replaced { if err == nil {
if !replaced {
dirty.addTx(tx) dirty.addTx(tx)
} }
valid++
} }
validTxMeter.Mark(int64(len(dirty.accounts))) }
validTxMeter.Mark(valid)
return errs, dirty return errs, dirty
} }

View file

@ -240,7 +240,7 @@ type extblock struct {
// //
// The receipt's bloom must already calculated for the block's bloom to be // The receipt's bloom must already calculated for the block's bloom to be
// correctly calculated. // correctly calculated.
func NewBlock(header *Header, body *Body, receipts []*Receipt, hasher TrieHasher) *Block { func NewBlock(header *Header, body *Body, receipts []*Receipt, hasher ListHasher) *Block {
if body == nil { if body == nil {
body = &Body{} body = &Body{}
} }

View file

@ -27,7 +27,7 @@ import (
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
) )
// hasherPool holds LegacyKeccak256 hashers for rlpHash. // hasherPool holds LegacyKeccak256 buffer for rlpHash.
var hasherPool = sync.Pool{ var hasherPool = sync.Pool{
New: func() interface{} { return crypto.NewKeccakState() }, New: func() interface{} { return crypto.NewKeccakState() },
} }
@ -75,11 +75,17 @@ func prefixedRlpHash(prefix byte, x interface{}) (h common.Hash) {
return h return h
} }
// TrieHasher is the tool used to calculate the hash of derivable list. // ListHasher defines the interface for computing the hash of a derivable list.
// This is internal, do not use. type ListHasher interface {
type TrieHasher interface { // Reset clears the internal state of the hasher, preparing it for reuse.
Reset() Reset()
Update([]byte, []byte) error
// Update inserts the given key-value pair into the hasher.
// The implementation must copy the provided slices, allowing the caller
// to safely modify them after the call returns.
Update(key []byte, value []byte) error
// Hash computes and returns the final hash of all inserted key-value pairs.
Hash() common.Hash Hash() common.Hash
} }
@ -91,19 +97,20 @@ type DerivableList interface {
EncodeIndex(int, *bytes.Buffer) EncodeIndex(int, *bytes.Buffer)
} }
// encodeForDerive encodes the element in the list at the position i into the buffer.
func encodeForDerive(list DerivableList, i int, buf *bytes.Buffer) []byte { func encodeForDerive(list DerivableList, i int, buf *bytes.Buffer) []byte {
buf.Reset() buf.Reset()
list.EncodeIndex(i, buf) list.EncodeIndex(i, buf)
// It's really unfortunate that we need to perform this copy. return buf.Bytes()
// StackTrie holds onto the values until Hash is called, so the values
// written to it must not alias.
return common.CopyBytes(buf.Bytes())
} }
// DeriveSha creates the tree hashes of transactions, receipts, and withdrawals in a block header. // DeriveSha creates the tree hashes of transactions, receipts, and withdrawals in a block header.
func DeriveSha(list DerivableList, hasher TrieHasher) common.Hash { func DeriveSha(list DerivableList, hasher ListHasher) common.Hash {
hasher.Reset() hasher.Reset()
// Allocate a buffer for value encoding. As the hasher is claimed that all
// supplied key value pairs will be copied by hasher and safe to reuse the
// encoding buffer.
valueBuf := encodeBufferPool.Get().(*bytes.Buffer) valueBuf := encodeBufferPool.Get().(*bytes.Buffer)
defer encodeBufferPool.Put(valueBuf) defer encodeBufferPool.Put(valueBuf)

View file

@ -26,12 +26,10 @@ import (
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rlp"
"github.com/ethereum/go-ethereum/trie" "github.com/ethereum/go-ethereum/trie"
"github.com/ethereum/go-ethereum/triedb"
) )
func TestDeriveSha(t *testing.T) { func TestDeriveSha(t *testing.T) {
@ -40,7 +38,7 @@ func TestDeriveSha(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
for len(txs) < 1000 { for len(txs) < 1000 {
exp := types.DeriveSha(txs, trie.NewEmpty(triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil))) exp := types.DeriveSha(txs, trie.NewListHasher())
got := types.DeriveSha(txs, trie.NewStackTrie(nil)) got := types.DeriveSha(txs, trie.NewStackTrie(nil))
if !bytes.Equal(got[:], exp[:]) { if !bytes.Equal(got[:], exp[:]) {
t.Fatalf("%d txs: got %x exp %x", len(txs), got, exp) t.Fatalf("%d txs: got %x exp %x", len(txs), got, exp)
@ -76,30 +74,45 @@ func TestEIP2718DeriveSha(t *testing.T) {
} }
} }
// goos: darwin
// goarch: arm64
// pkg: github.com/ethereum/go-ethereum/core/types
// cpu: Apple M1 Pro
// BenchmarkDeriveSha200
// BenchmarkDeriveSha200/std_trie
// BenchmarkDeriveSha200/std_trie-8 6754 174074 ns/op 80054 B/op 1926 allocs/op
// BenchmarkDeriveSha200/stack_trie
// BenchmarkDeriveSha200/stack_trie-8 7296 162675 ns/op 745 B/op 19 allocs/op
func BenchmarkDeriveSha200(b *testing.B) { func BenchmarkDeriveSha200(b *testing.B) {
txs, err := genTxs(200) txs, err := genTxs(200)
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }
var exp common.Hash want := types.DeriveSha(txs, trie.NewListHasher())
var got common.Hash
b.Run("std_trie", func(b *testing.B) { b.Run("std_trie", func(b *testing.B) {
b.ReportAllocs() b.ReportAllocs()
var have common.Hash
for b.Loop() { for b.Loop() {
exp = types.DeriveSha(txs, trie.NewEmpty(triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil))) have = types.DeriveSha(txs, trie.NewListHasher())
}
if have != want {
b.Errorf("have %x want %x", have, want)
} }
}) })
st := trie.NewStackTrie(nil)
b.Run("stack_trie", func(b *testing.B) { b.Run("stack_trie", func(b *testing.B) {
b.ResetTimer()
b.ReportAllocs() b.ReportAllocs()
var have common.Hash
for b.Loop() { for b.Loop() {
got = types.DeriveSha(txs, trie.NewStackTrie(nil)) st.Reset()
have = types.DeriveSha(txs, st)
}
if have != want {
b.Errorf("have %x want %x", have, want)
} }
}) })
if got != exp {
b.Errorf("got %x exp %x", got, exp)
}
} }
func TestFuzzDeriveSha(t *testing.T) { func TestFuzzDeriveSha(t *testing.T) {
@ -107,7 +120,7 @@ func TestFuzzDeriveSha(t *testing.T) {
rndSeed := mrand.Int() rndSeed := mrand.Int()
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
seed := rndSeed + i seed := rndSeed + i
exp := types.DeriveSha(newDummy(i), trie.NewEmpty(triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil))) exp := types.DeriveSha(newDummy(i), trie.NewListHasher())
got := types.DeriveSha(newDummy(i), trie.NewStackTrie(nil)) got := types.DeriveSha(newDummy(i), trie.NewStackTrie(nil))
if !bytes.Equal(got[:], exp[:]) { if !bytes.Equal(got[:], exp[:]) {
printList(t, newDummy(seed)) printList(t, newDummy(seed))
@ -135,7 +148,7 @@ func TestDerivableList(t *testing.T) {
}, },
} }
for i, tc := range tcs[1:] { for i, tc := range tcs[1:] {
exp := types.DeriveSha(flatList(tc), trie.NewEmpty(triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil))) exp := types.DeriveSha(flatList(tc), trie.NewListHasher())
got := types.DeriveSha(flatList(tc), trie.NewStackTrie(nil)) got := types.DeriveSha(flatList(tc), trie.NewStackTrie(nil))
if !bytes.Equal(got[:], exp[:]) { if !bytes.Equal(got[:], exp[:]) {
t.Fatalf("case %d: got %x exp %x", i, got, exp) t.Fatalf("case %d: got %x exp %x", i, got, exp)

View file

@ -312,6 +312,18 @@ func (d *dummyChain) Config() *params.ChainConfig {
return nil return nil
} }
func (d *dummyChain) CurrentHeader() *types.Header {
return nil
}
func (d *dummyChain) GetHeaderByNumber(n uint64) *types.Header {
return d.GetHeader(common.Hash{}, n)
}
func (d *dummyChain) GetHeaderByHash(h common.Hash) *types.Header {
return nil
}
// TestBlockhash tests the blockhash operation. It's a bit special, since it internally // TestBlockhash tests the blockhash operation. It's a bit special, since it internally
// requires access to a chain reader. // requires access to a chain reader.
func TestBlockhash(t *testing.T) { func TestBlockhash(t *testing.T) {

View file

@ -100,7 +100,7 @@ type SimulatedBeacon struct {
func payloadVersion(config *params.ChainConfig, time uint64) engine.PayloadVersion { func payloadVersion(config *params.ChainConfig, time uint64) engine.PayloadVersion {
switch config.LatestFork(time) { switch config.LatestFork(time) {
case forks.Prague, forks.Cancun: case forks.BPO5, forks.BPO4, forks.BPO3, forks.BPO2, forks.BPO1, forks.Osaka, forks.Prague, forks.Cancun:
return engine.PayloadV3 return engine.PayloadV3
case forks.Paris, forks.Shanghai: case forks.Paris, forks.Shanghai:
return engine.PayloadV2 return engine.PayloadV2

View file

@ -43,6 +43,7 @@ var (
errPendingLogsUnsupported = errors.New("pending logs are not supported") errPendingLogsUnsupported = errors.New("pending logs are not supported")
errExceedMaxTopics = errors.New("exceed max topics") errExceedMaxTopics = errors.New("exceed max topics")
errExceedLogQueryLimit = errors.New("exceed max addresses or topics per search position") errExceedLogQueryLimit = errors.New("exceed max addresses or topics per search position")
errExceedMaxTxHashes = errors.New("exceed max number of transaction hashes allowed per transactionReceipts subscription")
) )
const ( const (
@ -50,6 +51,8 @@ const (
maxTopics = 4 maxTopics = 4
// The maximum number of allowed topics within a topic criteria // The maximum number of allowed topics within a topic criteria
maxSubTopics = 1000 maxSubTopics = 1000
// The maximum number of transaction hash criteria allowed in a single subscription
maxTxHashes = 200
) )
// filter is a helper struct that holds meta information over the filter type // filter is a helper struct that holds meta information over the filter type
@ -296,6 +299,83 @@ func (api *FilterAPI) Logs(ctx context.Context, crit FilterCriteria) (*rpc.Subsc
return rpcSub, nil return rpcSub, nil
} }
// TransactionReceiptsQuery defines criteria for transaction receipts subscription.
// Same as ethereum.TransactionReceiptsQuery but with UnmarshalJSON() method.
type TransactionReceiptsQuery ethereum.TransactionReceiptsQuery
// UnmarshalJSON sets *args fields with given data.
func (args *TransactionReceiptsQuery) UnmarshalJSON(data []byte) error {
type input struct {
TransactionHashes []common.Hash `json:"transactionHashes"`
}
var raw input
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
args.TransactionHashes = raw.TransactionHashes
return nil
}
// TransactionReceipts creates a subscription that fires transaction receipts when transactions are included in blocks.
func (api *FilterAPI) TransactionReceipts(ctx context.Context, filter *TransactionReceiptsQuery) (*rpc.Subscription, error) {
notifier, supported := rpc.NotifierFromContext(ctx)
if !supported {
return &rpc.Subscription{}, rpc.ErrNotificationsUnsupported
}
// Validate transaction hashes limit
if filter != nil && len(filter.TransactionHashes) > maxTxHashes {
return nil, errExceedMaxTxHashes
}
var (
rpcSub = notifier.CreateSubscription()
matchedReceipts = make(chan []*ReceiptWithTx)
txHashes []common.Hash
)
if filter != nil {
txHashes = filter.TransactionHashes
}
receiptsSub := api.events.SubscribeTransactionReceipts(txHashes, matchedReceipts)
go func() {
defer receiptsSub.Unsubscribe()
signer := types.LatestSigner(api.sys.backend.ChainConfig())
for {
select {
case receiptsWithTxs := <-matchedReceipts:
if len(receiptsWithTxs) > 0 {
// Convert to the same format as eth_getTransactionReceipt
marshaledReceipts := make([]map[string]interface{}, len(receiptsWithTxs))
for i, receiptWithTx := range receiptsWithTxs {
marshaledReceipts[i] = ethapi.MarshalReceipt(
receiptWithTx.Receipt,
receiptWithTx.Receipt.BlockHash,
receiptWithTx.Receipt.BlockNumber.Uint64(),
signer,
receiptWithTx.Transaction,
int(receiptWithTx.Receipt.TransactionIndex),
)
}
// Send a batch of tx receipts in one notification
notifier.Notify(rpcSub.ID, marshaledReceipts)
}
case <-rpcSub.Err():
return
}
}
}()
return rpcSub, nil
}
// FilterCriteria represents a request to create a new filter. // FilterCriteria represents a request to create a new filter.
// Same as ethereum.FilterQuery but with UnmarshalJSON() method. // Same as ethereum.FilterQuery but with UnmarshalJSON() method.
type FilterCriteria ethereum.FilterQuery type FilterCriteria ethereum.FilterQuery

View file

@ -25,6 +25,7 @@ import (
"time" "time"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/filtermaps" "github.com/ethereum/go-ethereum/core/filtermaps"
"github.com/ethereum/go-ethereum/core/history" "github.com/ethereum/go-ethereum/core/history"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
@ -551,3 +552,70 @@ func bloomFilter(bloom types.Bloom, addresses []common.Address, topics [][]commo
} }
return true return true
} }
// ReceiptWithTx contains a receipt and its corresponding transaction
type ReceiptWithTx struct {
Receipt *types.Receipt
Transaction *types.Transaction
}
// filterReceipts returns the receipts matching the given criteria
// In addition to returning receipts, it also returns the corresponding transactions.
// This is because receipts only contain low-level data, while user-facing data
// may require additional information from the Transaction.
func filterReceipts(txHashes []common.Hash, ev core.ChainEvent) []*ReceiptWithTx {
var ret []*ReceiptWithTx
receipts := ev.Receipts
txs := ev.Transactions
if len(receipts) != len(txs) {
log.Warn("Receipts and transactions length mismatch", "receipts", len(receipts), "transactions", len(txs))
return ret
}
if len(txHashes) == 0 {
// No filter, send all receipts with their transactions.
ret = make([]*ReceiptWithTx, len(receipts))
for i, receipt := range receipts {
ret[i] = &ReceiptWithTx{
Receipt: receipt,
Transaction: txs[i],
}
}
} else if len(txHashes) == 1 {
// Filter by single transaction hash.
// This is a common case, so we distinguish it from filtering by multiple tx hashes and made a small optimization.
for i, receipt := range receipts {
if receipt.TxHash == txHashes[0] {
ret = append(ret, &ReceiptWithTx{
Receipt: receipt,
Transaction: txs[i],
})
break
}
}
} else {
// Filter by multiple transaction hashes.
txHashMap := make(map[common.Hash]bool, len(txHashes))
for _, hash := range txHashes {
txHashMap[hash] = true
}
for i, receipt := range receipts {
if txHashMap[receipt.TxHash] {
ret = append(ret, &ReceiptWithTx{
Receipt: receipt,
Transaction: txs[i],
})
// Early exit if all receipts are found
if len(ret) == len(txHashes) {
break
}
}
}
}
return ret
}

View file

@ -158,6 +158,8 @@ const (
PendingTransactionsSubscription PendingTransactionsSubscription
// BlocksSubscription queries hashes for blocks that are imported // BlocksSubscription queries hashes for blocks that are imported
BlocksSubscription BlocksSubscription
// TransactionReceiptsSubscription queries for transaction receipts when transactions are included in blocks
TransactionReceiptsSubscription
// LastIndexSubscription keeps track of the last index // LastIndexSubscription keeps track of the last index
LastIndexSubscription LastIndexSubscription
) )
@ -182,6 +184,8 @@ type subscription struct {
logs chan []*types.Log logs chan []*types.Log
txs chan []*types.Transaction txs chan []*types.Transaction
headers chan *types.Header headers chan *types.Header
receipts chan []*ReceiptWithTx
txHashes []common.Hash // contains transaction hashes for transactionReceipts subscription filtering
installed chan struct{} // closed when the filter is installed installed chan struct{} // closed when the filter is installed
err chan error // closed when the filter is uninstalled err chan error // closed when the filter is uninstalled
} }
@ -268,6 +272,7 @@ func (sub *Subscription) Unsubscribe() {
case <-sub.f.logs: case <-sub.f.logs:
case <-sub.f.txs: case <-sub.f.txs:
case <-sub.f.headers: case <-sub.f.headers:
case <-sub.f.receipts:
} }
} }
@ -353,6 +358,7 @@ func (es *EventSystem) subscribeLogs(crit ethereum.FilterQuery, logs chan []*typ
logs: logs, logs: logs,
txs: make(chan []*types.Transaction), txs: make(chan []*types.Transaction),
headers: make(chan *types.Header), headers: make(chan *types.Header),
receipts: make(chan []*ReceiptWithTx),
installed: make(chan struct{}), installed: make(chan struct{}),
err: make(chan error), err: make(chan error),
} }
@ -369,6 +375,7 @@ func (es *EventSystem) SubscribeNewHeads(headers chan *types.Header) *Subscripti
logs: make(chan []*types.Log), logs: make(chan []*types.Log),
txs: make(chan []*types.Transaction), txs: make(chan []*types.Transaction),
headers: headers, headers: headers,
receipts: make(chan []*ReceiptWithTx),
installed: make(chan struct{}), installed: make(chan struct{}),
err: make(chan error), err: make(chan error),
} }
@ -385,6 +392,26 @@ func (es *EventSystem) SubscribePendingTxs(txs chan []*types.Transaction) *Subsc
logs: make(chan []*types.Log), logs: make(chan []*types.Log),
txs: txs, txs: txs,
headers: make(chan *types.Header), headers: make(chan *types.Header),
receipts: make(chan []*ReceiptWithTx),
installed: make(chan struct{}),
err: make(chan error),
}
return es.subscribe(sub)
}
// SubscribeTransactionReceipts creates a subscription that writes transaction receipts for
// transactions when they are included in blocks. If txHashes is provided, only receipts
// for those specific transaction hashes will be delivered.
func (es *EventSystem) SubscribeTransactionReceipts(txHashes []common.Hash, receipts chan []*ReceiptWithTx) *Subscription {
sub := &subscription{
id: rpc.NewID(),
typ: TransactionReceiptsSubscription,
created: time.Now(),
logs: make(chan []*types.Log),
txs: make(chan []*types.Transaction),
headers: make(chan *types.Header),
receipts: receipts,
txHashes: txHashes,
installed: make(chan struct{}), installed: make(chan struct{}),
err: make(chan error), err: make(chan error),
} }
@ -415,6 +442,14 @@ func (es *EventSystem) handleChainEvent(filters filterIndex, ev core.ChainEvent)
for _, f := range filters[BlocksSubscription] { for _, f := range filters[BlocksSubscription] {
f.headers <- ev.Header f.headers <- ev.Header
} }
// Handle transaction receipts subscriptions when a new block is added
for _, f := range filters[TransactionReceiptsSubscription] {
matchedReceipts := filterReceipts(f.txHashes, ev)
if len(matchedReceipts) > 0 {
f.receipts <- matchedReceipts
}
}
} }
// eventLoop (un)installs filters and processes mux events. // eventLoop (un)installs filters and processes mux events.

View file

@ -31,6 +31,7 @@ import (
"github.com/ethereum/go-ethereum/core/filtermaps" "github.com/ethereum/go-ethereum/core/filtermaps"
"github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethdb" "github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/event"
"github.com/ethereum/go-ethereum/internal/ethapi" "github.com/ethereum/go-ethereum/internal/ethapi"
@ -781,3 +782,143 @@ func TestPendingTxFilterDeadlock(t *testing.T) {
} }
} }
} }
// TestTransactionReceiptsSubscription tests the transaction receipts subscription functionality
func TestTransactionReceiptsSubscription(t *testing.T) {
t.Parallel()
const txNum = 5
// Setup test environment
var (
db = rawdb.NewMemoryDatabase()
backend, sys = newTestFilterSystem(db, Config{})
api = NewFilterAPI(sys)
key1, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
addr1 = crypto.PubkeyToAddress(key1.PublicKey)
signer = types.NewLondonSigner(big.NewInt(1))
genesis = &core.Genesis{
Alloc: types.GenesisAlloc{addr1: {Balance: big.NewInt(1000000000000000000)}}, // 1 ETH
Config: params.TestChainConfig,
BaseFee: big.NewInt(params.InitialBaseFee),
}
_, chain, _ = core.GenerateChainWithGenesis(genesis, ethash.NewFaker(), 1, func(i int, gen *core.BlockGen) {
// Add transactions to the block
for j := 0; j < txNum; j++ {
toAddr := common.HexToAddress("0xb794f5ea0ba39494ce83a213fffba74279579268")
tx, _ := types.SignTx(types.NewTx(&types.LegacyTx{
Nonce: uint64(j),
GasPrice: gen.BaseFee(),
Gas: 21000,
To: &toAddr,
Value: big.NewInt(1000),
Data: nil,
}), signer, key1)
gen.AddTx(tx)
}
})
)
// Insert the blocks into the chain
blockchain, err := core.NewBlockChain(db, genesis, ethash.NewFaker(), nil)
if err != nil {
t.Fatalf("failed to create tester chain: %v", err)
}
if n, err := blockchain.InsertChain(chain); err != nil {
t.Fatalf("block %d: failed to insert into chain: %v", n, err)
}
// Prepare test data
receipts := blockchain.GetReceiptsByHash(chain[0].Hash())
if receipts == nil {
t.Fatalf("failed to get receipts")
}
chainEvent := core.ChainEvent{
Header: chain[0].Header(),
Receipts: receipts,
Transactions: chain[0].Transactions(),
}
txHashes := make([]common.Hash, txNum)
for i := 0; i < txNum; i++ {
txHashes[i] = chain[0].Transactions()[i].Hash()
}
testCases := []struct {
name string
filterTxHashes []common.Hash
expectedReceiptTxHashes []common.Hash
expectError bool
}{
{
name: "no filter - should return all receipts",
filterTxHashes: nil,
expectedReceiptTxHashes: txHashes,
expectError: false,
},
{
name: "single tx hash filter",
filterTxHashes: []common.Hash{txHashes[0]},
expectedReceiptTxHashes: []common.Hash{txHashes[0]},
expectError: false,
},
{
name: "multiple tx hashes filter",
filterTxHashes: []common.Hash{txHashes[0], txHashes[1], txHashes[2]},
expectedReceiptTxHashes: []common.Hash{txHashes[0], txHashes[1], txHashes[2]},
expectError: false,
},
}
// Run test cases
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
receiptsChan := make(chan []*ReceiptWithTx)
sub := api.events.SubscribeTransactionReceipts(tc.filterTxHashes, receiptsChan)
// Send chain event
backend.chainFeed.Send(chainEvent)
// Wait for receipts
timeout := time.After(1 * time.Second)
var receivedReceipts []*types.Receipt
for {
select {
case receiptsWithTx := <-receiptsChan:
for _, receiptWithTx := range receiptsWithTx {
receivedReceipts = append(receivedReceipts, receiptWithTx.Receipt)
}
case <-timeout:
t.Fatalf("timeout waiting for receipts")
}
if len(receivedReceipts) >= len(tc.expectedReceiptTxHashes) {
break
}
}
// Verify receipt count
if len(receivedReceipts) != len(tc.expectedReceiptTxHashes) {
t.Errorf("Expected %d receipts, got %d", len(tc.expectedReceiptTxHashes), len(receivedReceipts))
}
// Verify specific transaction hashes are present
if tc.expectedReceiptTxHashes != nil {
receivedHashes := make(map[common.Hash]bool)
for _, receipt := range receivedReceipts {
receivedHashes[receipt.TxHash] = true
}
for _, expectedHash := range tc.expectedReceiptTxHashes {
if !receivedHashes[expectedHash] {
t.Errorf("Expected receipt for tx %x not found", expectedHash)
}
}
}
// Cleanup
sub.Unsubscribe()
<-sub.Err()
})
}
}

View file

@ -22,7 +22,6 @@ import (
"time" "time"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/forkid" "github.com/ethereum/go-ethereum/core/forkid"
"github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/metrics"
"github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/p2p"
@ -36,7 +35,7 @@ const (
// Handshake executes the eth protocol handshake, negotiating version number, // Handshake executes the eth protocol handshake, negotiating version number,
// network IDs, difficulties, head and genesis blocks. // network IDs, difficulties, head and genesis blocks.
func (p *Peer) Handshake(networkID uint64, chain *core.BlockChain, rangeMsg BlockRangeUpdatePacket) error { func (p *Peer) Handshake(networkID uint64, chain forkid.Blockchain, rangeMsg BlockRangeUpdatePacket) error {
switch p.version { switch p.version {
case ETH69: case ETH69:
return p.handshake69(networkID, chain, rangeMsg) return p.handshake69(networkID, chain, rangeMsg)
@ -47,10 +46,10 @@ func (p *Peer) Handshake(networkID uint64, chain *core.BlockChain, rangeMsg Bloc
} }
} }
func (p *Peer) handshake68(networkID uint64, chain *core.BlockChain) error { func (p *Peer) handshake68(networkID uint64, chain forkid.Blockchain) error {
var ( var (
genesis = chain.Genesis() genesis = chain.Genesis()
latest = chain.CurrentBlock() latest = chain.CurrentHeader()
forkID = forkid.NewID(chain.Config(), genesis, latest.Number.Uint64(), latest.Time) forkID = forkid.NewID(chain.Config(), genesis, latest.Number.Uint64(), latest.Time)
forkFilter = forkid.NewFilter(chain) forkFilter = forkid.NewFilter(chain)
) )
@ -92,10 +91,10 @@ func (p *Peer) readStatus68(networkID uint64, status *StatusPacket68, genesis co
return nil return nil
} }
func (p *Peer) handshake69(networkID uint64, chain *core.BlockChain, rangeMsg BlockRangeUpdatePacket) error { func (p *Peer) handshake69(networkID uint64, chain forkid.Blockchain, rangeMsg BlockRangeUpdatePacket) error {
var ( var (
genesis = chain.Genesis() genesis = chain.Genesis()
latest = chain.CurrentBlock() latest = chain.CurrentHeader()
forkID = forkid.NewID(chain.Config(), genesis, latest.Number.Uint64(), latest.Time) forkID = forkid.NewID(chain.Config(), genesis, latest.Number.Uint64(), latest.Time)
forkFilter = forkid.NewFilter(chain) forkFilter = forkid.NewFilter(chain)
) )

View file

@ -74,8 +74,11 @@ func (r *hashRange) End() common.Hash {
// incHash returns the next hash, in lexicographical order (a.k.a plus one) // incHash returns the next hash, in lexicographical order (a.k.a plus one)
func incHash(h common.Hash) common.Hash { func incHash(h common.Hash) common.Hash {
var a uint256.Int for i := len(h) - 1; i >= 0; i-- {
a.SetBytes32(h[:]) h[i]++
a.AddUint64(&a, 1) if h[i] != 0 {
return common.Hash(a.Bytes32()) break
}
}
return h
} }

View file

@ -80,6 +80,7 @@ type StateReleaseFunc func()
type Backend interface { type Backend interface {
HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error)
HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error) HeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Header, error)
CurrentHeader() *types.Header
BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error)
BlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error) BlockByNumber(ctx context.Context, number rpc.BlockNumber) (*types.Block, error)
GetCanonicalTransaction(txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64) GetCanonicalTransaction(txHash common.Hash) (bool, *types.Transaction, common.Hash, uint64, uint64)

View file

@ -142,6 +142,10 @@ func (b *testBackend) ChainDb() ethdb.Database {
return b.chaindb return b.chaindb
} }
func (b *testBackend) CurrentHeader() *types.Header {
return b.chain.CurrentHeader()
}
// teardown releases the associated resources. // teardown releases the associated resources.
func (b *testBackend) teardown() { func (b *testBackend) teardown() {
b.chain.Stop() b.chain.Stop()

View file

@ -350,6 +350,15 @@ func (ec *Client) TransactionReceipt(ctx context.Context, txHash common.Hash) (*
return r, err return r, err
} }
// SubscribeTransactionReceipts subscribes to notifications about transaction receipts.
func (ec *Client) SubscribeTransactionReceipts(ctx context.Context, q *ethereum.TransactionReceiptsQuery, ch chan<- []*types.Receipt) (ethereum.Subscription, error) {
sub, err := ec.c.EthSubscribe(ctx, ch, "transactionReceipts", q)
if err != nil {
return nil, err
}
return sub, nil
}
// SyncProgress retrieves the current progress of the sync algorithm. If there's // SyncProgress retrieves the current progress of the sync algorithm. If there's
// no sync currently running, it returns nil. // no sync currently running, it returns nil.
func (ec *Client) SyncProgress(ctx context.Context) (*ethereum.SyncProgress, error) { func (ec *Client) SyncProgress(ctx context.Context) (*ethereum.SyncProgress, error) {

4
go.mod
View file

@ -5,7 +5,7 @@ go 1.24.0
require ( require (
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.0 github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.2.0
github.com/Microsoft/go-winio v0.6.2 github.com/Microsoft/go-winio v0.6.2
github.com/VictoriaMetrics/fastcache v1.12.2 github.com/VictoriaMetrics/fastcache v1.13.0
github.com/aws/aws-sdk-go-v2 v1.21.2 github.com/aws/aws-sdk-go-v2 v1.21.2
github.com/aws/aws-sdk-go-v2/config v1.18.45 github.com/aws/aws-sdk-go-v2/config v1.18.45
github.com/aws/aws-sdk-go-v2/credentials v1.13.43 github.com/aws/aws-sdk-go-v2/credentials v1.13.43
@ -31,7 +31,7 @@ require (
github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff github.com/gballet/go-libpcsclite v0.0.0-20190607065134-2772fd86a8ff
github.com/gofrs/flock v0.12.1 github.com/gofrs/flock v0.12.1
github.com/golang-jwt/jwt/v4 v4.5.2 github.com/golang-jwt/jwt/v4 v4.5.2
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb github.com/golang/snappy v1.0.0
github.com/google/gofuzz v1.2.0 github.com/google/gofuzz v1.2.0
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/gorilla/websocket v1.4.2 github.com/gorilla/websocket v1.4.2

10
go.sum
View file

@ -16,8 +16,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0=
github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI= github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU=
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8= github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156 h1:eMwmnE/GDgah4HI848JfFxHt+iPb26b4zyfspmqY0/8=
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
github.com/aws/aws-sdk-go-v2 v1.21.2 h1:+LXZ0sgo8quN9UOKXXzAWRT3FWd4NxeXWOZom9pE7GA= github.com/aws/aws-sdk-go-v2 v1.21.2 h1:+LXZ0sgo8quN9UOKXXzAWRT3FWd4NxeXWOZom9pE7GA=
@ -52,7 +52,6 @@ github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3M
github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk= github.com/cespare/cp v0.1.0 h1:SE+dxFebS7Iik5LK0tsi1k9ZCxEaFX4AjQmoyA+1dJk=
github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s= github.com/cespare/cp v0.1.0/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY= github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=
@ -165,8 +164,8 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y= github.com/golangci/lint-1 v0.0.0-20181222135242-d2cdd8c08219/go.mod h1:/X8TswGSh1pIozq4ZwCfxS0WA5JGXguxk94ar/4c87Y=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
@ -450,7 +449,6 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=

View file

@ -62,6 +62,13 @@ type ChainReader interface {
SubscribeNewHead(ctx context.Context, ch chan<- *types.Header) (Subscription, error) SubscribeNewHead(ctx context.Context, ch chan<- *types.Header) (Subscription, error)
} }
// TransactionReceiptsQuery defines criteria for transaction receipts subscription.
// If TransactionHashes is empty, receipts for all transactions included in new blocks will be delivered.
// Otherwise, only receipts for the specified transactions will be delivered.
type TransactionReceiptsQuery struct {
TransactionHashes []common.Hash
}
// TransactionReader provides access to past transactions and their receipts. // TransactionReader provides access to past transactions and their receipts.
// Implementations may impose arbitrary restrictions on the transactions and receipts that // Implementations may impose arbitrary restrictions on the transactions and receipts that
// can be retrieved. Historic transactions may not be available. // can be retrieved. Historic transactions may not be available.
@ -81,6 +88,11 @@ type TransactionReader interface {
// transaction may not be included in the current canonical chain even if a receipt // transaction may not be included in the current canonical chain even if a receipt
// exists. // exists.
TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error) TransactionReceipt(ctx context.Context, txHash common.Hash) (*types.Receipt, error)
// SubscribeTransactionReceipts subscribes to notifications about transaction receipts.
// The receipts are delivered in batches when transactions are included in blocks.
// If q is nil or has empty TransactionHashes, all receipts from new blocks will be delivered.
// Otherwise, only receipts for the specified transaction hashes will be delivered.
SubscribeTransactionReceipts(ctx context.Context, q *TransactionReceiptsQuery, ch chan<- []*types.Receipt) (Subscription, error)
} }
// ChainStateReader wraps access to the state trie of the canonical blockchain. Note that // ChainStateReader wraps access to the state trie of the canonical blockchain. Note that

View file

@ -23,6 +23,7 @@
package blocktest package blocktest
import ( import (
"bytes"
"hash" "hash"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
@ -48,8 +49,8 @@ func (h *testHasher) Reset() {
// Update updates the hash state with the given key and value. // Update updates the hash state with the given key and value.
func (h *testHasher) Update(key, val []byte) error { func (h *testHasher) Update(key, val []byte) error {
h.hasher.Write(key) h.hasher.Write(bytes.Clone(key))
h.hasher.Write(val) h.hasher.Write(bytes.Clone(val))
return nil return nil
} }

View file

@ -627,7 +627,7 @@ func (api *BlockChainAPI) GetBlockReceipts(ctx context.Context, blockNrOrHash rp
result := make([]map[string]interface{}, len(receipts)) result := make([]map[string]interface{}, len(receipts))
for i, receipt := range receipts { for i, receipt := range receipts {
result[i] = marshalReceipt(receipt, block.Hash(), block.NumberU64(), signer, txs[i], i) result[i] = MarshalReceipt(receipt, block.Hash(), block.NumberU64(), signer, txs[i], i)
} }
return result, nil return result, nil
} }
@ -636,6 +636,8 @@ func (api *BlockChainAPI) GetBlockReceipts(ctx context.Context, blockNrOrHash rp
type ChainContextBackend interface { type ChainContextBackend interface {
Engine() consensus.Engine Engine() consensus.Engine
HeaderByNumber(context.Context, rpc.BlockNumber) (*types.Header, error) HeaderByNumber(context.Context, rpc.BlockNumber) (*types.Header, error)
HeaderByHash(context.Context, common.Hash) (*types.Header, error)
CurrentHeader() *types.Header
ChainConfig() *params.ChainConfig ChainConfig() *params.ChainConfig
} }
@ -669,6 +671,20 @@ func (context *ChainContext) Config() *params.ChainConfig {
return context.b.ChainConfig() return context.b.ChainConfig()
} }
func (context *ChainContext) CurrentHeader() *types.Header {
return context.b.CurrentHeader()
}
func (context *ChainContext) GetHeaderByNumber(number uint64) *types.Header {
header, _ := context.b.HeaderByNumber(context.ctx, rpc.BlockNumber(number))
return header
}
func (context *ChainContext) GetHeaderByHash(hash common.Hash) *types.Header {
header, _ := context.b.HeaderByHash(context.ctx, hash)
return header
}
func doCall(ctx context.Context, b Backend, args TransactionArgs, state *state.StateDB, header *types.Header, overrides *override.StateOverride, blockOverrides *override.BlockOverrides, timeout time.Duration, globalGasCap uint64) (*core.ExecutionResult, error) { func doCall(ctx context.Context, b Backend, args TransactionArgs, state *state.StateDB, header *types.Header, overrides *override.StateOverride, blockOverrides *override.BlockOverrides, timeout time.Duration, globalGasCap uint64) (*core.ExecutionResult, error) {
blockCtx := core.NewEVMBlockContext(header, NewChainContext(ctx, b), nil) blockCtx := core.NewEVMBlockContext(header, NewChainContext(ctx, b), nil)
if blockOverrides != nil { if blockOverrides != nil {
@ -1472,11 +1488,11 @@ func (api *TransactionAPI) GetTransactionReceipt(ctx context.Context, hash commo
return nil, err return nil, err
} }
// Derive the sender. // Derive the sender.
return marshalReceipt(receipt, blockHash, blockNumber, api.signer, tx, int(index)), nil return MarshalReceipt(receipt, blockHash, blockNumber, api.signer, tx, int(index)), nil
} }
// marshalReceipt marshals a transaction receipt into a JSON object. // MarshalReceipt marshals a transaction receipt into a JSON object.
func marshalReceipt(receipt *types.Receipt, blockHash common.Hash, blockNumber uint64, signer types.Signer, tx *types.Transaction, txIndex int) map[string]interface{} { func MarshalReceipt(receipt *types.Receipt, blockHash common.Hash, blockNumber uint64, signer types.Signer, tx *types.Transaction, txIndex int) map[string]interface{} {
from, _ := types.Sender(signer, tx) from, _ := types.Sender(signer, tx)
fields := map[string]interface{}{ fields := map[string]interface{}{

View file

@ -1331,6 +1331,7 @@ func TestSimulateV1(t *testing.T) {
Topics []common.Hash `json:"topics"` Topics []common.Hash `json:"topics"`
Data hexutil.Bytes `json:"data"` Data hexutil.Bytes `json:"data"`
BlockNumber hexutil.Uint64 `json:"blockNumber"` BlockNumber hexutil.Uint64 `json:"blockNumber"`
BlockTimestamp hexutil.Uint64 `json:"blockTimestamp"`
// Skip txHash // Skip txHash
//TxHash common.Hash `json:"transactionHash" gencodec:"required"` //TxHash common.Hash `json:"transactionHash" gencodec:"required"`
TxIndex hexutil.Uint `json:"transactionIndex"` TxIndex hexutil.Uint `json:"transactionIndex"`
@ -1680,6 +1681,7 @@ func TestSimulateV1(t *testing.T) {
Address: randomAccounts[2].addr, Address: randomAccounts[2].addr,
Topics: []common.Hash{common.HexToHash("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")}, Topics: []common.Hash{common.HexToHash("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff")},
BlockNumber: hexutil.Uint64(11), BlockNumber: hexutil.Uint64(11),
BlockTimestamp: hexutil.Uint64(0x70),
Data: hexutil.Bytes{}, Data: hexutil.Bytes{},
}}, }},
GasUsed: "0x5508", GasUsed: "0x5508",
@ -1855,6 +1857,7 @@ func TestSimulateV1(t *testing.T) {
}, },
Data: hexutil.Bytes(common.BigToHash(big.NewInt(50)).Bytes()), Data: hexutil.Bytes(common.BigToHash(big.NewInt(50)).Bytes()),
BlockNumber: hexutil.Uint64(11), BlockNumber: hexutil.Uint64(11),
BlockTimestamp: hexutil.Uint64(0x70),
}, { }, {
Address: transferAddress, Address: transferAddress,
Topics: []common.Hash{ Topics: []common.Hash{
@ -1864,6 +1867,7 @@ func TestSimulateV1(t *testing.T) {
}, },
Data: hexutil.Bytes(common.BigToHash(big.NewInt(100)).Bytes()), Data: hexutil.Bytes(common.BigToHash(big.NewInt(100)).Bytes()),
BlockNumber: hexutil.Uint64(11), BlockNumber: hexutil.Uint64(11),
BlockTimestamp: hexutil.Uint64(0x70),
Index: hexutil.Uint(1), Index: hexutil.Uint(1),
}}, }},
Status: "0x1", Status: "0x1",

View file

@ -53,15 +53,17 @@ type tracer struct {
count int count int
traceTransfers bool traceTransfers bool
blockNumber uint64 blockNumber uint64
blockTimestamp uint64
blockHash common.Hash blockHash common.Hash
txHash common.Hash txHash common.Hash
txIdx uint txIdx uint
} }
func newTracer(traceTransfers bool, blockNumber uint64, blockHash, txHash common.Hash, txIndex uint) *tracer { func newTracer(traceTransfers bool, blockNumber uint64, blockTimestamp uint64, blockHash, txHash common.Hash, txIndex uint) *tracer {
return &tracer{ return &tracer{
traceTransfers: traceTransfers, traceTransfers: traceTransfers,
blockNumber: blockNumber, blockNumber: blockNumber,
blockTimestamp: blockTimestamp,
blockHash: blockHash, blockHash: blockHash,
txHash: txHash, txHash: txHash,
txIdx: txIndex, txIdx: txIndex,
@ -119,6 +121,7 @@ func (t *tracer) captureLog(address common.Address, topics []common.Hash, data [
Topics: topics, Topics: topics,
Data: data, Data: data,
BlockNumber: t.blockNumber, BlockNumber: t.blockNumber,
BlockTimestamp: t.blockTimestamp,
BlockHash: t.blockHash, BlockHash: t.blockHash,
TxHash: t.txHash, TxHash: t.txHash,
TxIndex: t.txIdx, TxIndex: t.txIdx,

View file

@ -244,7 +244,7 @@ func (sim *simulator) processBlock(ctx context.Context, block *simBlock, header,
callResults = make([]simCallResult, len(block.Calls)) callResults = make([]simCallResult, len(block.Calls))
receipts = make([]*types.Receipt, len(block.Calls)) receipts = make([]*types.Receipt, len(block.Calls))
// Block hash will be repaired after execution. // Block hash will be repaired after execution.
tracer = newTracer(sim.traceTransfers, blockContext.BlockNumber.Uint64(), common.Hash{}, common.Hash{}, 0) tracer = newTracer(sim.traceTransfers, blockContext.BlockNumber.Uint64(), blockContext.Time, common.Hash{}, common.Hash{}, 0)
vmConfig = &vm.Config{ vmConfig = &vm.Config{
NoBaseFee: !sim.validate, NoBaseFee: !sim.validate,
Tracer: tracer.Hooks(), Tracer: tracer.Hooks(),
@ -541,3 +541,23 @@ func (b *simBackend) HeaderByNumber(ctx context.Context, number rpc.BlockNumber)
func (b *simBackend) ChainConfig() *params.ChainConfig { func (b *simBackend) ChainConfig() *params.ChainConfig {
return b.b.ChainConfig() return b.b.ChainConfig()
} }
func (b *simBackend) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) {
if b.base.Hash() == hash {
return b.base, nil
}
if header, err := b.b.HeaderByHash(ctx, hash); err == nil {
return header, nil
}
// Check simulated headers
for _, header := range b.headers {
if header.Hash() == hash {
return header, nil
}
}
return nil, errors.New("header not found")
}
func (b *simBackend) CurrentHeader() *types.Header {
return b.b.CurrentHeader()
}

View file

@ -174,11 +174,12 @@ type AsyncFilterFunc func(context.Context, *Node) *Node
// AsyncFilter creates an iterator which checks nodes in parallel. // AsyncFilter creates an iterator which checks nodes in parallel.
// The 'check' function is called on multiple goroutines to filter each node // The 'check' function is called on multiple goroutines to filter each node
// from the upstream iterator. When check returns nil, the node will be skipped. // from the upstream iterator. When check returns nil, the node will be skipped.
// It can also return a new node to be returned by the iterator instead of the . // It can also return a new node to be returned by the iterator instead of the
// original one.
func AsyncFilter(it Iterator, check AsyncFilterFunc, workers int) Iterator { func AsyncFilter(it Iterator, check AsyncFilterFunc, workers int) Iterator {
f := &asyncFilterIter{ f := &asyncFilterIter{
it: ensureSourceIter(it), it: ensureSourceIter(it),
slots: make(chan struct{}, workers+1), slots: make(chan struct{}, workers+1), // extra 1 slot to make sure all the goroutines can be completed
passed: make(chan iteratorItem), passed: make(chan iteratorItem),
} }
for range cap(f.slots) { for range cap(f.slots) {
@ -193,6 +194,9 @@ func AsyncFilter(it Iterator, check AsyncFilterFunc, workers int) Iterator {
return return
case <-f.slots: case <-f.slots:
} }
defer func() {
f.slots <- struct{}{} // the iterator has ended
}()
// read from the iterator and start checking nodes in parallel // read from the iterator and start checking nodes in parallel
// when a node is checked, it will be sent to the passed channel // when a node is checked, it will be sent to the passed channel
// and the slot will be released // and the slot will be released
@ -201,7 +205,11 @@ func AsyncFilter(it Iterator, check AsyncFilterFunc, workers int) Iterator {
nodeSource := f.it.NodeSource() nodeSource := f.it.NodeSource()
// check the node async, in a separate goroutine // check the node async, in a separate goroutine
<-f.slots select {
case <-ctx.Done():
return
case <-f.slots:
}
go func() { go func() {
if nn := check(ctx, node); nn != nil { if nn := check(ctx, node); nn != nil {
item := iteratorItem{nn, nodeSource} item := iteratorItem{nn, nodeSource}
@ -213,8 +221,6 @@ func AsyncFilter(it Iterator, check AsyncFilterFunc, workers int) Iterator {
f.slots <- struct{}{} f.slots <- struct{}{}
}() }()
} }
// the iterator has ended
f.slots <- struct{}{}
}() }()
return f return f

View file

@ -45,7 +45,7 @@ func TestReadNodesCycle(t *testing.T) {
nodes := ReadNodes(iter, 10) nodes := ReadNodes(iter, 10)
checkNodes(t, nodes, 3) checkNodes(t, nodes, 3)
if iter.count != 10 { if iter.count != 10 {
t.Fatalf("%d calls to Next, want %d", iter.count, 100) t.Fatalf("%d calls to Next, want %d", iter.count, 10)
} }
} }

View file

@ -624,7 +624,7 @@ func (c *ChainConfig) Description() string {
} }
banner += fmt.Sprintf(" - Tangerine Whistle (EIP 150): #%-8v\n", c.EIP150Block) banner += fmt.Sprintf(" - Tangerine Whistle (EIP 150): #%-8v\n", c.EIP150Block)
banner += fmt.Sprintf(" - Spurious Dragon/1 (EIP 155): #%-8v\n", c.EIP155Block) banner += fmt.Sprintf(" - Spurious Dragon/1 (EIP 155): #%-8v\n", c.EIP155Block)
banner += fmt.Sprintf(" - Spurious Dragon/2 (EIP 158): #%-8v\n", c.EIP155Block) banner += fmt.Sprintf(" - Spurious Dragon/2 (EIP 158): #%-8v\n", c.EIP158Block)
banner += fmt.Sprintf(" - Byzantium: #%-8v\n", c.ByzantiumBlock) banner += fmt.Sprintf(" - Byzantium: #%-8v\n", c.ByzantiumBlock)
banner += fmt.Sprintf(" - Constantinople: #%-8v\n", c.ConstantinopleBlock) banner += fmt.Sprintf(" - Constantinople: #%-8v\n", c.ConstantinopleBlock)
banner += fmt.Sprintf(" - Petersburg: #%-8v\n", c.PetersburgBlock) banner += fmt.Sprintf(" - Petersburg: #%-8v\n", c.PetersburgBlock)

View file

@ -264,7 +264,7 @@ func TestSignTx(t *testing.T) {
t.Errorf("Expected nil-response, got %v", res) t.Errorf("Expected nil-response, got %v", res)
} }
if err != keystore.ErrDecrypt { if err != keystore.ErrDecrypt {
t.Errorf("Expected ErrLocked! %v", err) t.Errorf("Expected ErrDecrypt! %v", err)
} }
control.approveCh <- "No way" control.approveCh <- "No way"
res, err = api.SignTransaction(t.Context(), tx, &methodSig) res, err = api.SignTransaction(t.Context(), tx, &methodSig)

View file

@ -202,7 +202,7 @@ func TestSignData(t *testing.T) {
t.Errorf("Expected nil-data, got %x", signature) t.Errorf("Expected nil-data, got %x", signature)
} }
if err != keystore.ErrDecrypt { if err != keystore.ErrDecrypt {
t.Errorf("Expected ErrLocked! '%v'", err) t.Errorf("Expected ErrDecrypt! '%v'", err)
} }
control.approveCh <- "No way" control.approveCh <- "No way"
signature, err = api.SignData(context.Background(), apitypes.TextPlain.Mime, a, hexutil.Encode([]byte("EHLO world"))) signature, err = api.SignData(context.Background(), apitypes.TextPlain.Mime, a, hexutil.Encode([]byte("EHLO world")))

View file

@ -559,3 +559,6 @@ type dummyChain struct {
func (d *dummyChain) Engine() consensus.Engine { return nil } func (d *dummyChain) Engine() consensus.Engine { return nil }
func (d *dummyChain) GetHeader(h common.Hash, n uint64) *types.Header { return nil } func (d *dummyChain) GetHeader(h common.Hash, n uint64) *types.Header { return nil }
func (d *dummyChain) Config() *params.ChainConfig { return d.config } func (d *dummyChain) Config() *params.ChainConfig { return d.config }
func (d *dummyChain) CurrentHeader() *types.Header { return nil }
func (d *dummyChain) GetHeaderByNumber(n uint64) *types.Header { return nil }
func (d *dummyChain) GetHeaderByHash(h common.Hash) *types.Header { return nil }

View file

@ -32,8 +32,8 @@ func newBytesPool(sliceCap, nitems int) *bytesPool {
} }
} }
// Get returns a slice. Safe for concurrent use. // get returns a slice. Safe for concurrent use.
func (bp *bytesPool) Get() []byte { func (bp *bytesPool) get() []byte {
select { select {
case b := <-bp.c: case b := <-bp.c:
return b return b
@ -42,18 +42,18 @@ func (bp *bytesPool) Get() []byte {
} }
} }
// GetWithSize returns a slice with specified byte slice size. // getWithSize returns a slice with specified byte slice size.
func (bp *bytesPool) GetWithSize(s int) []byte { func (bp *bytesPool) getWithSize(s int) []byte {
b := bp.Get() b := bp.get()
if cap(b) < s { if cap(b) < s {
return make([]byte, s) return make([]byte, s)
} }
return b[:s] return b[:s]
} }
// Put returns a slice to the pool. Safe for concurrent use. This method // put returns a slice to the pool. Safe for concurrent use. This method
// will ignore slices that are too small or too large (>3x the cap) // will ignore slices that are too small or too large (>3x the cap)
func (bp *bytesPool) Put(b []byte) { func (bp *bytesPool) put(b []byte) {
if c := cap(b); c < bp.w || c > 3*bp.w { if c := cap(b); c < bp.w || c > 3*bp.w {
return return
} }
@ -62,3 +62,40 @@ func (bp *bytesPool) Put(b []byte) {
default: default:
} }
} }
// unsafeBytesPool is a pool for byte slices. It is not safe for concurrent use.
type unsafeBytesPool struct {
items [][]byte
w int
}
// newUnsafeBytesPool creates a new unsafeBytesPool. The sliceCap sets the
// capacity of newly allocated slices, and the nitems determines how many
// items the pool will hold, at maximum.
func newUnsafeBytesPool(sliceCap, nitems int) *unsafeBytesPool {
return &unsafeBytesPool{
items: make([][]byte, 0, nitems),
w: sliceCap,
}
}
// Get returns a slice with pre-allocated space.
func (bp *unsafeBytesPool) get() []byte {
if len(bp.items) > 0 {
last := bp.items[len(bp.items)-1]
bp.items = bp.items[:len(bp.items)-1]
return last
}
return make([]byte, 0, bp.w)
}
// put returns a slice to the pool. This method will ignore slices that are
// too small or too large (>3x the cap)
func (bp *unsafeBytesPool) put(b []byte) {
if c := cap(b); c < bp.w || c > 3*bp.w {
return
}
if len(bp.items) < cap(bp.items) {
bp.items = append(bp.items, b)
}
}

56
trie/list_hasher.go Normal file
View file

@ -0,0 +1,56 @@
// 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 trie
import (
"bytes"
"github.com/ethereum/go-ethereum/common"
)
// ListHasher is a wrapper of the Merkle-Patricia-Trie, which implements
// types.ListHasher. Compared to a Trie instance, the Update method of this
// type always deep-copies its input slices.
//
// This implementation is very inefficient in terms of memory allocation,
// compared with StackTrie. It exists only for correctness comparison purposes.
type ListHasher struct {
tr *Trie
}
// NewListHasher initializes the list hasher.
func NewListHasher() *ListHasher {
return &ListHasher{
tr: NewEmpty(nil),
}
}
// Reset clears the internal state prepares the ListHasher for reuse.
func (h *ListHasher) Reset() {
h.tr.reset()
}
// Update inserts a key-value pair into the trie.
func (h *ListHasher) Update(key []byte, value []byte) error {
key, value = bytes.Clone(key), bytes.Clone(value)
return h.tr.Update(key, value)
}
// Hash computes the root hash of all inserted key-value pairs.
func (h *ListHasher) Hash() common.Hash {
return h.tr.Hash()
}

View file

@ -28,7 +28,7 @@ import (
var ( var (
stPool = sync.Pool{New: func() any { return new(stNode) }} stPool = sync.Pool{New: func() any { return new(stNode) }}
bPool = newBytesPool(32, 100) bPool = newBytesPool(32, 100)
_ = types.TrieHasher((*StackTrie)(nil)) _ = types.ListHasher((*StackTrie)(nil))
) )
// OnTrieNode is a callback method invoked when a trie node is committed // OnTrieNode is a callback method invoked when a trie node is committed
@ -50,6 +50,7 @@ type StackTrie struct {
onTrieNode OnTrieNode onTrieNode OnTrieNode
kBuf []byte // buf space used for hex-key during insertions kBuf []byte // buf space used for hex-key during insertions
pBuf []byte // buf space used for path during insertions pBuf []byte // buf space used for path during insertions
vPool *unsafeBytesPool
} }
// NewStackTrie allocates and initializes an empty trie. The committed nodes // NewStackTrie allocates and initializes an empty trie. The committed nodes
@ -61,6 +62,7 @@ func NewStackTrie(onTrieNode OnTrieNode) *StackTrie {
onTrieNode: onTrieNode, onTrieNode: onTrieNode,
kBuf: make([]byte, 64), kBuf: make([]byte, 64),
pBuf: make([]byte, 64), pBuf: make([]byte, 64),
vPool: newUnsafeBytesPool(300, 20),
} }
} }
@ -74,6 +76,9 @@ func (t *StackTrie) grow(key []byte) {
} }
// Update inserts a (key, value) pair into the stack trie. // Update inserts a (key, value) pair into the stack trie.
//
// Note the supplied key value pair is copied and managed internally,
// they are safe to be modified after this method returns.
func (t *StackTrie) Update(key, value []byte) error { func (t *StackTrie) Update(key, value []byte) error {
if len(value) == 0 { if len(value) == 0 {
return errors.New("trying to insert empty (deletion)") return errors.New("trying to insert empty (deletion)")
@ -88,7 +93,14 @@ func (t *StackTrie) Update(key, value []byte) error {
} else { } else {
t.last = append(t.last[:0], k...) // reuse key slice t.last = append(t.last[:0], k...) // reuse key slice
} }
t.insert(t.root, k, value, t.pBuf[:0]) vBuf := t.vPool.get()
if cap(vBuf) < len(value) {
vBuf = common.CopyBytes(value)
} else {
vBuf = vBuf[:len(value)]
copy(vBuf, value)
}
t.insert(t.root, k, vBuf, t.pBuf[:0])
return nil return nil
} }
@ -108,14 +120,16 @@ func (t *StackTrie) TrieKey(key []byte) []byte {
// stNode represents a node within a StackTrie // stNode represents a node within a StackTrie
type stNode struct { type stNode struct {
typ uint8 // node type (as in branch, ext, leaf) typ uint8 // node type (as in branch, ext, leaf)
key []byte // key chunk covered by this (leaf|ext) node key []byte // exclusive owned key chunk covered by this (leaf|ext) node
val []byte // value contained by this node if it's a leaf val []byte // exclusive owned value contained by this node (leaf: value; hash: hash)
children [16]*stNode // list of children (for branch and exts) children [16]*stNode // list of children (for branch and ext)
} }
// newLeaf constructs a leaf node with provided node key and value. The key // newLeaf constructs a leaf node with provided node key and value.
// will be deep-copied in the function and safe to modify afterwards, but //
// value is not. // The key is deep-copied within the function, so it can be safely modified
// afterwards. The value is retained directly without copying, as it is
// exclusively owned by the stackTrie.
func newLeaf(key, val []byte) *stNode { func newLeaf(key, val []byte) *stNode {
st := stPool.Get().(*stNode) st := stPool.Get().(*stNode)
st.typ = leafNode st.typ = leafNode
@ -146,9 +160,9 @@ const (
func (n *stNode) reset() *stNode { func (n *stNode) reset() *stNode {
if n.typ == hashedNode { if n.typ == hashedNode {
// On hashnodes, we 'own' the val: it is guaranteed to be not held // On hashnodes, we 'own' the val: it is guaranteed to be not held
// by external caller. Hence, when we arrive here, we can put it back // by external caller. Hence, when we arrive here, we can put it
// into the pool // back into the pool
bPool.Put(n.val) bPool.put(n.val)
} }
n.key = n.key[:0] n.key = n.key[:0]
n.val = nil n.val = nil
@ -172,11 +186,6 @@ func (n *stNode) getDiffIndex(key []byte) int {
} }
// Helper function to that inserts a (key, value) pair into the trie. // Helper function to that inserts a (key, value) pair into the trie.
//
// - The key is not retained by this method, but always copied if needed.
// - The value is retained by this method, as long as the leaf that it represents
// remains unhashed. However: it is never modified.
// - The path is not retained by this method.
func (t *StackTrie) insert(st *stNode, key, value []byte, path []byte) { func (t *StackTrie) insert(st *stNode, key, value []byte, path []byte) {
switch st.typ { switch st.typ {
case branchNode: /* Branch */ case branchNode: /* Branch */
@ -235,16 +244,14 @@ func (t *StackTrie) insert(st *stNode, key, value []byte, path []byte) {
} }
var p *stNode var p *stNode
if diffidx == 0 { if diffidx == 0 {
// the break is on the first byte, so // the break is on the first byte, so the current node
// the current node is converted into // is converted into a branch node.
// a branch node.
st.children[0] = nil st.children[0] = nil
p = st
st.typ = branchNode st.typ = branchNode
p = st
} else { } else {
// the common prefix is at least one byte // the common prefix is at least one byte long, insert
// long, insert a new intermediate branch // a new intermediate branch node.
// node.
st.children[0] = stPool.Get().(*stNode) st.children[0] = stPool.Get().(*stNode)
st.children[0].typ = branchNode st.children[0].typ = branchNode
p = st.children[0] p = st.children[0]
@ -280,8 +287,8 @@ func (t *StackTrie) insert(st *stNode, key, value []byte, path []byte) {
if diffidx == 0 { if diffidx == 0 {
// Convert current leaf into a branch // Convert current leaf into a branch
st.typ = branchNode st.typ = branchNode
p = st
st.children[0] = nil st.children[0] = nil
p = st
} else { } else {
// Convert current node into an ext, // Convert current node into an ext,
// and insert a child branch node. // and insert a child branch node.
@ -307,9 +314,7 @@ func (t *StackTrie) insert(st *stNode, key, value []byte, path []byte) {
st.val = nil st.val = nil
case emptyNode: /* Empty */ case emptyNode: /* Empty */
st.typ = leafNode *st = *newLeaf(key, value)
st.key = append(st.key, key...) // deep-copy the key as it's volatile
st.val = value
case hashedNode: case hashedNode:
panic("trying to insert into hash") panic("trying to insert into hash")
@ -393,18 +398,23 @@ func (t *StackTrie) hash(st *stNode, path []byte) {
st.typ = hashedNode st.typ = hashedNode
st.key = st.key[:0] st.key = st.key[:0]
st.val = nil // Release reference to potentially externally held slice. // Release reference to value slice which is exclusively owned
// by stackTrie itself.
if cap(st.val) > 0 && t.vPool != nil {
t.vPool.put(st.val)
}
st.val = nil
// Skip committing the non-root node if the size is smaller than 32 bytes // Skip committing the non-root node if the size is smaller than 32 bytes
// as tiny nodes are always embedded in their parent except root node. // as tiny nodes are always embedded in their parent except root node.
if len(blob) < 32 && len(path) > 0 { if len(blob) < 32 && len(path) > 0 {
st.val = bPool.GetWithSize(len(blob)) st.val = bPool.getWithSize(len(blob))
copy(st.val, blob) copy(st.val, blob)
return return
} }
// Write the hash to the 'val'. We allocate a new val here to not mutate // Write the hash to the 'val'. We allocate a new val here to not mutate
// input values. // input values.
st.val = bPool.GetWithSize(32) st.val = bPool.getWithSize(32)
t.h.hashDataTo(st.val, blob) t.h.hashDataTo(st.val, blob)
// Invoke the callback it's provided. Notably, the path and blob slices are // Invoke the callback it's provided. Notably, the path and blob slices are

View file

@ -19,6 +19,7 @@ package trie
import ( import (
"errors" "errors"
"fmt" "fmt"
"slices"
"sync" "sync"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
@ -553,7 +554,7 @@ func (s *Sync) children(req *nodeRequest, object node) ([]*nodeRequest, error) {
} }
children = []childNode{{ children = []childNode{{
node: node.Val, node: node.Val,
path: append(append([]byte(nil), req.path...), key...), path: slices.Concat(req.path, key),
}} }}
// Mark all internal nodes between shortNode and its **in disk** // Mark all internal nodes between shortNode and its **in disk**
// child as invalid. This is essential in the case of path mode // child as invalid. This is essential in the case of path mode
@ -595,7 +596,7 @@ func (s *Sync) children(req *nodeRequest, object node) ([]*nodeRequest, error) {
if node.Children[i] != nil { if node.Children[i] != nil {
children = append(children, childNode{ children = append(children, childNode{
node: node.Children[i], node: node.Children[i],
path: append(append([]byte(nil), req.path...), byte(i)), path: append(slices.Clone(req.path), byte(i)),
}) })
} }
} }

View file

@ -784,8 +784,8 @@ func (t *Trie) Witness() map[string][]byte {
return t.prevalueTracer.Values() return t.prevalueTracer.Values()
} }
// Reset drops the referenced root node and cleans all internal state. // reset drops the referenced root node and cleans all internal state.
func (t *Trie) Reset() { func (t *Trie) reset() {
t.root = nil t.root = nil
t.owner = common.Hash{} t.owner = common.Hash{}
t.unhashed = 0 t.unhashed = 0

View file

@ -232,7 +232,7 @@ func (db *Database) repairHistory() error {
// Purge all state history indexing data first // Purge all state history indexing data first
batch := db.diskdb.NewBatch() batch := db.diskdb.NewBatch()
rawdb.DeleteStateHistoryIndexMetadata(batch) rawdb.DeleteStateHistoryIndexMetadata(batch)
rawdb.DeleteStateHistoryIndex(batch) rawdb.DeleteStateHistoryIndexes(batch)
if err := batch.Write(); err != nil { if err := batch.Write(); err != nil {
log.Crit("Failed to purge state history index", "err", err) log.Crit("Failed to purge state history index", "err", err)
} }
@ -426,7 +426,7 @@ func (db *Database) Enable(root common.Hash) error {
// Purge all state history indexing data first // Purge all state history indexing data first
batch.Reset() batch.Reset()
rawdb.DeleteStateHistoryIndexMetadata(batch) rawdb.DeleteStateHistoryIndexMetadata(batch)
rawdb.DeleteStateHistoryIndex(batch) rawdb.DeleteStateHistoryIndexes(batch)
if err := batch.Write(); err != nil { if err := batch.Write(); err != nil {
return err return err
} }

View file

@ -32,6 +32,9 @@ type historyType uint8
const ( const (
// typeStateHistory indicates history data related to account or storage changes. // typeStateHistory indicates history data related to account or storage changes.
typeStateHistory historyType = 0 typeStateHistory historyType = 0
// typeTrienodeHistory indicates history data related to trie node changes.
typeTrienodeHistory historyType = 1
) )
// String returns the string format representation. // String returns the string format representation.
@ -39,6 +42,8 @@ func (h historyType) String() string {
switch h { switch h {
case typeStateHistory: case typeStateHistory:
return "state" return "state"
case typeTrienodeHistory:
return "trienode"
default: default:
return fmt.Sprintf("unknown type: %d", h) return fmt.Sprintf("unknown type: %d", h)
} }
@ -50,6 +55,7 @@ type elementType uint8
const ( const (
typeAccount elementType = 0 // represents the account data typeAccount elementType = 0 // represents the account data
typeStorage elementType = 1 // represents the storage slot data typeStorage elementType = 1 // represents the storage slot data
typeTrienode elementType = 2 // represents the trie node data
) )
// String returns the string format representation. // String returns the string format representation.
@ -59,6 +65,8 @@ func (e elementType) String() string {
return "account" return "account"
case typeStorage: case typeStorage:
return "storage" return "storage"
case typeTrienode:
return "trienode"
default: default:
return fmt.Sprintf("unknown element type: %d", e) return fmt.Sprintf("unknown element type: %d", e)
} }
@ -69,11 +77,14 @@ func toHistoryType(typ elementType) historyType {
if typ == typeAccount || typ == typeStorage { if typ == typeAccount || typ == typeStorage {
return typeStateHistory return typeStateHistory
} }
if typ == typeTrienode {
return typeTrienodeHistory
}
panic(fmt.Sprintf("unknown element type %v", typ)) panic(fmt.Sprintf("unknown element type %v", typ))
} }
// stateIdent represents the identifier of a state element, which can be // stateIdent represents the identifier of a state element, which can be
// an account or a storage slot. // an account, a storage slot or a trienode.
type stateIdent struct { type stateIdent struct {
typ elementType typ elementType
@ -91,6 +102,12 @@ type stateIdent struct {
// //
// This field is null if the identifier refers to an account or a trie node. // This field is null if the identifier refers to an account or a trie node.
storageHash common.Hash storageHash common.Hash
// The trie node path within the trie.
//
// This field is null if the identifier refers to an account or a storage slot.
// String type is chosen to make stateIdent comparable.
path string
} }
// String returns the string format state identifier. // String returns the string format state identifier.
@ -98,7 +115,10 @@ func (ident stateIdent) String() string {
if ident.typ == typeAccount { if ident.typ == typeAccount {
return ident.addressHash.Hex() return ident.addressHash.Hex()
} }
if ident.typ == typeStorage {
return ident.addressHash.Hex() + ident.storageHash.Hex() return ident.addressHash.Hex() + ident.storageHash.Hex()
}
return ident.addressHash.Hex() + ident.path
} }
// newAccountIdent constructs a state identifier for an account. // newAccountIdent constructs a state identifier for an account.
@ -120,8 +140,18 @@ func newStorageIdent(addressHash common.Hash, storageHash common.Hash) stateIden
} }
} }
// stateIdentQuery is the extension of stateIdent by adding the account address // newTrienodeIdent constructs a state identifier for a trie node.
// and raw storage key. // The address denotes the address hash of the associated account;
// the path denotes the path of the node within the trie;
func newTrienodeIdent(addressHash common.Hash, path string) stateIdent {
return stateIdent{
typ: typeTrienode,
addressHash: addressHash,
path: path,
}
}
// stateIdentQuery is the extension of stateIdent by adding the raw storage key.
type stateIdentQuery struct { type stateIdentQuery struct {
stateIdent stateIdent
@ -150,8 +180,19 @@ func newStorageIdentQuery(address common.Address, addressHash common.Hash, stora
} }
} }
// history defines the interface of historical data, implemented by stateHistory // newTrienodeIdentQuery constructs a state identifier for a trie node.
// and trienodeHistory (in the near future). // the addressHash denotes the address hash of the associated account;
// the path denotes the path of the node within the trie;
//
// nolint:unused
func newTrienodeIdentQuery(addrHash common.Hash, path []byte) stateIdentQuery {
return stateIdentQuery{
stateIdent: newTrienodeIdent(addrHash, string(path)),
}
}
// history defines the interface of historical data, shared by stateHistory
// and trienodeHistory.
type history interface { type history interface {
// typ returns the historical data type held in the history. // typ returns the historical data type held in the history.
typ() historyType typ() historyType

View file

@ -376,6 +376,8 @@ func readStateIndex(ident stateIdent, db ethdb.KeyValueReader) []byte {
return rawdb.ReadAccountHistoryIndex(db, ident.addressHash) return rawdb.ReadAccountHistoryIndex(db, ident.addressHash)
case typeStorage: case typeStorage:
return rawdb.ReadStorageHistoryIndex(db, ident.addressHash, ident.storageHash) return rawdb.ReadStorageHistoryIndex(db, ident.addressHash, ident.storageHash)
case typeTrienode:
return rawdb.ReadTrienodeHistoryIndex(db, ident.addressHash, []byte(ident.path))
default: default:
panic(fmt.Errorf("unknown type: %v", ident.typ)) panic(fmt.Errorf("unknown type: %v", ident.typ))
} }
@ -389,6 +391,8 @@ func writeStateIndex(ident stateIdent, db ethdb.KeyValueWriter, data []byte) {
rawdb.WriteAccountHistoryIndex(db, ident.addressHash, data) rawdb.WriteAccountHistoryIndex(db, ident.addressHash, data)
case typeStorage: case typeStorage:
rawdb.WriteStorageHistoryIndex(db, ident.addressHash, ident.storageHash, data) rawdb.WriteStorageHistoryIndex(db, ident.addressHash, ident.storageHash, data)
case typeTrienode:
rawdb.WriteTrienodeHistoryIndex(db, ident.addressHash, []byte(ident.path), data)
default: default:
panic(fmt.Errorf("unknown type: %v", ident.typ)) panic(fmt.Errorf("unknown type: %v", ident.typ))
} }
@ -402,6 +406,8 @@ func deleteStateIndex(ident stateIdent, db ethdb.KeyValueWriter) {
rawdb.DeleteAccountHistoryIndex(db, ident.addressHash) rawdb.DeleteAccountHistoryIndex(db, ident.addressHash)
case typeStorage: case typeStorage:
rawdb.DeleteStorageHistoryIndex(db, ident.addressHash, ident.storageHash) rawdb.DeleteStorageHistoryIndex(db, ident.addressHash, ident.storageHash)
case typeTrienode:
rawdb.DeleteTrienodeHistoryIndex(db, ident.addressHash, []byte(ident.path))
default: default:
panic(fmt.Errorf("unknown type: %v", ident.typ)) panic(fmt.Errorf("unknown type: %v", ident.typ))
} }
@ -415,6 +421,8 @@ func readStateIndexBlock(ident stateIdent, db ethdb.KeyValueReader, id uint32) [
return rawdb.ReadAccountHistoryIndexBlock(db, ident.addressHash, id) return rawdb.ReadAccountHistoryIndexBlock(db, ident.addressHash, id)
case typeStorage: case typeStorage:
return rawdb.ReadStorageHistoryIndexBlock(db, ident.addressHash, ident.storageHash, id) return rawdb.ReadStorageHistoryIndexBlock(db, ident.addressHash, ident.storageHash, id)
case typeTrienode:
return rawdb.ReadTrienodeHistoryIndexBlock(db, ident.addressHash, []byte(ident.path), id)
default: default:
panic(fmt.Errorf("unknown type: %v", ident.typ)) panic(fmt.Errorf("unknown type: %v", ident.typ))
} }
@ -428,6 +436,8 @@ func writeStateIndexBlock(ident stateIdent, db ethdb.KeyValueWriter, id uint32,
rawdb.WriteAccountHistoryIndexBlock(db, ident.addressHash, id, data) rawdb.WriteAccountHistoryIndexBlock(db, ident.addressHash, id, data)
case typeStorage: case typeStorage:
rawdb.WriteStorageHistoryIndexBlock(db, ident.addressHash, ident.storageHash, id, data) rawdb.WriteStorageHistoryIndexBlock(db, ident.addressHash, ident.storageHash, id, data)
case typeTrienode:
rawdb.WriteTrienodeHistoryIndexBlock(db, ident.addressHash, []byte(ident.path), id, data)
default: default:
panic(fmt.Errorf("unknown type: %v", ident.typ)) panic(fmt.Errorf("unknown type: %v", ident.typ))
} }
@ -441,6 +451,8 @@ func deleteStateIndexBlock(ident stateIdent, db ethdb.KeyValueWriter, id uint32)
rawdb.DeleteAccountHistoryIndexBlock(db, ident.addressHash, id) rawdb.DeleteAccountHistoryIndexBlock(db, ident.addressHash, id)
case typeStorage: case typeStorage:
rawdb.DeleteStorageHistoryIndexBlock(db, ident.addressHash, ident.storageHash, id) rawdb.DeleteStorageHistoryIndexBlock(db, ident.addressHash, ident.storageHash, id)
case typeTrienode:
rawdb.DeleteTrienodeHistoryIndexBlock(db, ident.addressHash, []byte(ident.path), id)
default: default:
panic(fmt.Errorf("unknown type: %v", ident.typ)) panic(fmt.Errorf("unknown type: %v", ident.typ))
} }

View file

@ -36,8 +36,10 @@ const (
// The batch size for reading state histories // The batch size for reading state histories
historyReadBatch = 1000 historyReadBatch = 1000
stateIndexV0 = uint8(0) // initial version of state index structure stateHistoryIndexV0 = uint8(0) // initial version of state index structure
stateIndexVersion = stateIndexV0 // the current state index version stateHistoryIndexVersion = stateHistoryIndexV0 // the current state index version
trienodeHistoryIndexV0 = uint8(0) // initial version of trienode index structure
trienodeHistoryIndexVersion = trienodeHistoryIndexV0 // the current trienode index version
) )
// indexVersion returns the latest index version for the given history type. // indexVersion returns the latest index version for the given history type.
@ -45,7 +47,9 @@ const (
func indexVersion(typ historyType) uint8 { func indexVersion(typ historyType) uint8 {
switch typ { switch typ {
case typeStateHistory: case typeStateHistory:
return stateIndexVersion return stateHistoryIndexVersion
case typeTrienodeHistory:
return trienodeHistoryIndexVersion
default: default:
panic(fmt.Errorf("unknown history type: %d", typ)) panic(fmt.Errorf("unknown history type: %d", typ))
} }
@ -63,6 +67,8 @@ func loadIndexMetadata(db ethdb.KeyValueReader, typ historyType) *indexMetadata
switch typ { switch typ {
case typeStateHistory: case typeStateHistory:
blob = rawdb.ReadStateHistoryIndexMetadata(db) blob = rawdb.ReadStateHistoryIndexMetadata(db)
case typeTrienodeHistory:
blob = rawdb.ReadTrienodeHistoryIndexMetadata(db)
default: default:
panic(fmt.Errorf("unknown history type %d", typ)) panic(fmt.Errorf("unknown history type %d", typ))
} }
@ -90,6 +96,8 @@ func storeIndexMetadata(db ethdb.KeyValueWriter, typ historyType, last uint64) {
switch typ { switch typ {
case typeStateHistory: case typeStateHistory:
rawdb.WriteStateHistoryIndexMetadata(db, blob) rawdb.WriteStateHistoryIndexMetadata(db, blob)
case typeTrienodeHistory:
rawdb.WriteTrienodeHistoryIndexMetadata(db, blob)
default: default:
panic(fmt.Errorf("unknown history type %d", typ)) panic(fmt.Errorf("unknown history type %d", typ))
} }
@ -101,6 +109,8 @@ func deleteIndexMetadata(db ethdb.KeyValueWriter, typ historyType) {
switch typ { switch typ {
case typeStateHistory: case typeStateHistory:
rawdb.DeleteStateHistoryIndexMetadata(db) rawdb.DeleteStateHistoryIndexMetadata(db)
case typeTrienodeHistory:
rawdb.DeleteTrienodeHistoryIndexMetadata(db)
default: default:
panic(fmt.Errorf("unknown history type %d", typ)) panic(fmt.Errorf("unknown history type %d", typ))
} }
@ -215,7 +225,11 @@ func (b *batchIndexer) finish(force bool) error {
func indexSingle(historyID uint64, db ethdb.KeyValueStore, freezer ethdb.AncientReader, typ historyType) error { func indexSingle(historyID uint64, db ethdb.KeyValueStore, freezer ethdb.AncientReader, typ historyType) error {
start := time.Now() start := time.Now()
defer func() { defer func() {
indexHistoryTimer.UpdateSince(start) if typ == typeStateHistory {
stateIndexHistoryTimer.UpdateSince(start)
} else if typ == typeTrienodeHistory {
trienodeIndexHistoryTimer.UpdateSince(start)
}
}() }()
metadata := loadIndexMetadata(db, typ) metadata := loadIndexMetadata(db, typ)
@ -234,7 +248,7 @@ func indexSingle(historyID uint64, db ethdb.KeyValueStore, freezer ethdb.Ancient
if typ == typeStateHistory { if typ == typeStateHistory {
h, err = readStateHistory(freezer, historyID) h, err = readStateHistory(freezer, historyID)
} else { } else {
// h, err = readTrienodeHistory(freezer, historyID) h, err = readTrienodeHistory(freezer, historyID)
} }
if err != nil { if err != nil {
return err return err
@ -253,7 +267,11 @@ func indexSingle(historyID uint64, db ethdb.KeyValueStore, freezer ethdb.Ancient
func unindexSingle(historyID uint64, db ethdb.KeyValueStore, freezer ethdb.AncientReader, typ historyType) error { func unindexSingle(historyID uint64, db ethdb.KeyValueStore, freezer ethdb.AncientReader, typ historyType) error {
start := time.Now() start := time.Now()
defer func() { defer func() {
unindexHistoryTimer.UpdateSince(start) if typ == typeStateHistory {
stateUnindexHistoryTimer.UpdateSince(start)
} else if typ == typeTrienodeHistory {
trienodeUnindexHistoryTimer.UpdateSince(start)
}
}() }()
metadata := loadIndexMetadata(db, typ) metadata := loadIndexMetadata(db, typ)
@ -272,7 +290,7 @@ func unindexSingle(historyID uint64, db ethdb.KeyValueStore, freezer ethdb.Ancie
if typ == typeStateHistory { if typ == typeStateHistory {
h, err = readStateHistory(freezer, historyID) h, err = readStateHistory(freezer, historyID)
} else { } else {
// h, err = readTrienodeHistory(freezer, historyID) h, err = readTrienodeHistory(freezer, historyID)
} }
if err != nil { if err != nil {
return err return err
@ -546,13 +564,13 @@ func (i *indexIniter) index(done chan struct{}, interrupt *atomic.Int32, lastID
return return
} }
} else { } else {
// histories, err = readTrienodeHistories(i.freezer, current, count) histories, err = readTrienodeHistories(i.freezer, current, count)
// if err != nil { if err != nil {
// // The history read might fall if the history is truncated from // The history read might fall if the history is truncated from
// // head due to revert operation. // head due to revert operation.
// i.log.Error("Failed to read history for indexing", "current", current, "count", count, "err", err) i.log.Error("Failed to read history for indexing", "current", current, "count", count, "err", err)
// return return
// } }
} }
for _, h := range histories { for _, h := range histories {
if err := batch.process(h, current); err != nil { if err := batch.process(h, current); err != nil {
@ -570,7 +588,7 @@ func (i *indexIniter) index(done chan struct{}, interrupt *atomic.Int32, lastID
done = current - beginID done = current - beginID
) )
eta := common.CalculateETA(done, left, time.Since(start)) eta := common.CalculateETA(done, left, time.Since(start))
i.log.Info("Indexing state history", "processed", done, "left", left, "elapsed", common.PrettyDuration(time.Since(start)), "eta", common.PrettyDuration(eta)) i.log.Info("Indexing history", "processed", done, "left", left, "elapsed", common.PrettyDuration(time.Since(start)), "eta", common.PrettyDuration(eta))
} }
} }
i.indexed.Store(current - 1) // update indexing progress i.indexed.Store(current - 1) // update indexing progress
@ -657,6 +675,8 @@ func checkVersion(disk ethdb.KeyValueStore, typ historyType) {
var blob []byte var blob []byte
if typ == typeStateHistory { if typ == typeStateHistory {
blob = rawdb.ReadStateHistoryIndexMetadata(disk) blob = rawdb.ReadStateHistoryIndexMetadata(disk)
} else if typ == typeTrienodeHistory {
blob = rawdb.ReadTrienodeHistoryIndexMetadata(disk)
} else { } else {
panic(fmt.Errorf("unknown history type: %v", typ)) panic(fmt.Errorf("unknown history type: %v", typ))
} }
@ -666,24 +686,32 @@ func checkVersion(disk ethdb.KeyValueStore, typ historyType) {
return return
} }
// Short circuit if the metadata is found and the version is matched // Short circuit if the metadata is found and the version is matched
ver := stateHistoryIndexVersion
if typ == typeTrienodeHistory {
ver = trienodeHistoryIndexVersion
}
var m indexMetadata var m indexMetadata
err := rlp.DecodeBytes(blob, &m) err := rlp.DecodeBytes(blob, &m)
if err == nil && m.Version == stateIndexVersion { if err == nil && m.Version == ver {
return return
} }
// Version is not matched, prune the existing data and re-index from scratch // Version is not matched, prune the existing data and re-index from scratch
batch := disk.NewBatch()
if typ == typeStateHistory {
rawdb.DeleteStateHistoryIndexMetadata(batch)
rawdb.DeleteStateHistoryIndexes(batch)
} else {
rawdb.DeleteTrienodeHistoryIndexMetadata(batch)
rawdb.DeleteTrienodeHistoryIndexes(batch)
}
if err := batch.Write(); err != nil {
log.Crit("Failed to purge history index", "type", typ, "err", err)
}
version := "unknown" version := "unknown"
if err == nil { if err == nil {
version = fmt.Sprintf("%d", m.Version) version = fmt.Sprintf("%d", m.Version)
} }
log.Info("Cleaned up obsolete history index", "type", typ, "version", version, "want", version)
batch := disk.NewBatch()
rawdb.DeleteStateHistoryIndexMetadata(batch)
rawdb.DeleteStateHistoryIndex(batch)
if err := batch.Write(); err != nil {
log.Crit("Failed to purge state history index", "err", err)
}
log.Info("Cleaned up obsolete state history index", "version", version, "want", stateIndexVersion)
} }
// newHistoryIndexer constructs the history indexer and launches the background // newHistoryIndexer constructs the history indexer and launches the background

View file

@ -605,9 +605,9 @@ func writeStateHistory(writer ethdb.AncientWriter, dl *diffLayer) error {
if err := rawdb.WriteStateHistory(writer, dl.stateID(), history.meta.encode(), accountIndex, storageIndex, accountData, storageData); err != nil { if err := rawdb.WriteStateHistory(writer, dl.stateID(), history.meta.encode(), accountIndex, storageIndex, accountData, storageData); err != nil {
return err return err
} }
historyDataBytesMeter.Mark(int64(dataSize)) stateHistoryDataBytesMeter.Mark(int64(dataSize))
historyIndexBytesMeter.Mark(int64(indexSize)) stateHistoryIndexBytesMeter.Mark(int64(indexSize))
historyBuildTimeMeter.UpdateSince(start) stateHistoryBuildTimeMeter.UpdateSince(start)
log.Debug("Stored state history", "id", dl.stateID(), "block", dl.block, "data", dataSize, "index", indexSize, "elapsed", common.PrettyDuration(time.Since(start))) log.Debug("Stored state history", "id", dl.stateID(), "block", dl.block, "data", dataSize, "index", indexSize, "elapsed", common.PrettyDuration(time.Since(start)))
return nil return nil

View file

@ -98,13 +98,13 @@ func testEncodeDecodeStateHistory(t *testing.T, rawStorageKey bool) {
if !compareSet(dec.accounts, obj.accounts) { if !compareSet(dec.accounts, obj.accounts) {
t.Fatal("account data is mismatched") t.Fatal("account data is mismatched")
} }
if !compareStorages(dec.storages, obj.storages) { if !compareMapSet(dec.storages, obj.storages) {
t.Fatal("storage data is mismatched") t.Fatal("storage data is mismatched")
} }
if !compareList(dec.accountList, obj.accountList) { if !compareList(dec.accountList, obj.accountList) {
t.Fatal("account list is mismatched") t.Fatal("account list is mismatched")
} }
if !compareStorageList(dec.storageList, obj.storageList) { if !compareMapList(dec.storageList, obj.storageList) {
t.Fatal("storage list is mismatched") t.Fatal("storage list is mismatched")
} }
} }
@ -292,32 +292,32 @@ func compareList[k comparable](a, b []k) bool {
return true return true
} }
func compareStorages(a, b map[common.Address]map[common.Hash][]byte) bool { func compareMapSet[K1 comparable, K2 comparable](a, b map[K1]map[K2][]byte) bool {
if len(a) != len(b) { if len(a) != len(b) {
return false return false
} }
for h, subA := range a { for key, subsetA := range a {
subB, ok := b[h] subsetB, ok := b[key]
if !ok { if !ok {
return false return false
} }
if !compareSet(subA, subB) { if !compareSet(subsetA, subsetB) {
return false return false
} }
} }
return true return true
} }
func compareStorageList(a, b map[common.Address][]common.Hash) bool { func compareMapList[K comparable, V comparable](a, b map[K][]V) bool {
if len(a) != len(b) { if len(a) != len(b) {
return false return false
} }
for h, la := range a { for key, listA := range a {
lb, ok := b[h] listB, ok := b[key]
if !ok { if !ok {
return false return false
} }
if !compareList(la, lb) { if !compareList(listA, listB) {
return false return false
} }
} }

View file

@ -0,0 +1,730 @@
// 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 pathdb
import (
"bytes"
"encoding/binary"
"fmt"
"iter"
"maps"
"math"
"slices"
"sort"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/ethdb"
"github.com/ethereum/go-ethereum/log"
)
// Each trie node history entry consists of three parts (stored in three freezer
// tables according):
//
// # Header
// The header records metadata, including:
//
// - the history version (1 byte)
// - the parent state root (32 bytes)
// - the current state root (32 bytes)
// - block number (8 bytes)
//
// - a lexicographically sorted list of trie IDs
// - the corresponding offsets into the key and value sections for each trie data chunk
//
// Although some fields (e.g., parent state root, block number) are duplicated
// between the state history and the trienode history, these two histories
// operate independently. To ensure each remains self-contained and self-descriptive,
// we have chosen to maintain these duplicate fields.
//
// # Key section
// The key section stores trie node keys (paths) in a compressed format.
// It also contains relative offsets into the value section for resolving
// the corresponding trie node data. Note that these offsets are relative
// to the data chunk for the trie; the chunk offset must be added to obtain
// the absolute position.
//
// # Value section
// The value section is a concatenated byte stream of all trie node data.
// Each trie node can be retrieved using the offset and length specified
// by its index entry.
//
// The header and key sections are sufficient for locating a trie node,
// while a partial read of the value section is enough to retrieve its data.
// Header section:
//
// +----------+------------------+---------------------+---------------------+-------+------------------+---------------------+---------------------|
// | metadata | TrieID(32 bytes) | key offset(4 bytes) | val offset(4 bytes) | ... | TrieID(32 bytes) | key offset(4 bytes) | val offset(4 bytes) |
// +----------+------------------+---------------------+---------------------+-------+------------------+---------------------+---------------------|
//
//
// Key section:
//
// + restart point + restart point (depends on restart interval)
// / /
// +---------------+---------------+---------------+---------------+---------+
// | node entry 1 | node entry 2 | ... | node entry n | trailer |
// +---------------+---------------+---------------+---------------+---------+
// \ /
// +---- restart block ------+
//
// node entry:
//
// +---- key len ----+
// / \
// +-------+---------+-----------+---------+-----------------------+-----------------+
// | shared (varint) | not shared (varint) | value length (varlen) | key (varlen) |
// +-----------------+---------------------+-----------------------+-----------------+
//
// trailer:
//
// +---- 4-bytes ----+ +---- 4-bytes ----+
// / \ / \
// +----------------------+------------------------+-----+--------------------------+
// | restart_1 key offset | restart_1 value offset | ... | restart number (4-bytes) |
// +----------------------+------------------------+-----+--------------------------+
//
// Note: Both the key offset and the value offset are relative to the start of
// the trie data chunk. To obtain the absolute offset, add the offset of the
// trie data chunk itself.
//
// Value section:
//
// +--------------+--------------+-------+---------------+
// | node data 1 | node data 2 | ... | node data n |
// +--------------+--------------+-------+---------------+
//
// NOTE: All fixed-length integer are big-endian.
const (
trienodeHistoryV0 = uint8(0) // initial version of node history structure
trienodeHistoryVersion = trienodeHistoryV0 // the default node history version
trienodeMetadataSize = 1 + 2*common.HashLength + 8 // the size of metadata in the history
trienodeTrieHeaderSize = 8 + common.HashLength // the size of a single trie header in history
trienodeDataBlockRestartLen = 16 // The restart interval length of trie node block
)
// trienodeMetadata describes the meta data of trienode history.
type trienodeMetadata struct {
version uint8 // version tag of history object
parent common.Hash // prev-state root before the state transition
root common.Hash // post-state root after the state transition
block uint64 // associated block number
}
// trienodeHistory represents a set of trie node changes resulting from a state
// transition across the main account trie and all associated storage tries.
type trienodeHistory struct {
meta *trienodeMetadata // Metadata of the history
owners []common.Hash // List of trie identifier sorted lexicographically
nodeList map[common.Hash][]string // Set of node paths sorted lexicographically
nodes map[common.Hash]map[string][]byte // Set of original value of trie nodes before state transition
}
// newTrienodeHistory constructs a trienode history with the provided trie nodes.
func newTrienodeHistory(root common.Hash, parent common.Hash, block uint64, nodes map[common.Hash]map[string][]byte) *trienodeHistory {
nodeList := make(map[common.Hash][]string)
for owner, subset := range nodes {
keys := sort.StringSlice(slices.Collect(maps.Keys(subset)))
keys.Sort()
nodeList[owner] = keys
}
return &trienodeHistory{
meta: &trienodeMetadata{
version: trienodeHistoryVersion,
parent: parent,
root: root,
block: block,
},
owners: slices.SortedFunc(maps.Keys(nodes), common.Hash.Cmp),
nodeList: nodeList,
nodes: nodes,
}
}
// sharedLen returns the length of the common prefix shared by a and b.
func sharedLen(a, b []byte) int {
n := min(len(a), len(b))
for i := 0; i < n; i++ {
if a[i] != b[i] {
return i
}
}
return n
}
// typ implements the history interface, returning the historical data type held.
func (h *trienodeHistory) typ() historyType {
return typeTrienodeHistory
}
// forEach implements the history interface, returning an iterator to traverse the
// state entries in the history.
func (h *trienodeHistory) forEach() iter.Seq[stateIdent] {
return func(yield func(stateIdent) bool) {
for _, owner := range h.owners {
for _, path := range h.nodeList[owner] {
if !yield(newTrienodeIdent(owner, path)) {
return
}
}
}
}
}
// encode serializes the contained trie nodes into bytes.
func (h *trienodeHistory) encode() ([]byte, []byte, []byte, error) {
var (
buf = make([]byte, 64)
headerSection bytes.Buffer
keySection bytes.Buffer
valueSection bytes.Buffer
)
binary.Write(&headerSection, binary.BigEndian, h.meta.version) // 1 byte
headerSection.Write(h.meta.parent.Bytes()) // 32 bytes
headerSection.Write(h.meta.root.Bytes()) // 32 bytes
binary.Write(&headerSection, binary.BigEndian, h.meta.block) // 8 byte
for _, owner := range h.owners {
// Fill the header section with offsets at key and value section
headerSection.Write(owner.Bytes()) // 32 bytes
binary.Write(&headerSection, binary.BigEndian, uint32(keySection.Len())) // 4 bytes
// The offset to the value section is theoretically unnecessary, since the
// individual value offset is already tracked in the key section. However,
// we still keep it here for two reasons:
// - It's cheap to store (only 4 bytes for each trie).
// - It can be useful for decoding the trie data when key is not required (e.g., in hash mode).
binary.Write(&headerSection, binary.BigEndian, uint32(valueSection.Len())) // 4 bytes
// Fill the key section with node index
var (
prevKey []byte
restarts []uint32
prefixLen int
internalKeyOffset uint32 // key offset for the trie internally
internalValOffset uint32 // value offset for the trie internally
)
for i, path := range h.nodeList[owner] {
key := []byte(path)
if i%trienodeDataBlockRestartLen == 0 {
restarts = append(restarts, internalKeyOffset)
restarts = append(restarts, internalValOffset)
prefixLen = 0
} else {
prefixLen = sharedLen(prevKey, key)
}
value := h.nodes[owner][path]
// key section
n := binary.PutUvarint(buf[0:], uint64(prefixLen)) // key length shared (varint)
n += binary.PutUvarint(buf[n:], uint64(len(key)-prefixLen)) // key length not shared (varint)
n += binary.PutUvarint(buf[n:], uint64(len(value))) // value length (varint)
if _, err := keySection.Write(buf[:n]); err != nil {
return nil, nil, nil, err
}
// unshared key
if _, err := keySection.Write(key[prefixLen:]); err != nil {
return nil, nil, nil, err
}
n += len(key) - prefixLen
prevKey = key
// value section
if _, err := valueSection.Write(value); err != nil {
return nil, nil, nil, err
}
internalKeyOffset += uint32(n)
internalValOffset += uint32(len(value))
}
// Encode trailer, the number of restart sections is len(restarts))/2,
// as we track the offsets of both key and value sections.
var trailer []byte
for _, number := range append(restarts, uint32(len(restarts))/2) {
binary.BigEndian.PutUint32(buf[:4], number)
trailer = append(trailer, buf[:4]...)
}
if _, err := keySection.Write(trailer); err != nil {
return nil, nil, nil, err
}
}
return headerSection.Bytes(), keySection.Bytes(), valueSection.Bytes(), nil
}
// decodeHeader resolves the metadata from the header section. An error
// should be returned if the header section is corrupted.
func decodeHeader(data []byte) (*trienodeMetadata, []common.Hash, []uint32, []uint32, error) {
if len(data) < trienodeMetadataSize {
return nil, nil, nil, nil, fmt.Errorf("trienode history is too small, index size: %d", len(data))
}
version := data[0]
if version != trienodeHistoryVersion {
return nil, nil, nil, nil, fmt.Errorf("unregonized trienode history version: %d", version)
}
parent := common.BytesToHash(data[1 : common.HashLength+1]) // 32 bytes
root := common.BytesToHash(data[common.HashLength+1 : common.HashLength*2+1]) // 32 bytes
block := binary.BigEndian.Uint64(data[common.HashLength*2+1 : trienodeMetadataSize]) // 8 bytes
size := len(data) - trienodeMetadataSize
if size%trienodeTrieHeaderSize != 0 {
return nil, nil, nil, nil, fmt.Errorf("truncated trienode history data, size %d", len(data))
}
count := size / trienodeTrieHeaderSize
var (
owners = make([]common.Hash, 0, count)
keyOffsets = make([]uint32, 0, count)
valOffsets = make([]uint32, 0, count)
)
for i := 0; i < count; i++ {
n := trienodeMetadataSize + trienodeTrieHeaderSize*i
owner := common.BytesToHash(data[n : n+common.HashLength])
if i != 0 && bytes.Compare(owner.Bytes(), owners[i-1].Bytes()) <= 0 {
return nil, nil, nil, nil, fmt.Errorf("trienode owners are out of order, prev: %v, cur: %v", owners[i-1], owner)
}
owners = append(owners, owner)
// Decode the offset to the key section
keyOffset := binary.BigEndian.Uint32(data[n+common.HashLength : n+common.HashLength+4])
if i != 0 && keyOffset <= keyOffsets[i-1] {
return nil, nil, nil, nil, fmt.Errorf("key offset is out of order, prev: %v, cur: %v", keyOffsets[i-1], keyOffset)
}
keyOffsets = append(keyOffsets, keyOffset)
// Decode the offset into the value section. Note that identical value offsets
// are valid if the node values in the last trie chunk are all zero (e.g., after
// a trie deletion).
valOffset := binary.BigEndian.Uint32(data[n+common.HashLength+4 : n+common.HashLength+8])
if i != 0 && valOffset < valOffsets[i-1] {
return nil, nil, nil, nil, fmt.Errorf("value offset is out of order, prev: %v, cur: %v", valOffsets[i-1], valOffset)
}
valOffsets = append(valOffsets, valOffset)
}
return &trienodeMetadata{
version: version,
parent: parent,
root: root,
block: block,
}, owners, keyOffsets, valOffsets, nil
}
func decodeSingle(keySection []byte, onValue func([]byte, int, int) error) ([]string, error) {
var (
prevKey []byte
items int
keyOffsets []uint32
valOffsets []uint32
keyOff int // the key offset within the single trie data
valOff int // the value offset within the single trie data
keys []string
)
// Decode the number of restart section
if len(keySection) < 4 {
return nil, fmt.Errorf("key section too short, size: %d", len(keySection))
}
nRestarts := binary.BigEndian.Uint32(keySection[len(keySection)-4:])
if len(keySection) < int(8*nRestarts)+4 {
return nil, fmt.Errorf("key section too short, restarts: %d, size: %d", nRestarts, len(keySection))
}
for i := 0; i < int(nRestarts); i++ {
o := len(keySection) - 4 - (int(nRestarts)-i)*8
keyOffset := binary.BigEndian.Uint32(keySection[o : o+4])
if i != 0 && keyOffset <= keyOffsets[i-1] {
return nil, fmt.Errorf("key offset is out of order, prev: %v, cur: %v", keyOffsets[i-1], keyOffset)
}
keyOffsets = append(keyOffsets, keyOffset)
// Same value offset is allowed just in case all the trie nodes in the last
// section have zero-size value.
valOffset := binary.BigEndian.Uint32(keySection[o+4 : o+8])
if i != 0 && valOffset < valOffsets[i-1] {
return nil, fmt.Errorf("value offset is out of order, prev: %v, cur: %v", valOffsets[i-1], valOffset)
}
valOffsets = append(valOffsets, valOffset)
}
keyLimit := len(keySection) - 4 - int(nRestarts)*8
// Decode data
for keyOff < keyLimit {
// Validate the key and value offsets within the single trie data chunk
if items%trienodeDataBlockRestartLen == 0 {
if keyOff != int(keyOffsets[items/trienodeDataBlockRestartLen]) {
return nil, fmt.Errorf("key offset is not matched, recorded: %d, want: %d", keyOffsets[items/trienodeDataBlockRestartLen], keyOff)
}
if valOff != int(valOffsets[items/trienodeDataBlockRestartLen]) {
return nil, fmt.Errorf("value offset is not matched, recorded: %d, want: %d", valOffsets[items/trienodeDataBlockRestartLen], valOff)
}
}
// Resolve the entry from key section
nShared, nn := binary.Uvarint(keySection[keyOff:]) // key length shared (varint)
keyOff += nn
nUnshared, nn := binary.Uvarint(keySection[keyOff:]) // key length not shared (varint)
keyOff += nn
nValue, nn := binary.Uvarint(keySection[keyOff:]) // value length (varint)
keyOff += nn
// Resolve unshared key
if keyOff+int(nUnshared) > len(keySection) {
return nil, fmt.Errorf("key length too long, unshared key length: %d, off: %d, section size: %d", nUnshared, keyOff, len(keySection))
}
unsharedKey := keySection[keyOff : keyOff+int(nUnshared)]
keyOff += int(nUnshared)
// Assemble the full key
var key []byte
if items%trienodeDataBlockRestartLen == 0 {
if nShared != 0 {
return nil, fmt.Errorf("unexpected non-zero shared key prefix: %d", nShared)
}
key = unsharedKey
} else {
if int(nShared) > len(prevKey) {
return nil, fmt.Errorf("unexpected shared key prefix: %d, prefix key length: %d", nShared, len(prevKey))
}
key = append([]byte{}, prevKey[:nShared]...)
key = append(key, unsharedKey...)
}
if items != 0 && bytes.Compare(prevKey, key) >= 0 {
return nil, fmt.Errorf("trienode paths are out of order, prev: %v, cur: %v", prevKey, key)
}
prevKey = key
// Resolve value
if onValue != nil {
if err := onValue(key, valOff, valOff+int(nValue)); err != nil {
return nil, err
}
}
valOff += int(nValue)
items++
keys = append(keys, string(key))
}
if keyOff != keyLimit {
return nil, fmt.Errorf("excessive key data after decoding, offset: %d, size: %d", keyOff, keyLimit)
}
return keys, nil
}
func decodeSingleWithValue(keySection []byte, valueSection []byte) ([]string, map[string][]byte, error) {
var (
offset int
nodes = make(map[string][]byte)
)
paths, err := decodeSingle(keySection, func(key []byte, start int, limit int) error {
if start != offset {
return fmt.Errorf("gapped value section offset: %d, want: %d", start, offset)
}
// start == limit is allowed for zero-value trie node (e.g., non-existent node)
if start > limit {
return fmt.Errorf("invalid value offsets, start: %d, limit: %d", start, limit)
}
if start > len(valueSection) || limit > len(valueSection) {
return fmt.Errorf("value section out of range: start: %d, limit: %d, size: %d", start, limit, len(valueSection))
}
nodes[string(key)] = valueSection[start:limit]
offset = limit
return nil
})
if err != nil {
return nil, nil, err
}
if offset != len(valueSection) {
return nil, nil, fmt.Errorf("excessive value data after decoding, offset: %d, size: %d", offset, len(valueSection))
}
return paths, nodes, nil
}
// decode deserializes the contained trie nodes from the provided bytes.
func (h *trienodeHistory) decode(header []byte, keySection []byte, valueSection []byte) error {
metadata, owners, keyOffsets, valueOffsets, err := decodeHeader(header)
if err != nil {
return err
}
h.meta = metadata
h.owners = owners
h.nodeList = make(map[common.Hash][]string)
h.nodes = make(map[common.Hash]map[string][]byte)
for i := 0; i < len(owners); i++ {
// Resolve the boundary of key section
keyStart := keyOffsets[i]
keyLimit := len(keySection)
if i != len(owners)-1 {
keyLimit = int(keyOffsets[i+1])
}
if int(keyStart) > len(keySection) || keyLimit > len(keySection) {
return fmt.Errorf("invalid key offsets: keyStart: %d, keyLimit: %d, size: %d", keyStart, keyLimit, len(keySection))
}
// Resolve the boundary of value section
valStart := valueOffsets[i]
valLimit := len(valueSection)
if i != len(owners)-1 {
valLimit = int(valueOffsets[i+1])
}
if int(valStart) > len(valueSection) || valLimit > len(valueSection) {
return fmt.Errorf("invalid value offsets: valueStart: %d, valueLimit: %d, size: %d", valStart, valLimit, len(valueSection))
}
// Decode the key and values for this specific trie
paths, nodes, err := decodeSingleWithValue(keySection[keyStart:keyLimit], valueSection[valStart:valLimit])
if err != nil {
return err
}
h.nodeList[owners[i]] = paths
h.nodes[owners[i]] = nodes
}
return nil
}
type iRange struct {
start uint32
limit uint32
}
// singleTrienodeHistoryReader provides read access to a single trie within the
// trienode history. It stores an offset to the trie's position in the history,
// along with a set of per-node offsets that can be resolved on demand.
type singleTrienodeHistoryReader struct {
id uint64
reader ethdb.AncientReader
valueRange iRange // value range within the total value section
valueInternalOffsets map[string]iRange // value offset within the single trie data
}
func newSingleTrienodeHistoryReader(id uint64, reader ethdb.AncientReader, keyRange iRange, valueRange iRange) (*singleTrienodeHistoryReader, error) {
// TODO(rjl493456442) partial freezer read should be supported
keyData, err := rawdb.ReadTrienodeHistoryKeySection(reader, id)
if err != nil {
return nil, err
}
keyStart := int(keyRange.start)
keyLimit := int(keyRange.limit)
if keyLimit == math.MaxUint32 {
keyLimit = len(keyData)
}
if len(keyData) < keyStart || len(keyData) < keyLimit {
return nil, fmt.Errorf("key section too short, start: %d, limit: %d, size: %d", keyStart, keyLimit, len(keyData))
}
valueOffsets := make(map[string]iRange)
_, err = decodeSingle(keyData[keyStart:keyLimit], func(key []byte, start int, limit int) error {
valueOffsets[string(key)] = iRange{
start: uint32(start),
limit: uint32(limit),
}
return nil
})
if err != nil {
return nil, err
}
return &singleTrienodeHistoryReader{
id: id,
reader: reader,
valueRange: valueRange,
valueInternalOffsets: valueOffsets,
}, nil
}
// read retrieves the trie node data with the provided node path.
func (sr *singleTrienodeHistoryReader) read(path string) ([]byte, error) {
offset, exists := sr.valueInternalOffsets[path]
if !exists {
return nil, fmt.Errorf("trienode %v not found", []byte(path))
}
// TODO(rjl493456442) partial freezer read should be supported
valueData, err := rawdb.ReadTrienodeHistoryValueSection(sr.reader, sr.id)
if err != nil {
return nil, err
}
if len(valueData) < int(sr.valueRange.start) {
return nil, fmt.Errorf("value section too short, start: %d, size: %d", sr.valueRange.start, len(valueData))
}
entryStart := sr.valueRange.start + offset.start
entryLimit := sr.valueRange.start + offset.limit
if len(valueData) < int(entryStart) || len(valueData) < int(entryLimit) {
return nil, fmt.Errorf("value section too short, start: %d, limit: %d, size: %d", entryStart, entryLimit, len(valueData))
}
return valueData[int(entryStart):int(entryLimit)], nil
}
// trienodeHistoryReader provides read access to node data in the trie node history.
// It resolves data from the underlying ancient store only when needed, minimizing
// I/O overhead.
type trienodeHistoryReader struct {
id uint64 // ID of the associated trienode history
reader ethdb.AncientReader // Database reader of ancient store
keyRanges map[common.Hash]iRange // Key ranges identifying trie chunks
valRanges map[common.Hash]iRange // Value ranges identifying trie chunks
iReaders map[common.Hash]*singleTrienodeHistoryReader // readers for each individual trie chunk
}
// newTrienodeHistoryReader constructs the reader for specific trienode history.
func newTrienodeHistoryReader(id uint64, reader ethdb.AncientReader) (*trienodeHistoryReader, error) {
r := &trienodeHistoryReader{
id: id,
reader: reader,
keyRanges: make(map[common.Hash]iRange),
valRanges: make(map[common.Hash]iRange),
iReaders: make(map[common.Hash]*singleTrienodeHistoryReader),
}
if err := r.decodeHeader(); err != nil {
return nil, err
}
return r, nil
}
// decodeHeader decodes the header section of trienode history.
func (r *trienodeHistoryReader) decodeHeader() error {
header, err := rawdb.ReadTrienodeHistoryHeader(r.reader, r.id)
if err != nil {
return err
}
_, owners, keyOffsets, valOffsets, err := decodeHeader(header)
if err != nil {
return err
}
for i, owner := range owners {
// Decode the key range for this trie chunk
var keyLimit uint32
if i == len(owners)-1 {
keyLimit = math.MaxUint32
} else {
keyLimit = keyOffsets[i+1]
}
r.keyRanges[owner] = iRange{
start: keyOffsets[i],
limit: keyLimit,
}
// Decode the value range for this trie chunk
var valLimit uint32
if i == len(owners)-1 {
valLimit = math.MaxUint32
} else {
valLimit = valOffsets[i+1]
}
r.valRanges[owner] = iRange{
start: valOffsets[i],
limit: valLimit,
}
}
return nil
}
// read retrieves the trie node data with the provided TrieID and node path.
func (r *trienodeHistoryReader) read(owner common.Hash, path string) ([]byte, error) {
ir, ok := r.iReaders[owner]
if !ok {
keyRange, exists := r.keyRanges[owner]
if !exists {
return nil, fmt.Errorf("trie %x is unknown", owner)
}
valRange, exists := r.valRanges[owner]
if !exists {
return nil, fmt.Errorf("trie %x is unknown", owner)
}
var err error
ir, err = newSingleTrienodeHistoryReader(r.id, r.reader, keyRange, valRange)
if err != nil {
return nil, err
}
r.iReaders[owner] = ir
}
return ir.read(path)
}
// writeTrienodeHistory persists the trienode history associated with the given diff layer.
// nolint:unused
func writeTrienodeHistory(writer ethdb.AncientWriter, dl *diffLayer) error {
start := time.Now()
h := newTrienodeHistory(dl.rootHash(), dl.parent.rootHash(), dl.block, dl.nodes.nodeOrigin)
header, keySection, valueSection, err := h.encode()
if err != nil {
return err
}
// Write history data into five freezer table respectively.
if err := rawdb.WriteTrienodeHistory(writer, dl.stateID(), header, keySection, valueSection); err != nil {
return err
}
trienodeHistoryDataBytesMeter.Mark(int64(len(valueSection)))
trienodeHistoryIndexBytesMeter.Mark(int64(len(header) + len(keySection)))
trienodeHistoryBuildTimeMeter.UpdateSince(start)
log.Debug(
"Stored trienode history", "id", dl.stateID(), "block", dl.block,
"header", common.StorageSize(len(header)),
"keySection", common.StorageSize(len(keySection)),
"valueSection", common.StorageSize(len(valueSection)),
"elapsed", common.PrettyDuration(time.Since(start)),
)
return nil
}
// readTrienodeMetadata resolves the metadata of the specified trienode history.
// nolint:unused
func readTrienodeMetadata(reader ethdb.AncientReader, id uint64) (*trienodeMetadata, error) {
header, err := rawdb.ReadTrienodeHistoryHeader(reader, id)
if err != nil {
return nil, err
}
metadata, _, _, _, err := decodeHeader(header)
if err != nil {
return nil, err
}
return metadata, nil
}
// readTrienodeHistory resolves a single trienode history object with specific id.
func readTrienodeHistory(reader ethdb.AncientReader, id uint64) (*trienodeHistory, error) {
header, keySection, valueSection, err := rawdb.ReadTrienodeHistory(reader, id)
if err != nil {
return nil, err
}
var h trienodeHistory
if err := h.decode(header, keySection, valueSection); err != nil {
return nil, err
}
return &h, nil
}
// readTrienodeHistories resolves a list of trienode histories with the specific range.
func readTrienodeHistories(reader ethdb.AncientReader, start uint64, count uint64) ([]history, error) {
headers, keySections, valueSections, err := rawdb.ReadTrienodeHistoryList(reader, start, count)
if err != nil {
return nil, err
}
var res []history
for i, header := range headers {
var h trienodeHistory
if err := h.decode(header, keySections[i], valueSections[i]); err != nil {
return nil, err
}
res = append(res, &h)
}
return res, nil
}

View file

@ -0,0 +1,736 @@
// 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 pathdb
import (
"bytes"
"encoding/binary"
"math/rand"
"reflect"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/internal/testrand"
)
// randomTrienodes generates a random trienode set.
func randomTrienodes(n int) (map[common.Hash]map[string][]byte, common.Hash) {
var (
root common.Hash
nodes = make(map[common.Hash]map[string][]byte)
)
for i := 0; i < n; i++ {
owner := testrand.Hash()
if i == 0 {
owner = common.Hash{}
}
nodes[owner] = make(map[string][]byte)
for j := 0; j < 10; j++ {
path := testrand.Bytes(rand.Intn(10))
for z := 0; z < len(path); z++ {
nodes[owner][string(path[:z])] = testrand.Bytes(rand.Intn(128))
}
}
// zero-size trie node, representing it was non-existent before
for j := 0; j < 10; j++ {
path := testrand.Bytes(32)
nodes[owner][string(path)] = nil
}
// root node with zero-size path
rnode := testrand.Bytes(256)
nodes[owner][""] = rnode
if owner == (common.Hash{}) {
root = crypto.Keccak256Hash(rnode)
}
}
return nodes, root
}
func makeTrienodeHistory() *trienodeHistory {
nodes, root := randomTrienodes(10)
return newTrienodeHistory(root, common.Hash{}, 1, nodes)
}
func makeTrienodeHistories(n int) []*trienodeHistory {
var (
parent common.Hash
result []*trienodeHistory
)
for i := 0; i < n; i++ {
nodes, root := randomTrienodes(10)
result = append(result, newTrienodeHistory(root, parent, uint64(i+1), nodes))
parent = root
}
return result
}
func TestEncodeDecodeTrienodeHistory(t *testing.T) {
var (
dec trienodeHistory
obj = makeTrienodeHistory()
)
header, keySection, valueSection, err := obj.encode()
if err != nil {
t.Fatalf("Failed to encode trienode history: %v", err)
}
if err := dec.decode(header, keySection, valueSection); err != nil {
t.Fatalf("Failed to decode trienode history: %v", err)
}
if !reflect.DeepEqual(obj.meta, dec.meta) {
t.Fatal("trienode metadata is mismatched")
}
if !compareList(dec.owners, obj.owners) {
t.Fatal("trie owner list is mismatched")
}
if !compareMapList(dec.nodeList, obj.nodeList) {
t.Fatal("trienode list is mismatched")
}
if !compareMapSet(dec.nodes, obj.nodes) {
t.Fatal("trienode content is mismatched")
}
// Re-encode again, ensuring the encoded blob still match
header2, keySection2, valueSection2, err := dec.encode()
if err != nil {
t.Fatalf("Failed to encode trienode history: %v", err)
}
if !bytes.Equal(header, header2) {
t.Fatal("re-encoded header is mismatched")
}
if !bytes.Equal(keySection, keySection2) {
t.Fatal("re-encoded key section is mismatched")
}
if !bytes.Equal(valueSection, valueSection2) {
t.Fatal("re-encoded value section is mismatched")
}
}
func TestTrienodeHistoryReader(t *testing.T) {
var (
hs = makeTrienodeHistories(10)
freezer, _ = rawdb.NewTrienodeFreezer(t.TempDir(), false, false)
)
defer freezer.Close()
for i, h := range hs {
header, keySection, valueSection, _ := h.encode()
if err := rawdb.WriteTrienodeHistory(freezer, uint64(i+1), header, keySection, valueSection); err != nil {
t.Fatalf("Failed to write trienode history: %v", err)
}
}
for i, h := range hs {
tr, err := newTrienodeHistoryReader(uint64(i+1), freezer)
if err != nil {
t.Fatalf("Failed to construct the history reader: %v", err)
}
for _, owner := range h.owners {
nodes := h.nodes[owner]
for key, value := range nodes {
blob, err := tr.read(owner, key)
if err != nil {
t.Fatalf("Failed to read trienode history: %v", err)
}
if !bytes.Equal(blob, value) {
t.Fatalf("Unexpected trie node data, want: %v, got: %v", value, blob)
}
}
}
}
for i, h := range hs {
metadata, err := readTrienodeMetadata(freezer, uint64(i+1))
if err != nil {
t.Fatalf("Failed to read trienode history metadata: %v", err)
}
if !reflect.DeepEqual(h.meta, metadata) {
t.Fatalf("Unexpected trienode metadata, want: %v, got: %v", h.meta, metadata)
}
}
}
// TestEmptyTrienodeHistory tests encoding/decoding of empty trienode history
func TestEmptyTrienodeHistory(t *testing.T) {
h := newTrienodeHistory(common.Hash{}, common.Hash{}, 1, make(map[common.Hash]map[string][]byte))
// Test encoding empty history
header, keySection, valueSection, err := h.encode()
if err != nil {
t.Fatalf("Failed to encode empty trienode history: %v", err)
}
// Verify sections are minimal but valid
if len(header) == 0 {
t.Fatal("Header should not be empty")
}
if len(keySection) != 0 {
t.Fatal("Key section should be empty for empty history")
}
if len(valueSection) != 0 {
t.Fatal("Value section should be empty for empty history")
}
// Test decoding empty history
var decoded trienodeHistory
if err := decoded.decode(header, keySection, valueSection); err != nil {
t.Fatalf("Failed to decode empty trienode history: %v", err)
}
if len(decoded.owners) != 0 {
t.Fatal("Decoded history should have no owners")
}
if len(decoded.nodeList) != 0 {
t.Fatal("Decoded history should have no node lists")
}
if len(decoded.nodes) != 0 {
t.Fatal("Decoded history should have no nodes")
}
}
// TestSingleTrieHistory tests encoding/decoding of history with single trie
func TestSingleTrieHistory(t *testing.T) {
nodes := make(map[common.Hash]map[string][]byte)
owner := testrand.Hash()
nodes[owner] = make(map[string][]byte)
// Add some nodes with various sizes
nodes[owner][""] = testrand.Bytes(32) // empty key
nodes[owner]["a"] = testrand.Bytes(1) // small value
nodes[owner]["bb"] = testrand.Bytes(100) // medium value
nodes[owner]["ccc"] = testrand.Bytes(1000) // large value
nodes[owner]["dddd"] = testrand.Bytes(0) // empty value
h := newTrienodeHistory(common.Hash{}, common.Hash{}, 1, nodes)
testEncodeDecode(t, h)
}
// TestMultipleTries tests multiple tries with different node counts
func TestMultipleTries(t *testing.T) {
nodes := make(map[common.Hash]map[string][]byte)
// First trie with many small nodes
owner1 := testrand.Hash()
nodes[owner1] = make(map[string][]byte)
for i := 0; i < 100; i++ {
key := string(testrand.Bytes(rand.Intn(10)))
nodes[owner1][key] = testrand.Bytes(rand.Intn(50))
}
// Second trie with few large nodes
owner2 := testrand.Hash()
nodes[owner2] = make(map[string][]byte)
for i := 0; i < 5; i++ {
key := string(testrand.Bytes(rand.Intn(20)))
nodes[owner2][key] = testrand.Bytes(1000 + rand.Intn(1000))
}
// Third trie with nil values (zero-size nodes)
owner3 := testrand.Hash()
nodes[owner3] = make(map[string][]byte)
for i := 0; i < 10; i++ {
key := string(testrand.Bytes(rand.Intn(15)))
nodes[owner3][key] = nil
}
h := newTrienodeHistory(common.Hash{}, common.Hash{}, 1, nodes)
testEncodeDecode(t, h)
}
// TestLargeNodeValues tests encoding/decoding with very large node values
func TestLargeNodeValues(t *testing.T) {
nodes := make(map[common.Hash]map[string][]byte)
owner := testrand.Hash()
nodes[owner] = make(map[string][]byte)
// Test with progressively larger values
sizes := []int{1024, 10 * 1024, 100 * 1024, 1024 * 1024} // 1KB, 10KB, 100KB, 1MB
for _, size := range sizes {
key := string(testrand.Bytes(10))
nodes[owner][key] = testrand.Bytes(size)
h := newTrienodeHistory(common.Hash{}, common.Hash{}, 1, nodes)
testEncodeDecode(t, h)
t.Logf("Successfully tested encoding/decoding with %dKB value", size/1024)
}
}
// TestNilNodeValues tests encoding/decoding with nil (zero-length) node values
func TestNilNodeValues(t *testing.T) {
nodes := make(map[common.Hash]map[string][]byte)
owner := testrand.Hash()
nodes[owner] = make(map[string][]byte)
// Mix of nil and non-nil values
nodes[owner]["nil"] = nil
nodes[owner]["data1"] = []byte("some data")
nodes[owner]["data2"] = []byte("more data")
h := newTrienodeHistory(common.Hash{}, common.Hash{}, 1, nodes)
testEncodeDecode(t, h)
// Verify nil values are preserved
_, ok := h.nodes[owner]["nil"]
if !ok {
t.Fatal("Nil value should be preserved")
}
}
// TestCorruptedHeader tests error handling for corrupted header data
func TestCorruptedHeader(t *testing.T) {
h := makeTrienodeHistory()
header, keySection, valueSection, _ := h.encode()
// Test corrupted version
corruptedHeader := make([]byte, len(header))
copy(corruptedHeader, header)
corruptedHeader[0] = 0xFF // Invalid version
var decoded trienodeHistory
if err := decoded.decode(corruptedHeader, keySection, valueSection); err == nil {
t.Fatal("Expected error for corrupted version")
}
// Test truncated header
truncatedHeader := header[:len(header)-5]
if err := decoded.decode(truncatedHeader, keySection, valueSection); err == nil {
t.Fatal("Expected error for truncated header")
}
// Test header with invalid trie header size
invalidHeader := make([]byte, len(header))
copy(invalidHeader, header)
invalidHeader = invalidHeader[:trienodeMetadataSize+5] // Not divisible by trie header size
if err := decoded.decode(invalidHeader, keySection, valueSection); err == nil {
t.Fatal("Expected error for invalid header size")
}
}
// TestCorruptedKeySection tests error handling for corrupted key section data
func TestCorruptedKeySection(t *testing.T) {
h := makeTrienodeHistory()
header, keySection, valueSection, _ := h.encode()
// Test empty key section when header indicates data
if len(keySection) > 0 {
var decoded trienodeHistory
if err := decoded.decode(header, []byte{}, valueSection); err == nil {
t.Fatal("Expected error for empty key section with non-empty header")
}
}
// Test truncated key section
if len(keySection) > 10 {
truncatedKeySection := keySection[:len(keySection)-10]
var decoded trienodeHistory
if err := decoded.decode(header, truncatedKeySection, valueSection); err == nil {
t.Fatal("Expected error for truncated key section")
}
}
// Test corrupted key section with invalid varint
corruptedKeySection := make([]byte, len(keySection))
copy(corruptedKeySection, keySection)
if len(corruptedKeySection) > 5 {
corruptedKeySection[5] = 0xFF // Corrupt varint encoding
var decoded trienodeHistory
if err := decoded.decode(header, corruptedKeySection, valueSection); err == nil {
t.Fatal("Expected error for corrupted varint in key section")
}
}
}
// TestCorruptedValueSection tests error handling for corrupted value section data
func TestCorruptedValueSection(t *testing.T) {
h := makeTrienodeHistory()
header, keySection, valueSection, _ := h.encode()
// Test truncated value section
if len(valueSection) > 10 {
truncatedValueSection := valueSection[:len(valueSection)-10]
var decoded trienodeHistory
if err := decoded.decode(header, keySection, truncatedValueSection); err == nil {
t.Fatal("Expected error for truncated value section")
}
}
// Test empty value section when key section indicates data exists
if len(valueSection) > 0 {
var decoded trienodeHistory
if err := decoded.decode(header, keySection, []byte{}); err == nil {
t.Fatal("Expected error for empty value section with non-empty key section")
}
}
}
// TestInvalidOffsets tests error handling for invalid offsets in encoded data
func TestInvalidOffsets(t *testing.T) {
h := makeTrienodeHistory()
header, keySection, valueSection, _ := h.encode()
// Corrupt key offset in header (make it larger than key section)
corruptedHeader := make([]byte, len(header))
copy(corruptedHeader, header)
corruptedHeader[trienodeMetadataSize+common.HashLength] = 0xff
var dec1 trienodeHistory
if err := dec1.decode(corruptedHeader, keySection, valueSection); err == nil {
t.Fatal("Expected error for invalid key offset")
}
// Corrupt value offset in header (make it larger than value section)
corruptedHeader = make([]byte, len(header))
copy(corruptedHeader, header)
corruptedHeader[trienodeMetadataSize+common.HashLength+4] = 0xff
var dec2 trienodeHistory
if err := dec2.decode(corruptedHeader, keySection, valueSection); err == nil {
t.Fatal("Expected error for invalid value offset")
}
}
// TestTrienodeHistoryReaderNonExistentPath tests reading non-existent paths
func TestTrienodeHistoryReaderNonExistentPath(t *testing.T) {
var (
h = makeTrienodeHistory()
freezer, _ = rawdb.NewTrienodeFreezer(t.TempDir(), false, false)
)
defer freezer.Close()
header, keySection, valueSection, _ := h.encode()
if err := rawdb.WriteTrienodeHistory(freezer, 1, header, keySection, valueSection); err != nil {
t.Fatalf("Failed to write trienode history: %v", err)
}
tr, err := newTrienodeHistoryReader(1, freezer)
if err != nil {
t.Fatalf("Failed to construct history reader: %v", err)
}
// Try to read a non-existent path
_, err = tr.read(testrand.Hash(), "nonexistent")
if err == nil {
t.Fatal("Expected error for non-existent trie owner")
}
// Try to read from existing owner but non-existent path
owner := h.owners[0]
_, err = tr.read(owner, "nonexistent-path")
if err == nil {
t.Fatal("Expected error for non-existent path")
}
}
// TestTrienodeHistoryReaderNilValues tests reading nil (zero-length) values
func TestTrienodeHistoryReaderNilValues(t *testing.T) {
nodes := make(map[common.Hash]map[string][]byte)
owner := testrand.Hash()
nodes[owner] = make(map[string][]byte)
// Add some nil values
nodes[owner]["nil1"] = nil
nodes[owner]["nil2"] = nil
nodes[owner]["data1"] = []byte("some data")
h := newTrienodeHistory(common.Hash{}, common.Hash{}, 1, nodes)
var freezer, _ = rawdb.NewTrienodeFreezer(t.TempDir(), false, false)
defer freezer.Close()
header, keySection, valueSection, _ := h.encode()
if err := rawdb.WriteTrienodeHistory(freezer, 1, header, keySection, valueSection); err != nil {
t.Fatalf("Failed to write trienode history: %v", err)
}
tr, err := newTrienodeHistoryReader(1, freezer)
if err != nil {
t.Fatalf("Failed to construct history reader: %v", err)
}
// Test reading nil values
data1, err := tr.read(owner, "nil1")
if err != nil {
t.Fatalf("Failed to read nil value: %v", err)
}
if len(data1) != 0 {
t.Fatal("Expected nil data for nil value")
}
data2, err := tr.read(owner, "nil2")
if err != nil {
t.Fatalf("Failed to read nil value: %v", err)
}
if len(data2) != 0 {
t.Fatal("Expected nil data for nil value")
}
// Test reading non-nil value
data3, err := tr.read(owner, "data1")
if err != nil {
t.Fatalf("Failed to read non-nil value: %v", err)
}
if !bytes.Equal(data3, []byte("some data")) {
t.Fatal("Data mismatch for non-nil value")
}
}
// TestTrienodeHistoryReaderNilKey tests reading nil (zero-length) key
func TestTrienodeHistoryReaderNilKey(t *testing.T) {
nodes := make(map[common.Hash]map[string][]byte)
owner := testrand.Hash()
nodes[owner] = make(map[string][]byte)
// Add some nil values
nodes[owner][""] = []byte("some data")
nodes[owner]["data1"] = []byte("some data")
h := newTrienodeHistory(common.Hash{}, common.Hash{}, 1, nodes)
var freezer, _ = rawdb.NewTrienodeFreezer(t.TempDir(), false, false)
defer freezer.Close()
header, keySection, valueSection, _ := h.encode()
if err := rawdb.WriteTrienodeHistory(freezer, 1, header, keySection, valueSection); err != nil {
t.Fatalf("Failed to write trienode history: %v", err)
}
tr, err := newTrienodeHistoryReader(1, freezer)
if err != nil {
t.Fatalf("Failed to construct history reader: %v", err)
}
// Test reading nil values
data1, err := tr.read(owner, "")
if err != nil {
t.Fatalf("Failed to read nil value: %v", err)
}
if !bytes.Equal(data1, []byte("some data")) {
t.Fatal("Data mismatch for nil key")
}
// Test reading non-nil value
data2, err := tr.read(owner, "data1")
if err != nil {
t.Fatalf("Failed to read non-nil value: %v", err)
}
if !bytes.Equal(data2, []byte("some data")) {
t.Fatal("Data mismatch for non-nil key")
}
}
// TestTrienodeHistoryReaderIterator tests the iterator functionality
func TestTrienodeHistoryReaderIterator(t *testing.T) {
h := makeTrienodeHistory()
// Count expected entries
expectedCount := 0
expectedNodes := make(map[stateIdent]bool)
for owner, nodeList := range h.nodeList {
expectedCount += len(nodeList)
for _, node := range nodeList {
expectedNodes[stateIdent{
typ: typeTrienode,
addressHash: owner,
path: node,
}] = true
}
}
// Test the iterator
actualCount := 0
for x := range h.forEach() {
_ = x
actualCount++
}
if actualCount != expectedCount {
t.Fatalf("Iterator count mismatch: expected %d, got %d", expectedCount, actualCount)
}
// Test that iterator yields expected state identifiers
seen := make(map[stateIdent]bool)
for ident := range h.forEach() {
if ident.typ != typeTrienode {
t.Fatal("Iterator should only yield trienode history identifiers")
}
key := stateIdent{typ: ident.typ, addressHash: ident.addressHash, path: ident.path}
if seen[key] {
t.Fatal("Iterator yielded duplicate identifier")
}
seen[key] = true
if !expectedNodes[key] {
t.Fatalf("Unexpected yielded identifier %v", key)
}
}
}
// TestSharedLen tests the sharedLen helper function
func TestSharedLen(t *testing.T) {
tests := []struct {
a, b []byte
expected int
}{
// Empty strings
{[]byte(""), []byte(""), 0},
// One empty string
{[]byte(""), []byte("abc"), 0},
{[]byte("abc"), []byte(""), 0},
// No common prefix
{[]byte("abc"), []byte("def"), 0},
// Partial common prefix
{[]byte("abc"), []byte("abx"), 2},
{[]byte("prefix"), []byte("pref"), 4},
// Complete common prefix (shorter first)
{[]byte("ab"), []byte("abcd"), 2},
// Complete common prefix (longer first)
{[]byte("abcd"), []byte("ab"), 2},
// Identical strings
{[]byte("identical"), []byte("identical"), 9},
// Binary data
{[]byte{0x00, 0x01, 0x02}, []byte{0x00, 0x01, 0x03}, 2},
// Large strings
{bytes.Repeat([]byte("a"), 1000), bytes.Repeat([]byte("a"), 1000), 1000},
{bytes.Repeat([]byte("a"), 1000), append(bytes.Repeat([]byte("a"), 999), []byte("b")...), 999},
}
for i, test := range tests {
result := sharedLen(test.a, test.b)
if result != test.expected {
t.Errorf("Test %d: sharedLen(%q, %q) = %d, expected %d",
i, test.a, test.b, result, test.expected)
}
// Test commutativity
resultReverse := sharedLen(test.b, test.a)
if result != resultReverse {
t.Errorf("Test %d: sharedLen is not commutative: sharedLen(a,b)=%d, sharedLen(b,a)=%d",
i, result, resultReverse)
}
}
}
// TestDecodeHeaderCorruptedData tests decodeHeader with corrupted data
func TestDecodeHeaderCorruptedData(t *testing.T) {
// Create valid header data first
h := makeTrienodeHistory()
header, _, _, _ := h.encode()
// Test with empty header
_, _, _, _, err := decodeHeader([]byte{})
if err == nil {
t.Fatal("Expected error for empty header")
}
// Test with invalid version
corruptedVersion := make([]byte, len(header))
copy(corruptedVersion, header)
corruptedVersion[0] = 0xFF
_, _, _, _, err = decodeHeader(corruptedVersion)
if err == nil {
t.Fatal("Expected error for invalid version")
}
// Test with truncated header (not divisible by trie header size)
truncated := header[:trienodeMetadataSize+5]
_, _, _, _, err = decodeHeader(truncated)
if err == nil {
t.Fatal("Expected error for truncated header")
}
// Test with unordered trie owners
unordered := make([]byte, len(header))
copy(unordered, header)
// Swap two owner hashes to make them unordered
hash1Start := trienodeMetadataSize
hash2Start := trienodeMetadataSize + trienodeTrieHeaderSize
hash1 := unordered[hash1Start : hash1Start+common.HashLength]
hash2 := unordered[hash2Start : hash2Start+common.HashLength]
// Only swap if they would be out of order
copy(unordered[hash1Start:hash1Start+common.HashLength], hash2)
copy(unordered[hash2Start:hash2Start+common.HashLength], hash1)
_, _, _, _, err = decodeHeader(unordered)
if err == nil {
t.Fatal("Expected error for unordered trie owners")
}
}
// TestDecodeSingleCorruptedData tests decodeSingle with corrupted data
func TestDecodeSingleCorruptedData(t *testing.T) {
h := makeTrienodeHistory()
_, keySection, _, _ := h.encode()
// Test with empty key section
_, err := decodeSingle([]byte{}, nil)
if err == nil {
t.Fatal("Expected error for empty key section")
}
// Test with key section too small for trailer
if len(keySection) > 0 {
_, err := decodeSingle(keySection[:3], nil) // Less than 4 bytes for trailer
if err == nil {
t.Fatal("Expected error for key section too small for trailer")
}
}
// Test with corrupted varint in key section
corrupted := make([]byte, len(keySection))
copy(corrupted, keySection)
corrupted[5] = 0xFF // Corrupt varint
_, err = decodeSingle(corrupted, nil)
if err == nil {
t.Fatal("Expected error for corrupted varint")
}
// Test with corrupted trailer (invalid restart count)
corrupted = make([]byte, len(keySection))
copy(corrupted, keySection)
// Set restart count to something too large
binary.BigEndian.PutUint32(corrupted[len(corrupted)-4:], 10000)
_, err = decodeSingle(corrupted, nil)
if err == nil {
t.Fatal("Expected error for invalid restart count")
}
}
// Helper function to test encode/decode cycle
func testEncodeDecode(t *testing.T, h *trienodeHistory) {
header, keySection, valueSection, err := h.encode()
if err != nil {
t.Fatalf("Failed to encode trienode history: %v", err)
}
var decoded trienodeHistory
if err := decoded.decode(header, keySection, valueSection); err != nil {
t.Fatalf("Failed to decode trienode history: %v", err)
}
// Compare the decoded history with original
if !compareList(decoded.owners, h.owners) {
t.Fatal("Trie owner list mismatch")
}
if !compareMapList(decoded.nodeList, h.nodeList) {
t.Fatal("Trienode list mismatch")
}
if !compareMapSet(decoded.nodes, h.nodes) {
t.Fatal("Trienode content mismatch")
}
}

View file

@ -69,12 +69,21 @@ var (
gcStorageMeter = metrics.NewRegisteredMeter("pathdb/gc/storage/count", nil) gcStorageMeter = metrics.NewRegisteredMeter("pathdb/gc/storage/count", nil)
gcStorageBytesMeter = metrics.NewRegisteredMeter("pathdb/gc/storage/bytes", nil) gcStorageBytesMeter = metrics.NewRegisteredMeter("pathdb/gc/storage/bytes", nil)
historyBuildTimeMeter = metrics.NewRegisteredResettingTimer("pathdb/history/time", nil) stateHistoryBuildTimeMeter = metrics.NewRegisteredResettingTimer("pathdb/history/state/time", nil)
historyDataBytesMeter = metrics.NewRegisteredMeter("pathdb/history/bytes/data", nil) stateHistoryDataBytesMeter = metrics.NewRegisteredMeter("pathdb/history/state/bytes/data", nil)
historyIndexBytesMeter = metrics.NewRegisteredMeter("pathdb/history/bytes/index", nil) stateHistoryIndexBytesMeter = metrics.NewRegisteredMeter("pathdb/history/state/bytes/index", nil)
indexHistoryTimer = metrics.NewRegisteredResettingTimer("pathdb/history/index/time", nil) //nolint:unused
unindexHistoryTimer = metrics.NewRegisteredResettingTimer("pathdb/history/unindex/time", nil) trienodeHistoryBuildTimeMeter = metrics.NewRegisteredResettingTimer("pathdb/history/trienode/time", nil)
//nolint:unused
trienodeHistoryDataBytesMeter = metrics.NewRegisteredMeter("pathdb/history/trienode/bytes/data", nil)
//nolint:unused
trienodeHistoryIndexBytesMeter = metrics.NewRegisteredMeter("pathdb/history/trienode/bytes/index", nil)
stateIndexHistoryTimer = metrics.NewRegisteredResettingTimer("pathdb/history/state/index/time", nil)
stateUnindexHistoryTimer = metrics.NewRegisteredResettingTimer("pathdb/history/state/unindex/time", nil)
trienodeIndexHistoryTimer = metrics.NewRegisteredResettingTimer("pathdb/history/trienode/index/time", nil)
trienodeUnindexHistoryTimer = metrics.NewRegisteredResettingTimer("pathdb/history/trienode/unindex/time", nil)
lookupAddLayerTimer = metrics.NewRegisteredResettingTimer("pathdb/lookup/add/time", nil) lookupAddLayerTimer = metrics.NewRegisteredResettingTimer("pathdb/lookup/add/time", nil)
lookupRemoveLayerTimer = metrics.NewRegisteredResettingTimer("pathdb/lookup/remove/time", nil) lookupRemoveLayerTimer = metrics.NewRegisteredResettingTimer("pathdb/lookup/remove/time", nil)